docs(spec): bump active-agent epoch on foreign-agent boundary retry

Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
This commit is contained in:
yinjialu 2026-05-31 16:28:03 +08:00
parent 35d7701d0c
commit 8b3e5039e1

View file

@ -210,10 +210,19 @@ turn N+1: resolveResumeDecision(conv, agent, epoch, forceFresh, caps)
- `apps/daemon/src/server.ts`: `resolveResumeDecision()`, pin-on-success, - `apps/daemon/src/server.ts`: `resolveResumeDecision()`, pin-on-success,
resume-aware `skipTranscript`, anti-echo guard line, runtime-rejection resume-aware `skipTranscript`, anti-echo guard line, runtime-rejection
fallback. fallback.
- `apps/web/src/`: force-fresh control in the composer; bump the live - `apps/web/src/`: force-fresh control in the composer; on a history
agent-scoped epoch on history edit/retry, scoped to the agent(s) whose edit/truncate/retry, bump the live epoch of **every agent whose
`scopeHistoryToAgent` slice the edited/truncated turn belongs to (so editing a `scopeHistoryToAgent` output changes** as a result of the mutation — defined by
Codex-only turn does not invalidate Claude's pinned session). diffing each agent's scoped slice before vs after, not by which agent's slice
literally contains the mutated turn. This matters because `scopeHistoryToAgent`
cuts each agent's slice at the most recent assistant turn from a *different*
agent (`apps/web/src/providers/daemon.ts:127-135`), so truncating or retrying
that foreign-agent boundary turn (or any of its descendants) shifts or removes
the active agent's resumed suffix even though the mutated turn is not inside the
active agent's slice — that case must bump the active agent's epoch too. The
diff-the-scoped-output rule still prevents a Codex-only edit that does not change
Claude's scoped slice (e.g. editing the *content* of a Codex turn that stays the
boundary) from invalidating Claude's pinned session.
- `apps/daemon/src/cli.ts`: `--fresh-session` flag on the run path (dual-track). - `apps/daemon/src/cli.ts`: `--fresh-session` flag on the run path (dual-track).
### Design Decisions ### Design Decisions
@ -248,9 +257,15 @@ turn N+1: resolveResumeDecision(conv, agent, epoch, forceFresh, caps)
history — the actual prompt source — never changed, making the invalidation history — the actual prompt source — never changed, making the invalidation
model stricter than the prompt source. The live epoch therefore lives in the model stricter than the prompt source. The live epoch therefore lives in the
new `conversation_agent_epoch` table keyed on `(conversationId, agentId)` (the new `conversation_agent_epoch` table keyed on `(conversationId, agentId)` (the
web edit/retry path issues the bump only for the agent(s) whose scoped slice web edit/retry path bumps the epoch of every agent whose `scopeHistoryToAgent`
the mutation touches); the pinned `conversation_agent_session.historyEpoch` output changes under the mutation — which includes the active agent when a
records the epoch a session was produced under. At run start the daemon reads truncate/retry lands on the most recent *foreign-agent* boundary turn or its
descendants, since `scopeHistoryToAgent` cuts the slice at that boundary
(`apps/web/src/providers/daemon.ts:127-135`) and dropping/regenerating it shifts
or removes the active agent's resumed suffix even though the mutated turn sits
outside that agent's slice); the pinned
`conversation_agent_session.historyEpoch` records the epoch a session was
produced under. At run start the daemon reads
`conversation_agent_epoch.history_epoch` for the active `(conversation, agent)` `conversation_agent_epoch.history_epoch` for the active `(conversation, agent)`
to form `currentHistoryEpoch`, then `resolveResumeDecision()` compares it to form `currentHistoryEpoch`, then `resolveResumeDecision()` compares it
against the pinned row; a mismatch invalidates resume (Claude's immutable against the pinned row; a mismatch invalidates resume (Claude's immutable
@ -294,9 +309,18 @@ per `AGENTS.md`:
full transcript present. full transcript present.
- **History-edit epoch guard.** Editing a prior turn in the active agent's scoped - **History-edit epoch guard.** Editing a prior turn in the active agent's scoped
history bumps that agent's epoch → no `--resume` AND full transcript present. history bumps that agent's epoch → no `--resume` AND full transcript present.
Conversely, editing a turn that belongs only to a *different* agent's scoped Conversely, editing the *content* of a turn that belongs only to a *different*
history (e.g. a Codex-only turn) must NOT bump Claude's epoch → Claude still agent's scoped history without changing Claude's scoped slice (e.g. a Codex-only
resumes with `--resume`. turn that stays the boundary) must NOT bump Claude's epoch → Claude still resumes
with `--resume`.
- **Cross-agent boundary retry/truncate guard.** History contains a Codex
(foreign-agent) assistant turn followed by later Claude turns, so Claude's
`scopeHistoryToAgent` slice starts just after that Codex boundary turn. A
retry-from-message or truncate **on the Codex boundary turn** (or its
descendants) drops or shifts Claude's resumed suffix, so it MUST bump Claude's
epoch → no `--resume` AND full transcript present — even though the mutated turn
sits outside Claude's own slice. (This is the case a "mutated turn is inside the
slice" rule would miss: Claude could otherwise resume against stale history.)
- **Work-dir guard.** A pinned session whose `workDir` differs from the current - **Work-dir guard.** A pinned session whose `workDir` differs from the current
run's (e.g. the conversation's project cwd moved) → no `--resume` AND full run's (e.g. the conversation's project cwd moved) → no `--resume` AND full
transcript present. transcript present.