Skip to content

feat: live steer and truthful stop for sessions - #555

Merged
pedronauck merged 7 commits into
mainfrom
session-improvs
Sep 5, 2026
Merged

pedronauck merged 7 commits into
mainfrom
session-improvs

Conversation

@pedronauck

Copy link
Copy Markdown
Member

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 default session.busy_input.default_mode = "steer" and returns the full SendOutcome envelope (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 reports interrupt_fallback and the replacement runs. Delivery is persisted (steer_delivery column, global migration 00101), the transcript records prompt_steered / prompt_superseded markers, session status and compozy__session_describe answer busy_input.steer_capability before any send, compozy__session_prompt defaults to wait: 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 truthful stopping + stop_verification_failed attention instead of a fabricated stopped. 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 actionable provider_error events with occurrence context, permission timeouts attribute resolved_by: "timeout", and the web Stop control flips to a guarded "Stopping…" and no longer remounts the chat runtime.

Also in this PR: store.SessionMeta regained headroom under the hugeParam lint by grouping RuntimeFailure + RuntimeSelection into the optional *SessionRuntimeBindingState embed (flat JSON unchanged), and the acpmock driver gained a hold_ignoring_cancel control 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

  • The full make gate is delegated to this PR's CI by decision (the local run was cut short after the codegen-check and go-lint lanes passed). Locally: make go-lint clean, make codegen-check pass, go test -race -count=1 -parallel 4 green for internal/session (full package, 142s), internal/extension, internal/observe, internal/api/{core,httpapi,udsapi}, internal/daemon, internal/cli, and root Turborepo test + typecheck for ./web.
  • Late fixes found by the full internal/session run (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.
  • Real-provider steer walk (mandatory per the task): Claude Code 2.1.261 announced steering → steer_delivery: injected, transcript_marker.prompt_steered inside the same turn, the agent acknowledged the guidance (STEER_ACK_7731) on the same turn id with done end_turn. Codex 0.153.2 and Cursor 2026.09.02 do not announce steering on their current ACP builds and fall back truthfully.
  • Isolated lab (eng-qa-bootstrap, fresh COMPOZY_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 -9 the daemon mid-turn → restart → stopped / agent_crashed, no phantom, no orphan. Report: docs/qa/reports/2026-09-05-sessions-stability-task01-02.md; scenarios RT-session-live-steer (new, pass), RT-session-prompt-cancel (pass), RT-018 and RT-session-native-stop (blocked-verify for the branches not walked: provider auth-lapse simulation, native-tool/UDS parity, unverifiable-kill, Daytona).
  • Visual Contract bundles for task_01 VC-01..06 and task_02 VC-01..02 validated with validate-visual-contract.mjs (0 blocking divergences).
  • New/changed tests live in their canonical suites (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 · ⌘⏎ queue hint, Queue / Steer / Interrupt actions.

Live Claude turn in the web composer

Enter during that turn: "Steering — delivered into the live turn injected" — no cancel, same turn.

Web steer injected into a live Claude turn

Transcript after a CLI steer on Claude Code: the agent course-corrected mid-flight and answered the steer acknowledgement inside the same turn.

Transcript with the steer acknowledgement

No-capability agent: "Interrupted and replaced — this agent can't take guidance mid-turn interrupt_fallback", replacement already answered.

Interrupt fallback disposition

Opposite modifier (⌘⏎): "Queued #1 — runs after the current turn" with the queue strip row.

Queued follow-up with position

Stop on a cancel-ignoring turn: the guarded "Stopping…" pill holds through the cooperative grace while the ladder escalates.

Stopping pill during the escalation ladder

After the ladder verified the kill: the composer returns to idle and the session stays promptable (turn scope).

Stopped confirmed

Settings → Sessions: daemon-owned Follow-up behavior (Steer immediately / Queue until the turn ends).

Follow-up behavior setting

Truthful unverified stop: stopping + stop_verification_failed attention with a retry, never a fabricated stopped.

Stop verification attention notice

Impact

  • CLI: compozy session prompt (default busy mode steer, --expected-turn strict fence, --expected-turn-id deprecated alias with warning, full envelope in -o json), compozy session stop [--wait] (truthful verified / 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).
  • HTTP/UDS: POST …/sessions/{id}/prompt and …/steer return the SendOutcome envelope (steer_delivery, turn_id, entry_id, queue_position, replayed; active_turn_mismatch carries current_turn_id); POST …/stop accepts wait and returns the truthful stop outcome; session resources carry escalated, verified, busy_input; new canonical events session.stop_escalated, session.stop_verification_failed, provider_error, and the prompt_steered / prompt_superseded / post-stop transcript markers. Legacy delivery / status / queue_entry_id / estimated_send_at fields and the legacy interrupt default stay readable through the v0.4 compatibility window (SD-013) and are scheduled for removal in v0.5.0.
  • Native tools: compozy__session_prompt (wait defaults to false; envelope), compozy__session_describe (busy_input.steer_delivery), compozy__session_stop / compozy__session_prompt_cancel (truthful envelopes). Descriptors and digests regenerated.
  • config.toml: session.busy_input.default_mode now defaults to "steer" and validates steer|queue (legacy interrupt accepted with a warning until v0.5.0); new providers.<name>.steer_capability; new session.stop.cooperative_grace ("10s").
  • SQLite: global stream migrations 00101 (session_input_queue.steer_delivery), 00102/00103 (stop_escalated, stop_verification_failed), all lossless.
  • Web: composer Enter/modifier dispositions, draft restore on refusal, Stop → "Stopping…" without remount, Settings Follow-up behavior, permission receipt attribution, post-stop marker refresh.
  • Docs (packages/site): new sessions/control-plane.mdx, updated sessions/lifecycle.mdx, CLI session prompt / stop / input steer pages, configuration/config-toml.mdx, providers page; official skill skills/compozy/references/runtime-operations.md.
  • Dependency: github.com/compozy/acp-go-sdk bumped to the async-request fork build (SendRequestAsync, Add asynchronous request submission with ordered completion acp-go-sdk#1).

  • make gate passes locally — not run to completion locally by decision (see above); this PR is delivered only after its required CI checks are green
  • New or changed behavior is covered by tests, or I explained above why not
  • If an agent wrote or co-wrote this, I named it above and verified the result myself — implemented with Claude (Fable 5.1 controller + Fable frontend workers, Herdr-orchestrated); every claim above was re-verified by the controller in the lab and through the gate.

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]>
@vercel

vercel Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
compozy-site Skipped Skipped Sep 5, 2026 10:19pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Important

Approval pending

CodeRabbit 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.

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit c373214.

…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]>
@pedronauck

Copy link
Copy Markdown
Member Author

@greptile-apps review

@pedronauck

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review skipped: 438 files exceed the limit of 300.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR substantially extends session control semantics across the ACP runtime, daemon, persistence, public APIs, CLI, native tools, Web UI, generated contracts, and documentation.

  • Adds live steering with persisted delivery outcomes and interrupt fallback.
  • Introduces a shared, verified escalation ladder for turn cancellation and session stopping, including restart recovery.
  • Adds structured provider-error events, busy-input capability reporting, and new Web composer/stop states.
  • Ships SQLite migrations and regenerated OpenAPI/Go/TypeScript contracts.
  • The implementation has extensive focused coverage, but two changed QA scenarios retain unverified branches without documenting a qualifying blocker as required by the repository delivery rules.

Confidence Score: 4/5

The implementation appears technically safe, but the repository’s explicit QA requirement must be satisfied before merging.

No concrete runtime, persistence, or security defect remained after tracing the changed paths; the accepted finding is the documented delivery-rule violation for required scenario branches left unverified without an external or decision blocker.

Files Needing Attention: docs/qa/scenarios/RT-018.md, docs/qa/scenarios/RT-session-native-stop.md

Important Files Changed

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]
Loading

Reviews (1): Last reviewed commit: "fix: stop settled-session reads from rep..." | Re-trigger Greptile

Comment thread docs/qa/scenarios/RT-018.md
Comment thread web/src/components/assistant-ui/session-composer-action-row.tsx
Comment thread web/src/systems/session/components/runtime-activity-notice.tsx Outdated
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]>
@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown

Too many files changed for review (475 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

Comment thread web/src/systems/session/lib/query-options.ts Outdated
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]>
Comment thread web/src/components/assistant-ui/session-composer-action-row.tsx
Comment thread web/src/systems/session/components/runtime-activity-notice.tsx Outdated
@pedronauck
pedronauck merged commit 0e6cff5 into main Sep 5, 2026
46 of 48 checks passed
@pedronauck
pedronauck deleted the session-improvs branch September 5, 2026 22:49
This was referenced Sep 15, 2026
This was referenced Sep 27, 2026

This branch was previously deployed

1 inactive deployment
Preview — c3732142 Deployed Sep 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant