From e8cffa7a42c064257eb1d84f1eead90e859ef00c Mon Sep 17 00:00:00 2001 From: Mukul Sharma Date: Wed, 7 Oct 2026 12:37:21 -0700 Subject: [PATCH] fix(observer): B5 must not fire when last assistant text is a question MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two clarifications to stop the repeated-nudge loop on needs_user workspaces: 1. B1 re-fires after a stall nudge: a workspace that responds to a nudge with a question or "your call" close is in B1 state. Do not re-classify as stranded on the next idle cycle. 2. B5's mid-narrative condition now has an explicit bullet: a turn ending with a question or handoff is not mid-narrative — B5 does not fire for it. 3. Cheat-sheet: add a row for the previously uncovered case (idle > 1800s + last assistant text is a question → needs_user, not stranded). Root cause: workspace 17 received four stall nudges today. Each time it responded "nothing stalled, your call: alert-only or pause?" and the Observer correctly emitted needs_user. But on the next idle cycle (30+ min) the LLM re-classified the same question text as mid-narrative and fired stranded again. The NO_INGEST_GUARD stopped a fifth nudge (delta=0), but four was already too many. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- prompts/observer-batch-prompt.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/prompts/observer-batch-prompt.md b/prompts/observer-batch-prompt.md index 2a1711a..3305fd1 100644 --- a/prompts/observer-batch-prompt.md +++ b/prompts/observer-batch-prompt.md @@ -163,6 +163,8 @@ Use `pr_data`. If it is null, continue to section B. Do not infer an open PR fro Idle time does NOT gate this verdict — a recap that ends with "want me to…?" is awaiting you whether it landed 10 seconds or 10 hours ago. Do NOT downgrade a fresh awaiting-review recap to `active` just because `last_turn_age_sec` is small; the agent has stopped and will not move without you. + **B1 re-fires after a stall nudge**: a workspace that responds to a stall nudge with a question or "your call" close is in B1 state. Do NOT re-classify as `stranded` on the next idle cycle. Emit `needs_user`. + **B1 vs B2 (cleanup vs awaiting-review) — the dividing line:** cleanup is for work that is *finished and disposable* — the deliverable was an action already taken (probe ran, PR merged, test executed) and nothing is left to look at. Awaiting-review is for work whose *deliverable is a thing you must still consume or authorize*. When a recap says "done" AND names something for you to read/decide/approve/activate/merge, it is B2 (`needs_user`), not B1. When in doubt between cleanup and awaiting-review, choose `needs_user` — an extra card is cheap; a wrongly-sent `/cleanup` destroys the deliverable. 2. **Definitive workspace-level recap + idle >30 min + clean cwd + nothing left for the user** → `ready_for_cleanup`. ALL must hold: @@ -185,6 +187,7 @@ Use `pr_data`. If it is null, continue to section B. Do not infer an open PR fro - `last_turn_age_sec > 1800` (strictly greater than 30 minutes). - `agent_status == "idle"`. - Last assistant text is **mid-narrative** — it trailed off inside a step ("now running…", "checking X", "moving to spec 5") with no handoff to the user, NOT a recap and NOT a question. + - A turn ending with "your call", "which would you prefer", "let me know", or any question is a handoff — B5 does NOT fire for it. If all four hold → `stranded` with `nudge_text` grounded in the transcript. Otherwise → `active`. @@ -201,6 +204,7 @@ Use `pr_data`. If it is null, continue to section B. Do not infer an open PR fro | recap hands back a deliverable / decision / go-ahead (any idle time) | `needs_user` (B1 — awaiting your review) | | idle ≤ 1800s AND not an awaiting-review recap | `active` (between turns) | | idle > 1800s + mid-narrative (trailed off, no handoff) | `stranded` | +| idle > 1800s + last assistant text is a question or "your call" | `needs_user` (NOT `stranded`) | | idle > 1800s + recap + clean cwd + nothing left for the user | `ready_for_cleanup` | | idle > 1800s + bare question | `needs_user` | | cleanup already ran | `no_action` (wins over `ready_for_cleanup`) |