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 changed files with 62 additions and 9 deletions
@@ -1005,6 +1005,47 @@ describe('workspace context request injection', () => {
}
})
it('recomposes the baseline from current files when a resumed session edited it offline', async () => {
const root = await tempRepo()
const home = await tempRepo()
try {
await mkdir(join(root, '.git'), { recursive: true })
await write(join(root, 'AGENTS.md'), 'old root rule')
const ctx = new Context()
await mountWorkspaceContext(ctx, { dshHome: home, maxBytes: 65536 })
const original = stubAgent(root)
await composeBaselinePrefix(ctx, original)
// Offline edit to the baseline file, then resume on a fresh session whose
// seeded log already carries the original baseline. A resumed session is
// registered after this mount's apply(), so the remount guard never seeds
// it: its first step re-composes a fresh baseline from current files,
// reflecting the offline edit before the first resumed request. The old
// baseline stays in history unmutated (note: resume without mutating an
// earlier history event).
await write(join(root, 'AGENTS.md'), 'new root rule after offline edit')
const resumed = stubAgent(root, [...original.session.events])
// Resume announces its lifecycle start before the first step.
agentEvents(ctx, resumed).emit('agent/session-start', 'resume')
await composeBaselinePrefix(ctx, resumed)
const pluginMessages = resumed.session.events.filter(event =>
event.type === 'user/message' && event.data.source.kind === 'plugin'
&& event.data.source.plugin === 'workspace-context')
expect(pluginMessages).toHaveLength(2)
const latest = pluginMessages.at(-1)
expect(latest?.type === 'user/message' && blocksText(latest.data.content))
.toContain('new root rule after offline edit')
const original0 = pluginMessages[0]
expect(original0?.type === 'user/message' && blocksText(original0.data.content))
.toContain('old root rule')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
}
})
it('tracks only baseline files that were actually included under the byte budget', async () => {
const root = await tempRepo()
const home = await tempRepo()