Repository navigation
Make Freshclaude turns and alerts follow accepted commands - #874
Merged
Merged
Conversation
C15 of the freshclaude core-runtime design (2026-10-22, §3.2, §5.5, §5.6). A behavior-preserving extraction, so the claude lane can reuse the codex deadman (C22) and mint every edge on one per-session clock (C18). attention.rs (new, shared): - AttentionClock: one strictly-monotonic `at` timeline per session (mint, observe, floor). Clones share the timeline, so a clock carries across a re-key or death/resume. `From<Arc<Mutex<Option<i64>>>>` adopts an existing cell both ways. - turn_complete_event: the client edge shape. - mint_synthesized_edge: mints on the clock and logs `freshagent.turn.complete` (provider, session_id, source, at). quiet_deadman.rs (new, shared): - QuietDeadman's state machine (feed, arm_if_absent, disarm, try_fire, is_stuck), the waiter wait_for_quiet_deadline, and window_from_env. Lanes keep what counts as activity, "in flight", the stuck broadcast, the edge and the phase logs. Codex becomes a thin wrapper: - note_codex_activity, arm_codex_quiet_if_absent, disarm_codex_quiet, watch_codex_quiet_deadline and the snapshot stuck overlay call the shared deadman; QuietDeadman is re-exported from codex, so its callers (and codex_sidecar_tracking_tests) are unchanged. - mint_synthesized_attention_edge mints through attention; the TurnComplete frame uses turn_complete_event; the controls' waiting `at` is an AttentionClock. - Opencode's byte-identical turn_complete_event is replaced by the shared one. Tests: - Red: the new attention::tests (7) and quiet_deadman::tests (11) failed to compile before the modules existed (E0433/E0425: AttentionClock, QuietDeadman, wait_for_quiet_deadline, mint_synthesized_edge, ...). - Green: 17/17, then 18/18 after tightening the in-flight waiter test. They include observe(T) then mint() with now <= T returning T+1. The waiter tests run on a paused tokio clock (dev-dependency test-util). - Codex tests are byte-identical and green: `cargo test -p freshell-freshagent --lib codex` 265 passed before and after (the five quiet_deadman_* tests among them). Whole crate lib suite: 1498 passed. Deviations from the design text: - Codex still stores each clock as the bare Arc<Mutex<Option<i64>>> cell: freshell-codex's CodexSubscription mints ordinary completions on it and cannot name AttentionClock. Codex adopts the cell at its mint sites. That keeps the codex test fixtures unchanged. - Codex's synthesized-edge log line gains the `at` field (§5.5 shape). - Claude's interim last_synthesized_complete_at is left for C18's edge hub to replace; AttentionClock::observe/floor carry a dead_code allow until then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core-runtime design C16 (docs/plans/2026-10-22-freshclaude-core-runtime.md
§5.1, §6, §9.3 I2).
Every send the server writes to the claude sidecar now carries a uuid the
server mints (claude_turns::mint_send_uuid). The sidecar puts it on the
SDKUserMessage, so the CLI reports the command's lifecycle under it and
writes the user line with it: freshAgent.send.accepted.submittedTurnId is
that uuid and equals the snapshot turnId of the landed message. A
checkpointed send's attempt binds under the same uuid. A compact is a
command with its own uuid too.
Sidecar (index.mjs, new lifecycle-frames.mjs):
- forwards facts for the commands it sent only: sdk.command
(queued/started/completed/cancelled/discarded, plus replayed on the first
replay), sdk.session.state (the sidecar sets
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS=1 for the CLI), sdk.status null
(compactResult/compactError/permissionMode) and requesting, and
sdk.heartbeat from tool_progress, task_* and api_retry, at most one per
kind every 5 s;
- sdk.interrupt_settled carries stillQueued, filtered to sent uuids;
- sdk.result also carries subtype, isError, terminalReason, numTurns and
errorText (the CLI's own errors without [ede_diagnostic] lines);
- a bad image (an unreadable type, missing or non-base64 data, an unknown
kind, a non-web URL) or a send without a valid uuid is refused with
sdk.send.rejected (INVALID_IMAGE / INVALID_SEND, logged at warn) and the
process keeps serving the session.
Staging rule: sdk.turn.complete, sdk.turn.waiting and the sidecar's own
edges are still emitted unchanged.
Rust:
- claude_turns.rs: mint_send_uuid, RecentSends (bounded uuid -> request),
SidecarFact::from_line for the new frames, debug logging of facts.
- handle_send returns ClaudeSendOutcome (Accepted{session_key,
submitted_turn_id} | Refused{code, message}); every refusal goes through
refuse_send so the request error and the outcome cannot disagree.
FreshClaudeState::session_busy(id) -> Option<bool>.
- The stdout consumer never broadcasts the facts and never folds a
requesting/null status into the tracked status. On sdk.send.rejected it
takes the send back: retires the op the send armed (busy clears), and
answers the request with CLAUDE_SEND_REJECTED.
- The WS dispatcher and the session host ignore the outcome (they read the
bus, as before).
- e2e fake sidecars write the user line under the send's uuid, as the real
CLI does.
Tests (red first, then green):
- test/unit/claude-sidecar/lifecycle-frames.test.ts (new, 15): pure mapping,
unknown uuids, replay once, bounded memory, heartbeat limiter, receipts.
Red: module missing.
- test/unit/claude-sidecar/lifecycle-facts.test.ts (new, 11, real index.mjs
over the simulator): uuid on the SDK message, lifecycle order, no facts
for CLI-made commands, checkpoint binding under the send uuid, session
state + env flag, requesting and status-null facts, rate-limited tool and
task heartbeats, stillQueued, enriched result, bad image and missing
uuid rejected while the process survives. Red: all 11 failed (timeouts or
missing fields).
- session-settings.test.ts (+7 image validation cases), sdk-message-frames
.test.ts (+3 cases, resultErrors; the exact-shape success case now lists
the new fields). Red: no INVALID_IMAGE code, resultErrors missing.
- claude_turns::tests (11). Red: types missing.
- claude.rs: a_send_line_carries_its_uuid_and_the_acceptance_names_it (new).
- crates/freshell-ws/tests/freshagent_claude_turns.rs (new): submittedTurnId
equals the snapshot turnId (sim and real-CLI rigs), lifecycle facts reach
the server and no client (sim and real CLI), handle_send outcome and
session_busy, a bad image is taken back and the session keeps working.
Red shown by mutation: with the pre-C16 sidecar both submittedTurnId lanes
fail (CLI-minted turn ids); with the consumer ignoring rejections the
image lane times out; with facts not withheld a client is shown
freshAgent.status "requesting".
Tests rewritten for the new protocol (same properties):
- user-message-replay.test.ts: the checkpointed send carries the uuid at the
top level; the attempt has none of its own.
- stream-end.test.ts: requesting/null statuses are lifecycle facts, not
alerts; the mid-compaction lane waits for compacting explicitly.
- stdout-loss-shutdown.test.ts, claude-cli-sim.test.ts,
claude-sidecar-shutdown.mjs: their raw sends carry a uuid.
- claude.rs checkpoint_turn_identity_claude_send_carries_valid_uuid_and_
attempt_correlation -> ..._carries_the_attempt_correlation (the uuid moved
to the send line, pinned by the new send-line test);
a_small_fixture_pipe_blocks_compact_until_the_reader_resumes checks the
compact line's uuid then the rest of the line.
- harness.ts stamps a uuid on a send that has none, the way the server does.
Deviations from the design:
- CLAUDE_SEND_REJECTED reaches the sender as the top-level request error
(the client's existing owned-send path restores the composer with
"Message not sent: <reason>"), not the nested freshAgent.error §6 lists;
a compact, which has no request, gets the nested error.
- The interim TurnTracker has no op ids, so a rejected send retires the
newest op of its kind; C17's ledger removes the command by uuid.
- sdk.result keeps the older `result` field next to `subtype`.
- The sidecar refuses image types the Messages API does not read (PNG,
JPEG, GIF and WebP only) instead of passing them to the CLI.
- sdk.status requesting is forwarded now (the §5.1 row lists it as turn
evidence); Rust withholds requesting/null from clients already in C16
because forwarding them would show clients an unknown status.
- Added real-CLI lanes for submittedTurnId and the lifecycle facts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core-runtime design §5.2 (docs/plans/2026-10-22-freshclaude-core-runtime.md).
The server's busy truth for a claude session is now one pure ClaudeTurnLedger
(claude_turns.rs): every command the server sent and the CLI has not ended
(armed under the turn lock before the write, by its uuid), the turns the CLI
runs (including its own, T19) and its compactions. A result retires every
started command, so a send absorbed into a tool call ends with the turn that
absorbed it (G08), and a send queued behind a turn stays owed until its own
result. The stdout consumer folds every sidecar line into it first
(claude/turn_status.rs), and the session's effective status is derived from it.
- Status is server-authored: `freshAgent.status` is published from the ledger
on change (running at the accept, before any assistant frame; compacting;
idle), re-sent after a result when not idle, and announced right after
`freshAgent.created`. No sidecar `sdk.status` reaches a client any more.
- `freshAgent.result` carries `sessionBusy`; the client's turnResult keeps the
pane running while it is true (a queued send runs next).
- The rollback, fork and configure gates, `session_busy` (I2), the attach
acknowledgements and the snapshot overlay read the ledger (G21). The rollback
gate is `ledger.busy()` at admission plus a recheck as the last step before
the replacement boots.
- A send the sidecar rejects is taken back by its uuid (the ledger replaces
C16's RecentSends).
- The sidecar forwards the init's `capabilities` (msg_lifecycle_v1).
Deleted: TurnTracker, the compact candidate (mark/confirm), the interrupt
settle promotion, absorb_unstarted_compact_debt, fold_terminal_edge, the
rollback quiesce protocol (Rust probe, rollback_probe_slot, QuiesceVerdict,
the sidecar's rollback.quiesce handler and queue drain, the e2e and in-crate
fakes' quiesce arms and their knobs), last_status, in_turn, and their tests.
Tests (red first: the 4 new sim WS lanes, the rollback lane and the client
tests failed on the previous commit, the ledger tests failed to compile):
- claude_turns ledger tests (32): the recorded traces H I J K L M N and the
final-probe CLI-initiated turn as table tests, the fold rules one at a time,
and two property tests over seeded random interleavings (4000 with lifecycle,
2000 for an older CLI): the ledger is busy exactly while the CLI owes work,
ends idle exactly when the CLI is idle, and settles each result exactly once
(mutating the idle rule or the result rule fails them).
- WS (freshagent_claude_turns.rs): an absorbed send from a second connection
leaves both idle, a configure is accepted and the next turn rings (sim and
real CLI); a queued send keeps the client running between results
(sessionBusy); running at accept before any assistant frame; a CLI-initiated
turn is busy; a new session is announced idle after created.
- WS (freshagent_claude_rollback.rs): an undo of a turn that absorbed a send is
ROLLBACK_REFUSED by the CLI, not BUSY_TURN (G08 at the gate); an undo whose
session starts a CLI turn after the gate is refused at the recheck.
- Client: turnResult honors sessionBusy (slice and ws fold).
- Sidecar: the per-turn init carries the CLI's capabilities.
Deleted tests and the ledger/WS tests that re-express their properties:
- legacy idle releases busy; the four contract edges -> a_legacy_idle_status_is_
the_session_idle, an_idle_ends_the_turn_and_every_started_command_but_not_a_
just_pushed_send, only_a_result_an_idle_or_the_end_of_output_ends_a_turn.
- queued compact + garlanded send(s); a send between the arm and the prior
result; the prior turn's edge during the compact write window ->
trace_l_a_queued_send_keeps_the_session_busy_between_its_results,
a_result_during_a_sends_write_settles_only_the_running_command, the property
tests.
- automatic compaction never attributed; arming during an active compaction;
atomic candidate publication; revived compact closes the gate at its status
-> an_automatic_compaction_mid_turn_settles_no_queued_compact,
a_compaction_is_busy_from_its_status, trace_m.
- the six interrupt attribution tests (compact window, ahead-queued drops,
queue-advanced turn, turn behind a dropped compact, gate held until queued
work and the turn's own frames land, trailing result never retires the next
op) -> an_aborted_result_retires_only_the_running_command, trace_h, trace_k.
- rejected interrupt never retires; receipts end nothing ->
only_a_result_an_idle_or_the_end_of_output_ends_a_turn.
- signed session-not-found opens the gate -> a_session_the_sidecar_lost_owes_
nothing.
- dropped compacts (disarm on the send's result, on sdk.error, remnant debt,
extinguish only ahead, two quiescent compacts absorb) ->
a_dropped_command_ends_with_its_own_lifecycle_only,
an_armed_command_is_owed_before_the_cli_sees_it, the property tests.
- compaction starting mid-rollback / behind the absorb aborts at the recheck ->
WS an_undo_refuses_when_the_cli_starts_a_turn_of_its_own_after_the_gate.
- the three quiesce-probe tests and rollback-quiesce.test.ts (drain, handed,
in-flight, pulled, unrelated result, probeId echo) ->
an_armed_command_is_owed_before_the_cli_sees_it (no command hides in a
queue the server cannot see), an_aborted_result_retires_only_the_running_
command (a queued compact survives an unrelated result), trace_m; the probe
and its receipts no longer exist.
- failed compact writes (gate held for an owed send; the spent arm released)
-> a_send_is_taken_back_by_its_own_uuid.
- a_small_fixture_pipe_blocks_compact_until_the_reader_resumes tested only
the deleted SIGSTOP pipe rig.
- C16's RecentSends tests -> a_send_is_taken_back_by_its_own_uuid.
Kept and moved onto the ledger: handle_rollback_mid_turn_is_busy_and_never_
touches_the_sidecar, rollback_during_a_compact_turn_is_refused_busy_turn_with_
zero_teardown_traffic.
Deviations from the design:
1. The per-turn init opens no turn (only its capability is read): every real
turn opened earlier (started, or session state running), and the protocol
fakes announce an init at create, when nothing runs.
2. A `replayed` lifecycle does not make the CLI lifecycle-capable, so the
head-retire fallback still covers an older CLI that replays sends but
reports no lifecycle.
3. Subagent output (a parentToolUseId) is not turn evidence.
4. The server announces the status right after freshAgent.created (create and
adopt-live paths). It replaces the legacy fakes' forwarded create-time
idle; real-sidecar panes now read idle after create instead of connected.
5. The rollback recheck runs after the rollback-boot test hold, as the last
step before the boot; the env knobs ROLLBACK_PRE_TEARDOWN_MS and
ROLLBACK_PROBE_TIMEOUT_MS are gone.
6. The sidecar keeps its configure guard (turnOpen, handedCompactLikely) until
C18 deletes it, as planned; only the quiesce request and the queue drain go.
7. A plain sdk.error no longer clears queued commands (each command ends with
its own lifecycle); sdk.error{sessionNotFound} clears the ledger.
8. The local fake Anthropic endpoint reads the last USER message: the pinned
CLI, configured as the sidecar configures it, appends a system-role agent
listing after the prompt, which hid every prompt marker from the real-CLI
lane (the contract suite still passes).
9. The in-crate fake sidecar lost its unused magic texts (__emit_idle__,
__emit_error__, __emit_compacting__, the compact boundaries,
__drop_session__) and interrupt knobs; the attach test fake answers a send
with an assistant frame instead of a status.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…G06, G24)
The server now owns every claude attention edge (design §5.3-§5.5). The
sidecar forwards facts and mints none.
Rust:
- claude_edges.rs: ClaudeEdgeHub, the one choke point. Each edge takes its
`at` from the session's AttentionClock, is logged at info with its source
(result, death or pending), is broadcast unchanged, reaches the
AttentionObserver (§9.2 I-1) and is published as a typed ClaudeTurnEvent
(§9.3 I3). FreshClaudeState::subscribe_turn_events() delivers
Settled{session_key, retired_request_ids, retired_command_ids, outcome, at}
(a silenced result too, with at None), Waiting and Ended.
with_attention_observer() installs the observer on every clone.
- claude_turns.rs: the Stop latch and the result verdict, in the ledger.
A Stop targets the running turn, else the oldest command not started,
else a turn the CLI runs by itself. An idle Stop sends nothing.
Commands queued behind the running turn are not targets (decision 1).
Re-send rule: case A (receipt lists the command, or came before its
lifecycle) at its start; case B (empty receipt after its lifecycle) on
its first content. Verdict: silent only for an aborted result that
retired nothing but the latch's targets. A failed subtype rings with a
CLAUDE_TURN_FAILED banner (its own text, else the copy for its kind). A
success with is_error (API error) rings with no banner.
sdk.interrupt_settled, the result facts and turn content (Content) are
now ledger facts.
- claude/stop.rs: handle_interrupt latches under the ledger lock and
writes {interrupt, stopId}; the consumer re-sends when the latch says
so. Logged as freshagent.claude.stop with an outcome (sent, idle,
reissued, unsent, refused).
- Consumer: result -> sessionBusy frame -> banner -> edge -> status. The
waiting edge is minted on the pending fold's 0 -> >=1 change. A death
rings whenever a turn was owed, however the sidecar ended (the interim
C08 rule and death_rings are gone). Legacy sdk.turn.complete/waiting
lines from a protocol fake are dropped (debug log); sdk.exit is no
longer mapped.
- One AttentionClock per session (renamed from
last_synthesized_complete_at). It is carried by an undo's replacement
and by the dead-session record into the resume that follows.
Sidecar:
- Deleted turn-complete-gate.mjs, the sdk.turn.complete / sdk.turn.waiting
/ sdk.exit emissions, the consumeStream finally mint, emitWaitingEdge,
pendingResults, turnOpen, handedCompactLikely and the compact handoff
hook. The configure guard is the server's (its ledger); the sidecar
honours only an explicit `busy`.
- resultFrames emits sdk.result alone (no sdk.error{turnFailure}).
- interrupt echoes stopId on sdk.interrupt_settled.
- A request for a session this sidecar does not host: a send is
sdk.send.rejected{SESSION_NOT_FOUND}, an interrupt settles ok:false
(sdk.error{sessionNotFound} is gone, §5.1).
- monotonic-clock.mjs deleted: nothing uses it any more.
Simulator: FRESHELL_CLAUDE_SIM_COLD_COMMAND=empty|queued|started makes the
first command after the boot miss a Stop in each T13 receipt shape. The
ledger records every interrupt the CLI takes.
Fakes (until C30): both e2e claude fakes and the kilroy runtime mint no
edges. They forward C16 result facts (an aborted terminalReason for a
stopped turn), echo stopId, and settle ok:false for an unhosted session.
The kilroy approval knob raises a permission request instead of an edge.
The in-crate and WS-test fakes do the same.
Tests, red first (deleted test -> where its property now lives):
- claude_turns_stop_tests.rs (30): target selection; every verdict row,
including the non-target command that rings; cases A and B in both
orders; one re-send per Stop; ok:false and unsent disarm; clearing on
retire, lifecycle, idle, EOF and take-back; older CLI; merged Stops.
They failed to compile before the latch existed.
- claude_edges tests (4): clock order across sources and the same
millisecond, logging, observer hooks and turn events. A death minted on
a separate clock fails two of them.
- freshagent_claude_turns.rs (13 new WS lanes, sim; the warm Stop and a
Stop right after the send also on the real CLI). Run on the pre-C18
code these failed: the three cold Stops (the turn ran to success), the
Stop racing completion (no re-send), the idle Stop (no stop log), the
CLI-initiated turn and the waiting edge (sources freshclaude/sidecar),
and error_max_turns (no CLAUDE_TURN_FAILED).
- interrupt-idle-gate.test.ts now asserts the sidecar emits no edge, no
banner and no sdk.exit, echoes stopId, and refuses unhosted requests.
All 7 failed against the old sidecar.
- claude.rs: an idle session's interrupt reaches no sidecar; a resumed
session keeps its death's clock (red: a fresh clock).
- Deleted test/unit/sidecar/claude-turn-complete-gate.test.ts (10). Its
properties now live in claude_turns_stop_tests.rs: every result rings
with no Stop; the stopped turn's aborted result silent and only it
(a_warm_stop_that_landed_is_silent,
a_queued_command_another_device_sent_...); a missed Stop rings
(a_cold_stop_whose_command_finished_first_rings,
a_stop_racing_completion_rings); no stale mark
(a_cleared_latch_never_silences_the_next_turn); a failed turn under a
missed Stop rings (a_failed_turn_under_a_stop_that_missed_rings);
older-CLI and boot-refusal shapes (an_older_cli_reports_...); ok:false
(a_refused_interrupt_disarms_the_latch); no reset at send (merged and
queued-command cases); nothing pending arms nothing
(an_idle_session_has_nothing_to_stop).
- Deleted test/unit/sidecar/monotonic-clock.test.ts -> attention.rs mint
tests plus a_death_after_a_result_in_the_same_millisecond_gets_a_larger_at.
- Deleted ledger test a_session_the_sidecar_lost_owes_nothing
(sessionNotFound is gone) -> a_send_is_taken_back_by_its_own_uuid plus
the sidecar's SESSION_NOT_FOUND rejection test.
- Deleted claude_sidecar_exit::only_an_unreported_death_mid_turn_rings
(interim rule) -> a_stream_end_is_reported_once_... now rings once,
sidecar_death_while_idle_emits_no_completion, freshagent_claude_lifecycle.
- Deleted emit_attention_edge_logs_each_edge_and_broadcasts_its_frame ->
claude_edges every_edge_is_minted_logged_broadcast_and_observed.
attention_edge_lines_leave_only_through_the_edge_exit became
sidecar_edge_lines_never_become_frames.
- sdk-message-frames resultErrorText and failure-frame tests ->
the_copy_for_each_failed_result_kind,
a_failed_result_rings_with_its_own_text_or_the_copy_for_its_subtype,
an_aborted_result_with_no_stop_rings_with_a_banner. Diagnostic
stripping stays pinned by the resultErrors tests.
- Updated to the new contract: stream-end, lifecycle-facts,
session-settings, permission-channel, the claude-cli-sim seam, the
real-CLI edge log tests (sources result and pending), the kill and
interrupt WS test (the session is busy first), the e2e fixture tests
and the harness-03, harness-06, fresh-agent-control-rust,
fresh-agent-error-turn-attention and fresh-agent-mobile-attention
fixture-wire checks.
Deviations from the design:
1. Under protocol v2 a Stop cannot race the CLI's boot (`created` comes
after it), so the cold shapes are emulated after the boot by the new
simulator knob. In the 'started' shape the CLI answers the Stop once
the command has started (case B).
2. A missing Stop target or a refused receipt is logged only. The nested
CLAUDE_INTERRUPT_FAILED with a requestId is C21, because
FreshAgentInterrupt carries no requestId. A write failure still sends
the top-level CLAUDE_INTERRUPT_FAILED.
3. Assistant and stream lines are a new TurnFact::Content (case B's first
content), split from TurnEvidence.
4. A second Stop keeps the earlier latch's targets. MAX_STOP_REISSUES (2)
bounds re-sends across merged Stops; one Stop re-sends at most once.
5. monotonic-clock.mjs was deleted here, not in C30: nothing used it.
6. The fixture's deny completion (subtype 'error') now gets the server's
CLAUDE_TURN_FAILED banner, like every failed subtype. The real sidecar
already sent one.
7. The edge hub tests were written with the module. Their red is the
module's absence plus the separate-clock mutation above.
8. The observer's identity hooks are called where ids change today:
try_rebind_to_live and broadcast_materialized (rekeyed), establish
(registered) and the kill reply (forgotten).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… idle Stop C18 made an idle Stop a no-op: the server sends nothing to the sidecar when nothing is running (design §5.4). fresh-agent-hidden-approval-reveal used an idle `freshAgent.interrupt` as the fake's trigger for raising its approval, so the approval was never raised and the spec failed on cloud (all three attempts: "the sidecar raises the approval", 0 instead of 1). The approval is now raised by a `freshAgent.send` written to the hidden pane's session, and the turn parks on it. The Stop (the Stream Deck frame) then reaches the sidecar because a turn is running, and it cancels the approval as before. The spec checks the same things: the card is current on each reveal and the blocking overlay is never drawn. The interrupts the server forwards are now 1, not 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve live transcript frames, native approvals, permission metadata and MCP launch context alongside the Rust turn ledger. The mapper no longer synthesizes a running status at compaction end; the lifecycle fact feeds the Rust ledger instead. Keep command UUID and checkpoint identity aligned in live-frame coverage.
C19: each live runtime owns a FIFO stdin writer. Send, settings, approvals, questions, compact and Stop clone its handle under the sessions map, then await outside that map lock. A broken pipe or configurable write timeout kills only the owning incarnation; the registered runtime reaches the existing EOF/death/attention path. Requested teardown preempts a blocked write and retains the existing shutdown deadline and process-tree escalation. Red: the sandbox isolation regression failed because a stalled sidecar blocked a healthy session. Green: both writer WS cases pass; five focused writer unit tests cover FIFO flushes, broken pipes, timeout, shutdown preemption and isolated timeout settings. The isolation case uses a 5s timeout and a 1s healthy-operation budget, so a timeout while holding the shared map cannot satisfy it. A separate 500ms case verifies exactly one owed-turn edge, exited state and a working other session.
Read, mutate and classify the pending set under one guard. Publish a pending request and its waiting/drained transition within that same ordering boundary. Post-flush response completion reacquires the sessions map briefly and validates the captured runtime ownership and pending-set identity, so a delayed response or consumer from a replaced runtime cannot clear current attention. Red: concurrent response/insertion reported 19228 waiting edges for19532 actual nonempty intervals. Green: three focused pending regressions, including a controlled stale-response replacement with reused request ids; all29 turn WS cases (5 pinned real CLI) and2 writer WS cases pass in the runtime sandbox. New sequential-approval WS coverage asserts two waiting edges and one completion.
The blocking architecture assertion scans source text rather than runtime behavior and rejected valid comments explaining provider retry/notice frames. Remove that individual test and its unused helper as required by the repository testing policy. Keep the remaining architecture tests; focused file passes9/9.
A writer failure can reach its send handler after stdout EOF cleared the turn ledger and reported exited. Only publish the unwritten-send status when undo_arm removed its command, and keep undo plus status publication under one ledger guard so EOF cannot overtake that update. Sandbox regression failed with idle replacing exited before the fix. Both late-failure and live-failure status cases now pass; a live failed send still returns to idle.
…icted Clear the old runtime pending-attention interval synchronously at the incarnation-checked map removal, before reaping can await or a resumed runtime can publish another approval. Include pending-set identity in the EOF eviction fence so reused durable sidecar ids cannot let an old consumer evict a replacement. The sandbox observer regression failed before the change. It now passes: old waiting is cleared before the exit-report hold, a replacement raises a new waiting interval during the old teardown, and that interval survives completion of the old death.
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.
Freshclaude and Kilroy could remain busy after queued sends, mishandle a Stop during startup, or miss an ending when the SDK stream closed. Track each submitted command in Rust and use SDK lifecycle facts to settle it, so status and attention have one source of truth. Stop targets one command, and unrequested process death clears pending controls and reports the ending once.
Give each runtime its own FIFO stdin writer so a blocked sidecar cannot stall other sessions. Preserve every outstanding command UUID until it settles, expose typed send/turn outcomes for the following REST work, and keep late write failures or old runtime exits from changing a replacement's state. Extract shared attention and quiet-deadman helpers while retaining Codex behavior.
Validation: