openclaw - 💡(How to fix) Fix Fix Codex /btw parent thread lookup after compaction checkpoints [1 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

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.

Error Message

} catch (error) { resolveSessionTranscriptPath failed: sessionId=${params.sessionId} err=${String(error)}, } catch (error) { resolveSessionTranscriptPath failed: sessionId=${params.sessionId} err=${String(error)},

Root Cause

This is misleading because the active post-compaction transcript still has a valid Codex app-server binding.

Fix Action

Fixed

Code Example

session-1.jsonl.codex-app-server.json

---

${resolvedSessionFile}.codex-app-server.json

---

session-1.checkpoint.<id>.jsonl

---

session-1.checkpoint.<id>.jsonl.codex-app-server.json

---

Codex /btw needs an active Codex thread. Send a normal message first, then try /btw again.

---

commands-btw.ts
  -> runBtwSideQuestion()
  -> resolveBtwSessionTranscriptPath()
  -> resolveSessionFilePath()
  -> harness.runSideQuestion()
  -> runCodexAppServerSideQuestion()
  -> readCodexAppServerBinding(sessionFile)

---

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;
  }
}

---

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;
  }
}

---

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;
}

---

src/agents/btw-transcript.test.ts

---

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);
});

---

./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

---

Test Files  5 passed (5)
Tests       83 passed (83)
RAW_BUFFERClick to expand / collapse

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:

  1. If the stored session file points at session-1.checkpoint.<id>.jsonl, resolveBtwSessionTranscriptPath() returns the matching post-compaction session-1.jsonl.
  2. 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.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING