feat(session): derive agent phase server-side and emit it as an additive field - #548
Draft
RonenMars wants to merge 2 commits into
Draft
feat(session): derive agent phase server-side and emit it as an additive field#548RonenMars wants to merge 2 commits into
RonenMars wants to merge 2 commits into
Conversation
…ive field Adds `subStatus`, the agent's phase within a running turn, scraped from the rendered PTY screen alongside the detectors that already share that read. Codex only for now — its status bar is binary and needs no footer archaeology — while the Claude branch returns null until its marker grammar is re-verified against a capture, because a guessed phase is worse than none. The field is always serialised and null when there is no phase. Absence must never carry meaning: clients merge session frames, a merge cannot express a removed key, and an omitted field would keep its previous value and latch the indicator on a finished turn. For the same reason the phase is cleared server-side in markReady rather than left for consumers to age out, and the mapper emits it unconditionally rather than through the `!= null` guard the neighbouring optional fields use. The frame is a scoped `session_phase` message rather than a session copy, because managedToResponse recomputes elapsedMs on every call and a copy would differ on every tick regardless of the phase. Bumps the pty-host protocol to 3 — the detector runs in the host, so without the event the feature silently no-ops when that flag is on. Refs #538
… tests Replaces the `!` assertions with a `responseFor` helper that throws on a missing session, so a broken store fails with a named error rather than a TypeError on a null property. Verified the tests still discriminate afterwards: moving `subStatus` into managedToResponse's `!= null` guard block fails two, and deleting the `phase-change` relay case fails two. Refs #538
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.
Server-side agent sub-status, Codex half. Replaces the client-side approach abandoned in tb-mobile #647.
Draft: four verification gaps remain, tracked in #541. Opened for visibility and early review of the contract rather than to merge as-is.
What this adds
subStatus— the agent's phase within a running turn (thinking | streaming | hooks | acting | working) — scraped from the rendered PTY screen alongside the detectors that already share that read, and emitted as an additive field plus a scopedsession_phaseWS frame.Codex only for now. Its status bar is binary (
Ready/Working) and needs no footer archaeology, so it validates the whole transport end to end. The Claude branch ofparseAgentPhasedeliberately returnsnulluntil its marker grammar is re-verified against a tool-heavy capture — reporting no phase is the correct pre-feature behaviour, and a guessed phase is worse than none.Three decisions worth reviewing
The field is never optional, and is
nullwhen there is no phase. Clients merge session frames ({...prev, ...next}), and a merge cannot express a removed key — so an omitted field keeps its previous value and the indicator latches on a finished turn. That is precisely the bug #647 shipped. For the same reasonmanagedToResponseemits it unconditionally rather than through the...(x != null && { x })guard the neighbouring optional fields use:!=catchesnullas well asundefinedand would convert an explicit clear back into absence. There is a comment on it saying not to tidy it into that block.The phase is cleared server-side, in
markReady. The exit edge is not an output event, so it cannot be read off the screen —pty-manager.ts:1103-1108documents that a mid-conversation return to idle can go undetected forever when a differential repaint omits the marker. Leaving liveness to consumers is how #647 failed.markReadyis the single idempotentrunning → waiting_inputtransition and is driven by the waiting-for-input OSC, which arrives even when no further chunk will.The frame is a minimal
session_phasemessage, not aSessionResponsecopy, and is scoped to that session's subscribers.managedToResponserecomputeselapsedMsfromnew Date()on every call for a live session, so a session copy would differ on every tick regardless of the phase — and a merging client would get a fresh object identity several times a second for a whole turn. The scoped path (ws-hub.ts:74) exists for exactly this message class; its existing tenants are also bespoke frames.Also rejected, and recorded so it is not rediscovered as a saving: riding the existing
onStatusChange, which needs no new plumbing but routes a cosmetic field throughrecordStatus, whose update statement has no same-status guard.Verification
tsc --noEmitclean, biome clean, 21 tests across three files.The contract tests were checked against deliberately-broken variants rather than trusted green, because every failure mode here is silent — nothing throws and nothing fails a typecheck:
subStatusintomanagedToResponse's!= nullguard blockphase-changecase in the pty-host relayThe second matters because declaring the event in the union type is not the same as wiring it — the type alone typechecks and silently does nothing when
THREADBASE_FEATURE_PTY_HOST=1.What is missing — #541
__tests__/session-status-line.test.ts:54detectLivePromptsis swallowed into awarnat both call sitessetPhase/markReadyunit testsCompatibility
docs/compatibility/tb-mobile.mdupdated per its own header requirement. BumpsPTY_HOST_PROTOCOL_VERSIONto 3 — a private streamer↔host change with no mobile-visible effect, but required, because the detector runs in the host.Refs #538. Plan: #537 →
docs/plans/2026-08-12-agent-sub-status-server-side.md.