Compare commits

...

1 Commits

Author SHA1 Message Date
Alex Knight
66354a4258 fix: preserve codex preflight compaction route 2026-05-25 12:21:54 +10:00
4 changed files with 165 additions and 4 deletions

View File

@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
- Scripts: remove stale Knip unused-file allowlist entries so the dead-code gate fails only on current findings.
- Tests: normalize bundled plugin lifecycle probe paths and state-root lookup so native Windows release sweeps accept valid packaged plugin installs.
- Config: keep benign legacy metadata write anomalies out of default doctor and config command output while preserving explicit anomaly logging for diagnostics.
- Agents/Codex: route budget preflight compaction through the persisted Codex session model so Slack threads do not require separate plain OpenAI auth. Thanks @amknight.
- 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.
- 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.
- 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.

View File

@@ -78,6 +78,10 @@ type CompactEmbeddedPiSessionParams = {
sessionFile?: string;
sessionId?: string;
trigger?: string;
provider?: string;
model?: string;
authProfileId?: string;
agentHarnessId?: string;
};
function requireRefreshQueuedFollowupSessionCall(index = 0) {
@@ -865,6 +869,59 @@ describe("runMemoryFlushIfNeeded", () => {
expect(compactCall.currentTokenCount).toBe(347_000);
});
it("uses the persisted Codex model route for OpenAI preflight compaction", async () => {
registerMemoryFlushPlanResolverForTest(() => ({
softThresholdTokens: 4_000,
forceFlushTranscriptBytes: 1_000_000_000,
reserveTokensFloor: 0,
prompt: "Pre-compaction memory flush.\nNO_REPLY",
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
relativePath: "memory/2023-11-14.md",
}));
const sessionEntry: SessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
totalTokens: 250_000,
totalTokensFresh: true,
modelProvider: "codex",
model: "gpt-5.5",
agentHarnessId: "codex",
authProfileOverride: "codex:work",
};
await runPreflightCompactionIfNeeded({
cfg: {
models: {
providers: {
openai: { models: [{ id: "gpt-5.5", contextWindow: 100_000 }] },
codex: { models: [{ id: "gpt-5.5", contextWindow: 100_000 }] },
},
},
agents: { defaults: { compaction: { memoryFlush: {} } } },
} as never,
followupRun: createTestFollowupRun({
provider: "openai",
model: "gpt-5.5",
sessionId: "session",
sessionKey: "main",
}),
defaultModel: "gpt-5.5",
sessionEntry,
sessionStore: { main: sessionEntry },
sessionKey: "main",
storePath: path.join(rootDir, "sessions.json"),
isHeartbeat: false,
replyOperation: createReplyOperation(),
});
expect(compactEmbeddedPiSessionMock).toHaveBeenCalledTimes(1);
const compactCall = requireCompactEmbeddedPiSessionCall();
expect(compactCall.provider).toBe("codex");
expect(compactCall.model).toBe("gpt-5.5");
expect(compactCall.agentHarnessId).toBe("codex");
expect(compactCall.authProfileId).toBe("codex:work");
});
it("still compacts when a fresh persisted token total is over the threshold", async () => {
registerMemoryFlushPlanResolverForTest(() => ({
softThresholdTokens: 4_000,

View File

@@ -265,6 +265,25 @@ function resolveFollowupContextConfigProvider(params: {
});
}
function resolvePreflightCompactionRunTarget(params: {
followupRun: FollowupRun;
sessionEntry?: SessionEntry;
}): { provider: string; model: string; authProfileId?: string; agentHarnessId?: string } {
const run = params.followupRun.run;
const matchingSessionEntry =
params.sessionEntry?.sessionId === run.sessionId ? params.sessionEntry : undefined;
const authProfileId =
normalizeOptionalString(matchingSessionEntry?.authProfileOverride) ??
normalizeOptionalString(run.authProfileId);
const agentHarnessId = normalizeOptionalString(matchingSessionEntry?.agentHarnessId);
return {
provider: normalizeOptionalString(matchingSessionEntry?.modelProvider) ?? run.provider,
model: normalizeOptionalString(matchingSessionEntry?.model) ?? run.model,
...(authProfileId ? { authProfileId } : {}),
...(agentHarnessId ? { agentHarnessId } : {}),
};
}
function resolveVisibleMemoryFlushErrorPayloads(payloads?: ReplyPayload[]): ReplyPayload[] {
return (payloads ?? []).filter(
(payload) => payload.isError === true && isRenderablePayload(payload),
@@ -735,6 +754,10 @@ export async function runPreflightCompactionIfNeeded(params: {
params.sessionKey ?? params.followupRun.run.sessionKey,
{ storePath: params.storePath },
);
const compactionRunTarget = resolvePreflightCompactionRunTarget({
followupRun: params.followupRun,
sessionEntry: entry,
});
const result = await memoryDeps.compactEmbeddedPiSession({
sessionId: entry.sessionId,
sessionKey: params.sessionKey,
@@ -753,10 +776,14 @@ export async function runPreflightCompactionIfNeeded(params: {
agentDir: params.followupRun.run.agentDir,
config: params.cfg,
skillsSnapshot: entry.skillsSnapshot ?? params.followupRun.run.skillsSnapshot,
provider: params.followupRun.run.provider,
model: params.followupRun.run.model,
agentHarnessId:
entry.sessionId === params.followupRun.run.sessionId ? entry.agentHarnessId : undefined,
provider: compactionRunTarget.provider,
model: compactionRunTarget.model,
...(compactionRunTarget.authProfileId
? { authProfileId: compactionRunTarget.authProfileId }
: {}),
...(compactionRunTarget.agentHarnessId
? { agentHarnessId: compactionRunTarget.agentHarnessId }
: {}),
thinkLevel: params.followupRun.run.thinkLevel,
bashElevated: params.followupRun.run.bashElevated,
trigger: "budget",

View File

@@ -413,6 +413,82 @@ describe("runReplyAgent heartbeat followup guard", () => {
});
});
describe("runReplyAgent preflight compaction", () => {
it("keeps Codex-backed Slack sessions on the persisted Codex compaction route", async () => {
const dir = await mkdtemp(join(tmpdir(), "openclaw-codex-preflight-"));
try {
const sessionFile = join(dir, "session.jsonl");
const storePath = join(dir, "sessions.json");
await writeFile(
sessionFile,
`${JSON.stringify({ message: { role: "user", content: "hi" } })}\n`,
"utf8",
);
const sessionEntry: SessionEntry = {
sessionId: "session",
sessionFile,
updatedAt: Date.now(),
totalTokens: 250_000,
totalTokensFresh: true,
modelProvider: "codex",
model: "gpt-5.5",
agentHarnessId: "codex",
};
const sessionStore = { main: sessionEntry };
await writeFile(storePath, JSON.stringify(sessionStore), "utf8");
state.compactEmbeddedPiSessionMock.mockResolvedValueOnce({
ok: true,
compacted: true,
result: { tokensAfter: 42 },
});
const { run } = createMinimalRun({
sessionEntry,
sessionStore,
sessionKey: "main",
storePath,
sessionCtx: { Provider: "slack" },
runOverrides: {
provider: "openai",
model: "gpt-5.5",
sessionId: "session",
sessionFile,
messageProvider: "slack",
config: {
models: {
providers: {
openai: { models: [{ id: "gpt-5.5", contextWindow: 100_000 }] },
codex: { models: [{ id: "gpt-5.5", contextWindow: 100_000 }] },
},
},
agents: { defaults: { compaction: { memoryFlush: {} } } },
},
},
});
await run();
expect(state.compactEmbeddedPiSessionMock).toHaveBeenCalledTimes(1);
const compactCall = requireRecord(
mockCallArgs(state.compactEmbeddedPiSessionMock, "preflight compaction")[0],
"preflight compaction call",
);
expect(compactCall.provider).toBe("codex");
expect(compactCall.model).toBe("gpt-5.5");
expect(compactCall.agentHarnessId).toBe("codex");
const runCall = requireRecord(
mockCallArgs(state.runEmbeddedPiAgentMock, "run embedded pi agent")[0],
"embedded run call",
);
expect(runCall.provider).toBe("openai");
expect(runCall.model).toBe("gpt-5.5");
} finally {
await rm(dir, { recursive: true, force: true });
}
});
});
describe("runReplyAgent pending final delivery capture", () => {
async function createSessionStoreFile(entry: SessionEntry) {
const dir = await mkdtemp(join(tmpdir(), "openclaw-agent-runner-pending-"));