Commit Graph

8 Commits

Author SHA1 Message Date
harshitsinghbhandari f03c7c8c88 fix(session): harden teardown/restore safety + drop dead reaction flag
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>
2026-05-27 14:21:47 +05:30
harshitsinghbhandari 538210844e feat(session): implement Session Manager (spawn/kill/list/get/send/restore/cleanup)
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>
2026-05-27 13:43:40 +05:30
harshitsinghbhandari 5081cf794b fix(lifecycle): kill clears trackers, send-failure budget, inclusive escalation (Copilot review)
- 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>
2026-05-27 13:11:00 +05:30
harshitsinghbhandari b2161d5582 fix(lifecycle): clear ci-failed tracker on recovery/incident-over (PR #6 review)
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>
2026-05-27 02:26:45 +05:30
harshitsinghbhandari 8b8da8e6a4 feat(lifecycle): ACT layer — reaction table + escalation engine (split B)
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>
2026-05-27 02:17:51 +05:30
harshitsinghbhandari 9eb5348604 feat(lifecycle): activity resolves detecting + review polish
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>
2026-05-27 01:49:28 +05:30
harshitsinghbhandari 3945b10f20 fix(lifecycle): address PR #5 review
- 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>
2026-05-27 01:37:14 +05:30
harshitsinghbhandari 1420bb9493 feat(lifecycle): implement LCM Apply* pipeline (split A)
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>
2026-05-27 01:26:15 +05:30