Summary
Codex /btw side questions can fail after an auto-compaction if the active session entry resolves to a pre-compaction checkpoint transcript.
The Codex app-server parent thread binding is persisted next to the active post-compaction transcript:
session-1.jsonl.codex-app-server.json
But /btw resolves its parent session transcript through resolveBtwSessionTranscriptPath() and then the Codex side-question hook reads:
${resolvedSessionFile}.codex-app-server.json
If resolvedSessionFile is the checkpoint transcript:
session-1.checkpoint.<id>.jsonl
then Codex looks for:
session-1.checkpoint.<id>.jsonl.codex-app-server.json
That file is not written for the checkpoint, so /btw reports:
Codex /btw needs an active Codex thread. Send a normal message first, then try /btw again.
This is misleading because the active post-compaction transcript still has a valid Codex app-server binding.
Affected path
commands-btw.ts
-> runBtwSideQuestion()
-> resolveBtwSessionTranscriptPath()
-> resolveSessionFilePath()
-> harness.runSideQuestion()
-> runCodexAppServerSideQuestion()
-> readCodexAppServerBinding(sessionFile)
The failure happens before model/provider execution is relevant. It is not primarily an openai vs openai-codex provider mismatch. The immediate problem is that the side-question hook receives a checkpoint transcript path that does not have a Codex app-server binding sidecar.
Old code
src/agents/btw-transcript.ts
export function resolveBtwSessionTranscriptPath(params: {
sessionId: string;
sessionEntry?: StoredSessionEntry;
sessionKey?: string;
storePath?: string;
}): string | undefined {
try {
const agentId = params.sessionKey?.split(":")[1];
const pathOpts = resolveSessionFilePathOptions({
agentId,
storePath: params.storePath,
});
return resolveSessionFilePath(params.sessionId, params.sessionEntry, pathOpts);
} catch (error) {
diag.debug(
`resolveSessionTranscriptPath failed: sessionId=${params.sessionId} err=${String(error)}`,
);
return undefined;
}
}
Proposed fix
When /btw resolves to a checkpoint transcript, map it back to the matching checkpoint's post-compaction transcript before invoking the harness. This keeps the Codex side-question hook pointed at the active transcript and its existing .codex-app-server.json sidecar.
src/agents/btw-transcript.ts
export function resolveBtwSessionTranscriptPath(params: {
sessionId: string;
sessionEntry?: StoredSessionEntry;
sessionKey?: string;
storePath?: string;
}): string | undefined {
try {
const agentId = params.sessionKey?.split(":")[1];
const pathOpts = resolveSessionFilePathOptions({
agentId,
storePath: params.storePath,
});
const sessionFile = resolveSessionFilePath(params.sessionId, params.sessionEntry, pathOpts);
return (
resolvePostCompactionSessionTranscriptPath({
sessionFile,
sessionEntry: params.sessionEntry,
pathOpts,
}) ?? sessionFile
);
} catch (error) {
diag.debug(
`resolveSessionTranscriptPath failed: sessionId=${params.sessionId} err=${String(error)}`,
);
return undefined;
}
}
New helper:
function resolvePostCompactionSessionTranscriptPath(params: {
sessionFile: string;
sessionEntry?: StoredSessionEntry;
pathOpts?: ReturnType<typeof resolveSessionFilePathOptions>;
}): string | undefined {
const checkpoints = params.sessionEntry?.compactionCheckpoints;
if (!Array.isArray(checkpoints) || checkpoints.length === 0) {
return undefined;
}
const resolvedSessionFile = path.resolve(params.sessionFile);
for (let index = checkpoints.length - 1; index >= 0; index -= 1) {
const checkpoint = checkpoints[index];
const preSessionId = checkpoint.preCompaction.sessionId?.trim();
if (!preSessionId) {
continue;
}
const preSessionFile = resolveSessionFilePath(
preSessionId,
{ sessionFile: checkpoint.preCompaction.sessionFile },
params.pathOpts,
);
if (path.resolve(preSessionFile) !== resolvedSessionFile) {
continue;
}
const postSessionId = checkpoint.postCompaction.sessionId?.trim();
if (!postSessionId) {
return undefined;
}
return resolveSessionFilePath(
postSessionId,
{ sessionFile: checkpoint.postCompaction.sessionFile },
params.pathOpts,
);
}
return undefined;
}
Proposed regression test
New test file:
src/agents/btw-transcript.test.ts
Coverage:
- If the stored session file points at
session-1.checkpoint.<id>.jsonl, resolveBtwSessionTranscriptPath() returns the matching post-compaction session-1.jsonl.
- If the stored session file already points at the active transcript, the resolver leaves it unchanged.
Representative test:
it("uses the active post-compaction transcript when the stored session file points at a checkpoint", () => {
const sessionsDir = "/tmp/openclaw-btw-session";
const sessionId = "session-1";
const checkpointFile = path.join(
sessionsDir,
"session-1.checkpoint.98904e8a-a402-4d92-8795-4a2d7bba2276.jsonl",
);
const postCompactionFile = path.join(sessionsDir, "session-1.jsonl");
const entry: SessionEntry = {
sessionId,
sessionFile: checkpointFile,
compactionCheckpoints: [
{
checkpointId: "98904e8a-a402-4d92-8795-4a2d7bba2276",
sessionKey: "agent:main:telegram:direct:user",
sessionId,
createdAt: 1_779_695_000_000,
reason: "threshold",
preCompaction: {
sessionId,
sessionFile: checkpointFile,
leafId: "pre-leaf",
},
postCompaction: {
sessionId,
sessionFile: postCompactionFile,
leafId: "post-leaf",
},
},
],
};
expect(
resolveBtwSessionTranscriptPath({
sessionId,
sessionEntry: entry,
sessionKey: "agent:main:telegram:direct:user",
storePath: path.join(sessionsDir, "sessions.json"),
}),
).toBe(postCompactionFile);
});
Local verification
Command:
./node_modules/.bin/vitest run src/agents/btw-transcript.test.ts src/agents/btw.test.ts extensions/codex/src/app-server/side-question.test.ts
Result:
Test Files 5 passed (5)
Tests 83 passed (83)
Security / redaction
This report uses synthetic session ids, checkpoint ids, session keys, and auth profile placeholders only. It does not include OAuth tokens, API keys, real account ids, real thread ids, or real auth profile ids.
Summary
Codex
/btwside questions can fail after an auto-compaction if the active session entry resolves to a pre-compaction checkpoint transcript.The Codex app-server parent thread binding is persisted next to the active post-compaction transcript:
But
/btwresolves its parent session transcript throughresolveBtwSessionTranscriptPath()and then the Codex side-question hook reads:If
resolvedSessionFileis the checkpoint transcript:then Codex looks for:
That file is not written for the checkpoint, so
/btwreports:This is misleading because the active post-compaction transcript still has a valid Codex app-server binding.
Affected path
The failure happens before model/provider execution is relevant. It is not primarily an
openaivsopenai-codexprovider mismatch. The immediate problem is that the side-question hook receives a checkpoint transcript path that does not have a Codex app-server binding sidecar.Old code
src/agents/btw-transcript.tsProposed fix
When
/btwresolves to a checkpoint transcript, map it back to the matching checkpoint's post-compaction transcript before invoking the harness. This keeps the Codex side-question hook pointed at the active transcript and its existing.codex-app-server.jsonsidecar.src/agents/btw-transcript.tsNew helper:
Proposed regression test
New test file:
Coverage:
session-1.checkpoint.<id>.jsonl,resolveBtwSessionTranscriptPath()returns the matching post-compactionsession-1.jsonl.Representative test:
Local verification
Command:
Result:
Security / redaction
This report uses synthetic session ids, checkpoint ids, session keys, and auth profile placeholders only. It does not include OAuth tokens, API keys, real account ids, real thread ids, or real auth profile ids.