feat: truly-idle alerting — server-authoritative terminal.idle bell/shade + persistent green#524
Merged
Merged
Conversation
Pinned contract shared with the Rust server port:
{ terminalId, at (server epoch ms), reason: 'grace' | 'queue-empty' }.
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ne-shot grace Provider-agnostic emitter fed by each activity tracker's 'changed' + 'turn.complete' streams. Turn boundary landing idle arms a one-shot 2s grace timer (cancelled by any busy flip); a boundary while still busy is a queued turn (hold the bell, reason queue-empty on drain); deadman / signal-loss idle flips and PTY exit/crash removals never emit. Zero polling: one-shot unref'd timers only. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…kers wireTrulyIdleEmitter on claude/codex/amplifier/opencode trackers -> wsHandler.broadcastTerminalIdle. Wirings disposed on shutdown. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…dle edge Dedicated at-monotonic dedupe namespace (lastIdleAtByTerminalId) so idle edges can never poison (or be poisoned by) turn-complete baselines; reset alongside the completion baselines on a real server restart. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…rmational - App.tsx folds terminal.idle in via applyServerIdle; the terminal.turn.complete handler and the latestTurnCompletions replay loops are removed (they minted false bells on subagent finishes and mid-queue turn ends). - All four terminal CLI modes (claude/codex/opencode/amplifier) are now server-authoritative for turn completion: the client BEL path no longer mints completions for them (custom CLI modes keep it). - completionSeq dedupe machinery removed from the slice/persistence — the idle edge dedupes by monotonic at; fresh-agent edges keep the at regime. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
resolvePaneIdleGreen: green when the pane's CLI session is known (activity record, bound sessionRef, or resume id) and the pane is not busy. Replaces the one-shot needs-attention green for claude/codex/opencode/amplifier panes; fresh-agent panes keep the attention-based green. Independent of tab shading and its click-clearing. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…fixme) Fake CLAUDE_CMD CLI drives the full pipeline with zero Redux shortcuts: PTY submit -> blue; BEL + quiet 2s grace -> terminal.idle -> persistent green header + tab shade + exactly one alert edge; activating the tab clears the shade while green persists. Added to MATRIX_SPECS; the rust leg is test.fixme pending the rust terminal.idle emitter (feat/rust-terminal-activity-idle). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ve CLIs 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
danshapiro
pushed a commit
that referenced
this pull request
Jul 24, 2026
Conflict resolutions (both sides' behavior preserved everywhere): - crates/freshell-ws/src/lib.rs, terminal.rs, opencode_association.rs + 12 tests/*.rs WsState constructors: kept BOTH this branch's `activity: None` field and #523's new `session_existence` probe field (trivial adjacent-field conflicts). #523's brand-new tests/pane_reconcile.rs gained `activity: None` too. - crates/freshell-terminal/src/registry.rs: kept BOTH the TERM-15/16 activity tap (ActivityEvent/ActivityObserver, tap field + init, the Created-tap fire in create()) and #523's reconciliation additions (respawn liveness/cap consts + fields, keyed_create_inflight, the §5.4 duplicate-live-PTY backstop — ordered before the Created tap). create() gained #523's create_request_id param; this branch's two activity-observer tests updated to pass None for it. - crates/freshell-server/src/main.rs: kept #523's session_index block first (ws_state.session_existence references it), then this branch's activity-hub construction + registry observer install. - docs checklist + playwright.config.ts auto-merged (both sides' notes and MATRIX/rust-only spec entries retained). Cross-track integration (#524 truly-idle client): the Rust terminal.idle emission matches shared/ws-protocol.ts's new TerminalIdleSchema byte-for-byte ({type, terminalId, at, reason: 'grace'|'queue-empty'}), so the rust leg of test/e2e-browser/specs/truly-idle-alerting.spec.ts is now ENABLED (test.fixme removed) — the full production pipeline (PTY submit -> busy blue -> BEL -> idle + turn.complete -> 2s grace -> terminal.idle -> one alert edge + tab shade) passes against the real Rust server. Verification: cargo test --workspace 1656 passed / 0 failed; clippy clean on touched files; rust-chromium PW (3 activity + truly-idle rust leg) green 2x (4 passed 1.1m, 4 passed 42.5s); coordinated npm test full-suite success exit=0 (3982 + 4547 + 350 passed; two earlier runs each hit one unrelated pre-existing flake — DOM-identity, sidebar-filter, EADDRINUSE:40301 — each passes in isolation). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.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.
Truly-idle alerting semantics (Track 4)
The bell used to ring on every
terminal.turn.complete— including subagent finishes and turn ends with another prompt already queued. This PR makes the bell/shade edge for terminal-mode CLI panes (claude/codex/opencode/amplifier) a server-authoritative truly-idle edge, and makes green a persistent idle state. Fresh-agent (fresh*) panes are untouched.Pinned wire contract (shared with the Rust track)
terminal.turn.complete: still emitted at turn boundaries; the client no longer rings/shades on it for terminal CLI panes.terminal.idle:{ terminalId, at (server epoch ms), reason: 'grace' | 'queue-empty' }— once per busy→truly-idle transition, after a 2000ms grace (TERMINAL_IDLE_GRACE_MS) with no new session-file activity AND no queued prompt; never after crash/interrupt/exit; subagent completions mid-turn never produce it.Server (legacy Node)
server/coding-cli/truly-idle-emitter.ts: provider-agnostic per-terminal state machine fed by each tracker's existingchanged+turn.completestreams. One-shot unref'd grace timers armed only at turn boundaries — zero polling, no per-pane intervals (chokidar untouched; the amplifier events tailer is reused, not rewritten — no new file watching was needed at all: the queued/subagent knowledge already lives in the tracker state machines).server/index.ts→wsHandler.broadcastTerminalIdle(disposed on shutdown).Client
resolvePaneIdleGreen): shown when the pane's CLI session is known (activity record, boundsessionRef, or resume id) and not busy. Replaces one-shot needs-attention green for the four CLI panes; fresh-agent panes keep the attention green.terminal.idleedge; deduped per terminal by monotonicatin its own namespace (lastIdleAtByTerminalId), reset on real server restart. Never on turn.complete; never on crash/exit (TERM-18 preserved — trackers remove state on exit and the emitter cancels + never emits).dismissTabGreen; 'type' mode untouched).terminal.turn.completebell path,latestTurnCompletionsreplay-to-green loops,completionSeqdedupe machinery, and client BEL minting for all four CLI modes (isServerAuthoritativeTurnCompleteModenow covers opencode + amplifier; custom CLI modes keep the BEL path).Legacy-health findings per CLI (Task 1)
pending, completion only when landing idle (unit suite green). The user-reported "codex/amplifier never turn blue" was NOT reproducible against the legacy trackers' unit/ws suites; consistent with it being the Rust server's gap (owned by another track).opencode-activity-tracker.ts, SSE ownership reducer, busy-only records + turn.complete) — healthy.Queued/subagent detection per CLI (Task 2)
reason: 'queue-empty'. Subagent stops don't fire the Stop hook; stray BELs are filtered (countTrackerTurnCompleteSignals) and BELs while idle are ignored.queuedSubmitAtre-arms busy→pendingat turn clear (emitter treats it as queue evidence) and only records a completion when landing idle — queue drained by construction.prompt:submit/prompt:completerecords via the existing incremental tailer; a queued prompt starts a newprompt:submitinside the grace window → cancels. Subagent files are rejected by the locator/reducer (session:fork,parent_id) and never feed the tracker.Suppression cases proven (test names)
Emitter (
test/unit/server/coding-cli/truly-idle-emitter.test.ts):suppresses the bell between back-to-back turns and emits once at the very endholds the bell while a queued prompt keeps the terminal busy, then emits queue-empty after the queue drains,treats a codex busy->pending re-arm as queue evidencenever emits after a crash/exit (activity remove), even with a grace timer armed(+ ws-leveldoes not broadcast terminal.idle when the terminal exits inside the grace windowintest/server/ws-terminal-idle.test.ts)never emits on a deadman/signal-loss idle flip…,never emits on a codex busy->unknown deadman flipClient:
never bells or shades on terminal.turn.complete (informational only for terminal CLI panes)(test/e2e/turn-complete-notification-flow.test.tsx),ignores latestTurnCompletions in activity list responses…(App.ws-bootstrap.test.tsx)rings exactly once when a reconnect replays the same terminal.idle edge; slice-leveldedupes idle edges per terminal by monotonic at…bells and highlights on background completion, clears on tab click (default click mode),click mode: clicking the already-active tab clears attention,type mode: attention persists after tab switch, clears on terminal inputshows persistent idle green on a claude pane header whenever the session is known and not busy(pane-activity-indicator-flow.test.tsx) +resolvePaneIdleGreenunit matrix (pane-activity.test.ts)Playwright (legacy-chromium, passing 2x)
test/e2e-browser/specs/truly-idle-alerting.spec.ts— fakeCLAUDE_CMDCLI drives the full production pipeline (no Redux shortcuts): PTY submit → blue; BEL + quiet 2s grace →terminal.idle→ persistent green header + tab shade + exactly one alert edge (stable across a settle window); activating the tab clears the shade while green persists. Added toMATRIX_SPECS; the rust leg istest.fixmeannotated "pending rust terminal.idle emitter — feat/rust-terminal-activity-idle" so it flips on trivially.Verification
npm run lint0 errors (10 pre-existing warnings in untouched files).npm test: client 3982 passed, server 4547 passed, electron 350 passed — all green.Deferred
terminal.idleemitter + flipping the PW rust leg on (owned by feat/rust-terminal-activity-idle).docs/index.html: untouched — the mock has no attention/indicator visuals to update; the default visual experience is unchanged.🤖 Generated with Amplifier