Clear "waiting on you" badges once you have read the task - #8
Open
Frailrain wants to merge 1 commit into
Open
Conversation
A fleet of settled awaiting tasks keeps yelling on every project badge and the titlebar pill until each gets a reply — opening and reading a task doesn't clear it. Add read-tracking: a task counts as viewed once its transcript stream has delivered the snapshot with the tab visible for ~2 seconds; the "waiting on you" surfaces clear once viewed and re-arm only if fresh agent activity postdates that view. A parked ask (running=1 + awaiting_input=1) stays lit regardless — reading a question isn't answering it. Schema: tasks.last_viewed_at INTEGER NOT NULL DEFAULT 0 with an idempotent ALTER; existing rows backfill to 0 so they show as awaiting on the next agent activity, which is the right behavior on upgrade. Predicate: one shared NEEDS_YOU now reads suggested = 0 AND status = 'in_progress' AND awaiting_input = 1 AND (running = 1 OR MAX(messages.created_at) for assistant/tool/system > last_viewed_at). Endpoint: POST /api/tasks/[id]/view stamps last_viewed_at and publishes task_updated so /api/events broadcasts the fresh derived awaiting_input + project awaiting_count to every open tab; no polling. Client: useTaskStream owns a 2s timer armed by snapshot delivery and by subsequent assistant/tool/turn_end events, cancelled by visibility drop or stream close. The needsYou.test.ts matrix is extended in the same commit as the predicate so the parked-ask-viewed case and settled-viewed-clears case are pinned together with the change that introduced them. Signed-off-by: Matt Hersee <Matthersee@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
"Waiting on you" badges clear on read
The story
Right now the badge is pure turn state:
tasks.awaiting_input, no memory of whether the user has actually seen the task. Open a settled task, read it, close it — the badge stays lit until you reply. A fleet of finished sessions yells across every project badge and the titlebar pill in parallel, one dot per task, until each gets a message it doesn't need. It scales badly with how you actually use Operator.This PR adds read-tracking. Opening a task with the transcript on screen for ~2 seconds stamps a
last_viewed_at; the "waiting on you" surfaces clear once viewed and re-arm only when fresh agent activity postdates that stamp. The finished fleet stops nagging as you scroll through it. A parked ask still counts (see below).Design decisions worth reviewing explicitly
awaiting_inputon the wire changes meaningThis is a deliberate reshape of an API field — please don't discover it in the diff.
Before: everything (server + client) read
awaiting_inputstraight from the DB column, which is the raw agent-state flag ("the agent's turn ended and it's your move").After: the DB column is unchanged — internal writers (runner, ask_user tool, status PATCH, /clear,
/api/instance/idletelemetry,getInstanceUsagerollup) still read and write the raw value. But every user-facing surface projects a derived value in that same field (listProjects.awaiting_count,listTasks.awaiting_input,listAllTasksLite.awaiting_input,GET /api/tasks/[id],GET /api/eventspayloads,countAwaiting,listNeedsYou). The derived value is the sharedNEEDS_YOUpredicate applied per row.Why same field name, not a new one? Because
format.ts isAwaiting(t)and every client-side row-mutation site already readt.awaiting_inputand interpret it as "should this task show as waiting on you?" — which is what the derived value now answers correctly. Adding a parallelderived_awaitingfield would leave the raw one on the wire as a footgun. Reviewers should confirm each writer keeps the raw semantics and each reader wants the derived one; the per-site map is in the source comments where each query lives.A parked ask is exempt from view-clearing
The predicate is
NEEDS_YOU AND (running = 1 OR last_agent_activity > last_viewed_at). Therunning = 1short-circuit means an unanswered AskUserQuestion (which parks mid-turn withrunning=1 + awaiting_input=1) always counts, even after view. Reading a question isn't answering it — the badge stays until the ask is resolved byask_answered(or the turn ends). Only settled turns (running=0 + awaiting_input=1) clear on read.This also preserves the fix from
0001fe4(July 2026) that removed therunning=0filter after a launch-feedback user hit a phantom "N need you" pill. That behavior is pinned by the extended matrix inneedsYou.test.ts.last_agent_activityis a subquery, not a columnlast_agent_activityis derived per row asMAX(messages.created_at)whererole IN ('assistant','tool','system'). This is the same shapelistNeedsYou'swaiting_sincehas always used, and it hitsidx_messages_task.Honesty note: I did not run measurements against any hot path. The subquery is theoretically fine (indexed by task_id, small message-per-task cardinality in practice), but if
listProjectsorlistTasksshows up on a profile after this ships, the escape hatch is to add atasks.last_agent_activity_at INTEGERcolumn, stamped by the runner alongside every persisted assistant/tool/system message, and replace the subquery inNEEDS_YOU's definition in one place (the constant is deliberately factored intoLAST_AGENT_ACTIVITY_SUBso this swap is one edit). Not doing it preemptively per the brief.What "viewed" means, precisely
A task counts as viewed when its transcript stream has delivered the
snapshotevent, while the tab is visible, sustained for ~2 seconds. Cancelled by visibility drop or stream close. Fired byuseTaskStream.ts— which is where selection, tab visibility, and deep-link navigation already converge — viaPOST /api/tasks/[id]/view, a simple stamp that publishestask_updatedso/api/eventsbroadcasts the fresh derived value to every tab without polling.The 2-second gate handles the two obvious foot-shooters: a tab parked-in-the-background whose SSE snapshotted anyway, and a short-outcome task that got clicked to bounce off before actually being read. The timer rearms on subsequent
assistant | tool | turn_endevents so a live turn ending on your screen also clears once you've read it.Migration
tasks.last_viewed_at INTEGER NOT NULL DEFAULT 0, added inlib/db.ts migrate()via the existing idempotent-ALTER pattern. Existing rows backfill to 0 = never viewed, which is the correct upgrade behavior: they show as awaiting on the next agent activity, which is exactly what an upgrader who was mid-workflow expects.Test plan
tests/needsYou.test.ts— extended in the same commit as the predicate change:POST /api/tasks/[id]/view: stamps, flips derived, publishestask_updated; 404 on unknown; parked ask stays lit after view.migrate()is a no-op) + backfill correctness.npm test→ 278 passing across 37 files.