---
title: Waiting detector (part 3)
---

# Waiting detector (part 3)

## What it is

This is part 3 of the [Waiting detector](waiting-detector.md) page. It covers the limits of the detector — every case it deliberately leaves alone — together with the opt-in AI second opinion that double-checks an ambiguous turn, and the first half of the reference material for anyone with the code open: the single chokepoint every needs-you flip passes through and the pattern library it matches against.

## Where to find it

The same surface as the [parent page](waiting-detector.md): the session chat, where the orange **⏱ Auto-waited** pill sits above the agent reply that fired and the **Keep waiting** and **Check in** buttons sit above the message box; the **Needs You** inbox row and its right-click menu; the controls under **Settings → Sessions**; and the audit log at **Settings → Diagnostics → Waiting Detector activity**.

## How it behaves

### What it does NOT do

- **It does not block real questions.** `pendingAction === 'question'` is checked first; question turns always flip to **Needs You** no matter what waiting language is in the prose.
- **It does not suppress waits on YOU.** A pattern hit on _"Waiting on your call"_, _"awaiting your merge"_, _"Holding until you confirm"_, _"I'll wait until you decide"_ — anything where the wait is directed at the human — is a real **Needs You**. The user-directed guard at the gate vetoes the suppression. This is the single most consequential of the three rejection guards (see "Three rejection guards" below).
- **It does not suppress long _single-paragraph_ messages.** A single block of prose over 2,000 characters is treated as a substantive summary and flips to **Needs You** even when waiting vocabulary is present. (A long _multi-paragraph_ report can still suppress when it ENDS on a terse handoff line — see the next bullet.)
- **It suppresses multi-paragraph messages only if the final paragraph is a clean wait declaration.** A genuine "I'm waiting" turn-end is often a terse status line stapled to the bottom of a longer dispatch summary — e.g. _"Dispatched the deploy to staging. Watching the build dashboard. Waiting for completion notification."_ The **trailing-wait carve-out** (added 2026-05-26) admits this shape: when the message is multi-paragraph, the gate extracts the final paragraph, requires it to be ≤ 400 characters AND not user-directed AND to itself contain a high-confidence pattern match. The bulk of the message is allowed to be a substantive report; only the closing paragraph carries the suppression. **For a _long_ (over-2,000-char) multi-paragraph message** the closing line must additionally be an explicit _handoff_ — a forward commitment (_"I'll report back when the build lands"_) or a poll-avoidance commitment — not just any wait phrase (long-message forward-commit carve-out, 2026-06-06); this is what lets the screenshot case _"…I'll report back the moment the cloud tests finish."_ suppress while a long _answer_ that merely ends on _"…was waiting for the index to initialize"_ still flips to **Needs You**. **Penultimate carve-out (2026-06-07):** the **second-to-last** paragraph is also scanned when it is a clean hand-off (a forward commitment such as _"the harness will auto-resume me when it finishes"_, or a poll-avoidance commitment) AND the message carries no attention-grab alert anywhere — this catches a wait that closes the prose but is then followed by a trailing status-dashboard paragraph (_"Gate so far: typecheck … · lint … · build …"_). **A wait can also LEAD the turn:** when the _opening_ paragraph is itself a clean hand-off (a forward-commitment or poll-avoidance pledge), it suppresses too — and as of 2026-06-08 that holds even when the opening paragraph is long and the wait _closes_ it (the same trailing-sentence extraction the closing paragraph gets). Other interior paragraphs are still never scanned. Single-paragraph behaviour is unchanged.
- **It does not suppress policy/rule status reports.** A hit on _"blocked by branch protection"_, _"blocked by content filtering policy"_, or _"blocked by content policy"_ describes a permanent rule the agent ran into — not a wait it is actively holding for. The `blocked-on-by` pattern carries a negative lookahead (added 2026-05-26) that excludes these so the session flips to **Needs You** correctly, while genuine waits like _"blocked on the deploy"_ / _"blocked until tests pass"_ / _"blocked by CI"_ still suppress.
- **It does not hold on a bare "parking" phrase with nothing real behind it (2026-07-23).** A poll-avoidance sign-off (_"won't poll"_, _"no polling"_) or a bare self-hold marker (_"I'm waiting."_, _"Sitting tight."_, _"Awaiting."_, _"Going quiet."_, _"The wait is not over."_) now auto-waits **only when the same message also shows something real to wait on** — a named build/test/deploy, a _"the harness will notify me"_ hand-off, a forward commitment, OR plain-English background activity (_"Build kicked off"_, _"dispatched to the cloud"_, _"the watcher is armed"_, _"…until it lands"_). A bare done-report sign-off (_"Typecheck's green. Won't poll further."_, a lone _"Awaiting."_) is treated as _finished_, not waiting, so it flips to **Needs You** immediately instead of parking the session for a full waiting window. This closed the dominant real-world false-hold: a read-only backtest measured the always-on poll-avoidance phrases holding an already-**done** session ~57% of the time (vs ~0.1% for the canonical keep-alive sentinel). The genuine _"won't poll — it'll notify me on completion"_ hand-off still holds, as do the sentinel and every _"waiting on you"_ safeguard (this gate runs after both). See `waiting-detector-contract.md` invariant **`an-uncorroborated-operative-does-not-hold`**.
- **It does not override explicit tool intent.** If the agent invokes `ScheduleWakeup`, or `Agent`/`Task` with `run_in_background: true`, those paths populate the same `deferredWork` field directly, and the prose detector only runs when no tool-based deferral is already in place. (A background `Bash` does **not** create a deferral — that was reverted 2026-05-21 — so a bg-`Bash` turn relies on the prose sentinel, not a tool deferral.) **Two carve-outs. First (2026-06-14):** a _stale_ background-task deferral — the agent dispatched a background `Agent`/`Task` early, then kept working and ended the turn on the canonical waiting sentinel — no longer shadows the detector. Omniscio clears the stale flag and re-runs Gate 6 on the closing prose, so the sentinel still auto-waits instead of a premature **Needs You**. **Second (2026-07-15):** the mirror case — the agent dispatched a background `Agent`/`Task` as its **final** tool AND closed the turn on the canonical sentinel carrying an `ETA <N> minutes.` — no longer drops that ETA. Rather than the flat background-task window, Omniscio honors the ETA (it becomes the wait window, and Omniscio checks in at the ETA _unconditionally_, exactly like a foreground ETA wait) — the fix for a wait that showed **⏱ Auto-waited** with "ETA 20 minutes" yet surfaced to the inbox without checking in. Only an ETA-bearing sentinel diverts; a bare background-task wait is unchanged. See `waiting-detector-contract.md` invariant `a-background-hold-arms-only-when-it-was-the-last-tool` (both carve-outs) + `a-declared-eta-sets-the-window`.
- **It honors the canonical sentinel even when a turn never finishes (2026-06-20).** Gate 6 runs only on the CLI's turn-end `result` event. If the agent streams the full canonical waiting sentinel (_"Waiting on a background process to finish…"_) but the CLI never emits that result event — it hung or was CPU-starved on its own background jobs — Gate 6 never runs, and before this fix the **stall watchdog** marked the session **Stalled?** once the stall-detection window passed. Omniscio now checks the agent's in-flight text the moment a stall would fire: if it is the canonical sentinel (and not a question), the session **auto-waits** (the same **⏱ Auto-waited** hold + cap) instead of stalling. Only the unambiguous canonical sentinel is honored on this path — not the broad patterns — because a genuinely silent turn that did NOT declare the sentinel should still stall. See `waiting-detector-contract.md` "Stall-path canonical-sentinel honor".
- **It keeps auto-waiting when the sentinel closes a turn that still has background subagents in flight (2026-07-23).** When the agent dispatches background `Agent`/`Task` subagents and then closes the turn on the canonical waiting sentinel (_"Waiting on a background process to finish… ETA N minutes."_), the subagent defense-in-depth gate (Gate 5.5) used to fire first and short-circuit the sentinel's own auto-wait — so it suppressed only that one turn-end and, the moment the subagents reported back, the session fell through to a bare **Needs You** (live session `d7e04aa1`: it surfaced ~9s after the sentinel turn and self-recovered ~3 min later; the turn 84s earlier, with no subagents, auto-waited correctly). Now Gate 5.5 **yields** to the canonical sentinel (when the detector is on AND the process is still live) so the sentinel arms its durable **⏱ Auto-waited** + ETA hold that survives the subagents finishing. A non-sentinel subagent turn, a dead process, or the detector turned off keep the old defense-in-depth suppress unchanged. See `waiting-detector-contract.md` invariant **I4b**.
- **It honors the canonical sentinel even after an earlier in-turn question (2026-07-19).** A long, multi-step turn can trip Omniscio's "this turn asked a question" flag on an EARLIER step — a rhetorical self-question the agent reasoned through, or a decision it weighed — and that flag is _sticky_ for the whole turn. Before this fix, a sticky earlier-turn question would veto the auto-wait even when the turn's FINAL message was the exact canonical sentinel (_"Waiting on a background process to finish…"_), so a turn that legitimately ended on the keep-alive line flipped to **Needs You** and landed in your inbox (live session `95662a3a`, 2026-07-18 — the keep-alive line surfaced for ~50s until a dispatched subagent replied). Now the canonical sentinel as the turn's FINAL text is treated as the agent's authoritative "I need nothing from you" close and always auto-waits, on both the normal turn-end path and the stall path. A GENUINE question in the FINAL message still wins (a clean sentinel has no `?` and no option-list widget), so you're never held back from a real question. See `waiting-detector-contract.md` invariant **`a-question-always-wins`** (canonical-sentinel carve-out).
- **It never drops a still-working session into your inbox when its final line is the keep-alive sentence, even if something transient shadowed the detector (2026-08-07).** When an agent wakes itself the instant its own background job finishes, reads the result, and closes again on the exact _"Waiting on a background process to finish…"_ line, a rare, split-second internal state could occasionally slip past the main detector and let that turn flip to **Needs You** — the same keep-alive line that auto-waited a moment earlier and a moment later (live session `b3632f18`: three of four byte-identical waits auto-waited, the fourth surfaced and then healed itself ~6 minutes later on its own). A last-resort guard now sits at the exact "about to drop into the inbox" moment: if the agent's final message IS the keep-alive sentence, its process is still alive, and it isn't actually asking you anything, Omniscio keeps it **Running** (honoring any ETA) instead of surfacing — no matter what shadowed the detector upstream. A real question, a real "approve my plan," and error/rate-limit cases still reach you exactly as before, and Omniscio logs a warning each time the guard has to step in so the underlying cause stays diagnosable. See `waiting-detector-contract.md` invariant **`the-canonical-sentence-holds-even-when-shadowed`** (live-child sentinel last-resort dominance).
- **It does not fire on a _quoted_ or _mentioned_ sentinel (2026-07-14).** An agent that writes Omniscio's exact wait phrase inside quotes or backticks — e.g. while _explaining_ the waiting feature, as in _'the agent emits its exact line ("Waiting on a background process to finish…")'_ — is talking ABOUT the wait, not declaring one, so it is not auto-waited (the same way a phrase quoted inside a ` ``` ` code block is already ignored). This matters most in the default **high-precision** mode, where the broad corpus is off — a message describing the feature used to trip a spurious auto-wait; the same short-quote strip now also guards the tier's own paraphrase patterns (`will-continue-when-once-after`, `waiting-on-ci-noun`). A genuine, _unquoted_ emission still auto-waits, even when the message also contains unrelated quoted text (a quoted file path, a tool echo): only a **short** quoted span — a plausible quote of the ~80-char phrase — is ignored, never a long span that merely happens to bracket a real emission. Validated read-only against the live history (16,722 agent messages): 38 quoted mentions stopped false-waiting, 0 real waits affected. See `waiting-detector-contract.md` "Canonical-sentinel quoted-mention guard".
- **It does not affect search, export, or audit.** The suppression still writes a normal `notable-system` message row — the chat just renders it as the **⏱ Auto-waited** tag rather than a prose bubble; the underlying message rows are untouched. Tools that read history (Search, Share, Audit Framework) see the same data they did before.
- **It does not affect rate-limit, auth, or API-error paths.** Those have their own dedicated gates (Gates 1–3 in `handleTurnComplete`) that run earlier and always pierce.
- **The auto-wait survives a transient auth-token refresh (2026-07-13).** If your login token briefly `401`s while a session is auto-waiting, Omniscio's silent auth-retry refreshes the token and re-runs the last turn. If that re-run comes back **empty** (the agent had nothing new to say), Omniscio now **restores the auto-wait** instead of dropping the session in your inbox a full window early — the earlier behaviour that surfaced a still-waiting session ~10 minutes into a 13-minute wait during a token blip (live session `869d8973`). The wait resumes with a fresh window; its overall "waited too long" ceiling is preserved, so it still can't wait forever. A re-run that actually produces an answer, asks a question, or re-declares its own wait ends the hold normally. Currently scoped to the auth-retry path. See [waiting-detector-contract.md](/.claude/memory/contracts/waiting-detector-contract.md) invariant **`a-transient-auth-failure-does-not-discard-a-wait`**.
- **The auto-wait survives a rate limit hit during an ETA check-in (2026-07-16).** When Omniscio auto-checks-in on a waiting session at its ETA and that re-run immediately hits your Claude session/rate limit, the session used to be misfiled as _awaiting your reply_ (its last message is the wait sentinel) — so rate-limit recovery skipped it, stopped tracking it, and the stall watchdog then dropped it into your inbox as a **bare Needs You** with no ⏱ wait buttons and no rate-limit **Continue** (live session `89a27b7f`). Omniscio now recognises that an Omniscio-managed auto-wait is _not_ a wait on you, so a rate-limited waiting session recovers normally — it moves to a healthy account, or auto-restarts at the limit reset — instead of stranding. A wait directed at _you_ (_"waiting on your input"_) is still a real **Needs You**. This is the recovery-path sibling of the auth-token-refresh case above. See [rate-limit-recovery-contract.md](/.claude/memory/contracts/rate-limit-recovery-contract.md) invariant **18** (waiting-yield carve-out).
- **It does not run on past turns retroactively.** A session that flipped to **Needs You** before this feature shipped stays where it is. Only new turn-completes go through the detector.

### AI second opinion (Stage 2 — opt-in, default off)

The pattern library is fast and free but it is a heuristic: it has accreted dozens of patterns and several rejection guards, and it still both fires on the occasional false positive and misses phrasings no pattern covers. **Stage 2** adds a cheap model (Haiku) as a second opinion _behind_ the regex — **off by default**; enable at **Settings → Sessions → "AI second-opinion on waiting detection"** (needs an AI key, like the other AI features). Kill switch: `AMC_DISABLE_WAITING_MODEL=1`.

**The regex is always the floor.** Stage 1 (the patterns above) runs first on every turn, for free. The model is consulted ONLY on the _ambiguous band_ — never on a clear turn-end:

| The regex…                                                  | Stage 2 acts   | Outcome                                                                   |
| ----------------------------------------------------------- | -------------- | ------------------------------------------------------------------------- |
| **fired** (a clean suppressible hit)                        | model confirms | model says needs-you → flip (kills a false alarm); otherwise stay running |
| **near-missed** (a pattern matched but a guard rejected it) | model checks   | model says it's a real wait → stay running (catches a miss)               |
| saw **no waiting language at all**                          | not consulted  | the regex decision stands (the big majority of turns)                     |

If the model is off, slow, errors, or you have no AI key, the regex verdict stands unchanged — Stage 2 can only _refine_, never break, today's behavior. The model call is bounded (a few-second timeout, a concurrency cap, a per-process ceiling) and logged to your cost tracker (`source = waiting_detector`).

**When the model layer goes dark, Omniscio now tells you (2026-06-09).** The second opinion is silent when it can't run, so a _sustained_ outage used to go unnoticed: if the AI key it uses stops working — e.g. a saved key is reset by a hardware or Windows change — every check quietly falls back to the regex with nothing surfaced (a live 2026-06-09 incident ran the layer at 100% fallback for a full day before anyone noticed). Omniscio now watches the durable adjudication log and, when the model has been unreachable across a sustained run of recent checks (a full window of fallbacks spanning ≥20 minutes — a single successful check clears it), raises **ONE** inbox alert — _"the model double-check is offline"_ — with the plain remedy (re-add the key under **Settings → Accounts**; it clears itself once a usable key is back). It's conservatively thresholded so transient slowness under load never alerts, deduped so it can never stack, only relevant when you've turned the second opinion on, and bulletproof (it can never disturb the detector itself). Env kill switch: `AMC_DISABLE_WAITING_MODEL_OUTAGE_ALERT=1`. (The Settings → Diagnostics activity panel is **not** the surface — it's hidden for performance.)

**The learning loop.** Every Stage-2 consultation is recorded in the `waiting_detector_adjudications` table with its disposition (confirmed / caught-false-positive / caught-miss / fallback). A scheduled weekly **Opus** session mines the catches, drafts new regex patterns, back-tests them, and opens a PR — so the cheap regex grows and the model is consulted less over time. Runbook: [docs/waiting-detector-weekly-evaluator.md](../waiting-detector-weekly-evaluator.md). (The weekly schedule stays off until enough data accrues.)

**Technical.** The model runs in a fire-and-forget async finalizer (`adjudicateProseWait`) so it never blocks the synchronous turn-completion path; a transient `agent_prose_wait_pending_model` deferred-work kind holds the session running (no banner) until the model resolves, guarded by `spawnEpoch`/status staleness checks. Module: [src/main/process/waiting-adjudicate.ts](/src/main/process/waiting-adjudicate.ts); contract invariants **S1–S4** in [.claude/memory/contracts/waiting-detector-contract.md](/.claude/memory/contracts/waiting-detector-contract.md).

## For agents

### How it works (technical)

#### The chokepoint

Every `needs_you` transition in Omniscio flows through `updateStatus()` in [src/main/process/process-manager.ts](/src/main/process/process-manager.ts). The primary site is inside `handleTurnComplete()` in [src/main/process/ndjson-event-handlers.ts](/src/main/process/ndjson-event-handlers.ts) (the ProcessManager method at the same name is a thin delegate), which fires on every `result` event the Claude CLI emits at turn-end. The waiting detector is **Gate 6** in a stack of seven suppression gates that all sit before that single `updateStatus` call.

| #   | Gate                        | Outcome on hit                                                                                       |
| --- | --------------------------- | ---------------------------------------------------------------------------------------------------- |
| 1   | Auth error                  | Silent retry, no status change                                                                       |
| 2   | Rate-limit recovery         | Auto-switch account or defer 30 min                                                                  |
| 3   | API error retry             | Silent re-prompt                                                                                     |
| 4   | `deferredWork` set          | Arm timer, return                                                                                    |
| 5   | Auto-continue under cap     | Inject "Please continue.", return                                                                    |
| 5.5 | `pendingSubagents.size > 0` | Suppress (defense-in-depth); yields to the canonical sentinel → Gate 6 arms a durable hold (**I4b**) |
| 6   | **Prose-pattern wait**      | Set `deferredWork = agent_prose_wait`, fall through to Gate 4                                        |

Only if all gates clear does `handleTurnComplete()` fire `updateStatus(..., 'needs_you', ...)`. Gate 6 is a _preprocessor_: it doesn't return directly. It populates `session.deferredWork`, and the unified deferred-work block (Gate 4) then arms the 30-minute cap timer. This keeps every kind of deferred work (scheduled wakeup, background task, prose wait) flowing through one timer-arming branch instead of three parallel ones.

**What counts as "the agent started background work" is ONE shared rule** ([`isBackgroundDispatchTool`](/src/shared/agent-background-tools.ts), 2026-09-01): any tool whose input carries `run_in_background: true` (`Bash`/`Agent`/`Task` today, and any future harness tool with **no code change**), plus a small named set of tools that are inherently background and carry no flag — currently `Monitor` and `Workflow`. Before this, three separate recorders each hardcoded one tool NAME, so a background-capable tool stayed invisible until someone edited all three: that is how `Workflow` — the tool the **deep research** skill runs on — came to drop research sessions into the inbox with nothing to act on. `ScheduleWakeup` is deliberately outside the rule (it has its own kind and outranks a background task). One caveat carries over from `Monitor`: a dispatch the agent then works *past* in the same turn still surfaces, because the stale-flag gate deliberately drops a hold the agent moved beyond.

The `Monitor` harness watch tool is folded into the **background task** kind: a turn that ends by firing `Monitor` (watching a background job) arms the same cap as a `run_in_background` dispatch, so it stays quiet while the watch ticks. Because dispatching background work is itself an explicit "I'm waiting" signal (the same keep-alive class as the canonical waiting sentinel), the cap **checks in with the agent** — regardless of the **Auto check-in** toggle — rather than surfacing on the first window; it only lands in your inbox once the wait has genuinely gone idle (~2h) or run ~6h total, and then with the honest *"Checked in with the agent periodically…"* banner. _(Changed 2026-08-16: previously a background-task wait surfaced un-nudged at the first cap even while its job was still running.)_

#### The pattern library

Lives at [src/main/process/waiting-patterns.ts](/src/main/process/waiting-patterns.ts). 69 high-confidence patterns + 1 medium-confidence pattern. Each entry carries `match` and `noMatch` example arrays that the unit suite reads directly — every pattern has at least one positive and one negative case. Categories:

- **explicit-hold** — `"I'm waiting"`, `"awaiting your merge"`, `"standing by"`, `"on hold until"`, `"paused until"`, `"blocked on"`, `"idling"`
- **future-resume** — `"I'll continue once tests pass"`, `"I'll continue as each reports"` (bare-"as" distributive completion, 2026-06-08), `"resuming when the build is green"`, `"the harness will auto-resume me"` (external system resuming the agent)
- **proxy-notification** — `"let me know when X"` (medium-confidence; corroborating only in v1)
- **state-marker** — third-person reports on background work: `"just two tests left"` / `"only the build remaining"` (depletion shape) and `"three gates still running"` / `"two subagents in flight"` (count + work-noun + state-verb, added 2026-05-27)
- **temporal-anchor** — `"until X happens"`, `"pending X"`, `"once X is done"`

A negative lookbehind `(?<!\b(?:was|were|been|had|have|having|previously)\s+)` excludes past-tense forms so `"I was waiting"` and `"had been awaiting"` do NOT match.

Adding a new pattern is a four-step process documented at the top of `waiting-patterns.ts`: pick the smallest category that fits, choose `high` only if a positive match alone (in a short message) justifies suppression, add `match` and `noMatch` examples, re-run the analyzer for false-positive risk.

## Related

The overview, the coverage of non-Claude engines and the everyday controls are on the [parent page](waiting-detector.md); [part 2](waiting-detector-part-2.md) covers the cap, the agent-stated ETA window, the Keep waiting and Check in buttons, the automatic check-ins, the inbox right-click menu and the confirmation probe; [part 4](waiting-detector-part-4.md) covers the ScheduleWakeup check-in mode and the rest of the technical reference.