Skip to content

Commit b2dadef

Browse files
committed
fix(agents): defer bootstrap context-engine maintenance to background
Bootstrap/reconcile context-engine maintenance runs foreground, where deferred compaction debt cannot execute (allowDeferredCompactionExecution is background-only) and no background follow-up is scheduled — only turns defer. So debt created when bootstrap imports tail messages past the leaf trigger is stranded, leaving sessions repeating "deferred compaction still needed" (issue #67716, Case 1). Extend the deferred-maintenance gate so reason="bootstrap" also schedules the existing background debt consumer for engines that opt into background maintenance (turnMaintenanceMode === "background"). Foreground bootstrap is unchanged for engines without background maintenance, and the plugin-owned hot-cache sticky-debt path (Case 2) is intentionally left out of scope. Closes #67716
1 parent 8b546fa commit b2dadef

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

src/agents/embedded-agent-runner/context-engine-maintenance.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,67 @@ describe("runContextEngineMaintenance", () => {
674674
});
675675
});
676676

677+
it("defers bootstrap maintenance to a background debt consumer for background-mode engines", async () => {
678+
await withStateDirEnv("openclaw-bootstrap-maintenance-", async () => {
679+
resetCommandQueueStateForTest();
680+
resetTaskRegistryForTests({ persist: false });
681+
resetTaskFlowRegistryForTests({ persist: false });
682+
683+
const sessionKey = "agent:main:bootstrap-1";
684+
const maintain = vi.fn(async () => ({
685+
changed: false,
686+
bytesFreed: 0,
687+
rewrittenEntries: 0,
688+
}));
689+
const backgroundEngine = {
690+
info: {
691+
id: "test",
692+
name: "Test Engine",
693+
turnMaintenanceMode: "background" as const,
694+
},
695+
ingest: async () => ({ ingested: true }),
696+
assemble: async ({ messages }: { messages: unknown[] }) => ({
697+
messages,
698+
estimatedTokens: 0,
699+
}),
700+
compact: async () => ({ ok: true, compacted: false }),
701+
maintain,
702+
} as NonNullable<Parameters<typeof runContextEngineMaintenance>[0]["contextEngine"]>;
703+
704+
let deferred: Promise<void> | undefined;
705+
const result = await runContextEngineMaintenance({
706+
contextEngine: backgroundEngine,
707+
sessionId: "bootstrap-1",
708+
sessionKey,
709+
sessionFile: "/tmp/session.jsonl",
710+
reason: "bootstrap",
711+
runtimeContext: {
712+
workspaceDir: "/tmp/workspace",
713+
tokenBudget: 2048,
714+
currentTokenCount: 1536,
715+
},
716+
onDeferredMaintenance: (promise) => {
717+
deferred = promise;
718+
},
719+
});
720+
721+
// Foreground bootstrap maintenance cannot pay deferred compaction debt, so
722+
// a background consumer must be scheduled instead of running inline.
723+
expect(result).toBeUndefined();
724+
const queuedTasks = listTasksForOwnerKey(sessionKey).filter(
725+
(task) => task.taskKind === TURN_MAINTENANCE_TASK_KIND,
726+
);
727+
expect(queuedTasks).toHaveLength(1);
728+
729+
await deferred;
730+
expect(maintain).toHaveBeenCalledTimes(1);
731+
const maintainParams = firstMaintainParams(maintain);
732+
expectRecordFields(requireRecord(maintainParams.runtimeContext, "runtime context"), {
733+
allowDeferredCompactionExecution: true,
734+
});
735+
});
736+
});
737+
677738
it("coalesces repeated requests into one active run plus one follow-up run for the same session", async () => {
678739
await withStateDirEnv("openclaw-turn-maintenance-", async () => {
679740
vi.useFakeTimers();

src/agents/embedded-agent-runner/context-engine-maintenance.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -711,8 +711,12 @@ export async function runContextEngineMaintenance(params: {
711711
}
712712

713713
const executionMode = params.executionMode ?? "foreground";
714+
// Bootstrap/reconcile runs foreground, where deferred compaction debt cannot
715+
// execute (allowDeferredCompactionExecution is background-only). For engines
716+
// that opt into background maintenance, schedule the same background debt
717+
// consumer as turns so bootstrap-created debt is not stranded (issue #67716).
714718
const shouldDefer =
715-
params.reason === "turn" &&
719+
(params.reason === "turn" || params.reason === "bootstrap") &&
716720
executionMode !== "background" &&
717721
params.contextEngine.info.turnMaintenanceMode === "background";
718722

0 commit comments

Comments
 (0)