fix(omo-senpi): rebuild the plan gate on session resume by replaying the transcript

Merged
CO
41m ago
+416-30

Summary

Refs #10378 (item 2b). After a restart or a restore, the plan gate for plan-consultant / plan-reviewer forgot that the user had asked for ulw-plan and touched a plan. The lane had to re-send /skill:ulw-plan and touch the plan again.

skill-invocation-tracker.ts now rebuilds its three per-session channels on session_start by replaying the session's own branch through the same observers the live events use. This is option A from the design on the issue.

  • Tool-result channels replay for every session, whatever engine wrote it: a successful read of a SKILL.md, and read/write/edit/apply_patch touching .omo/plans/*.md. Each toolResult entry is paired with its assistant tool call by toolCallId to recover the arguments.
  • The user-request channel replays only from user entries that carry the engine's inputSource (code-yeongyu/senpi#3690), and never from extension. A user entry with no source can't be told apart from injected input, so it never arms the gate. On a senpi without #3690, replay rebuilds the tool channels and the user still re-arms ulw-plan once. Nothing is trusted that wasn't trusted live.
  • The live handlers and the replay share observeUserInput / observeToolResult, so classification and trust are identical by construction. The replay resets the session first, so a second session_start does not double-count touches.

Known residual: a persisted user entry holds the text after skill/template expansion and input transforms, while the live input event saw the raw text. Skill bodies and the injected <ultrawork-mode> / <system-reminder> blocks are stripped as before, but a prompt template's expanded body is classified on replay. The user still chose to run that template, and the entry is still human-sourced.

Evidence (head d693ecfc97)

  • RED: skill-invocation-tracker-replay.test.ts, 4 of 7 failed on the pre-fix tracker (the four positive replays). The three trust-boundary cases (extension-sourced, unsourced, failed or orphaned tool results) pass both before and after by design.
  • GREEN: bun test packages/omo-senpi/src/components/task/skill-invocation-tracker, 51/51 across 3 files.
  • tsgo --noEmit -p packages/omo-senpi/tsconfig.json: exit 0.
  • Real senpi, plan-gated-agents-e2e.mjs --scenario resume (new): results to follow in a comment.

Summary by cubic

Rebuilds the plan gate's per-session state on session_start by replaying the session's transcript, so a restarted or restored session no longer forgets that the user asked for ulw-plan and touched a plan.

  • Replays tool results for every session — successful read of a SKILL.md and read/write/edit/apply_patch touching .omo/plans/*.md — pairing each result with its tool call by toolCallId; failed and orphaned results never count.
  • Replays the user-request channel only from entries carrying inputSource of interactive or rpc; extension, absent, unknown, and message-level sources never arm the gate.
  • Live handlers and replay share observeUserInput/observeToolResult; session state resets first so a second session_start does not double-count touches, and a host that cannot read the branch keeps its live state.
  • Adds a resume scenario to the plan-gated-agents e2e that continues a session in a second process, with --expect replayed and --expect unsourced verdicts.