Skip to content

fix(quick-router): abstain on a cancel-verb timer command instead of setting a new timer - #882

Open
tryeverything24 wants to merge 1 commit into
GeniePod:mainfrom
tryeverything24:fix/cancel-verb-timer-hijack
Open

fix(quick-router): abstain on a cancel-verb timer command instead of setting a new timer#882
tryeverything24 wants to merge 1 commit into
GeniePod:mainfrom
tryeverything24:fix/cancel-verb-timer-hijack

Conversation

@tryeverything24

@tryeverything24 tryeverything24 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

timer_request keys on the word "timer" plus any parseable duration, so in "cancel the 10 minute timer" the duration that only names which timer is read as a request — the cancellation itself starts a fresh 10-minute timer, the exact opposite of what was asked. Gate the parse on a leading cancel/stop/pause/delete verb (and the "turn off"/"shut off" forms) and abstain, since there is no deterministic cancel tool for the router to emit. Closes #881.

utterance on main expected
cancel the 10 minute timer set_timer{seconds:600} abstain (LLM grounds the cancel)
Stop the 5 minute timer. set_timer{seconds:300} abstain
delete the 20 minute timer set_timer{seconds:1200} abstain
pause the 10 minute timer set_timer{seconds:600} abstain
turn off the 10 minute timer set_timer{seconds:600} abstain
set a 10 minute timer set_timer{seconds:600} unchanged
remind me to stop the dryer in 10 minutes set_timer{label:"stop the dryer"} unchanged

Changes

  • At the top of timer_request, after the existing timer/remind gate, abstain when the utterance leads with a cancel verb (cancel/stop/end/pause/resume/delete/remove/clear/dismiss, or the turn off /shut off prefixes). Only the leading verb is gated, so "remind me to stop the dryer in 10 minutes" — where the verb sits inside the task clause — still schedules, and every genuine set request is untouched.
  • A bare "cancel the timer" (no duration) already abstained via the duration parse; the duration-qualified forms were the gap, and they were the dangerous ones — a confidently-wrong actuation rather than a graceful fallthrough.
  • Add cancel_verb_timer_commands_abstain_instead_of_setting_a_timer: five cancellation phrasings (plus a speaker-prefixed one) abstain, and the two positive controls above keep routing to set_timer.

Real Behavior Proof

  • I have built and run the affected code locally (or noted why I could not).
  • I have verified the change end-to-end on Jetson hardware.
  • I have NOT verified on Jetson hardware, and I explain the equivalent verification path or validation gap below.

Tested profile / hardware (check all that apply):

  • jetson
  • raspberry_pi
  • portable_sbc
  • laptop
  • mac
  • CI-only / docs-only
  • Not run locally

What I ran

x86_64 Linux dev machine (laptop profile), rustc 1.97.1, branch cut from 9e54db4. The changed path is pure string routing inside quick::route — no audio, Home Assistant, or hardware dependency — so route() tests exercise it end-to-end. I probed route() over the cancellation phrasings against unmodified main first, then wrote the regression test and confirmed it fails before applying the fix.

# 1. repro against unmodified main (test added, fix absent)
cargo test -p genie-core --lib cancel_verb_timer_commands_abstain_instead_of_setting_a_timer

# 2. after the fix — the full local CI set
cargo fmt --all -- --check
RUSTFLAGS="-D warnings" cargo clippy --workspace --all-targets --locked -- -D warnings
GENIE_RUN_RELEASE_TESTS=1 cargo build --workspace --locked --all-targets
GENIE_RUN_RELEASE_TESTS=1 cargo test  --workspace --locked --all-targets
cargo test --workspace --locked --doc
cargo run --locked -p genie-ctl -- bfcl-predict-quick --cases tests/bfcl/home_tool_cases.jsonl --out target/bfcl-live-predictions.jsonl
cargo run --locked -p genie-ctl -- bfcl-score --cases tests/bfcl/home_tool_cases.jsonl --predictions target/bfcl-live-predictions.jsonl --min-strict 96
RUSTFLAGS="-D warnings" cargo clippy -p genie-core -p genie-ctl --no-default-features --all-targets --locked -- -D warnings
cargo test -p genie-core -p genie-ctl --no-default-features --all-targets --locked

What I observed

Probe on unmodified main:

"cancel the 10 minute timer"  => Some(ToolCall { name: "set_timer", arguments: {"label": "timer", "seconds": 600} })
"Stop the 5 minute timer."    => Some(ToolCall { name: "set_timer", arguments: {"label": "timer", "seconds": 300} })
"delete the 20 minute timer"  => Some(ToolCall { name: "set_timer", arguments: {"label": "timer", "seconds": 1200} })
"pause the 10 minute timer"   => Some(ToolCall { name: "set_timer", arguments: {"label": "timer", "seconds": 600} })
"Leo: cancel my 10 minute timer" => Some(ToolCall { name: "set_timer", arguments: {"label": "timer", "seconds": 600} })

Repro before the fix (test added, fix absent):

thread 'tools::quick::tests::cancel_verb_timer_commands_abstain_instead_of_setting_a_timer' panicked:
"Cancel the 10 minute timer." is a cancellation and must abstain, not set a new timer
test result: FAILED. 0 passed; 1 failed

After the fix: all six cancellation phrasings abstain; "set a 10 minute timer" and "remind me to stop the dryer in 10 minutes" route to set_timer unchanged.

Gate results on this branch:

  • cargo fmt --all -- --check — clean.
  • cargo clippy --workspace --all-targets --locked -- -D warnings — clean; --no-default-features clippy for genie-core/genie-ctl — clean.
  • GENIE_RUN_RELEASE_TESTS=1 cargo test --workspace --locked --all-targetsfully green: every workspace target ok (genie-core lib 966 passed / 6 ignored, tools::quick 108/108, and the release-gated tool_dispatch_test target with binary_size_budget passing). One documented load flake (llm::provider::tests::loopback_127_range_allowed_without_remote_flag) failed once while a second workspace build ran concurrently and passes clean in isolation and in the full quiet-machine pass; it is untouched by this diff.
  • cargo test --workspace --locked --doc — green.
  • BFCL live quick-router accuracy gatestrict_accuracy: 96.15% (25/26) against the --min-strict 96 floor, the single holdout being the documented two-call multi-homework-timer case (quick-router multi-intent prompts emit a single, wrong-priority tool #533). Predictions byte-identical to main (cmp clean against a fresh main baseline).
  • --no-default-features test for genie-core/genie-ctl — green.
  • Release genie-core binary: 7,080,248 bytes vs. the 7,079,800-byte main baseline built with the same toolchain — +448 bytes for the added verb gate; binary_size_budget passes.

Jetson gap: no Jetson hardware and no aarch64 cross toolchain on this box, so the cross build is left to the Cross-compile (aarch64 / Jetson) CI job. The change is a pure word-level &str comparison with no platform-dependent behaviour, no I/O, and no new dependency, so it is architecture-independent by construction.

Test plan

  1. cargo test -p genie-core --lib cancel_verb_timer_commands_abstain_instead_of_setting_a_timer — passes here, fails on main (the first phrasing sets a 600-second timer where an abstain is expected).
  2. cargo test -p genie-core --lib tools::quick — the other 107 router tests are unchanged and green, including every existing timer/reminder-label test.

Notes for reviewers

  • The BFCL multi-homework-timer case starts with "Start homework mode …"; start is deliberately not in the cancel-verb list (it is a set/activate verb here), and the BFCL prediction file is byte-identical to main.
  • resume is included because "resume the 10 minute timer" also names an existing timer; emitting a fresh one is wrong for it in exactly the same way.

Summary by CodeRabbit

  • Bug Fixes
    • Improved timer command handling to recognize cancellation, stopping, pausing, resuming, deletion, and dismissal requests.
    • Prevented cancellation phrases containing durations from accidentally starting a new timer.
    • Preserved support for valid timer creation requests, including reminders to stop something after a specified duration.

…setting a new timer

"Cancel the 10 minute timer" / "stop the 5 minute timer" name an existing
timer by its duration, but timer_request read the duration as a request and
did the exact opposite of what was asked: the cancellation itself started a
fresh 10-minute timer. Gate the parse on a leading cancel/stop/pause/delete
verb (and the "turn off"/"shut off" forms) and abstain, so the LLM grounds
the cancellation. "remind me to stop the dryer in 10 minutes" and every
genuine set request are untouched.
@github-actions github-actions Bot added the bug Something isn't working label Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The quick router now abstains when timer utterances begin with cancellation or termination verbs. Tests cover cancellation phrasing and confirm that valid timer-setting requests remain routed to set_timer.

Changes

Timer cancellation routing

Layer / File(s) Summary
Cancellation guard and validation
crates/genie-core/src/tools/quick.rs
timer_request detects cancellation-style prefixes and returns None; tests cover cancellation commands and valid timer-setting requests.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: bug

Suggested reviewers: ultrahighsuper, yurii214, jaytbarimbao-collab, davion-knight, matedev01

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the timer router fix and matches the main change.
Linked Issues check ✅ Passed The fix abstains on duration-qualified cancel/stop timer commands and preserves normal timer requests, matching #881.
Out of Scope Changes check ✅ Passed The changes stay focused on timer-command abstention and regression tests, with no unrelated scope evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/genie-core/src/tools/quick.rs (1)

6684-6691: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover every supported cancellation prefix.

The implementation adds eleven cancellation forms, but this regression test omits end, resume, remove, clear, dismiss, and shut off. Add those cases so future edits cannot silently break individual branches.

Suggested test additions
         for utterance in [
             "Cancel the 10 minute timer.",
             "Stop the 5 minute timer.",
+            "End the 10 minute timer.",
+            "Resume the 10 minute timer.",
             "Leo: cancel my 10 minute timer",
             "delete the 20 minute timer",
+            "remove the 10 minute timer",
+            "clear the 10 minute timer",
+            "dismiss the 10 minute timer",
             "pause the 10 minute timer",
             "turn off the 10 minute timer",
+            "shut off the 10 minute timer",
         ] {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/genie-core/src/tools/quick.rs` around lines 6684 - 6691, Expand the
utterance list in the cancellation regression test to include timer requests
using the missing prefixes “end,” “resume,” “remove,” “clear,” “dismiss,” and
“shut off,” while preserving the existing cases and test structure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/genie-core/src/tools/quick.rs`:
- Around line 2801-2816: Update the cancellation detection around
is_cancel_command to remove a leading politeness prefix such as “please” before
matching cancellation verbs and existing “turn off”/“shut off” forms, while
preserving normal command handling. Add a regression case covering “please
cancel the 10 minute timer” and verify it does not fall through to set_timer.

---

Nitpick comments:
In `@crates/genie-core/src/tools/quick.rs`:
- Around line 6684-6691: Expand the utterance list in the cancellation
regression test to include timer requests using the missing prefixes “end,”
“resume,” “remove,” “clear,” “dismiss,” and “shut off,” while preserving the
existing cases and test structure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 698b5853-220e-4b6a-9673-77ad84e37c99

📥 Commits

Reviewing files that changed from the base of the PR and between 9e54db4 and eb1a3a3.

📒 Files selected for processing (1)
  • crates/genie-core/src/tools/quick.rs

Comment on lines +2801 to +2816
let is_cancel_command = matches!(
text.split_whitespace().next(),
Some(
"cancel"
| "stop"
| "end"
| "pause"
| "resume"
| "delete"
| "remove"
| "clear"
| "dismiss"
)
) || text.starts_with("turn off ")
|| text.starts_with("shut off ");
if is_cancel_command {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle leading politeness before matching cancellation verbs.

please cancel the 10 minute timer does not match this guard because the first token is please; the duration parser can then still emit set_timer, recreating the cancellation bug. Strip a leading politeness prefix for this check and add a regression case.

Proposed fix
+    let command = text.strip_prefix("please ").unwrap_or(text);
     let is_cancel_command = matches!(
-        text.split_whitespace().next(),
+        command.split_whitespace().next(),
         Some(
             "cancel"
             // ...
         )
-    ) || text.starts_with("turn off ")
-        || text.starts_with("shut off ");
+    ) || command.starts_with("turn off ")
+        || command.starts_with("shut off ");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let is_cancel_command = matches!(
text.split_whitespace().next(),
Some(
"cancel"
| "stop"
| "end"
| "pause"
| "resume"
| "delete"
| "remove"
| "clear"
| "dismiss"
)
) || text.starts_with("turn off ")
|| text.starts_with("shut off ");
if is_cancel_command {
let command = text.strip_prefix("please ").unwrap_or(text);
let is_cancel_command = matches!(
command.split_whitespace().next(),
Some(
"cancel"
| "stop"
| "end"
| "pause"
| "resume"
| "delete"
| "remove"
| "clear"
| "dismiss"
)
) || command.starts_with("turn off ")
|| command.starts_with("shut off ");
if is_cancel_command {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/genie-core/src/tools/quick.rs` around lines 2801 - 2816, Update the
cancellation detection around is_cancel_command to remove a leading politeness
prefix such as “please” before matching cancellation verbs and existing “turn
off”/“shut off” forms, while preserving normal command handling. Add a
regression case covering “please cancel the 10 minute timer” and verify it does
not fall through to set_timer.

@matedev01 matedev01 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed CodeRabbit's finding directly: route("please cancel the 10 minute timer") still returns set_timer{seconds:600} — a throwaway test shows the leading-verb guard only checks the first token, so a leading 'please' bypasses it entirely and reproduces the exact inversion bug (cancel -> sets a new timer) this PR exists to fix. Worth stripping the politeness prefix before the guard check, as CodeRabbit suggested, with a regression case for the polite phrasing alongside the existing ones.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] quick-router: "cancel the 10 minute timer" starts a fresh 10-minute timer — a cancel verb is parsed as a set request

2 participants