fix(agent-loop): discard an interrupted prefix composition instead of caching it

A cancel/dispose landing inside the first agent/session-prefix waterfall
used to commit the listener chain's return value to the instance cache
before the interruption check dropped the turn; an abort-aware listener's
degraded fallback would then ship on every later request of the instance.
The commit now happens only after the composition survives the
interruption check — the cache only ever holds a fully composed prefix,
and the next turn recomposes under a live signal.
This commit is contained in:
Yichen Jiang
2026-07-08 21:46:03 +08:00
parent 765052a7d1
commit 959a3c3a8b
5 changed files with 67 additions and 20 deletions
+18 -9
View File
@@ -497,18 +497,27 @@ async function runTurn(
// CURRENT request.
if (transmission.sessionPrefix === undefined) {
const emptyPrefix: Message[] = deepFreeze([])
transmission.sessionPrefix = deepFreeze(structuredClone(await ctx.waterfall(
const composed = await ctx.waterfall(
'agent/session-prefix', agent, emptyPrefix, abort.signal,
() => Promise.resolve(emptyPrefix),
)))
}
)
// Interruption landing during prefix composition: mirror the assembly
// window above — drop the about-to-start step without running the seam.
if (handle.isCancelled() || handle.isDisposed()) {
handle.setAbort(undefined)
reason = handle.isDisposed() ? { kind: 'disposed' } : { kind: 'aborted', reason: handle.cancelReason() }
break
// Interruption landing during prefix composition: mirror the assembly
// window above — drop the about-to-start step without running the
// seam, and DISCARD the composition instead of caching it. An
// abort-aware listener may have returned a degraded fallback under
// the firing signal; committing it would ship a prefix no request
// ever used (and no header ever logged) on this instance's next real
// request. The next turn recomposes under a live signal — the cache
// only ever holds a fully composed prefix. The cache-hit path needs
// no such check: nothing awaits between the assembly check above and
// the pre-step seam.
if (handle.isCancelled() || handle.isDisposed()) {
handle.setAbort(undefined)
reason = handle.isDisposed() ? { kind: 'disposed' } : { kind: 'aborted', reason: handle.cancelReason() }
break
}
transmission.sessionPrefix = deepFreeze(structuredClone(composed))
}
// Pre-step surface-mutation checkpoint (compaction), fired OUTSIDE the