---
title: Waiting detector (part 4)
---

# Waiting detector (part 4)

## What it is

This is part 4 of the [Waiting detector](waiting-detector.md) page. It covers the timer side of the same wait decision — the ScheduleWakeup tool's opt-in check-in mode — and then the rest of the reference material for anyone with the code open: the three rejection guards that qualify a pattern match, the corpus validation behind the patterns, every setting and kill switch, the subagent defense-in-depth gate, and the guards against duplicate or racing timers.

## 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

### ScheduleWakeup check-in mode (agent opt-in, sister feature)

A sibling of Gate 6, on the **tool side** of the wait classifier. The `ScheduleWakeup` tool (the explicit deferral path Gate 6 falls back from) accepts an opt-in `checkIn: boolean` field in its input. When the agent passes `checkIn: true`, the timer-fire reframes the re-injection so the agent treats it as a **self-scheduled check-in** — should I keep waiting? — rather than a fresh user-style instruction. The session stays `running` either way; the difference is in how the agent perceives the re-injection and in how the banner reads to you.

| Mode                       | Tool call                                                                        | Banner                                                                                  | Re-injection at timer fire                                                                                                                | Operator-row `metadata.kind` |
| -------------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
| Default (today's behavior) | `ScheduleWakeup({delaySeconds, prompt})` or `ScheduleWakeup({…, checkIn:false})` | `"Agent scheduled wake-up in N minutes — staying running until then."`                  | Agent's `prompt` injected verbatim as an operator message                                                                                 | `'schedule-wakeup'`          |
| Check-in                   | `ScheduleWakeup({delaySeconds, prompt, checkIn:true})`                           | `"Agent scheduled check-in in N minutes — staying running, will not surface to inbox."` | Agent's `prompt` wrapped with the contract-locked `[Check-in]` framing prefix (decide: keep waiting, proceed, or end the turn to surface) | `'schedule-check-in'`        |

**Live minutes (both modes).** The banner shows human **minutes**, not raw seconds, and **counts down live** while the agent sleeps: the minutes-remaining is recomputed about every 30 seconds (so it reads `"in 20 minutes"`, then `"in 19 minutes"`, … down to `"less than a minute"`), letting you glance and see how long is left. Once the wake time passes the banner **freezes to the original scheduled duration** — it deliberately does NOT flip to "any moment now", so a banner that was superseded by the agent re-scheduling can't overclaim an imminent wake. Mechanics: the wording is produced by the shared `formatScheduledWakeBanner` helper ([`src/shared/scheduled-wake-format.ts`](../../src/shared/scheduled-wake-format.ts)); the backend stamps the absolute wake time (`scheduledWakeAt`) and mode into the message metadata; the renderer's [`ScheduledWakeBanner`](../../src/renderer/src/components/ui/ScheduledWakeBanner.tsx) reads them and re-renders on a shared 30 s tick. Locked by contract invariant **I10**.

**Scope.** Check-in mode is exclusive to `ScheduleWakeup` — it is **not** auto-applied to `Bash`-background deferrals or Gate 6 prose-pattern waits. Those paths keep their pre-feature behavior. The `checkIn` field only lives on the `schedule_wakeup` arm of `Session.deferredWork`; no other deferred-work kind even has the field. (Contract invariant **I7**.)

**When to use it (agent-side guidance).** The default mode is right when the agent has scheduled work that _should_ return to the user when the delay elapses (the prompt at fire-time is essentially the next user-style instruction). Check-in mode is right when the agent wants to **re-evaluate** an external condition silently — poll a queue, watch a CI run, or pace a `/loop` iteration — without surfacing to the inbox. Calling `ScheduleWakeup` again with `checkIn: true` from the check-in turn extends the wait by another delay window.

**What the user sees.** Two things: (1) the inline banner uses different wording so check-ins are visually distinct from wake-ups in the conversation log, and (2) the re-injection appears as a normal operator message whose first line starts with `[Check-in]` — that's the framing prefix the agent reads on re-entry. The session never flips to **Needs You** during a check-in cycle; only an end-of-turn that doesn't re-arm a check-in will surface.

**Kill switch.** There isn't one. The flag is opt-in per `ScheduleWakeup` call; the agent simply not setting `checkIn: true` is the off state. If the framing prefix ever needs to change, it's the single constant `CHECK_IN_FRAMING_PREFIX` in `process-manager.ts`, contract-locked by **I7**.

## For agents

### How it works (technical)

#### Three rejection guards (added 2026-05-24)

The pattern regex match alone is not enough to suppress an inbox flip. `detectSuppressibleWait` in [src/main/process/waiting-patterns.ts](/src/main/process/waiting-patterns.ts) — Gate 6's only entry point — composes three independent rejection guards on top of the pattern match. A suppressible wait must clear ALL three; any single failure short-circuits and the turn falls through to **Needs You**. The "fail on multiple fronts" composition is intentional: the regexes are noisy on long substantive prose, the user wanted matches to clear more than one independent bar.

1. **Length** — for a **single-paragraph** message, raw `lastAssistantText.length ≤ 2,000` characters (`MAX_WAITING_MESSAGE_LENGTH`, raised from 500 on 2026-05-26 after a live-DB back-test). Measured against the RAW assistant text, before fence-stripping, so a long message bulked out by a quoted code block stays long. Genuine wait declarations are terse — a status line or two, sometimes with a one-sentence summary of what was dispatched, hence the 2,000-char headroom. **As of 2026-06-06 this cap is single-paragraph-only** — a multi-paragraph message is bounded by the per-boundary 400-char cap in the Shape guard instead, so a long status report that ENDS on a terse handoff still reaches the carve-out rather than being hard-rejected on total length.

2. **Shape** — single paragraph after `stripCodeFences` + `trim`, OR multi-paragraph with a clean trailing-wait paragraph. `isMultiParagraph(prose)` looks for a blank-line break (`\n[whitespace]*\n`). When the message is multi-paragraph, the **trailing-wait carve-out** (added 2026-05-26) extracts the final paragraph via `extractFinalParagraph(stripped)` and runs the **pattern match** against the final paragraph alone — admitting the message iff that final paragraph is ≤ 400 chars (`MAX_TRAILING_PARAGRAPH_LENGTH`) AND carries a high-confidence pattern hit. **Long-paragraph-tail carve-out (2026-06-04):** when that closing paragraph is _itself_ longer than 400 chars, its trailing whole sentence(s) up to 400 chars are scanned (via `extractTrailingSentences`) instead of dropping the whole paragraph — so a wait that _closes_ a long status paragraph (_"…stamp the SHA-bound tag. Holding for the build result."_) still suppresses, while a wait buried mid-paragraph does not. **Long-leading-paragraph-tail carve-out (2026-06-08):** the same tail-extraction is applied to the FIRST paragraph too — when a multi-paragraph turn's _opening_ paragraph is itself longer than 400 chars, its trailing sentence(s) up to 400 chars are scanned (the same `trailingCandidate` helper the closing paragraph uses) rather than dropping it wholesale, still gated by the leading-paragraph hand-off check. So a wait that _closes_ a long opening status paragraph (_"…40 voice tests green. I'll wait for the lint result; I'll be notified when it completes."_ — a 489-char opener) suppresses, while an opening status headline with no hand-off still flips to **Needs You** and a wait buried _early_ in the opening paragraph (wait-free ending) is not scanned. Read-only back-test over 14,450 live agent rows: **+6 net-new, 0 regressions, 0 false positives**. **Long-message forward-commit carve-out (2026-06-06):** when the WHOLE message is over the 2,000-char cap (the Length guard above no longer hard-rejects a multi-paragraph message), the final paragraph must ALSO be an explicit _handoff_ — `hasPollAvoidanceCommitment` OR a non-attention-grab `hasForwardCommitment`, the SAME gate the leading paragraph uses — not merely any high-confidence wait, so a long _answer_ ending on a broad-pattern phrase (_"…was primarily waiting for the index"_) does not suppress while _"I'll report back when the build lands"_ does; a `≤ 2,000`-char multi-paragraph message keeps the unconditional trailing carve-out. **Penultimate carve-out (2026-06-07):** the candidate set also includes the **second-to-last** paragraph (`extractPenultimateParagraph`) when it `commitsToHandoff` (poll-avoidance OR non-attention-grab forward commitment) AND is ≤ 400 chars AND the FULL message carries no `LEADING_ATTENTION_GRAB` alert — recovering the "wait, then trailing status-dashboard paragraph" shape (the new `will-resume-me-active` pattern is the forward commitment that admits it; the matcher under `findFirstMatch` stays `waiting-for-on-any`). Other interior paragraphs are still never scanned; the full-message attention-grab gate is stricter than the per-paragraph one because an alert can sit in a different paragraph than the penultimate. Read-only back-test 2026-06-07 (13,709 agent `summary_prose` rows): **+30 net-new / 0 regressions**, with 1 false positive found & fixed by that full-message gate. The **Target (user-directed) veto, however, runs against the FULL message, not the final paragraph** (veto-scope fix 2026-05-27): a wait directed at you can sit in an earlier paragraph (_"…a gated discard I won't run without your explicit OK."_) under a terse, generic close (_"Standing by."_) that matches a hold pattern, and scoping the veto to the final paragraph alone wrongly auto-waited exactly that shape. This carve-out is what handles the common shape _"&lt;summary paragraphs&gt; ... Waiting for X."_

3. **Target** — not directed at the user. `isUserDirectedWait(prose)` is a six-regex `OR`: it matches any wait/await/hold/blocked/until/once phrasing whose object is the user (`USER_DIRECTED_WAIT`), imperatives of the form _"Tell me when X"_ / _"let me know when X"_ / _"ping me when X"_ (`IMPERATIVE_TO_USER_WAIT` — also fires after hyphen list-leaders), explicit waits _from_ the user / operator / human (`WAIT_X_FROM_USER`), user-decision idioms like _"Your call"_ / _"your move"_ / _"your nod"_ / _"your sign-off"_ / _"your go-ahead"_ (`USER_DECISION_PHRASE`), mid-paragraph agent-asking-user questions like _"Want me to … or …?"_ / _"Should I … ?"_ (`AGENT_ASKING_USER` — a bandaid for the end-of-text `isTextAskingQuestion` detector's blind spot), and noun-object input-waits like _"awaiting your sign-off"_ (`WAIT_ON_USER_INPUT_NOUN`). Any one regex matching vetoes the suppression. A wait on a human action IS a real **Needs You** (it's exactly what the inbox is for). **Auto-notify exemption (2026-06-01):** before those six regexes run, the gate blanks any _declarative auto-notification_ clause whose _"when you …"_ names an **ambient availability action** — _"It'll auto-notify me when you open the window"_, _"I'll be notified when you log in"_, _"the watcher will ping me when you're back"_. The agent armed an automated re-wake (a watcher / hook will re-engage it once you become reachable), so that _"when you"_ is the trigger of automation, not a wait on a human decision — the inbox shouldn't nag you. It's fenced both ways so a real Needs You is never hidden: a **bare imperative** _"Notify me when you…"_ (you must ping the agent) still vetoes, and a **decision/input verb** _"once you approve / confirm / decide / reply"_ stays a real Needs You — only ambient _becomes-available_ actions are exempt. **Release-of-control exemption (2026-06-07):** the gate ALSO blanks a _release_ clause where the agent hands a FUTURE git action to you — _"merge stays your call"_, _"Nothing touches master until you merge"_ — while it keeps waiting on an external gate (_"I'll report the moment CI lands"_). The eventual merge being your call is not a wait on a human decision _now_, so the inbox shouldn't flip. Fenced the same way: the handed-off action must be a git/release verb (merge/push/deploy/land/ship/…, never approve/confirm/decide/review), Arm A needs the _"stays/remains your call"_ idiom (so _"it's your call now"_ still vetoes), Arm B needs the _"nothing … until you …"_ reassurance (so the BLOCK form _"I won't continue until you merge"_ still vetoes), only the matched span is blanked (a real ask beside it still vetoes), and — belt-and-suspenders — a high-confidence external-wait pattern must STILL match for the turn to suppress at all (`RELEASE_OF_CONTROL`; live screenshot 2026-06-07; read-only back-test **+1 net-new / 0 regressions** across ~57K agent rows). **Approve-a-list exemption (2026-06-07):** the gate ALSO blanks a _"nothing … until you approve … a list"_ reassurance — the agent promising it won't act destructively until you approve a SET it will present later (the worktree-cleanup screenshot _"nothing is touched until you approve a specific list"_, whose turn also said _"I'll be notified when it completes"_) — while it keeps waiting on a background job. That eventual approval is a future safety gate, not a wait on you _now_. The sibling release-of-control exemption deliberately leaves "approve" out of its verb list (because bare _"until you approve"_ is a real Needs You), so this gets its own tightly-fenced clause: it requires BOTH the _"nothing …"_ lead AND a list-noun after _"approve"_ (list/selection/plan/set/batch/item/candidate), so a bare _"until you approve"_, a no-_"nothing"_ wait (_"Waiting until you approve the list"_), or _"approve the deploy/this"_ still vetoes; only the matched span is blanked (a real ask like _"your call"_ beside it still vetoes); and — belt-and-suspenders — a high-confidence external-wait pattern must STILL match to suppress at all (`APPROVE_LIST_RELEASE`; live screenshot 2026-06-07; read-only back-test **+2 net-new / 0 regressions, 0 false positives** across ~58K agent rows).

The 2026-05-24 trigger was a long handoff summary the agent posted: _"Not done (waiting on your call): — Push branch / open PR — needs explicit approval — …"_. Before the guards, the `waiting-for-on-any` pattern matched `"waiting on your call"` and Gate 6 suppressed the inbox flip. With the guards, the same message is vetoed because the target guard fires on `"your call"` (now matched by `USER_DECISION_PHRASE`) — the user-directed phrasing is enough by itself, regardless of paragraph count. (At the time it also tripped the length guard at the old 500-char cap; after the 2026-05-26 raise to 2,000 it no longer does, and the trailing-wait carve-out means the shape guard alone is no longer sufficient — but the target guard still catches it cleanly, which is exactly why the target veto was _broadened_ from 3 regexes to 6 on 2026-05-26 rather than relaxed.)

The pattern regexes themselves were left untouched (the regex still matches _"Waiting on your pick"_ and _"awaiting your merge"_ — the veto lives at the gate, not in the regex), but the `match` arrays in `waiting-patterns.ts` no longer carry user-directed examples since the gate now vetoes them. Pattern unit tests cover regex behavior; gate tests cover composed behavior. If you see a suppression (an **⏱ Auto-waited** tag) where the matched phrase IS user-directed, that's a bug — file it with the session id, rule id, and matched substring.

#### Corpus validation

The pattern library was validated against the user's `mission-control.db` on 2026-05-23:

- **36,903** agent messages scanned (alive sessions only)
- **2,335** total pattern hits (2,170 high-confidence)
- **1,277** distinct sessions hit (1,193 high-confidence) — roughly **25.5%** of alive sessions historically hit at least one pattern
- Top pattern: `waiting-for-on-any` with 1,036 hits
- Live bug repro (session `bd64ec4e`, `"Three checks running in parallel — waiting for completion notifications"`): hits `waiting-for-on-any` on `"waiting for completion"` ✓

Follow-up delta-validation (2026-05-27) for the `stop-polling` + `reinvoke-me-automatically` additions: baseline-vs-current diff over 24,160 agent prose rows, baseline 908 → current 912 suppressed, **+4 net-new** (+2 each, 0 FP). Harness: [scripts/waiting-detector-backtest-stop-polling-reinvoke.mjs](/scripts/waiting-detector-backtest-stop-polling-reinvoke.mjs).

Follow-up delta-validation (2026-06-08) for the `will-continue-as-distributive-completes` addition (the bare-"as" distributive-completion resume, e.g. _"I'll continue as each reports"_ — a first-person future resume joined by a bare "as" to a job-completion clause, the connector `will-continue-when-once-after` excludes): baseline-vs-current diff over 14,459 agent prose rows, **+8 net-new / 0 FP** — every net-new row a genuine background-job wait, including the originating screenshot. Harness: [scripts/waiting-detector-backtest-as-each-completes.mjs](/scripts/waiting-detector-backtest-as-each-completes.mjs).

#### Settings + kill switches

- **Setting (Zod-validated)**: `AppSettings.waitingDetectorEnabled` (default `true` since 2026-05-27 — flipped on after the rejection-guard + pattern tuning work through 2026-05-26 closed the false-positive classes seen in live-DB back-tests; history: shipped `true` 2026-05-23 → `false` 2026-05-23 → `true` 2026-05-27) — defined in [src/shared/types.ts](/src/shared/types.ts) and [src/shared/ipc-schemas.ts](/src/shared/ipc-schemas.ts). UI toggle at Settings → Sessions, `data-setting-id="waiting-detector"`.
- **Env override**: `AMC_DISABLE_WAITING_DETECTOR=1` — read once at startup, short-circuits Gate 6.
- **Flag service**: [src/main/services/waiting/waiting-detector-flag.ts](/src/main/services/waiting/waiting-detector-flag.ts) — `isWaitingDetectorEnabled(settings)` combines both; `isWaitingDetectorModelEnabled(settings)` gates the Stage-2 model layer (below).
- **Stage-2 model setting**: `AppSettings.waitingDetectorModelEnabled` (default `false`, opt-in) — UI toggle at Settings → Sessions, `data-setting-id="waiting-detector-model"`; env override `AMC_DISABLE_WAITING_MODEL=1`. See **AI second opinion (Stage 2)**.
- **Match-mode setting (high-precision tier vs full corpus)**: `AppSettings.waitingDetectorCorpusEnabled` (**default `false`** since 2026-07-21 = the high-precision tier) — UI toggle at Settings → Sessions, `data-setting-id="waiting-detector-match-mode"` ("Recognize all waiting phrasings"); defined in [chat-ui-settings.ts](/src/shared/types/settings/chat-ui-settings.ts) + its Zod slice; read via `isWaitingDetectorCorpusEnabled(settings)` (`?? false`, NO env override — it's a precision/recall preference, not a rollback). At the default `false`, `classifyWait` runs the curated `HIGH_PRECISION_PATTERN_IDS` tier — the canonical sentinel PLUS its paraphrases and a ~99%-precision subset (`will-continue-when-once-after`, `waiting-on-ci-noun`, and the audit-clean families like `wont-poll` / `standing-by` / `ci-still-running`) — through the same length/shape/veto guards, quote-stripped, but NO broad pattern. At `true` it ALSO matches the full ~40-pattern corpus + the Stage-2 model near-miss path (higher recall, more misfires). The canonical-sentinel hard override, the stall-path honor, and the Plain Speak overlay-skip fire in both modes; the default flip needs no migration (`getSettings()` is raw, so `?? false` reaches every unset user). See [waiting-detector-contract.md](/.claude/memory/contracts/waiting-detector-contract.md) invariant **`match-mode-selects-the-tier-and-the-sentinel-always-holds`**.
- **Hide-repeated-auto-waits setting (render-layer)**: `AppSettings.hideAutoWaitedUnlessFinal` (default `false`, opt-in) — UI toggle at Settings → Sessions, `data-setting-id="hide-auto-waited-unless-final"`; defined in [chat-ui-settings.ts](/src/shared/types/settings/chat-ui-settings.ts) + its Zod slice. PURE render-layer collapse of the repeated ⏱ turns in [`TurnGroup`](/src/renderer/src/features/sessions/TurnGroup.tsx) (keyed off the already-lifted `turn.waitingDetectorMetadata`; gated `hideAutoWaitedUnlessFinal && nextTurnIsContinuation && !isInFlight`, keeping the NEWEST wait of EACH run visible, where only a message the USER owns (`isRealUserReply`) ends a run — another agent's message or a scheduled wake does not (2026-09-22) — a self-resume wait folds whole, a wait fused to a user message folds ONLY the agent reply and keeps the user's own bubble) — no detector/banner/lift change, NO `TURNS_CACHE_BUILDER_VERSION` bump. Read LIVE; no env override. See [waiting-detector-contract.md](/.claude/memory/contracts/waiting-detector-contract.md) "Collapsing repeated auto-waits".
- **Auto-wait period (Zod-validated, user-configurable)**: `AppSettings.autoWaitMinutes` (default `30`, range `5–240`) — the user-set cap that BOTH the prose-wait and the background-task timers arm. Defined in [chat-ui-settings.ts](/src/shared/types/settings/chat-ui-settings.ts) + its Zod update slice; UI toggle at Settings → Sessions, `data-setting-id="auto-wait-minutes"`. Every arm site reads it through the single safe chokepoint `resolveAutoWaitMinutes(raw)` in [`src/main/process/ndjson-handler-constants.ts`](/src/main/process/ndjson-handler-constants.ts) — falls back to `AUTO_WAIT_DEFAULT_MINUTES = 30` and clamps to `[5, 240]`, so a missing/non-finite value can never pass `NaN` to `setTimeout` (which would fire immediately). The resolved minutes is captured at arm time and reused for the banner text, so the displayed window always matches the armed timer.
- **Keep-waiting button duration (Zod-validated, user-configurable)**: `AppSettings.keepWaitingMinutes` (default `60`, range `5–240`) — the SEPARATE window the inbox "Keep waiting" button re-arms, distinct from `autoWaitMinutes`. Defined in [chat-ui-settings.ts](/src/shared/types/settings/chat-ui-settings.ts) + its Zod update slice; UI at Settings → Sessions, `data-setting-id="keep-waiting-minutes"`. Read through `resolveKeepWaitingMinutes(raw)` (a sibling wrapper over the shared `clampWaitMinutes`, floors to 60). Only `manualResetWaitTimer` + the button label use it; everything else stays on `autoWaitMinutes`.
- **Auto check-in (Zod-validated, opt-in, default off)**: `AppSettings.autoNudgeEnabled` (default `false`; UI label **"Auto check-in on waiting sessions"** — the persisted key, flag, `data-setting-id`, and env switch all stay `*auto-nudge*` for back-compat, only the visible label is "Auto check-in", à la the KMS/Nothari split) — when on, the auto-wait cap auto-nudges the agent (up to `AUTO_NUDGE_MAX_CONSECUTIVE = 3` times) before surfacing. UI at Settings → Sessions, `data-setting-id="auto-nudge"`; gate `isAutoNudgeEnabled(settings)` in [waiting-detector-flag.ts](/src/main/services/waiting/waiting-detector-flag.ts); env override `AMC_DISABLE_AUTO_NUDGE=1`.
- **Inbox auto-check-in (Zod-validated, opt-in, default off)**: `AppSettings.inboxAutoCheckInEnabled` (default `false`; UI label **"Also check in after it's in your inbox"**) + `AppSettings.inboxAutoCheckInMultiple` (default `2`, range `1–10`; UI label **"Check in after (multiple of the waiting period)"**) — the POST-surface mirror of auto-nudge: when on, a session that surfaced to `wait_timeout` and sat untouched in the inbox for `inboxAutoCheckInMultiple × autoWaitMinutes` minutes is auto-checked-in (resume-capable send via `host.sendInboxCheckIn` → `sessionService.sendResponse`), bounded by `AUTO_INBOX_CHECKIN_MAX = 3`. Defined in [chat-ui-settings.ts](/src/shared/types/settings/chat-ui-settings.ts) + its Zod update slice; UI at Settings → Sessions, `data-setting-id="inbox-auto-check-in"` / `inbox-auto-check-in-multiple`; gate `isAutoInboxCheckInEnabled(settings)` in [waiting-detector-flag.ts](/src/main/services/waiting/waiting-detector-flag.ts); the multiple resolves through `resolveInboxCheckInMultiple(raw)` ([ndjson-handler-constants.ts](/src/main/process/ndjson-handler-constants.ts), clamps `[1,10]`, NaN-defends the `setTimeout` math); env override `AMC_DISABLE_AUTO_INBOX_CHECKIN=1`. Distinct from `autoNudgeEnabled` (the pre-inbox auto-nudge). See contract invariant **I17**.
- **At the cap**: `surfaceWaitTimeout()` in [`ndjson-event-handlers.ts`](/src/main/process/ndjson-event-handlers.ts) (via the prose-wait cluster) flips the session to **Needs You** with the `wait_timeout` pendingAction. With auto-nudge OFF (default) it no longer re-injects a nudge prompt or re-arms the timer — that unbounded re-arm was the bug fixed 2026-06-09. With auto-nudge ON it instead sends the bounded check-in (≤3×) and holds, surfacing only once the budget is spent. (The same applies to the background-task cap path.) When `inboxAutoCheckInEnabled` is ON, AFTER the surface it also arms the post-surface inbox auto-check-in timer (see above).

#### Gate 5.5 — the `pendingSubagents` defense-in-depth

Sits just before Gate 6. When a `Task` or `Agent` tool dispatches a subagent, Omniscio records the entry in `session.pendingSubagents`. The matching `tool_result` event drains it. Per the contract invariant **I4**, the set should always be empty at turn-complete because `Task`-tool subagents are synchronous — they block the parent's turn until they return their result. If the set is non-empty at turn-complete, that signals an accounting bug (missed `tool_result`, drift), not a routine wait.

Gate 5.5 suppresses the flip when this happens: `pendingSubagents.size > 0` AND `pendingAction === null` → suppress, no timer (the stall watchdog is the escape hatch). Independent of `waitingDetectorEnabled` — this guards an accounting-bug class, not a prose-detection heuristic, so the kill switch doesn't disable it.

#### Idempotency + race protection

- **Wakeup-timer guard**: `if (work.wakeupTimer) return` at the top of the deferred-work block. Duplicate `result` events (Liveness Phase 1a) cannot re-arm a timer or re-emit the banner.
- **RACE-05 protection**: the cap-timer callback short-circuits if `current.spawnEpoch !== capturedEpoch` (session was rotated/respawned) or if `current.status !== 'running'` (status already changed by some other path).
- **Code-fence stripping**: `stripWaitingCodeFences(lastText)` runs before the pattern scan so a fenced code block containing the word `"waiting"` can't trigger the detector. Real waiting language outside the fence still matches.

## 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 3](waiting-detector-part-3.md) covers what the detector deliberately never does, the optional AI second opinion, and the first half of the technical reference.