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.
Serialize replay reads with event appends, acquire the live receiver under that lock, and drain the event pump during shutdown. Recovery now removes its FIFO when opening the log fails.
Own the ACP update receiver inside SessionManager, append each notification before broadcasting it, and recover structured sidecars by loading their stored ACP session id without duplicating replayed history.
<sidecar_dir>/<id>.events.jsonl holds one session/update notification
per line; seq is the line's 1-based position. open_events returns an
append handle held for the session's lifetime, and terminates a
partial last line first so a hard kill mid-write never glues the next
event onto the fragment. read_events takes a Since (forward, exclusive)
or Before (backward, exclusive, None for the tail) cursor with a limit;
unparseable lines keep their seq and are skipped. delete removes the
log with the sidecar; load_all ignores .jsonl.
Each append is a single write_all to the kernel, no fsync: a killed
daemon loses nothing already written, and an fsync per streamed chunk
is the wrong trade for a log that is not meant to survive power loss.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SIGTERM now counts as a shutdown alongside ctrl-c: systemd stops the
unit with SIGTERM, and with KillMode=process nothing else would reach
the claude-code-acp children. After the listener drains the manager
terminates every adapter before the process exits.
A Structured spawn gets no tmux pane: the daemon starts the adapter in
the session cwd with the session env (including MAESTRO_SESSION_ID and
MAESTRO_URL), runs the handshake, and records the ACP session id on the
sidecar. An adapter that cannot come up still yields a session, Dead
with activity Error and the reason as activity_detail, so a missing
adapter reads as the install command in the sidebar rather than as a
session that never appeared.
The liveness pass checks a structured session's child instead of the
tmux list. kill sends session/cancel, waits, then SIGTERMs through the
ACP handle. SessionManager::shutdown ends every adapter concurrently
and marks those sessions Dead on disk. take_acp_updates hands #135 the
raw session/update stream once, with earlier notifications still queued.
A Structured session's adapter hands back an ACP session id from
session/new; a respawn will pass it to session/load (#135). This is a
new field rather than #161's agent_session_id: that one is the Claude
Code conversation id the activity hook reports and `claude --resume`
takes, minted by Claude Code itself. The ACP id is minted by the
adapter, only exists for Structured sessions, and is what the adapter
takes back. Overloading one field with two ids from two processes
would leave a respawn unable to tell which kind it holds.
49ab7e9 made a new-branch worktree add suffix the branch name instead of
failing, and documented it, but left two tests asserting the old hard
rejection. The rejection contract (#66) still holds for an existing
branch that is already checked out, so that test now exercises that
case.
add_fails_when_branch_already_exists asserted the pre-#77 behavior:
that a branch collision errors outright. Since 49ab7e9, add_new
sidesteps a branch collision with a numeric suffix instead, so the
test was failing CI on main regardless of any given change.
Replaces it with a test asserting the actual (and intended) suffixing
behavior.
Adds `SessionView { Terminal, Structured }` to maestro-protocol, a
defaulted `view` field on `Session` and `CreateSessionRequest`, and
sidecar round-trip tests for both a legacy payload with no `view` key
and a `Structured` session. Nothing reads the field yet; every other
struct literal across the workspace picks up `Default::default()`
(Terminal) to keep compiling.
Gitea #133.
git worktree add -b fails outright when the branch already exists, even
if the target directory is free. A random fast-forward handle commonly
collides with a branch an earlier, already-removed worktree left behind.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The hook installer only ever wrote ~/.claude/settings.json, so a codex
session reported no activity and never posted its first prompt — leaving
it on the daemon's generated handle with no haiku name.
Adds a Target enum (claude-code, codex) over the hook file's location and
event set. One script and one marker serve both; codex swaps Notification
for PermissionRequest and has no StopFailure, so it never reports Error.
Both agents spell the prompt event UserPromptSubmit and put the text in
the payload's `prompt`, so autoname needs no change.
Codex will not run a hook it has not been told to trust; `maestro hooks
install` says so, and the approval stays the operator's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>