fix(workspace-context): recompose the baseline on resume, skip only on remount

The mount-local baseline guard was seeded from "a baseline already exists in
the log", which a resumed session and a hot plugin remount both satisfy. That
made a resume skip its baseline, so offline AGENTS.md edits or removals never
reached the first resumed request — violating the documented resume contract.

Distinguish the two by agent/session-start: a startup or resume emits it before
the first step, while a remount attaches to an already-live session and never
witnesses it. Only a remount (no witnessed start, baseline already logged)
keeps the single logged baseline and skips; a resume falls through and
re-composes from current files. Adds a regression that resumes a session with
an offline baseline edit and asserts the fresh baseline reflects it.
This commit is contained in:
_Kerman
2026-07-27 01:35:36 +08:00
parent 9676696a31
commit 107e05e79b
5 files changed
+62 -9

No files matched your search

@@ -50,23 +50,35 @@ export function apply(ctx: Context, config: Config): void {
const instructionVersions: InstructionVersionCache = new WeakMap()
const pendingVersionUpdates = new Map<ToolExecutionToken, InstructionVersionUpdate[]>()
const baselineLoaded = new WeakSet<object>()
// Sessions whose lifecycle start this mount witnessed. A startup or resume
// emits agent/session-start before the first step; a hot remount attaches to
// an already-live session and never sees it. That difference is the only
// reliable way to tell a resumed session (re-compose the baseline from
// current files) from a remount over a live one (keep the single baseline
// already in the log) — the durable log looks identical in both cases.
const lifecycleWitnessed = new WeakSet<object>()
const pendingByParent = new Map<ToolExecutionToken, {
agent: Agent
changes: WorkspaceInstructionChange[]
versionUpdates: InstructionVersionUpdate[]
}>()
ctx.on('agent/session-start', (agent: Agent) => {
lifecycleWitnessed.add(agent.session)
})
ctx.on('session/event', (session, event) => {
observeInstructionSessionEvent(session, event, pendingNestedChanges, instructionVersions)
})
ctx.on('agent/step', async (agent: Agent, _turn, _step, signal): Promise<void> => {
if (baselineLoaded.has(agent.session)) return
// The guard is mount-local, but the baseline is durable: a hot remount
// over a live session must fold the already-appended baseline from the
// log instead of injecting a duplicate.
if (agent.session.events.some(event => event.type === 'user/message'
&& event.data.source.kind === 'plugin' && event.data.source.plugin === 'workspace-context')) {
// A baseline already in the log with no witnessed lifecycle start is a hot
// remount over a live session: keep that single baseline and skip. A
// resumed session witnessed its start, so it falls through and re-composes.
if (!lifecycleWitnessed.has(agent.session)
&& agent.session.events.some(event => event.type === 'user/message'
&& event.data.source.kind === 'plugin' && event.data.source.plugin === 'workspace-context')) {
baselineLoaded.add(agent.session)
return
}