Repository navigation
feat: live steer and truthful stop for sessions - #555
Conversation
Sessions-stability task_01 + task_02. Live steer: a second prompt during a running turn resolves the daemon default `session.busy_input.default_mode = "steer"`, injects guidance into the live turn when the runtime announces `_session/steering`, and reports `interrupt_fallback` truthfully when it cannot. Delivery is persisted (`steer_delivery`, migration 00101), the transcript records `prompt_steered` / `prompt_superseded` markers, and the web composer answers every busy send inline. Truthful stop: one escalation engine with turn and session scopes, cooperative cancel (`session.stop.cooperative_grace`) → forced stop → group kill, PID + start-time verification before any terminal state, a truthful `stopping` + `stop_verification_failed` attention when death cannot be verified, fresh-process rebind for killed turns, boot reconciliation with identity-checked orphan reap, post-stop markers for late events (waking the catalog so an open web transcript refreshes in place), actionable provider errors, and a guarded "Stopping…" web control that no longer remounts the chat runtime. Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Important Approval pendingCodeRabbit has no unresolved comments, but it has not reviewed the latest commit. Use the checkbox below to review the latest commit. CodeRabbit will approve the changes if it finds no blocking issues.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
…ed web sources Session-level notices (runtime recovery, stop attention, resume failure) now ride the transcript's inset rail with symmetric vertical spacing instead of running edge to edge flush against the scrolled transcript. CI lint follow-ups: `web/src/systems/session/index.ts` and `adapters/session-api.ts` exceeded the 500-line production budget, so the busy-input re-exports moved to `busy-input.ts`, the history/recap/usage/ ledger reads moved to `adapters/session-history-api.ts`, and the attachment surface is star-exported from its grouped module. The control-plane docs page is formatted with oxfmt (the pre-commit hook only formats `.md`). Co-Authored-By: Claude Fable 5.1 <[email protected]>
The full `internal/store/globaldb` package and the integration-tagged daemon lane only run on CI, and both still encoded the pre-ladder shape: - the raw `scanSessionInfo` fixtures and the sessions schema expectation gain the `stop_escalated` / `stop_verification_failed` columns; - the daemon integration fake driver proves exit through the driver seam (`VerifyExit`) and records a real, already-exited process identity instead of a placeholder PID the identity check could never verify; - the attention journey now asserts the accepted restart contract: a pre-restart permission is expired by boot reconciliation, leaves the open interaction list, clears attention, and a late answer is reported as already resolved instead of being delivered to a process that is gone. Co-Authored-By: Claude Fable 5.1 <[email protected]>
E2E-015 asserts "Stopping…" while the daemon has not yet confirmed the stop, but the acpmock `block_until_cancel` turn honors the cooperative cancel within milliseconds, so the guarded pill was gone before the assertions ran. The fault fixture gains a `hold ignoring cancel` rule (`hold_ignoring_cancel`, 30s) and the journey uses it: the pill now holds through the cooperative grace until the ladder escalates and the turn settles, which is the contract the assertions describe. The Playwright runtime fixture also copies the daemon's own log into the failed test's output directory, so CI artifacts explain daemon-side stalls instead of only the browser's view. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Every inactive session read reconciles the catalog projection with the session metadata. The comparison included sandbox fields the catalog never stores (the runtime root, additional dirs, and SSH access expiry), so a stopped local session always read as drifted: each read re-projected the row and published `session_catalog_changed`. A subscriber that re-reads on upserts — the web session window — then looped at request speed, holding the composer on "Stopping…" long after the daemon had stopped the session and returning 500s under the storm. The recovery projection now compares only the persisted sandbox fields, and the manager suite proves repeated reads of a settled session publish nothing. Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
@greptile-apps review |
|
@coderabbitai review |
|
|
| Filename | Overview |
|---|---|
| internal/session/stop_ladder.go | Implements the cooperative, forced, and process-group-kill escalation ladder with exit verification. |
| internal/session/stop_operation.go | Coordinates shared session-stop runs, preparation, joining, and retry behavior. |
| internal/session/manager_steer.go | Activates live steering, persists delivery disposition, and safely falls back when injection is unavailable. |
| internal/store/globaldb/schema/migrations/00101_schema.sql | Losslessly rebuilds the input queue to add constrained steer-delivery persistence. |
| internal/api/contract/prompt.go | Expands prompt and steer contracts with the full send-outcome envelope and optional strict turn fences. |
| web/src/hooks/routes/session-page-controls-store.ts | Adds request-fenced busy-input and stopping control state without remounting the session runtime. |
| docs/qa/scenarios/RT-018.md | Records the new provider-error journey but leaves it unverified without a qualifying documented blocker. |
| docs/qa/scenarios/RT-session-native-stop.md | Expands truthful-stop coverage requirements but leaves several required branches unverified without a qualifying documented blocker. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Busy session input] --> B{Resolved mode}
B -->|Queue| C[Persist queued input]
B -->|Steer| D{Runtime supports steering}
D -->|Yes| E[Inject into active turn]
D -->|Pending| F[Persist pending injection]
D -->|No or delivery failure| G[Interrupt fallback]
G --> H[Cancel active turn]
H --> I[Run replacement prompt]
J[Stop or prompt-cancel] --> K[Cooperative cancellation]
K -->|Not quiesced| L[Forced stop]
L -->|Still alive| M[Process-group kill]
M --> N{PID and start identity verified exited}
N -->|Yes| O[Publish terminal outcome]
N -->|No| P[Remain stopping with attention]
Reviews (1): Last reviewed commit: "fix: stop settled-session reads from rep..." | Re-trigger Greptile
The two stop/prompt scenarios that stay `blocked-verify` now name the qualifying blockers the tracker requires: the operator scoped this delivery to sessions-stability task_01 + task_02 and deferred the remaining walks to the program's QA tail, and the provider auth-lapse, Daytona remote-exit, and unverifiable-kill branches need external state the isolated lab cannot produce. Each deferred branch keeps green owning unit coverage. Also lets the query-key scope check bail out on a longer scope before comparing elements. Co-Authored-By: Claude Fable 5.1 <[email protected]>
|
Too many files changed for review (475 files, 100 file limit). Bypass the limit by tagging |
The stop-truth and busy-input work left six React functions above the complexity budget. Each keeps its behavior and test ids but reads from smaller parts now: - the composer controller derives its busy read model and Enter hint in pure helpers; - the action row renders the Enter hint and the busy cluster as their own components; - the page controls hook computes prompt flags and control availability in pure helpers; - the general settings page splits loading, load error, runtime facts, the advanced fold, and the apply-records sheet into components; - the session window's notice slot is its own component; - the runtime notice dispatches to per-kind components. The provider-error notice's CLI hint constant is renamed so the secret heuristic no longer matches a public command string. Co-Authored-By: Claude Fable 5.1 <[email protected]>
What & why
Steering a working session now redirects the live turn instead of killing it, and Stop is a contract you can believe.
Live steer (sessions-stability task_01). A second
compozy session prompt(or Enter in the web composer) during a running turn resolves the daemon defaultsession.busy_input.default_mode = "steer"and returns the fullSendOutcomeenvelope (disposition,steer_delivery,turn_id,entry_id,message_id,idempotency_key,queue_position,replayed). When the runtime announces_session/steering(Claude Code does) the guidance is injected into the live turn with no new turn boundary; when it cannot, the same send reportsinterrupt_fallbackand the replacement runs. Delivery is persisted (steer_deliverycolumn, global migration 00101), the transcript recordsprompt_steered/prompt_supersededmarkers,session statusandcompozy__session_describeanswerbusy_input.steer_capabilitybefore any send,compozy__session_promptdefaults towait: false, and Settings exposes Follow-up behavior (daemon-owned). The composer answers every busy send inline and restores the draft on refusals.Truthful stop and boot truth (task_02). One escalation engine with two scopes (
CancelTurn/AwaitTurnQuiesced,RequestStop/AwaitStopped): cooperative cancel (session.stop.cooperative_grace, default 10s) → forced stop (5s) → group kill, with PID + start-time verification before any terminal state. An unverifiable kill leaves a truthfulstopping+stop_verification_failedattention instead of a fabricatedstopped. Turn-scope cancels keep the session promptable and rebind a fresh process when escalation had to kill one (this also upgrades the steer fallback to the full ladder). Boot reconciliation projects crash classification into the catalog, reaps orphans only on PID + start-time identity, expires pre-crash pending decisions, and late events after a terminal state are discarded with a post-stop marker (which now wakes the catalog so an open transcript refreshes in place). Provider rate-limit / auth failures surface as actionableprovider_errorevents with occurrence context, permission timeouts attributeresolved_by: "timeout", and the web Stop control flips to a guarded "Stopping…" and no longer remounts the chat runtime.Also in this PR:
store.SessionMetaregained headroom under thehugeParamlint by groupingRuntimeFailure+RuntimeSelectioninto the optional*SessionRuntimeBindingStateembed (flat JSON unchanged), and the acpmock driver gained ahold_ignoring_cancelcontrol so labs and tests can exercise the escalation ladder against an agent that ignores cancel.Spec:
.compozy/tasks/sessions-stability(_spec.md, ADR-001/002/004/010). Tasks 03–10 of that program are intentionally not part of this PR.How you verified it
make gateis delegated to this PR's CI by decision (the local run was cut short after thecodegen-checkandgo-lintlanes passed). Locally:make go-lintclean,make codegen-checkpass,go test -race -count=1 -parallel 4green forinternal/session(full package, 142s),internal/extension,internal/observe,internal/api/{core,httpapi,udsapi},internal/daemon,internal/cli, and root Turborepotest+typecheckfor./web.internal/sessionrun (a panic in one test had been masking them): the stop terminal event and the ladder's escalation record now share one stop turn id (history no longer grows an extra turn per stop), an accepted logical session with no subprocess survives a daemon restart untouched instead of being classified as an unverifiable process, the rewind stop-tail check ignores the ladder's own escalation record, and the session test harness keeps turn ids unique across manager restarts.steer_delivery: injected,transcript_marker.prompt_steeredinside the same turn, the agent acknowledged the guidance (STEER_ACK_7731) on the same turn id withdone end_turn. Codex 0.153.2 and Cursor 2026.09.02 do not announce steering on their current ACP builds and fall back truthfully.eng-qa-bootstrap, freshCOMPOZY_HOME, Vite proxy to the lab daemon, torn down clean):session stop --wait -o json→{state: stopped, verified: true, escalated: true, phase: forced};session prompt-cancel→canceled+ session stays promptable, idle repeat exits 66; cancel-ignoring turn → web "Stopping…" through the 10s grace →session.stop_escalated(scope turn, phase forced, elapsed 10000ms) → idle;kill -9the daemon mid-turn → restart →stopped/agent_crashed, no phantom, no orphan. Report:docs/qa/reports/2026-09-05-sessions-stability-task01-02.md; scenariosRT-session-live-steer(new, pass),RT-session-prompt-cancel(pass),RT-018andRT-session-native-stop(blocked-verify for the branches not walked: provider auth-lapse simulation, native-tool/UDS parity, unverifiable-kill, Daytona).validate-visual-contract.mjs(0 blocking divergences).internal/session,internal/acp,internal/store/globaldb,internal/api/core,internal/cli,internal/daemon,internal/config,internal/testutil/acpmock,web/src/**/__tests__).Screenshots
Live Claude Code turn: Stop primary,
⏎ steer · ⌘⏎ queuehint, Queue / Steer / Interrupt actions.Enter during that turn: "Steering — delivered into the live turn
injected" — no cancel, same turn.Transcript after a CLI steer on Claude Code: the agent course-corrected mid-flight and answered the steer acknowledgement inside the same turn.
No-capability agent: "Interrupted and replaced — this agent can't take guidance mid-turn
interrupt_fallback", replacement already answered.Opposite modifier (⌘⏎): "Queued #1 — runs after the current turn" with the queue strip row.
Stop on a cancel-ignoring turn: the guarded "Stopping…" pill holds through the cooperative grace while the ladder escalates.
After the ladder verified the kill: the composer returns to idle and the session stays promptable (turn scope).
Settings → Sessions: daemon-owned Follow-up behavior (Steer immediately / Queue until the turn ends).
Truthful unverified stop:
stopping+stop_verification_failedattention with a retry, never a fabricatedstopped.Impact
compozy session prompt(default busy modesteer,--expected-turnstrict fence,--expected-turn-iddeprecated alias with warning, full envelope in-o json),compozy session stop [--wait](truthfulverified/escalated/phase/attention),compozy session prompt-cancel(turn scope; exit 66 when nothing is in flight),compozy session status(busy_input.default_mode/steer_capability).POST …/sessions/{id}/promptand…/steerreturn theSendOutcomeenvelope (steer_delivery,turn_id,entry_id,queue_position,replayed;active_turn_mismatchcarriescurrent_turn_id);POST …/stopacceptswaitand returns the truthful stop outcome; session resources carryescalated,verified,busy_input; new canonical eventssession.stop_escalated,session.stop_verification_failed,provider_error, and theprompt_steered/prompt_superseded/ post-stop transcript markers. Legacydelivery/status/queue_entry_id/estimated_send_atfields and the legacyinterruptdefault stay readable through the v0.4 compatibility window (SD-013) and are scheduled for removal in v0.5.0.compozy__session_prompt(waitdefaults tofalse; envelope),compozy__session_describe(busy_input.steer_delivery),compozy__session_stop/compozy__session_prompt_cancel(truthful envelopes). Descriptors and digests regenerated.session.busy_input.default_modenow defaults to"steer"and validatessteer|queue(legacyinterruptaccepted with a warning until v0.5.0); newproviders.<name>.steer_capability; newsession.stop.cooperative_grace("10s").session_input_queue.steer_delivery), 00102/00103 (stop_escalated,stop_verification_failed), all lossless.sessions/control-plane.mdx, updatedsessions/lifecycle.mdx, CLIsession prompt/stop/input steerpages,configuration/config-toml.mdx, providers page; official skillskills/compozy/references/runtime-operations.md.github.com/compozy/acp-go-sdkbumped to the async-request fork build (SendRequestAsync, Add asynchronous request submission with ordered completion acp-go-sdk#1).make gatepasses locally — not run to completion locally by decision (see above); this PR is delivered only after its required CI checks are green