Address PR #2 Copilot review comments on the merged LCM+SM lane:
- session: validate runtime handle + workspace path before Kill/Cleanup
teardown; refuse (ErrIncompleteTeardownMetadata) or skip rather than
hand empty args to a real adapter's Destroy (unsafe delete).
- session: reject Restore unless the session is terminal
(ErrNotRestorable) so a live session can't spawn a duplicate
runtime/workspace.
- ports: document SpawnConfig.OpenTerminal as reserved/not yet honored.
- lifecycle: remove the unread reactionConfig.auto field; note
approved-and-green is notify-only (human decides to merge).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements ports.SessionManager against fakes for the outbound ports. The SM is
the explicit-mutation half of the lane: it drives Runtime/Agent/Workspace, seeds
the initial lifecycle, and routes outcomes to the LCM (OnSpawnCompleted /
OnKillRequested). It never derives observed state and is the single producer of
the derived display status (attached on read, never persisted).
- Spawn: Workspace.Create -> Runtime.Create (AO_* identity env) -> Seed ->
OnSpawnCompleted, with eager rollback of completed steps on failure.
- Kill: OnKillRequested first -> Runtime.Destroy -> Workspace.Destroy, honoring
the worktree-remove safety (refusal surfaced, never forced).
- List/Get: derive status via DeriveLegacyStatus. Send: via AgentMessenger.
Restore: re-seed (reopen) + relaunch via GetRestoreCommand. Cleanup: reclaim
terminal sessions, skip worktrees holding uncommitted work.
Store-contract additions (co-owned with Tom's persistence layer, flagged for
review): LifecycleStore.Seed (explicit create-with-identity; OnSpawnCompleted
requires a seeded record) and LifecycleStore.Get (single record-with-identity
read; Load is lifecycle-only). Lifecycle test fake updated to satisfy both.
Tests route through the real LCM Manager (wrapped to record call order).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- OnKillRequested now clears the session's escalation trackers after a
successful kill, so a later duration-based TickEscalations can't emit
reaction.escalated for a dead session (dispatch is still skipped).
- sendToAgent rolls back the attempt (and firstAttemptAt when it set it) on a
messenger.Send error, so undelivered messages don't march a reaction toward
escalation — honoring "send failures retry next tick" (§4.3).
- Duration escalation now uses an inclusive boundary (>=) in both shouldEscalate
and TickEscalations, so a 30m reaction escalates at exactly 30m instead of
waiting for the next tick.
Tests: kill clears trackers + no post-kill escalation; repeated failed delivery
never escalates; duration escalation fires at exactly escalateAfter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address review finding #1: the persistent ci-failed tracker leaked and could
stale-silence a future regression. It was only cleared when leaving the
ci-failed reaction AND incidentOver held at that moment — so a recovery to
another open-PR state (ci-failed -> approved -> merged) never cleared it.
- react() now clears ALL of a session's trackers when the state REACHED is
incident-over (PR resolved / session terminal) OR a genuine recovery
(approved/mergeable, which the open-PR ladder guarantees means CI is no
longer failing). Keyed on the state reached, not the one left, since the
recovery transition is typically review_pending->approved (empty beforeKey).
- Persistent ci-failed still survives the ambiguous review_pending limbo, so
fail->pending->fail keeps one shared budget (§4.2).
- Document the out-of-lock react() dispatch caveat for the daemon integration
step (review #2) and the intentionally-skipped agent-stuck 10m threshold.
Tests: re-arm after a genuine recovery (regression re-nudges, not silenced);
all session trackers cleared once the incident is over.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the ACT half of the LCM: map persisted status transitions to reactions
(send-to-agent / notify / auto-merge) and drive escalation.
- reactions.go: the §4.2 default reaction table, reactionEventFor (mirrors
DeriveLegacyStatus for the ACT layer), in-memory per-(session,reaction)
escalation trackers, the react() dispatch chokepoint, and a real
TickEscalations for duration-based escalations the synchronous LCM can't
wake itself for. auto-merge action exists but is off by default;
bugbot-comments/merge-conflicts are configured but dormant (no decide-core
producer yet).
- manager.go: mutate now returns a transition; each Apply* path fires the
mapped reaction after persist via the single synchronous react() seam.
OnKillRequested intentionally does not react (explicit kill != inferred
event). Split-A load->decide->diff->persist behavior is unchanged.
ci-failed budget is persistent across fail->pending->fail oscillation;
non-persistent trackers reset when the status leaves the triggering state.
Escalation silences further auto-dispatch until the condition clears.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address Harshit's PR #5 review (approve w/ design confirms + polish):
- #1 (design decision): a valid activity signal is proof of life, so it now
resolves a detecting session — writes the activity-mapped session state and
clears the quarantine memory. Scoped to detecting only; a liveness-escalated
stuck stays the probe pipeline's to resolve. Terminal still never reopens.
- #2: document why a merged/closed PR parks the session axis even over an
activity-owned needs_input/blocked (a merge is a milestone), unlike the
open-PR path that defers to activity.
- #3: map plain idle activity to a neutral session reason instead of the
misleading research_complete (kept for ready, which implies completion).
- #6: cover all three kill kinds (manual/cleanup/error), the open-PR review
branches (changes_requested/mergeable/review_pending), and the neutral idle
reason. Coverage 86.5% -> 88.6%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- shouldWriteSessionRuntime: never resurrect a terminal session; an
observation may refresh the runtime axis but must touch neither the
session axis nor the detecting memory (gated in ApplyRuntimeObservation).
- OnSpawnCompleted: error on an unseeded session instead of fabricating a
partial record (SM must seed first — a missing seed is a contract violation).
- OnKillRequested: no-op on an unknown/already-gone session (benign race)
instead of fabricating a terminal record.
- keyedMutex: reference-count entries and evict on last release so the lock
map stays bounded in a long-running daemon.
- runtimeSubstateFromFacts: map RuntimeProbeIndeterminate to RuntimeUnknown
with a neutral reason, distinct from the probe_error of a failed probe.
Adds tests for terminal non-resurrection, unseeded spawn-completed error,
and unknown-session kill no-op.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements ports.LifecycleManager as a synchronous observe->decide->persist
reducer. Every entrypoint runs the shared pipeline under a per-session lock:
load canonical -> run the matching pure decider -> diff into a sparse
merge-patch -> persist. Never polls, never writes the display status.
- ApplyRuntimeObservation -> probe decider; always writes the runtime axis.
- ApplySCMObservation -> open-PR / terminal-PR deciders (failed fetch is a
no-op: failed probe != "no PR"). Open PRs write only the PR axis.
- ApplyActivitySignal -> updates the activity axis + maps onto the session
axis; only valid-confidence signals are authoritative.
- OnSpawnCompleted -> runtime alive + handles to metadata; session stays
not_started (display: spawning).
- OnKillRequested -> SM's explicit terminal-write authority.
- TickEscalations -> no-op stub (reaction/escalation engine is split B).
Composition rule (#1): liveness owns the runtime + death axis; activity owns
the working/idle/waiting axis. A healthy probe verdict writes the session axis
only to recover a liveness-owned state, so it never clobbers an activity-owned
needs_input/blocked. Activity is the mirror: it stays off the death axis.
Detecting clear (#3): a non-detecting probe verdict clears stale detecting
memory so the next probe reads no phantom Prior.
Built/tested against in-memory fakes (LifecycleStore with full merge-patch +
ExpectedRevision, recording Notifier/AgentMessenger). Per-session
serialisation verified under -race.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>