Advertise `elicitation.form` in the ACP handshake and handle
`elicitation/create` requests end-to-end through the daemon.
Adds `Elicitation` transcript row kind, `pending_elicitation` to
ReducedState, the elicitation pump, persistence, and a new
`POST /api/sessions/:id/structured/elicitations/:nonce` endpoint.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The adapter returns the session/prompt response before the agent
finishes working. Late-arriving notifications raced past the turn
marker and leaked into the next turn, making each response appear
one prompt behind.
The event pump now sends a settled oneshot through the done payload
and awaits it before processing further messages.
finish_queued_acp_turn fires it after persisting the turn marker and
dispatching any queued prompt.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Box the Notification variant to satisfy clippy's large_enum_variant
lint, fix paren mismatches from the boxing pass, and update test
helpers to unwrap AcpUpdate before inspecting the notification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The event pump now handles AcpUpdate::TurnDone markers that arrive
in the same FIFO as notifications. It fires the done oneshot only
after persisting every preceding notification, so finish_queued_acp_turn
can no longer race ahead of the transcript.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prompts sent while a turn is active are queued on the daemon instead of
in the browser's React state. The queue is persisted in the session
sidecar and drained as the next turn when the current one completes.
Fixes the bug where queued messages were delivered to the wrong session
when switching focus in the UI: the client-side queue captured the new
sessionId in its drain closure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After a graceful daemon restart, recovered structured sessions now
receive a synthetic continue prompt so the agent resumes without
manual intervention. The User row carries `"maestro": {"type":
"restart_continue"}` metadata for future UI differentiation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Structured sessions spawned with auto_approve lost the flag on daemon
restart because it lived only in the in-memory SessionRecord. Add it to
the Session struct (sidecar JSON) so recovery reads it back instead of
hardcoding false.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously --force left the worktree on disk so the operator could
inspect it. Now it removes the worktree unconditionally — the
confirmation prompt already shows the breakdown, so the decision
happens before the delete, not after.
Natural exit still leaves dirty worktrees on disk (no operator
confirmation in that path).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mark_session_dead_with_worktree_settlement cloned the session under the
lock, dropped it, did async work, then wrote the stale clone to the
sidecar. A haiku rename (or any other metadata update) that landed
between the clone and the write was silently overwritten, so the sidecar
reverted to name_source: Generated and the next daemon recovery
re-triggered autoname.
Re-read record.session under the lock at write time so the sidecar
always reflects the latest state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The sandbox has /tmp/.git, so the ancestor walk finds a git root
above the tempdir. Weakened the assertion: the result must not point
inside the .git-free tempdir, but may point above it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
A structured session still booting its adapter has no ACP handle and
no tmux pane, so the liveness pass misclassified it as a vanished
terminal session and reaped it. Claude Code adapters take long enough
to boot that the 2s liveness tick fires before the handshake finishes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests now assert `Starting` after create and use `wait_for_ready`
before checking post-handshake state. Unsupported command test
expects `BadRequest` instead of a persisted Dead session.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Register a structured session immediately with `activity: Starting`
and run the ACP handshake in a background task. The HTTP caller gets
an instant response; the user can begin typing a prompt while the
adapter boots.
- Add `ActivityState::Starting` to the protocol
- Open the event log eagerly so the structured WS can connect during
boot (empty transcript + idle state)
- `queue_acp_prompt` stashes a prompt while `Starting`; the background
task drains it on successful handshake
- `apply_structured_activity` skips overwriting `Starting`
- `subscribe_acp_projection` allows connecting during `Starting`
- TUI/CLI map `Starting` to idle colour / "starting" label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Structured sessions ignored the --dangerously-skip-permissions flags in
preset commands because the daemon discards CLI flags when spawning the
ACP adapter binary. This adds an `auto_approve` bool to SpawnPreset and
CreateSessionRequest so the operator can declare it in config.toml:
{ id = "claude", command = "claude", auto_approve = true }
When set, the daemon's permission pump immediately responds AllowAlways
to every ACP permission request without surfacing it to clients.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Notice only covers SessionUpdate variants the agent-client-protocol
dependency already decodes but the projection doesn't special-case.
SessionUpdate is an internally-tagged enum with no unknown-variant
fallback, so a genuinely new wire variant fails deserialization before
the projection ever sees it, rather than becoming a Notice. A new
variant needs the dependency upgraded first.
Adds a projection test asserting that an unrecognised sessionUpdate
tag is a deserialization error.
Refs #186
write() reported Ok(partial) and Err from the same call, which Write
forbids. Defer the error to the following call so write_all observes
a genuine short write, then a failure — matching what a real fd does.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A write_all that errors mid-line, or a flush that errors after a full
write, could leave last_seq and the log's on-disk line count out of
sync: a well-formed but uncounted line, or a partial one, would corrupt
every later cursor once the next append landed beside it. append now
captures the pre-append length and truncates back to it on either
failure, so the same EventLog can be appended to again immediately. If
the rollback itself can't restore that length, append returns a Fatal
variant so callers kill the session instead of promising consistency
they can no longer keep — record_permission_request and its pump now
route this through the session-death path used elsewhere.
Adds deterministic fault injection (a seekable/truncatable LogSink
double) to prove the same EventLog recovers with valid JSONL and
live/replay agreement, rather than swapping in a healthy handle. Fixes
the row-index rollback test to actually shift a later row, and
tightens the persistence-failure wording in STRUCTURED-VIEW.md.
request_approval() appended the Approval row to the live projection
before the daemon tried to persist it. On append failure the pending
nonce was cancelled but the row stayed in the in-memory transcript, so
a WebSocket snapshot taken afterward showed a row durable replay could
never reproduce.
Add Projection::rollback_row to undo the append and call it alongside
the existing cancel_pending_approvals on failure. Extract the pump's
per-request handling into record_permission_request so the failure
path is directly testable without a live ACP connection.
Fault-injection regression tests use a read-only file handle
(EventLog::for_test, cfg(test)) to force append() to fail
deterministically, then assert the live snapshot and durable replay
agree (both empty) and a subsequent request still persists and
replays correctly.
Fixes#184.