fix: re-assemble when afterTurn is unavailable

- Decouple lastSeenLength advancement from afterTurn existence so engines without afterTurn still advance the fence and trigger assemble on new messages
- Use hasNewMessages flag instead of didCallAfterTurn for the assemble gate
- Add test verifying assemble fires on every iteration with new messages when engine lacks afterTurn
This commit is contained in:
Bikkies
2026-04-09 19:00:11 +10:00
committed by Josh Lehman
parent 6a8b59c37d
commit ff9d68bdd9
2 changed files with 32 additions and 5 deletions

View File

@@ -463,6 +463,32 @@ describe("installContextEngineLoopHook", () => {
expect(engine.assemble).toHaveBeenCalledTimes(1);
});
it("keeps calling assemble on subsequent iterations when engine lacks afterTurn but new messages arrive", async () => {
const agent = makeGuardableAgent();
const engine = makeMockEngine({ omitAfterTurn: true });
installContextEngineLoopHook({
agent,
contextEngine: engine,
sessionId,
sessionKey,
sessionFile,
tokenBudget,
modelId,
});
const batch1 = [makeUser("first"), makeToolResult("call_1", "r1")];
await callTransform(agent, batch1);
expect(engine.assemble).toHaveBeenCalledTimes(1);
const batch2 = [...batch1, makeUser("second"), makeToolResult("call_2", "r2")];
await callTransform(agent, batch2);
expect(engine.assemble).toHaveBeenCalledTimes(2);
const batch3 = [...batch2, makeUser("third"), makeToolResult("call_3", "r3")];
await callTransform(agent, batch3);
expect(engine.assemble).toHaveBeenCalledTimes(3);
});
it("falls through to the source messages when engine.afterTurn throws", async () => {
const agent = makeGuardableAgent();
const engine = makeMockEngine({

View File

@@ -222,9 +222,9 @@ export function installContextEngineLoopHook(params: {
lastSeenLength = params.getPrePromptMessageCount?.() ?? 0;
}
let didCallAfterTurn = false;
const hasNewMessages = sourceMessages.length > lastSeenLength;
try {
if (sourceMessages.length > lastSeenLength && typeof contextEngine.afterTurn === "function") {
if (hasNewMessages && typeof contextEngine.afterTurn === "function") {
const prePromptCount = lastSeenLength;
await contextEngine.afterTurn({
sessionId,
@@ -234,13 +234,14 @@ export function installContextEngineLoopHook(params: {
prePromptMessageCount: prePromptCount,
tokenBudget,
});
}
if (hasNewMessages) {
lastSeenLength = sourceMessages.length;
didCallAfterTurn = true;
}
// Skip assemble when nothing has changed since the last call AND we
// already returned an assembled view at least once. The engine's view
// cannot have changed without new messages arriving via afterTurn.
if (didCallAfterTurn || !hasAssembledBefore) {
// cannot have changed without new messages arriving.
if (hasNewMessages || !hasAssembledBefore) {
const assembled = await contextEngine.assemble({
sessionId,
sessionKey,