Skip to content

Commit 66354a4

Browse files
committed
fix: preserve codex preflight compaction route
1 parent 4798264 commit 66354a4

4 files changed

Lines changed: 165 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
1111
- Scripts: remove stale Knip unused-file allowlist entries so the dead-code gate fails only on current findings.
1212
- Tests: normalize bundled plugin lifecycle probe paths and state-root lookup so native Windows release sweeps accept valid packaged plugin installs.
1313
- Config: keep benign legacy metadata write anomalies out of default doctor and config command output while preserving explicit anomaly logging for diagnostics.
14+
- Agents/Codex: route budget preflight compaction through the persisted Codex session model so Slack threads do not require separate plain OpenAI auth. Thanks @amknight.
1415
- Codex: log when implicit app-server `never` approvals are promoted for OpenClaw tool policy, including whether the trigger was a `before_tool_call` hook or trusted tool policy.
1516
- Google Vertex: support production ADC modes such as Workload Identity Federation, service-account credentials, and metadata-server ADC for the native Vertex transport. (#83971) Thanks @damianFelixPago.
1617
- Telegram: route normal `[telegram][diag]` polling diagnostics through `runtime.log` while keeping non-diag warnings and persistence failures on `runtime.error`, so healthy polling startup no longer looks like an error. Fixes #82957. (#82958) Thanks @galiniliev.

src/auto-reply/reply/agent-runner-memory.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ type CompactEmbeddedPiSessionParams = {
7878
sessionFile?: string;
7979
sessionId?: string;
8080
trigger?: string;
81+
provider?: string;
82+
model?: string;
83+
authProfileId?: string;
84+
agentHarnessId?: string;
8185
};
8286

8387
function requireRefreshQueuedFollowupSessionCall(index = 0) {
@@ -865,6 +869,59 @@ describe("runMemoryFlushIfNeeded", () => {
865869
expect(compactCall.currentTokenCount).toBe(347_000);
866870
});
867871

872+
it("uses the persisted Codex model route for OpenAI preflight compaction", async () => {
873+
registerMemoryFlushPlanResolverForTest(() => ({
874+
softThresholdTokens: 4_000,
875+
forceFlushTranscriptBytes: 1_000_000_000,
876+
reserveTokensFloor: 0,
877+
prompt: "Pre-compaction memory flush.\nNO_REPLY",
878+
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
879+
relativePath: "memory/2023-11-14.md",
880+
}));
881+
const sessionEntry: SessionEntry = {
882+
sessionId: "session",
883+
updatedAt: Date.now(),
884+
totalTokens: 250_000,
885+
totalTokensFresh: true,
886+
modelProvider: "codex",
887+
model: "gpt-5.5",
888+
agentHarnessId: "codex",
889+
authProfileOverride: "codex:work",
890+
};
891+
892+
await runPreflightCompactionIfNeeded({
893+
cfg: {
894+
models: {
895+
providers: {
896+
openai: { models: [{ id: "gpt-5.5", contextWindow: 100_000 }] },
897+
codex: { models: [{ id: "gpt-5.5", contextWindow: 100_000 }] },
898+
},
899+
},
900+
agents: { defaults: { compaction: { memoryFlush: {} } } },
901+
} as never,
902+
followupRun: createTestFollowupRun({
903+
provider: "openai",
904+
model: "gpt-5.5",
905+
sessionId: "session",
906+
sessionKey: "main",
907+
}),
908+
defaultModel: "gpt-5.5",
909+
sessionEntry,
910+
sessionStore: { main: sessionEntry },
911+
sessionKey: "main",
912+
storePath: path.join(rootDir, "sessions.json"),
913+
isHeartbeat: false,
914+
replyOperation: createReplyOperation(),
915+
});
916+
917+
expect(compactEmbeddedPiSessionMock).toHaveBeenCalledTimes(1);
918+
const compactCall = requireCompactEmbeddedPiSessionCall();
919+
expect(compactCall.provider).toBe("codex");
920+
expect(compactCall.model).toBe("gpt-5.5");
921+
expect(compactCall.agentHarnessId).toBe("codex");
922+
expect(compactCall.authProfileId).toBe("codex:work");
923+
});
924+
868925
it("still compacts when a fresh persisted token total is over the threshold", async () => {
869926
registerMemoryFlushPlanResolverForTest(() => ({
870927
softThresholdTokens: 4_000,

src/auto-reply/reply/agent-runner-memory.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,25 @@ function resolveFollowupContextConfigProvider(params: {
265265
});
266266
}
267267

268+
function resolvePreflightCompactionRunTarget(params: {
269+
followupRun: FollowupRun;
270+
sessionEntry?: SessionEntry;
271+
}): { provider: string; model: string; authProfileId?: string; agentHarnessId?: string } {
272+
const run = params.followupRun.run;
273+
const matchingSessionEntry =
274+
params.sessionEntry?.sessionId === run.sessionId ? params.sessionEntry : undefined;
275+
const authProfileId =
276+
normalizeOptionalString(matchingSessionEntry?.authProfileOverride) ??
277+
normalizeOptionalString(run.authProfileId);
278+
const agentHarnessId = normalizeOptionalString(matchingSessionEntry?.agentHarnessId);
279+
return {
280+
provider: normalizeOptionalString(matchingSessionEntry?.modelProvider) ?? run.provider,
281+
model: normalizeOptionalString(matchingSessionEntry?.model) ?? run.model,
282+
...(authProfileId ? { authProfileId } : {}),
283+
...(agentHarnessId ? { agentHarnessId } : {}),
284+
};
285+
}
286+
268287
function resolveVisibleMemoryFlushErrorPayloads(payloads?: ReplyPayload[]): ReplyPayload[] {
269288
return (payloads ?? []).filter(
270289
(payload) => payload.isError === true && isRenderablePayload(payload),
@@ -735,6 +754,10 @@ export async function runPreflightCompactionIfNeeded(params: {
735754
params.sessionKey ?? params.followupRun.run.sessionKey,
736755
{ storePath: params.storePath },
737756
);
757+
const compactionRunTarget = resolvePreflightCompactionRunTarget({
758+
followupRun: params.followupRun,
759+
sessionEntry: entry,
760+
});
738761
const result = await memoryDeps.compactEmbeddedPiSession({
739762
sessionId: entry.sessionId,
740763
sessionKey: params.sessionKey,
@@ -753,10 +776,14 @@ export async function runPreflightCompactionIfNeeded(params: {
753776
agentDir: params.followupRun.run.agentDir,
754777
config: params.cfg,
755778
skillsSnapshot: entry.skillsSnapshot ?? params.followupRun.run.skillsSnapshot,
756-
provider: params.followupRun.run.provider,
757-
model: params.followupRun.run.model,
758-
agentHarnessId:
759-
entry.sessionId === params.followupRun.run.sessionId ? entry.agentHarnessId : undefined,
779+
provider: compactionRunTarget.provider,
780+
model: compactionRunTarget.model,
781+
...(compactionRunTarget.authProfileId
782+
? { authProfileId: compactionRunTarget.authProfileId }
783+
: {}),
784+
...(compactionRunTarget.agentHarnessId
785+
? { agentHarnessId: compactionRunTarget.agentHarnessId }
786+
: {}),
760787
thinkLevel: params.followupRun.run.thinkLevel,
761788
bashElevated: params.followupRun.run.bashElevated,
762789
trigger: "budget",

src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,82 @@ describe("runReplyAgent heartbeat followup guard", () => {
413413
});
414414
});
415415

416+
describe("runReplyAgent preflight compaction", () => {
417+
it("keeps Codex-backed Slack sessions on the persisted Codex compaction route", async () => {
418+
const dir = await mkdtemp(join(tmpdir(), "openclaw-codex-preflight-"));
419+
try {
420+
const sessionFile = join(dir, "session.jsonl");
421+
const storePath = join(dir, "sessions.json");
422+
await writeFile(
423+
sessionFile,
424+
`${JSON.stringify({ message: { role: "user", content: "hi" } })}\n`,
425+
"utf8",
426+
);
427+
const sessionEntry: SessionEntry = {
428+
sessionId: "session",
429+
sessionFile,
430+
updatedAt: Date.now(),
431+
totalTokens: 250_000,
432+
totalTokensFresh: true,
433+
modelProvider: "codex",
434+
model: "gpt-5.5",
435+
agentHarnessId: "codex",
436+
};
437+
const sessionStore = { main: sessionEntry };
438+
await writeFile(storePath, JSON.stringify(sessionStore), "utf8");
439+
state.compactEmbeddedPiSessionMock.mockResolvedValueOnce({
440+
ok: true,
441+
compacted: true,
442+
result: { tokensAfter: 42 },
443+
});
444+
445+
const { run } = createMinimalRun({
446+
sessionEntry,
447+
sessionStore,
448+
sessionKey: "main",
449+
storePath,
450+
sessionCtx: { Provider: "slack" },
451+
runOverrides: {
452+
provider: "openai",
453+
model: "gpt-5.5",
454+
sessionId: "session",
455+
sessionFile,
456+
messageProvider: "slack",
457+
config: {
458+
models: {
459+
providers: {
460+
openai: { models: [{ id: "gpt-5.5", contextWindow: 100_000 }] },
461+
codex: { models: [{ id: "gpt-5.5", contextWindow: 100_000 }] },
462+
},
463+
},
464+
agents: { defaults: { compaction: { memoryFlush: {} } } },
465+
},
466+
},
467+
});
468+
469+
await run();
470+
471+
expect(state.compactEmbeddedPiSessionMock).toHaveBeenCalledTimes(1);
472+
const compactCall = requireRecord(
473+
mockCallArgs(state.compactEmbeddedPiSessionMock, "preflight compaction")[0],
474+
"preflight compaction call",
475+
);
476+
expect(compactCall.provider).toBe("codex");
477+
expect(compactCall.model).toBe("gpt-5.5");
478+
expect(compactCall.agentHarnessId).toBe("codex");
479+
480+
const runCall = requireRecord(
481+
mockCallArgs(state.runEmbeddedPiAgentMock, "run embedded pi agent")[0],
482+
"embedded run call",
483+
);
484+
expect(runCall.provider).toBe("openai");
485+
expect(runCall.model).toBe("gpt-5.5");
486+
} finally {
487+
await rm(dir, { recursive: true, force: true });
488+
}
489+
});
490+
});
491+
416492
describe("runReplyAgent pending final delivery capture", () => {
417493
async function createSessionStoreFile(entry: SessionEntry) {
418494
const dir = await mkdtemp(join(tmpdir(), "openclaw-agent-runner-pending-"));

0 commit comments

Comments
 (0)