@openclaw/codex 2026.8.1-beta.2 → 2026.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/api.js +146 -0
  2. package/dist/{attempt-client-cleanup-DMLXUfa8.js → attempt-client-cleanup-CVPvp9dL.js} +47 -5
  3. package/dist/{auth-binding-B4FfbcCg.js → auth-binding-Ds_QVm-X.js} +18 -3
  4. package/dist/{binding-connection-I5Bo-AsX.js → binding-connection-CCQFs8lf.js} +15 -3
  5. package/dist/{bounded-turn-qgZmzy_4.js → bounded-turn-rq5tiSXk.js} +27 -26
  6. package/dist/{capabilities-CybGI1oz.js → capabilities-CGm_94eL.js} +1 -1
  7. package/dist/cli-metadata.js +1 -1
  8. package/dist/{command-formatters-B1AEz8ZA.js → command-formatters-C54Yg3eA.js} +2 -2
  9. package/dist/{command-handlers-Db8pu_ds.js → command-handlers-BvOAHTrA.js} +111 -67
  10. package/dist/{compact-LspQaGfl.js → compact-Hd7SAc0N.js} +35 -30
  11. package/dist/{session-binding-DPxIvMgE.js → config-Cxw3Cl3k.js} +361 -1161
  12. package/dist/context-compaction-activity-BOeJf7yo.js +58 -0
  13. package/dist/doctor-contract-api.js +128 -26
  14. package/dist/{dynamic-tools-BVdyWkUD.js → dynamic-tools-BFoUcRdx.js} +1341 -1464
  15. package/dist/{effective-mcp-catalog-DIIhAtOZ.js → effective-mcp-catalog-BTJHnDSx.js} +2 -2
  16. package/dist/elicitation-input-DMkeki8d.js +71 -0
  17. package/dist/{event-projector-assistant-message-BwxV4d6N.js → event-projector-assistant-message-C5-Ds8mA.js} +18 -1
  18. package/dist/{event-projector-usage-e5huIQWu.js → event-projector-usage-D3PHFFXh.js} +18 -18
  19. package/dist/harness-DVfUQBlQ.js +248 -0
  20. package/dist/harness.js +2 -226
  21. package/dist/index.js +405 -299
  22. package/dist/{isolated-completion-C58NbL40.js → isolated-completion-Cbbx_5Ed.js} +4 -4
  23. package/dist/launch-args-sXOAg0SX.js +84 -0
  24. package/dist/managed-binary-ZkGodW19.js +248 -0
  25. package/dist/media-understanding-provider.js +2 -2
  26. package/dist/model-catalog-o6Cgjoss.js +101 -0
  27. package/dist/{models-BFlpn1Ex.js → models-DgFWSuxA.js} +25 -28
  28. package/dist/{native-subagent-monitor-DyTGmTUx.js → native-subagent-monitor-CwFyoylz.js} +385 -94
  29. package/dist/node-exec-server.runtime-DXrqpw1c.js +195 -0
  30. package/dist/plugin-approval-roundtrip-D80UZiJ_.js +95 -0
  31. package/dist/{plugin-marketplace-discovery-CzrGJdCR.js → plugin-marketplace-discovery-mdmScYOj.js} +60 -55
  32. package/dist/{plugin-metadata-cache-CNJPj4_b.js → plugin-metadata-cache-CulOABwu.js} +164 -134
  33. package/dist/{protocol-validators-B4F7L8Rf.js → protocol-validators-Bg5rpNxk.js} +203 -7
  34. package/dist/{rate-limits-DZXmOL8-.js → rate-limits-hFtzt2Up.js} +1 -1
  35. package/dist/{sandbox-guard-Cmwc9eIk.js → request-CCm4LaED.js} +207 -20
  36. package/dist/{run-attempt-P8BdxC1I.js → run-attempt-BwNQDH2u.js} +1506 -836
  37. package/dist/{runtime-artifact-BVSPdd6g.js → runtime-artifact-DeBvleJw.js} +4 -7
  38. package/dist/session-binding-q2WSTVmC.js +947 -0
  39. package/dist/{session-catalog-xwNi_SDM.js → session-catalog-C2Tae4rI.js} +1698 -1346
  40. package/dist/session-catalog-home-id-DAKUrm3C.js +112 -0
  41. package/dist/{session-cli-7hgXZDV7.js → session-cli-BuDjm5Ou.js} +20 -7
  42. package/dist/session-permission-policy-gLm40Vtq.js +160 -0
  43. package/dist/session-retirement-CdkI0bvb.js +90 -0
  44. package/dist/{session-upstream-marker-CrIFX06J.js → session-upstream-marker-Bcy33kam.js} +2 -3
  45. package/dist/{settled-turn-finalizer-BRWBO4MO.js → settled-turn-finalizer-B8JJcxCi.js} +27 -22
  46. package/dist/{shared-client-kIBbGjk4.js → shared-client-kNkljY-R.js} +2023 -607
  47. package/dist/{side-question-9HAL26l9.js → side-question-GwD73tX6.js} +109 -57
  48. package/dist/{thread-lifecycle-Bu1BTDXr.js → thread-lifecycle-Ss-uwnC4.js} +1208 -497
  49. package/dist/{thread-ownership-6kOQHlyC.js → thread-ownership-CeYBqg_4.js} +18 -8
  50. package/dist/{plugin-app-cache-key-C3H2CVp6.js → thread-resume-DF54hagX.js} +64 -6
  51. package/dist/{transcript-mirror-B9gGgnJS.js → transcript-mirror-CkEQIna-.js} +158 -41
  52. package/dist/transport-stdio-C7-s9TLG.js +708 -0
  53. package/dist/{upstream-prompt-provenance-ClRBije0.js → upstream-prompt-provenance-CFmsKwr9.js} +12 -14
  54. package/dist/{upstream-session-fork-CE-_LdnP.js → upstream-session-fork-BACGlOFa.js} +86 -88
  55. package/dist/{usage-CHlV0e-o.js → usage-C2l8Mils.js} +3 -3
  56. package/dist/{web-search-provider.runtime-DRw2Vl42.js → web-search-provider.runtime-B2XgRdFv.js} +3 -3
  57. package/openclaw.plugin.json +50 -2
  58. package/package.json +9 -8
  59. package/dist/app-server-policy-zcBDc1JU.js +0 -49
  60. package/dist/incognito-session-ET_ufDH-.js +0 -2
  61. package/dist/request-BRl5_UfX.js +0 -157
  62. package/dist/session-retirement-DoMMJFKm.js +0 -36
  63. package/dist/thread-resume-CPrJNQVM.js +0 -30
  64. package/dist/transport-stdio-DIImwDVS.js +0 -99
@@ -1,32 +1,41 @@
1
- import { c as reclaimCurrentCodexSessionGeneration, d as sessionBindingIdentity, g as resolveCodexAppServerRuntimeOptions, y as resolveCodexSupervisionAppServerRuntimeOptions } from "./session-binding-DPxIvMgE.js";
2
- import { R as resolveCodexAppServerClientInstanceId, _ as withTimeout, a as getLeasedSharedCodexAppServerClient, at as CODEX_INTERACTIVE_THREAD_SOURCE_KINDS, ct as isJsonObject, dt as resolveCodexAppServerLocalHomeDir, it as CODEX_INTERACTIVE_CUSTOM_THREAD_SOURCES, l as releaseLeasedSharedCodexAppServerClient, z as CodexAppServerRpcError } from "./shared-client-kIBbGjk4.js";
3
- import { n as assertCodexThreadForkParams } from "./protocol-validators-B4F7L8Rf.js";
4
- import { n as listCodexAppServerModels } from "./models-BFlpn1Ex.js";
5
- import { t as buildCodexAppServerConnectionFingerprint } from "./plugin-app-cache-key-C3H2CVp6.js";
6
- import { r as importCodexThreadHistoryToTranscript } from "./transcript-mirror-B9gGgnJS.js";
7
- import { n as describeControlFailure, t as CODEX_CONTROL_METHODS } from "./capabilities-CybGI1oz.js";
8
- import { r as formatCodexDisplayText } from "./command-formatters-B1AEz8ZA.js";
1
+ import { F as resolveCodexAppServerLocalHomeDir, I as resolveCodexAppServerUserHomeDir, P as resolveCodexAppServerHomeDir, T as readCodexPluginConfig, i as resolveCodexAppServerRuntimeOptions, s as resolveCodexSupervisionAppServerRuntimeOptions } from "./config-Cxw3Cl3k.js";
2
+ import { $ as CodexAppServerRpcError, Et as withTimeout, Ht as isJsonObject, Lt as resolveCodexAppServerPreparedAuthHandoff, Mt as resolveCodexAppServerAuthProfileId, Pt as resolveCodexAppServerAuthProfileStore, Q as resolveCodexAppServerClientInstanceId, Rt as CODEX_INTERACTIVE_CUSTOM_THREAD_SOURCES, c as getLeasedSharedCodexAppServerClient, h as releaseLeasedSharedCodexAppServerClient, zt as CODEX_INTERACTIVE_THREAD_SOURCE_KINDS } from "./shared-client-kNkljY-R.js";
3
+ import { c as reclaimCurrentCodexSessionGeneration, d as sessionBindingIdentity } from "./session-binding-q2WSTVmC.js";
4
+ import { i as assertCodexThreadForkParams } from "./protocol-validators-Bg5rpNxk.js";
5
+ import { a as closeCodexStartupClientBestEffort } from "./attempt-client-cleanup-CVPvp9dL.js";
6
+ import { n as listCodexAppServerModels } from "./models-DgFWSuxA.js";
7
+ import { a as replaceCodexCatalogConnectionHomes, n as buildCodexAppServerConnectionFingerprint, t as resumeCodexAppServerThread } from "./thread-resume-DF54hagX.js";
8
+ import { n as codexCatalogHomeId, r as isOpenClawManagedCodexThread, t as canonicalCodexCatalogHome } from "./session-catalog-home-id-DAKUrm3C.js";
9
+ import { i as requestCodexAppServerJson, o as withCodexAppServerJsonClient, r as requestCodexAppServerClientJson } from "./request-CCm4LaED.js";
10
+ import { p as projectCodexUserItemText, r as importCodexThreadHistoryToTranscript } from "./transcript-mirror-CkEQIna-.js";
11
+ import { n as describeControlFailure, t as CODEX_CONTROL_METHODS } from "./capabilities-CGm_94eL.js";
12
+ import { r as formatCodexDisplayText } from "./command-formatters-C54Yg3eA.js";
9
13
  import { n as createCodexCliNodeConversationBindingData } from "./conversation-binding-data-CW2GuupV.js";
10
- import { i as withCodexAppServerJsonClient, n as requestCodexAppServerClientJson, r as requestCodexAppServerJson } from "./request-BRl5_UfX.js";
11
- import { i as createImportedCodexSession, n as codexUpstreamBaseline, r as codexUpstreamContinueResult, t as codexLastTerminalTurnId } from "./session-upstream-marker-CrIFX06J.js";
14
+ import { n as prepareCodexAppServerAuthBinding } from "./auth-binding-Ds_QVm-X.js";
15
+ import { i as createImportedCodexSession, n as codexUpstreamBaseline, r as codexUpstreamContinueResult, t as codexLastTerminalTurnId } from "./session-upstream-marker-Bcy33kam.js";
12
16
  import { asFiniteNumber, isRecord, normalizeBoundedOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
13
17
  import fs from "node:fs/promises";
14
18
  import path from "node:path";
15
19
  import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
16
20
  import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
17
- import { createHash } from "node:crypto";
21
+ import { prepareAgentRuntimeAuth } from "openclaw/plugin-sdk/agent-harness-runtime";
22
+ import fsSync from "node:fs";
18
23
  import os from "node:os";
19
- import { listAgentIds, resolveAllowedModelRef, resolveDefaultAgentDir, resolveDefaultAgentId, resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
20
- import { parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
24
+ import { listAgentIds, resolveAgentDir as resolveAgentDir$1, resolveAgentWorkspaceDir, resolveAllowedModelRef, resolveDefaultAgentDir, resolveDefaultAgentId, resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
25
+ import { runCommandBuffered } from "openclaw/plugin-sdk/process-runtime";
26
+ import { resolveSessionAgentIdsStrict } from "openclaw/plugin-sdk/agent-scope-runtime";
21
27
  import { materializeWindowsSpawnProgram, resolveWindowsSpawnProgram } from "openclaw/plugin-sdk/windows-spawn";
22
- import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
23
- import { spawn } from "node:child_process";
28
+ import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
24
29
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
25
30
  import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
26
31
  import { sanitizeTerminalText } from "openclaw/plugin-sdk/text-chunking";
32
+ import { parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
27
33
  import process$1 from "node:process";
28
- import { listSessionCatalogEntries } from "openclaw/plugin-sdk/session-catalog";
34
+ import { resolveSessionModelRef } from "openclaw/plugin-sdk/model-session-runtime";
35
+ import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
36
+ import { createSessionCatalogAdoptionCoordinator, listSessionCatalogEntries, sessionCatalogAdoptedSessionKey, sessionCatalogAdoptedSourceKey } from "openclaw/plugin-sdk/session-catalog";
29
37
  import { decodeNodePtyResumeParams, resolveNodeHostExecutable, runNodePtyCommand } from "openclaw/plugin-sdk/node-host";
38
+ import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
30
39
  //#region extensions/codex/src/jsonl-lines.ts
31
40
  const JSONL_STREAM_THRESHOLD_BYTES = 4 * 1024 * 1024;
32
41
  const JSONL_READ_CHUNK_BYTES = 1024 * 1024;
@@ -132,11 +141,6 @@ const MAX_SESSION_LIMIT = 50;
132
141
  const DEFAULT_RESUME_TIMEOUT_MS = 20 * 6e4;
133
142
  const SESSION_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
134
143
  const activeResumeSessions = /* @__PURE__ */ new Set();
135
- const DEFAULT_RESUME_SPAWN_RUNTIME = {
136
- platform: process$1.platform,
137
- env: process$1.env,
138
- execPath: process$1.execPath
139
- };
140
144
  function createCodexCliSessionNodeHostCommands() {
141
145
  return [{
142
146
  command: CODEX_CLI_SESSIONS_LIST_COMMAND,
@@ -272,7 +276,7 @@ async function resumeLocalCodexCliSession(paramsJSON) {
272
276
  async function runCodexExecResume(params) {
273
277
  const outputPath = path.join(await fs.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "openclaw-codex-cli-")), "last-message.txt");
274
278
  try {
275
- const invocation = resolveCodexCliResumeSpawnInvocation([
279
+ const args = [
276
280
  "exec",
277
281
  "resume",
278
282
  "--skip-git-repo-check",
@@ -280,45 +284,27 @@ async function runCodexExecResume(params) {
280
284
  outputPath,
281
285
  params.sessionId,
282
286
  "-"
283
- ], {
287
+ ];
288
+ const invocation = materializeWindowsSpawnProgram(resolveWindowsSpawnProgram({
289
+ command: "codex",
284
290
  platform: process$1.platform,
285
291
  env: process$1.env,
286
- execPath: process$1.execPath
287
- });
288
- const child = spawn(invocation.command, invocation.args, {
292
+ execPath: process$1.execPath,
293
+ packageName: "@openai/codex"
294
+ }), args);
295
+ const result = await runCommandBuffered([invocation.command, ...invocation.argv], {
289
296
  cwd: params.cwd || process$1.cwd(),
290
- stdio: [
291
- "pipe",
292
- "pipe",
293
- "pipe"
294
- ],
297
+ input: params.prompt,
295
298
  env: process$1.env,
296
- shell: invocation.shell,
297
- windowsHide: invocation.windowsHide
298
- });
299
- const stdout = [];
300
- const stderr = [];
301
- let timedOut = false;
302
- let forceKillTimeout;
303
- const timeout = setTimeout(() => {
304
- timedOut = true;
305
- child.kill("SIGTERM");
306
- forceKillTimeout = setTimeout(() => child.kill("SIGKILL"), 2e3);
307
- forceKillTimeout.unref?.();
308
- }, params.timeoutMs);
309
- child.stdout.on("data", (chunk) => stdout.push(chunk));
310
- child.stderr.on("data", (chunk) => stderr.push(chunk));
311
- child.stdin.end(params.prompt);
312
- const exitCode = await new Promise((resolve, reject) => {
313
- child.on("error", reject);
314
- child.on("exit", (code) => resolve(code));
315
- }).finally(() => {
316
- clearTimeout(timeout);
317
- if (forceKillTimeout) clearTimeout(forceKillTimeout);
299
+ killGraceMs: 2e3,
300
+ killProcessTree: false,
301
+ terminateOnOutputError: true,
302
+ timeoutMs: params.timeoutMs
318
303
  });
319
- if (timedOut) throw new Error(`codex exec resume timed out after ${String(params.timeoutMs)}ms`);
320
- if (exitCode !== 0) {
321
- const message = Buffer.concat(stderr).toString("utf8").trim() || Buffer.concat(stdout).toString("utf8").trim() || `codex exec resume exited with code ${String(exitCode)}`;
304
+ if (result.termination === "timeout") throw new Error(`codex exec resume timed out after ${String(params.timeoutMs)}ms`);
305
+ if (result.termination === "error" && result.error) throw result.error;
306
+ if (result.code !== 0) {
307
+ const message = result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim() || `codex exec resume exited with code ${String(result.code)}`;
322
308
  throw new Error(message);
323
309
  }
324
310
  return await fs.readFile(outputPath, "utf8");
@@ -329,21 +315,6 @@ async function runCodexExecResume(params) {
329
315
  });
330
316
  }
331
317
  }
332
- function resolveCodexCliResumeSpawnInvocation(args, runtime = DEFAULT_RESUME_SPAWN_RUNTIME) {
333
- const resolved = materializeWindowsSpawnProgram(resolveWindowsSpawnProgram({
334
- command: "codex",
335
- platform: runtime.platform,
336
- env: runtime.env,
337
- execPath: runtime.execPath,
338
- packageName: "@openai/codex"
339
- }), args);
340
- return {
341
- command: resolved.command,
342
- args: resolved.argv,
343
- shell: resolved.shell,
344
- windowsHide: resolved.windowsHide
345
- };
346
- }
347
318
  async function readHistorySessions(codexHome) {
348
319
  const summaries = /* @__PURE__ */ new Map();
349
320
  if (!(await visitJsonlLines(path.join(codexHome, "history.jsonl"), (line) => {
@@ -580,6 +551,89 @@ function formatNodeLabel(node) {
580
551
  }
581
552
  //#endregion
582
553
  //#region extensions/codex/src/command-rpc.ts
554
+ async function prepareControlAuth(options, startOptions) {
555
+ if (!options.onResponse || !options.config || !options.sessionKey || options.authProfileId === null || startOptions.homeScope === "user") return {
556
+ authProfileId: options.authProfileId ?? void 0,
557
+ clientOptions: { authProfileId: options.authProfileId }
558
+ };
559
+ const config = options.config;
560
+ const { sessionAgentId } = resolveSessionAgentIdsStrict({
561
+ config,
562
+ sessionKey: options.sessionKey,
563
+ agentId: options.agentId
564
+ });
565
+ const agentDir = options.agentDir ?? resolveAgentDir$1(config, sessionAgentId);
566
+ const workspaceDir = resolveAgentWorkspaceDir(config, sessionAgentId);
567
+ const entry = getSessionEntry({
568
+ agentId: sessionAgentId,
569
+ storePath: resolveStorePath(config.session?.store, { agentId: sessionAgentId }),
570
+ sessionKey: options.sessionKey,
571
+ hydrateSkillPromptRefs: false,
572
+ readConsistency: "latest"
573
+ });
574
+ const model = resolveSessionModelRef(config, entry, sessionAgentId);
575
+ const store = resolveCodexAppServerAuthProfileStore({
576
+ agentDir,
577
+ config
578
+ });
579
+ const { plan, attempts } = prepareAgentRuntimeAuth({
580
+ provider: model.provider,
581
+ modelId: model.model,
582
+ config,
583
+ agentDir,
584
+ workspaceDir,
585
+ authProfileStore: store,
586
+ sessionAuthProfileId: entry?.authProfileOverride ?? options.authProfileId,
587
+ sessionAuthProfileSource: entry?.authProfileOverrideSource,
588
+ harnessId: "codex",
589
+ harnessAuthBootstrap: "harness"
590
+ });
591
+ const route = plan.modelRoute;
592
+ const resolvedAuth = route ? await resolveApiKeyForProvider({
593
+ provider: route.provider,
594
+ modelId: route.modelId,
595
+ modelApi: route.api,
596
+ cfg: config,
597
+ agentDir,
598
+ workspaceDir,
599
+ store,
600
+ profileId: attempts[0]?.profileId,
601
+ lockedProfile: plan.forwardedAuthProfileSource === "user",
602
+ allowAuthProfileFallback: attempts[0]?.allowAuthProfileFallback,
603
+ skipSetupProviderFallback: true
604
+ }) : void 0;
605
+ const handoff = await resolveCodexAppServerPreparedAuthHandoff({
606
+ authRequirement: route?.authRequirement,
607
+ resolvedApiKey: resolvedAuth?.apiKey,
608
+ authProfileId: route ? plan.forwardedAuthProfileId : resolveCodexAppServerAuthProfileId({
609
+ authProfileId: plan.forwardedAuthProfileId,
610
+ store,
611
+ config
612
+ }),
613
+ authProfileStore: store,
614
+ agentDir,
615
+ homeScope: startOptions.homeScope ?? "agent",
616
+ config,
617
+ subscriptionProfileRequiredError: "Prepared Codex subscription route requires a forwarded OpenAI OAuth or token profile.",
618
+ subscriptionProfileUnusableError: "Prepared Codex subscription auth profile is unusable."
619
+ });
620
+ const binding = handoff.authProfileId ? await prepareCodexAppServerAuthBinding({
621
+ authProfileId: handoff.authProfileId,
622
+ authProfileStore: store,
623
+ agentDir,
624
+ config
625
+ }) : void 0;
626
+ return {
627
+ authProfileId: handoff.authProfileId,
628
+ clientOptions: {
629
+ ...handoff.preparedAuth ? { preparedAuth: handoff.preparedAuth } : { authProfileId: handoff.authProfileId },
630
+ authRequirement: route?.authRequirement,
631
+ authProfileStore: binding?.authProfileStore ?? store,
632
+ authBindingFingerprint: binding?.fingerprint,
633
+ agentDir
634
+ }
635
+ };
636
+ }
583
637
  function requestOptions(pluginConfig, limit, config, agentDir) {
584
638
  const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig });
585
639
  return {
@@ -592,22 +646,45 @@ function requestOptions(pluginConfig, limit, config, agentDir) {
592
646
  }
593
647
  async function codexControlRequest(pluginConfig, method, requestParams, options = {}) {
594
648
  const runtime = options.startOptions ? resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig }) : resolveCodexAppServerRuntimeOptions({ pluginConfig });
649
+ const startOptions = options.startOptions ?? runtime.start;
650
+ const auth = await prepareControlAuth(options, startOptions);
595
651
  const controlRequestOptions = {
596
652
  timeoutMs: options.timeoutMs ?? runtime.requestTimeoutMs,
597
- startOptions: options.startOptions ?? runtime.start,
653
+ startOptions,
598
654
  config: options.config,
599
655
  sessionKey: options.sessionKey,
600
656
  sessionId: options.sessionId,
601
- authProfileId: options.authProfileId,
602
657
  agentDir: options.agentDir,
603
- isolated: options.isolated
658
+ isolated: options.isolated,
659
+ ...auth.clientOptions
604
660
  };
605
- if (options.onResponse) return await withCodexAppServerJsonClient(controlRequestOptions, async (request, client) => {
606
- const response = await request({
661
+ if (options.onResponse || options.beforeRequest) return await withCodexAppServerJsonClient(controlRequestOptions, async (request, client, scope) => {
662
+ await options.beforeRequest?.(request);
663
+ scope.assertCurrent();
664
+ let response;
665
+ if (method === "thread/resume") {
666
+ if (!isJsonObject(requestParams) || typeof requestParams.threadId !== "string") throw new Error("Codex thread/resume requires a thread id.");
667
+ response = await resumeCodexAppServerThread({
668
+ client,
669
+ request: {
670
+ ...requestParams,
671
+ threadId: requestParams.threadId
672
+ },
673
+ requestResume: () => request({
674
+ method,
675
+ requestParams
676
+ }),
677
+ abandonClient: () => closeCodexStartupClientBestEffort(client)
678
+ });
679
+ } else response = await request({
607
680
  method,
608
681
  requestParams
609
682
  });
610
- await options.onResponse(response, client);
683
+ await options.onResponse?.(response, client, {
684
+ authProfileId: auth.authProfileId,
685
+ assertCurrent: scope.assertCurrent
686
+ });
687
+ scope.assertCurrent();
611
688
  return response;
612
689
  });
613
690
  return await requestCodexAppServerJson({
@@ -714,29 +791,6 @@ async function assertCodexArchiveDescendantsUnowned(params) {
714
791
  throw new Error("Codex descendant enumeration exceeded its safety limit");
715
792
  }
716
793
  //#endregion
717
- //#region extensions/codex/src/session-catalog-create.ts
718
- const CODEX_AGENT_RUNTIME_ID = "codex";
719
- const CODEX_CATALOG_DEFAULT_MODEL_REF = "openai/gpt-5.6-sol";
720
- function resolveCodexCatalogCreateSession(config, requestedAgentId) {
721
- if (!config) return;
722
- const agentId = requestedAgentId ?? resolveDefaultAgentId(config);
723
- const defaultModel = resolveDefaultModelForAgent({
724
- cfg: config,
725
- agentId
726
- });
727
- return "error" in resolveAllowedModelRef({
728
- cfg: config,
729
- catalog: [],
730
- raw: CODEX_CATALOG_DEFAULT_MODEL_REF,
731
- defaultProvider: defaultModel.provider,
732
- defaultModel: defaultModel.model,
733
- agentId
734
- }) ? void 0 : {
735
- model: CODEX_CATALOG_DEFAULT_MODEL_REF,
736
- agentRuntime: CODEX_AGENT_RUNTIME_ID
737
- };
738
- }
739
- //#endregion
740
794
  //#region extensions/codex/src/session-catalog-parsing.ts
741
795
  const DEFAULT_PAGE_LIMIT = 50;
742
796
  const CODEX_APP_SERVER_THREADS_CAPABILITY = "codex-app-server-threads";
@@ -962,7 +1016,7 @@ function filterCatalogPageByTitle(page, searchTerm) {
962
1016
  if (!searchTerm) return page;
963
1017
  return {
964
1018
  ...page,
965
- sessions: page.sessions.filter((session) => session.name?.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()))
1019
+ sessions: page.sessions.filter((session) => (session.name ?? session.fallbackName)?.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()))
966
1020
  };
967
1021
  }
968
1022
  function unwrapNodeInvokePayload(value) {
@@ -1030,24 +1084,9 @@ function adoptionSessionKeyRest(sessionKey) {
1030
1084
  const trimmed = sessionKey.trim();
1031
1085
  return parseAgentSessionKey(trimmed)?.rest ?? trimmed;
1032
1086
  }
1033
- function listSupervisionAgentIds(config) {
1034
- const defaultAgentId = resolveDefaultAgentId(config);
1035
- return [defaultAgentId, ...listAgentIds(config).filter((agentId) => agentId !== defaultAgentId)];
1036
- }
1037
- function adoptedSourceKey(hostId, threadId) {
1038
- return `${hostId}\u0000${threadId}`;
1039
- }
1040
- function lastTerminalTurnId(thread) {
1041
- for (let index = (thread.turns?.length ?? 0) - 1; index >= 0; index -= 1) {
1042
- const turn = thread.turns?.[index];
1043
- const turnId = boundedCatalogString(turn?.id, 256);
1044
- if (!turnId) continue;
1045
- if (turn?.status === "completed" || turn?.status === "interrupted" || turn?.status === "failed") return turnId;
1046
- }
1047
- }
1048
1087
  function nodeAdoptionSessionKey(hostId, threadId) {
1049
- const digest = createHash("sha256").update(JSON.stringify([hostId, threadId])).digest("hex");
1050
- return `${CODEX_NODE_SESSION_KEY_PREFIX}${digest}`;
1088
+ const source = JSON.stringify([hostId, threadId]);
1089
+ return sessionCatalogAdoptedSessionKey(CODEX_NODE_SESSION_KEY_PREFIX, source);
1051
1090
  }
1052
1091
  function readNodeSessionMarker(entry) {
1053
1092
  const codex = isRecord(entry.pluginExtensions?.codex) ? entry.pluginExtensions.codex : void 0;
@@ -1063,6 +1102,7 @@ function readNodeSessionMarker(entry) {
1063
1102
  function listNodeAdoptedSessionEntries(params) {
1064
1103
  const adopted = /* @__PURE__ */ new Map();
1065
1104
  for (const { agentId, entry, sessionKey } of listSessionCatalogEntries({
1105
+ ...params.agentId ? { agentId: params.agentId } : {},
1066
1106
  config: params.config ?? {},
1067
1107
  runtime: params.runtime,
1068
1108
  sessionEntries: params.sessionEntries
@@ -1070,7 +1110,7 @@ function listNodeAdoptedSessionEntries(params) {
1070
1110
  const marker = readNodeSessionMarker(entry);
1071
1111
  const sessionId = entry.sessionId?.trim();
1072
1112
  if (!marker || marker.initializing === true && params.includeInitializing !== true || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true || !sessionId || adoptionSessionKeyRest(sessionKey) !== nodeAdoptionSessionKey(marker.sourceHostId, marker.sourceThreadId) || marker.sourceHostId !== `node:${marker.nodeId}`) continue;
1073
- const sourceKey = adoptedSourceKey(marker.sourceHostId, marker.sourceThreadId);
1113
+ const sourceKey = sessionCatalogAdoptedSourceKey(marker.sourceHostId, marker.sourceThreadId);
1074
1114
  if (adopted.has(sourceKey)) throw new Error(`multiple OpenClaw sessions adopt Codex thread ${marker.sourceThreadId} on ${marker.sourceHostId}`);
1075
1115
  adopted.set(sourceKey, {
1076
1116
  key: sessionKey,
@@ -1082,7 +1122,7 @@ function listNodeAdoptedSessionEntries(params) {
1082
1122
  return adopted;
1083
1123
  }
1084
1124
  function findNodeAdoptedSessionEntry(params) {
1085
- return listNodeAdoptedSessionEntries(params).get(adoptedSourceKey(params.hostId, params.threadId));
1125
+ return listNodeAdoptedSessionEntries(params).get(sessionCatalogAdoptedSourceKey(params.hostId, params.threadId));
1086
1126
  }
1087
1127
  function nodeSessionMarker(params) {
1088
1128
  return {
@@ -1130,6 +1170,7 @@ async function finalizeNodeAdoptedSession(params) {
1130
1170
  }
1131
1171
  async function createOrReuseNodeAdoptedSession(params) {
1132
1172
  const existing = findNodeAdoptedSessionEntry({
1173
+ agentId: params.agentId,
1133
1174
  config: params.config,
1134
1175
  runtime: params.api.runtime,
1135
1176
  hostId: params.hostId,
@@ -1149,9 +1190,9 @@ async function createOrReuseNodeAdoptedSession(params) {
1149
1190
  const created = await params.api.runtime.agent.session.createSessionEntry({
1150
1191
  cfg: params.config,
1151
1192
  key: nodeAdoptionSessionKey(params.hostId, params.record.threadId),
1152
- agentId: resolveDefaultAgentId(params.config),
1193
+ agentId: params.agentId,
1153
1194
  recoverMatchingInitialEntry: true,
1154
- ...params.record.name?.trim() ? { label: params.record.name.trim() } : {},
1195
+ displayName: params.record.name ?? void 0,
1155
1196
  ...params.record.cwd?.trim() ? { spawnedCwd: params.record.cwd.trim() } : {},
1156
1197
  initialEntry: {
1157
1198
  agentHarnessId: "codex",
@@ -1182,6 +1223,7 @@ async function createOrReuseNodeAdoptedSession(params) {
1182
1223
  };
1183
1224
  } catch (error) {
1184
1225
  const raced = findNodeAdoptedSessionEntry({
1226
+ agentId: params.agentId,
1185
1227
  config: params.config,
1186
1228
  runtime: params.api.runtime,
1187
1229
  hostId: params.hostId,
@@ -1193,346 +1235,127 @@ async function createOrReuseNodeAdoptedSession(params) {
1193
1235
  }
1194
1236
  }
1195
1237
  //#endregion
1196
- //#region extensions/codex/src/session-catalog-node-continue.ts
1197
- const CODEX_NODE_CONTINUE_COMMANDS = [
1198
- CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1199
- CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
1200
- CODEX_CLI_SESSION_RESUME_COMMAND
1201
- ];
1202
- const NODE_CATALOG_LIST_RESPONSE_TIMEOUT_MS = 8e3;
1203
- function nodeLabel(node) {
1204
- return node.displayName?.trim() || node.remoteIp?.trim() || node.nodeId;
1238
+ //#region extensions/codex/src/session-catalog-terminal.ts
1239
+ const CODEX_TERMINAL_RESUME_COMMAND = "codex.terminal.resume.v1";
1240
+ function resolveCodexCatalogTerminalHome(sources) {
1241
+ const runtimeConfig = sources.getRuntimeConfig();
1242
+ if (!runtimeConfig) throw new Error("OpenClaw runtime config is unavailable");
1243
+ const agentDir = sources.source?.agentDir ?? (sources.agentId ? resolveAgentDir$1(runtimeConfig, sources.agentId) : resolveDefaultAgentDir(runtimeConfig));
1244
+ return resolveCodexAppServerLocalHomeDir(sources.source?.appServer.start ?? resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig: sources.getPluginConfig() }).start, agentDir);
1205
1245
  }
1206
- function compareNodeLabels(left, right) {
1207
- const leftLabel = nodeLabel(left);
1208
- const rightLabel = nodeLabel(right);
1209
- if (leftLabel < rightLabel) return -1;
1210
- if (leftLabel > rightLabel) return 1;
1211
- return 0;
1246
+ function resolveLocalCodexTerminalExecutable(env = process.env) {
1247
+ return resolveLocalCodexTerminalResolution(env)?.executable;
1212
1248
  }
1213
- function canContinueCodexOnNode(node) {
1214
- return node.connected === true && CODEX_NODE_CONTINUE_COMMANDS.every((command) => node.commands?.includes(command) === true && node.invocableCommands?.includes(command) === true);
1249
+ function resolveLocalCodexTerminalResolution(env = process.env) {
1250
+ return resolveNodeHostExecutable("codex", {
1251
+ env,
1252
+ pathEnv: env.PATH ?? env.Path ?? "",
1253
+ strategy: "fallback"
1254
+ });
1215
1255
  }
1216
- async function listPairedNode(params) {
1217
- const hostId = `node:${params.node.nodeId}`;
1218
- const common = {
1219
- hostId,
1220
- label: nodeLabel(params.node),
1221
- kind: "node",
1222
- nodeId: params.node.nodeId,
1223
- canContinueCodex: canContinueCodexOnNode(params.node)
1224
- };
1225
- if (params.node.connected !== true) {
1226
- const host = {
1227
- ...common,
1228
- connected: false,
1229
- sessions: [],
1230
- error: {
1231
- code: "NODE_OFFLINE",
1232
- message: "Paired node is offline"
1233
- }
1234
- };
1235
- params.onHost?.(host);
1236
- return host;
1237
- }
1238
- const eventualHost = Promise.resolve().then(async () => {
1239
- const page = filterCatalogPageByTitle(parseCatalogPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1240
- nodeId: params.node.nodeId,
1241
- command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1242
- params: {
1243
- cursor: params.query.cursors?.[hostId],
1244
- limit: params.query.limitPerHost,
1245
- searchTerm: params.query.search
1246
- },
1247
- timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1248
- scopes: ["operator.write"]
1249
- }))), params.query.search);
1250
- return {
1251
- ...common,
1252
- connected: true,
1253
- ...page,
1254
- sessions: page.sessions.map((session) => {
1255
- const adopted = params.adoptedSessions.get(adoptedSourceKey(hostId, session.threadId));
1256
- return adopted ? Object.assign({}, session, { sessionKey: adopted.key }) : session;
1257
- })
1258
- };
1259
- }).catch((error) => ({
1260
- ...common,
1261
- connected: true,
1262
- sessions: [],
1263
- error: catalogError("NODE_INVOKE_FAILED", error)
1264
- }));
1265
- if (params.onHost) eventualHost.then(params.onHost).catch(() => void 0);
1266
- try {
1267
- return await withTimeout(eventualHost, NODE_CATALOG_LIST_RESPONSE_TIMEOUT_MS, "paired node Codex session catalog timed out");
1268
- } catch (error) {
1269
- return {
1270
- ...common,
1271
- connected: true,
1272
- sessions: [],
1273
- error: catalogError("NODE_INVOKE_FAILED", error)
1274
- };
1256
+ function codexNodeTerminalCapability(node) {
1257
+ const commands = node.invocableCommands ?? node.commands;
1258
+ return node.connected === true && commands?.includes("codex.terminal.resume.v1") === true ? { canOpenTerminalCodex: true } : {};
1259
+ }
1260
+ async function requireCatalogEligibleThread(control, threadId) {
1261
+ const cached = await findCatalogEligibleThread(control, threadId, false);
1262
+ if (cached) return cached;
1263
+ const refreshed = await findCatalogEligibleThread(control, threadId, true);
1264
+ if (refreshed) return refreshed;
1265
+ throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1266
+ }
1267
+ async function findCatalogEligibleThread(control, threadId, forceRefresh) {
1268
+ let cursor;
1269
+ const seenCursors = /* @__PURE__ */ new Set();
1270
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
1271
+ const page = await control.listPage({
1272
+ limit: 100,
1273
+ ...cursor ? { cursor } : {},
1274
+ ...forceRefresh ? { forceRefresh: true } : {}
1275
+ });
1276
+ const candidate = page.sessions.find((session) => session.threadId === threadId);
1277
+ if (candidate) {
1278
+ if (isInteractiveThreadSource(candidate.source)) return candidate;
1279
+ throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1280
+ }
1281
+ const nextCursor = page.nextCursor?.trim();
1282
+ if (!nextCursor) return;
1283
+ if (seenCursors.has(nextCursor)) throw new CatalogParamsError("Codex session eligibility could not be verified");
1284
+ seenCursors.add(nextCursor);
1285
+ cursor = nextCursor;
1275
1286
  }
1287
+ throw new CatalogParamsError("Codex session eligibility could not be verified");
1276
1288
  }
1277
- async function requireNodeForCodexContinue(params) {
1278
- const nodeId = params.hostId.slice(5).trim();
1279
- if (!nodeId || params.hostId !== `node:${nodeId}`) throw new CatalogParamsError("Codex session catalog hostId is invalid");
1280
- const node = (await params.runtime.nodes.list()).nodes.find((candidate) => candidate.nodeId === nodeId);
1281
- if (!node || !canContinueCodexOnNode(node)) throw new CatalogParamsError("paired node does not permit Codex session continuation");
1289
+ function createCodexTerminalNodeHostCommand(bindRequest, configSources) {
1282
1290
  return {
1283
- node,
1284
- nodeId
1291
+ command: CODEX_TERMINAL_RESUME_COMMAND,
1292
+ cap: CODEX_APP_SERVER_THREADS_CAPABILITY,
1293
+ dangerous: false,
1294
+ duplex: true,
1295
+ isAvailable: ({ env }) => Boolean(resolveNodeHostExecutable("codex", {
1296
+ env,
1297
+ pathEnv: env.PATH ?? env.Path ?? "",
1298
+ strategy: "direct"
1299
+ })),
1300
+ handle: async (paramsJSON, io) => {
1301
+ if (!io) throw new Error("Codex terminal command requires duplex transport");
1302
+ const request = bindRequest(paramsJSON);
1303
+ const resume = decodeNodePtyResumeParams(request.paramsJSON, (value) => {
1304
+ if (typeof value !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(value)) throw new CatalogParamsError("threadId must be a UUID");
1305
+ return value;
1306
+ });
1307
+ const record = await requireCatalogEligibleThread(request.control, resume.threadId);
1308
+ const resolution = resolveNodeHostExecutable("codex", {
1309
+ env: process.env,
1310
+ pathEnv: process.env.PATH ?? process.env.Path ?? "",
1311
+ strategy: "direct"
1312
+ });
1313
+ if (!resolution) throw new Error("Codex CLI is unavailable");
1314
+ return JSON.stringify(await runNodePtyCommand({
1315
+ file: resolution.executable,
1316
+ args: ["resume", resume.threadId],
1317
+ cwd: record.cwd,
1318
+ env: { CODEX_HOME: resolveCodexCatalogTerminalHome({
1319
+ ...configSources,
1320
+ agentId: request.agentId
1321
+ }) },
1322
+ cols: resume.cols,
1323
+ rows: resume.rows
1324
+ }, io));
1325
+ }
1285
1326
  };
1286
1327
  }
1287
- async function resolveNodeCodexRecord(params) {
1328
+ async function resolveNodeCatalogEligibleThread(params) {
1288
1329
  let cursor;
1289
1330
  const seenCursors = /* @__PURE__ */ new Set();
1290
1331
  for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
1291
- const page = parseCatalogPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1332
+ const raw = await params.runtime.nodes.invoke({
1292
1333
  nodeId: params.nodeId,
1293
1334
  command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1294
1335
  params: {
1336
+ agentId: params.agentId,
1295
1337
  limit: 100,
1296
1338
  ...cursor ? { cursor } : {}
1297
1339
  },
1298
1340
  timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1299
1341
  scopes: ["operator.write"]
1300
- })));
1342
+ });
1343
+ const page = params.parseCatalogPage(unwrapNodeInvokePayload(raw));
1301
1344
  const record = page.sessions.find((candidate) => candidate.threadId === params.threadId);
1302
- if (record) return record;
1345
+ if (record) {
1346
+ if (isInteractiveThreadSource(record.source)) return record;
1347
+ break;
1348
+ }
1303
1349
  const nextCursor = page.nextCursor?.trim();
1304
- if (!nextCursor) break;
1305
- if (seenCursors.has(nextCursor)) throw new CatalogParamsError("Codex session eligibility could not be verified");
1350
+ if (!nextCursor || seenCursors.has(nextCursor)) break;
1306
1351
  seenCursors.add(nextCursor);
1307
1352
  cursor = nextCursor;
1308
1353
  }
1309
- throw new CatalogParamsError("Codex session is unavailable on the paired node");
1310
- }
1311
- function requireContinuableNodeRecord(record) {
1312
- if (record.archived) throw new CatalogParamsError("Codex session is archived on the paired node");
1313
- if (!isInteractiveThreadSource(record.source)) throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1314
- if (record.status === "idle" || record.status === "notLoaded") return;
1315
- if (record.status === "active") throw new CatalogParamsError("Codex session is active on the paired node; wait for it to finish before continuing");
1316
- throw new CatalogParamsError("Codex session cannot be continued in its current state");
1317
- }
1318
- async function readNodeCodexHistory(params) {
1319
- const page = parseTranscriptPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1320
- nodeId: params.nodeId,
1321
- command: CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
1322
- params: {
1323
- threadId: params.record.threadId,
1324
- limit: 50
1325
- },
1326
- timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1327
- scopes: ["operator.write"]
1328
- })));
1329
- const thread = {
1330
- id: params.record.threadId,
1331
- createdAt: params.record.createdAt ?? 0,
1332
- modelProvider: params.record.modelProvider ?? "openai",
1333
- turns: page.data.toReversed()
1334
- };
1335
- return {
1336
- thread,
1337
- throughTurnId: lastTerminalTurnId(thread) ?? null
1338
- };
1339
- }
1340
- async function continueNodeCodexSessionInner(params) {
1341
- const { nodeId } = await requireNodeForCodexContinue({
1342
- runtime: params.api.runtime,
1343
- hostId: params.hostId
1344
- });
1345
- const record = await resolveNodeCodexRecord({
1346
- runtime: params.api.runtime,
1347
- nodeId,
1348
- threadId: params.threadId
1349
- });
1350
- requireContinuableNodeRecord(record);
1351
- const existing = findNodeAdoptedSessionEntry({
1352
- config: params.config,
1353
- runtime: params.api.runtime,
1354
- hostId: params.hostId,
1355
- threadId: params.threadId,
1356
- includeInitializing: true
1357
- });
1358
- let adopted;
1359
- let disposition;
1360
- if (existing) {
1361
- adopted = existing;
1362
- disposition = "existing";
1363
- } else {
1364
- const history = await readNodeCodexHistory({
1365
- runtime: params.api.runtime,
1366
- nodeId,
1367
- record
1368
- });
1369
- adopted = await createOrReuseNodeAdoptedSession({
1370
- api: params.api,
1371
- config: params.config,
1372
- hostId: params.hostId,
1373
- nodeId,
1374
- record,
1375
- history
1376
- });
1377
- disposition = "forked";
1378
- }
1379
- const marker = nodeSessionMarker({
1380
- hostId: params.hostId,
1381
- threadId: params.threadId,
1382
- nodeId
1383
- });
1384
- return {
1385
- sessionKey: adopted.key,
1386
- disposition,
1387
- conversationBinding: {
1388
- summary: "Continue this Codex session on its paired node.",
1389
- detachHint: "Start a new chat to leave the paired-node Codex session.",
1390
- data: createCodexCliNodeConversationBindingData({
1391
- nodeId,
1392
- sessionId: record.sessionId?.trim() || params.threadId,
1393
- agentId: adopted.agentId,
1394
- cwd: record.cwd
1395
- })
1396
- },
1397
- afterConversationBound: async () => await finalizeNodeAdoptedSession({
1398
- api: params.api,
1399
- adopted,
1400
- marker
1401
- })
1402
- };
1403
- }
1404
- async function continueNodeCodexSession(params) {
1405
- if (params.clientScopes?.includes("operator.admin") !== true) throw new CatalogParamsError("continuing a paired-node Codex session requires operator.admin");
1406
- const nodeId = params.hostId.slice(5).trim();
1407
- if (!nodeId || params.hostId !== `node:${nodeId}`) throw new CatalogParamsError("Codex session catalog hostId is invalid");
1408
- const sourceKey = adoptedSourceKey(`node:${nodeId}`, params.threadId);
1409
- const current = continueOperations.get(sourceKey);
1410
- if (current) return await current;
1411
- const operation = runSessionActionExclusive(sourceKey, async () => continueNodeCodexSessionInner(params));
1412
- continueOperations.set(sourceKey, operation);
1413
- try {
1414
- return await operation;
1415
- } finally {
1416
- if (continueOperations.get(sourceKey) === operation) continueOperations.delete(sourceKey);
1417
- }
1418
- }
1419
- //#endregion
1420
- //#region extensions/codex/src/session-catalog-terminal.ts
1421
- const CODEX_TERMINAL_RESUME_COMMAND = "codex.terminal.resume.v1";
1422
- function resolveCodexCatalogTerminalHome(sources) {
1423
- const runtimeConfig = sources.getRuntimeConfig();
1424
- if (!runtimeConfig) throw new Error("OpenClaw runtime config is unavailable");
1425
- const startOptions = resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig: sources.getPluginConfig() }).start;
1426
- return resolveCodexAppServerLocalHomeDir(startOptions, resolveDefaultAgentDir(runtimeConfig));
1427
- }
1428
- function resolveLocalCodexTerminalExecutable(env = process.env) {
1429
- return resolveLocalCodexTerminalResolution(env)?.executable;
1430
- }
1431
- function resolveLocalCodexTerminalResolution(env = process.env) {
1432
- return resolveNodeHostExecutable("codex", {
1433
- env,
1434
- pathEnv: env.PATH ?? env.Path ?? "",
1435
- strategy: "fallback"
1436
- });
1437
- }
1438
- function codexNodeTerminalCapability(node) {
1439
- const commands = node.invocableCommands ?? node.commands;
1440
- return node.connected === true && commands?.includes("codex.terminal.resume.v1") === true ? { canOpenTerminalCodex: true } : {};
1441
- }
1442
- async function requireCatalogEligibleThread(control, threadId) {
1443
- const cached = await findCatalogEligibleThread(control, threadId, false);
1444
- if (cached) return cached;
1445
- const refreshed = await findCatalogEligibleThread(control, threadId, true);
1446
- if (refreshed) return refreshed;
1447
- throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1448
- }
1449
- async function findCatalogEligibleThread(control, threadId, forceRefresh) {
1450
- let cursor;
1451
- const seenCursors = /* @__PURE__ */ new Set();
1452
- for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
1453
- const page = await control.listPage({
1454
- limit: 100,
1455
- ...cursor ? { cursor } : {},
1456
- ...forceRefresh ? { forceRefresh: true } : {}
1457
- });
1458
- const candidate = page.sessions.find((session) => session.threadId === threadId);
1459
- if (candidate) {
1460
- if (isInteractiveThreadSource(candidate.source)) return candidate;
1461
- throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1462
- }
1463
- const nextCursor = page.nextCursor?.trim();
1464
- if (!nextCursor) return;
1465
- if (seenCursors.has(nextCursor)) throw new CatalogParamsError("Codex session eligibility could not be verified");
1466
- seenCursors.add(nextCursor);
1467
- cursor = nextCursor;
1468
- }
1469
- throw new CatalogParamsError("Codex session eligibility could not be verified");
1470
- }
1471
- function createCodexTerminalNodeHostCommand(control, configSources) {
1472
- return {
1473
- command: CODEX_TERMINAL_RESUME_COMMAND,
1474
- cap: CODEX_APP_SERVER_THREADS_CAPABILITY,
1475
- dangerous: false,
1476
- duplex: true,
1477
- isAvailable: ({ env }) => Boolean(resolveNodeHostExecutable("codex", {
1478
- env,
1479
- pathEnv: env.PATH ?? env.Path ?? "",
1480
- strategy: "direct"
1481
- })),
1482
- handle: async (paramsJSON, io) => {
1483
- if (!io) throw new Error("Codex terminal command requires duplex transport");
1484
- const resume = decodeNodePtyResumeParams(paramsJSON, (value) => {
1485
- if (typeof value !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(value)) throw new CatalogParamsError("threadId must be a UUID");
1486
- return value;
1487
- });
1488
- const record = await requireCatalogEligibleThread(control, resume.threadId);
1489
- const resolution = resolveNodeHostExecutable("codex", {
1490
- env: process.env,
1491
- pathEnv: process.env.PATH ?? process.env.Path ?? "",
1492
- strategy: "direct"
1493
- });
1494
- if (!resolution) throw new Error("Codex CLI is unavailable");
1495
- return JSON.stringify(await runNodePtyCommand({
1496
- file: resolution.executable,
1497
- args: ["resume", resume.threadId],
1498
- cwd: record.cwd,
1499
- env: { CODEX_HOME: resolveCodexCatalogTerminalHome(configSources) },
1500
- cols: resume.cols,
1501
- rows: resume.rows
1502
- }, io));
1503
- }
1504
- };
1505
- }
1506
- async function resolveNodeCatalogEligibleThread(params) {
1507
- let cursor;
1508
- const seenCursors = /* @__PURE__ */ new Set();
1509
- for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
1510
- const raw = await params.runtime.nodes.invoke({
1511
- nodeId: params.nodeId,
1512
- command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1513
- params: {
1514
- limit: 100,
1515
- ...cursor ? { cursor } : {}
1516
- },
1517
- timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1518
- scopes: ["operator.write"]
1519
- });
1520
- const page = params.parseCatalogPage(unwrapNodeInvokePayload(raw));
1521
- const record = page.sessions.find((candidate) => candidate.threadId === params.threadId);
1522
- if (record) {
1523
- if (isInteractiveThreadSource(record.source)) return record;
1524
- break;
1525
- }
1526
- const nextCursor = page.nextCursor?.trim();
1527
- if (!nextCursor || seenCursors.has(nextCursor)) break;
1528
- seenCursors.add(nextCursor);
1529
- cursor = nextCursor;
1530
- }
1531
- throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1354
+ throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1532
1355
  }
1533
1356
  async function openCodexCatalogTerminal(params) {
1534
1357
  const title = `codex resume ${params.threadId.slice(0, 8)}…`;
1535
- if (params.hostId === "gateway:local") {
1358
+ if (params.hostId === "gateway:local" || params.hostId.startsWith(`gateway:local:`)) {
1536
1359
  const record = await requireCatalogEligibleThread(params.control, params.threadId);
1537
1360
  const resolution = resolveLocalCodexTerminalResolution();
1538
1361
  if (!resolution) throw new CatalogParamsError("Codex CLI is unavailable");
@@ -1556,6 +1379,7 @@ async function openCodexCatalogTerminal(params) {
1556
1379
  return candidate.nodeId === nodeId && candidate.connected === true && commands?.includes("codex.appServer.threads.list.v1") === true && commands.includes("codex.terminal.resume.v1");
1557
1380
  })) throw new CatalogParamsError("paired-node Codex terminal is unavailable");
1558
1381
  const record = await resolveNodeCatalogEligibleThread({
1382
+ agentId: params.agentId,
1559
1383
  runtime: params.api.runtime,
1560
1384
  nodeId,
1561
1385
  threadId: params.threadId,
@@ -1565,7 +1389,10 @@ async function openCodexCatalogTerminal(params) {
1565
1389
  kind: "node",
1566
1390
  nodeId,
1567
1391
  command: CODEX_TERMINAL_RESUME_COMMAND,
1568
- paramsJSON: JSON.stringify({ threadId: params.threadId }),
1392
+ paramsJSON: JSON.stringify({
1393
+ agentId: params.agentId,
1394
+ threadId: params.threadId
1395
+ }),
1569
1396
  ...record.cwd ? { cwd: record.cwd } : {},
1570
1397
  title
1571
1398
  };
@@ -1584,968 +1411,1446 @@ async function startCodexCatalogTerminal(params) {
1584
1411
  };
1585
1412
  }
1586
1413
  //#endregion
1587
- //#region extensions/codex/src/session-catalog-transcript-item.ts
1588
- const CODEX_MESSAGE_TYPES = /* @__PURE__ */ new Map([
1589
- ["userMessage", "userMessage"],
1590
- ["agentMessage", "agentMessage"],
1591
- ["reasoning", "reasoning"]
1592
- ]);
1593
- const CODEX_TOOL_TYPES = /* @__PURE__ */ new Set([
1594
- "commandExecution",
1595
- "fileChange",
1596
- "mcpToolCall",
1597
- "dynamicToolCall",
1598
- "collabAgentToolCall",
1599
- "webSearch",
1600
- "imageView",
1601
- "imageGeneration"
1602
- ]);
1603
- function toGenericTranscriptItem(item) {
1604
- let type = CODEX_MESSAGE_TYPES.get(item.type);
1605
- if (!type && CODEX_TOOL_TYPES.has(item.type)) type = item.result !== void 0 || Boolean(item.aggregatedOutput) ? "toolResult" : "toolCall";
1606
- type ??= "other";
1607
- const fallback = item.title ?? item.name ?? item.tool ?? item.command ?? item.query ?? void 0;
1608
- const resultText = item.aggregatedOutput || (item.result === void 0 ? void 0 : JSON.stringify(item.result, null, 2));
1609
- const changesText = Array.isArray(item.changes) ? item.changes.map((change) => `${change.kind}: ${change.path}`).join("\n") || void 0 : void 0;
1610
- const text = item.text || resultText || changesText || fallback;
1611
- return {
1612
- id: item.id,
1613
- type,
1614
- ...text ? { text } : {},
1615
- raw: item
1616
- };
1414
+ //#region extensions/codex/src/session-catalog-adoption.ts
1415
+ const CODEX_SUPERVISION_SESSION_KEY_PREFIX = "harness:codex:supervision:";
1416
+ const boundCatalogSessionId = (value) => boundedCatalogString(value, 256);
1417
+ function requireIdleThread(thread, action) {
1418
+ if (thread.status?.type === "idle" || action === "archive" && thread.status?.type === "notLoaded") return;
1419
+ if (thread.status?.type === "active") throw new CatalogParamsError(`Codex session is active in this App Server; wait for it to finish before ${action === "continue" ? "starting a branch" : "archiving"}`);
1420
+ throw new CatalogParamsError(action === "archive" ? "Codex session cannot be archived in its current state" : "Codex session cannot start a branch in its current state");
1617
1421
  }
1618
- //#endregion
1619
- //#region extensions/codex/src/session-upstream-activity.ts
1620
- const CODEX_UPSTREAM_TURN_LIMIT = 100;
1621
- const CODEX_APP_SERVER_INVALID_REQUEST_CODE = -32600;
1622
- const CODEX_THREAD_NOT_LOADED_MESSAGE_PREFIX = "thread not loaded:";
1623
- function isCodexThreadGoneError(error) {
1624
- return error instanceof CodexAppServerRpcError && error.code === CODEX_APP_SERVER_INVALID_REQUEST_CODE && error.message.startsWith(CODEX_THREAD_NOT_LOADED_MESSAGE_PREFIX);
1422
+ function adoptionSessionKey(threadId, sourceHomeId) {
1423
+ const source = sourceHomeId ? JSON.stringify([sourceHomeId, threadId]) : threadId;
1424
+ return sessionCatalogAdoptedSessionKey(CODEX_SUPERVISION_SESSION_KEY_PREFIX, source);
1625
1425
  }
1626
- function readMarker(probe) {
1627
- if (!isRecord(probe.marker)) return;
1628
- const turnId = probe.marker.turnId;
1629
- if (turnId !== null && typeof turnId !== "string") return;
1630
- const count = probe.marker.userMessageCount;
1631
- if (count !== void 0 && (!Number.isSafeInteger(count) || count < 0)) return;
1426
+ function isAdoptionSessionKeyForThread(sessionKey, threadId, sourceHomeId) {
1427
+ return adoptionSessionKeyRest(sessionKey) === adoptionSessionKey(threadId, sourceHomeId);
1428
+ }
1429
+ function readCodexSupervisionMarker(entry) {
1430
+ const codex = isRecord(entry.pluginExtensions?.codex) ? entry.pluginExtensions.codex : void 0;
1431
+ const marker = codex && isRecord(codex.supervision) ? codex.supervision : void 0;
1432
+ const sourceThreadId = marker?.sourceThreadId;
1433
+ const sourceHomeId = marker?.sourceHomeId;
1434
+ if (typeof sourceThreadId !== "string" || !sourceThreadId.trim() || sourceHomeId !== void 0 && (typeof sourceHomeId !== "string" || !sourceHomeId.trim())) return;
1632
1435
  return {
1633
- turnId,
1634
- ...count === void 0 ? {} : { userMessageCount: count }
1436
+ sourceThreadId: sourceThreadId.trim(),
1437
+ ...typeof sourceHomeId === "string" ? { sourceHomeId: sourceHomeId.trim() } : {}
1635
1438
  };
1636
1439
  }
1637
- function upstreamConnectionFingerprint(probe) {
1638
- return isRecord(probe.upstreamRef) && typeof probe.upstreamRef.connectionFingerprint === "string" ? probe.upstreamRef.connectionFingerprint : void 0;
1639
- }
1640
- function classifyCodexUpstreamTurns(params) {
1641
- const marker = readMarker(params.probe);
1642
- if (!marker) return;
1643
- const newest = params.turns[0];
1644
- if (!newest?.id) return;
1645
- const markerIndex = marker.turnId === null ? -1 : params.turns.findIndex((turn) => turn.id === marker.turnId);
1646
- const candidateTurns = markerIndex < 0 ? params.turns : params.turns.slice(0, markerIndex + 1);
1647
- const newestUserMessageCount = countUserMessages(newest);
1648
- if (!(marker.turnId !== newest.id || marker.userMessageCount === void 0 || newestUserMessageCount > marker.userMessageCount)) return;
1649
- const ownTexts = new Set(params.probe.ownRecentUserTexts);
1650
- let humanTurns = 0;
1651
- let occurredAt;
1652
- for (const turn of candidateTurns) {
1653
- const userMessages = turn.items.filter((item) => item.type === "userMessage");
1654
- const alreadySeen = turn.id === marker.turnId ? marker.userMessageCount ?? userMessages.length : 0;
1655
- for (const item of userMessages.slice(alreadySeen)) {
1656
- const texts = normalizeUserMessageTexts(item);
1657
- if (ownTexts.has(texts.join(" ")) || texts.length > 1 && texts.every((text) => ownTexts.has(text))) continue;
1658
- humanTurns += 1;
1659
- if (occurredAt === void 0) {
1660
- const timestampSeconds = turn.completedAt ?? turn.startedAt;
1661
- occurredAt = typeof timestampSeconds === "number" && Number.isFinite(timestampSeconds) ? timestampSeconds * 1e3 : params.now ?? Date.now();
1662
- }
1663
- }
1440
+ async function listAdoptedSessionEntries(params) {
1441
+ const adopted = /* @__PURE__ */ new Map();
1442
+ for (const { agentId, entry, sessionKey } of listSessionCatalogEntries({
1443
+ ...params.agentId ? { agentId: params.agentId } : {},
1444
+ config: params.config ?? {},
1445
+ runtime: params.runtime,
1446
+ sessionEntries: params.sessionEntries
1447
+ })) {
1448
+ const sessionKeyRest = adoptionSessionKeyRest(sessionKey);
1449
+ const marker = readCodexSupervisionMarker(entry);
1450
+ if (!sessionKeyRest.startsWith(CODEX_SUPERVISION_SESSION_KEY_PREFIX) || !marker || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true) continue;
1451
+ const sessionId = entry.sessionId?.trim();
1452
+ if (!sessionId) continue;
1453
+ const binding = await params.bindingStore.read(sessionBindingIdentity({
1454
+ sessionId,
1455
+ sessionKey,
1456
+ config: params.config
1457
+ }));
1458
+ const sourceThreadId = binding?.supervisionSourceThreadId?.trim();
1459
+ const boundThreadId = binding?.threadId.trim();
1460
+ if (binding?.connectionScope !== "supervision" || !sourceThreadId || !boundThreadId || sessionKeyRest !== adoptionSessionKey(sourceThreadId, marker.sourceHomeId)) continue;
1461
+ const sourceKey = sessionCatalogAdoptedSourceKey(marker.sourceHomeId ?? "gateway:local", sourceThreadId);
1462
+ if (adopted.has(sourceKey)) throw new Error(`multiple OpenClaw sessions adopt Codex thread ${sourceThreadId} from the same home`);
1463
+ adopted.set(sourceKey, {
1464
+ key: sessionKey,
1465
+ sessionId,
1466
+ agentId,
1467
+ boundThreadId
1468
+ });
1664
1469
  }
1665
- const activityId = `${newest.id}:${newestUserMessageCount}`;
1666
- return {
1667
- kind: "activity",
1668
- sessionKey: params.probe.sessionKey,
1669
- humanTurns,
1670
- nextMarker: {
1671
- turnId: newest.id,
1672
- userMessageCount: newestUserMessageCount
1673
- },
1674
- ...humanTurns > 0 ? {
1675
- occurredAt: occurredAt ?? params.now ?? Date.now(),
1676
- dedupeId: activityId
1677
- } : {}
1678
- };
1470
+ return adopted;
1679
1471
  }
1680
- function countUserMessages(turn) {
1681
- return turn.items.filter((item) => item.type === "userMessage").length;
1472
+ async function findAdoptedSessionEntry(params) {
1473
+ const adopted = await listAdoptedSessionEntries(params);
1474
+ return adopted.get(sessionCatalogAdoptedSourceKey(params.sourceHomeId ?? "gateway:local", params.threadId)) ?? (params.sourceHomeId && params.allowLegacy === true ? adopted.get(sessionCatalogAdoptedSourceKey("gateway:local", params.threadId)) : void 0);
1682
1475
  }
1683
- function normalizeUserMessageTexts(item) {
1684
- const typed = item;
1685
- const contentTexts = typed.content?.filter((input) => input.type === "text").map((input) => input.text.trim().replace(/\s+/g, " ")).filter(Boolean);
1686
- return contentTexts?.length ? contentTexts : [(typed.text ?? "").trim().replace(/\s+/g, " ")];
1476
+ async function clearCreatedAdoptionBinding(params) {
1477
+ let cleared = false;
1478
+ let clearError;
1479
+ try {
1480
+ cleared = await params.bindingStore.mutate(params.identity, {
1481
+ kind: "clear",
1482
+ threadId: params.sourceThreadId,
1483
+ expectedPendingSupervisionBranch: params.expectedPending
1484
+ });
1485
+ } catch (error) {
1486
+ clearError = error;
1487
+ }
1488
+ if (cleared) return;
1489
+ let current;
1490
+ try {
1491
+ current = await params.bindingStore.read(params.identity);
1492
+ } catch (readError) {
1493
+ throw new AggregateError([
1494
+ params.cause,
1495
+ ...clearError ? [clearError] : [],
1496
+ readError
1497
+ ], `OpenClaw session creation failed and the Codex binding could not be verified for ${params.sourceThreadId}`, { cause: readError });
1498
+ }
1499
+ if (!matchesPendingSupervisionOwner(current, params.expectedPending)) return;
1500
+ throw new AggregateError([params.cause, ...clearError ? [clearError] : []], `OpenClaw session creation failed and the Codex binding could not be cleared for ${params.sourceThreadId}`, { cause: params.cause });
1687
1501
  }
1688
- async function checkCodexUpstreamActivity(probes, control, resolveThreadId = async (probe) => probe.threadId) {
1689
- return await control.withPinnedConnection(async (pinned) => {
1690
- const activities = [];
1691
- for (const probe of probes) {
1692
- const fingerprint = upstreamConnectionFingerprint(probe);
1693
- if (probe.upstreamKind !== "codex-app-server" || !fingerprint || fingerprint !== pinned.connectionFingerprint) continue;
1694
- try {
1695
- const threadId = await resolveThreadId(probe);
1696
- const page = await pinned.listTurnPage({
1697
- threadId,
1698
- limit: CODEX_UPSTREAM_TURN_LIMIT,
1699
- sortDirection: "desc",
1700
- itemsView: "full"
1502
+ function matchesPendingAdoptionBinding(binding, expected) {
1503
+ const historyCoveredThrough = binding?.historyCoveredThrough;
1504
+ return binding?.threadId === expected.sourceThreadId && binding.connectionScope === "supervision" && binding.supervisionSourceThreadId === expected.sourceThreadId && binding.cwd === expected.cwd && binding.conversationSourceTransferComplete === true && binding.preserveNativeModel === true && binding.pendingSupervisionBranch?.sourceThreadId === expected.sourceThreadId && binding.pendingSupervisionBranch.connectionFingerprint === expected.connectionFingerprint && binding.pendingSupervisionBranch.lastTurnId === expected.lastTurnId && (binding.pendingSupervisionBranch.cleanupThreadIds?.length ?? 0) === 0 && typeof historyCoveredThrough === "string" && Number.isFinite(Date.parse(historyCoveredThrough));
1505
+ }
1506
+ function matchesPendingSupervisionOwner(binding, expected) {
1507
+ const pending = binding?.pendingSupervisionBranch;
1508
+ const cleanupThreadIds = pending?.cleanupThreadIds ?? [];
1509
+ const expectedCleanupThreadIds = expected.cleanupThreadIds ?? [];
1510
+ return binding?.threadId === expected.sourceThreadId && binding.connectionScope === "supervision" && binding.supervisionSourceThreadId === expected.sourceThreadId && pending?.sourceThreadId === expected.sourceThreadId && pending.connectionFingerprint === expected.connectionFingerprint && pending.lastTurnId === expected.lastTurnId && cleanupThreadIds.length === expectedCleanupThreadIds.length && cleanupThreadIds.every((threadId, index) => threadId === expectedCleanupThreadIds[index]);
1511
+ }
1512
+ async function ensurePendingAdoptionBinding(params) {
1513
+ const pending = {
1514
+ sourceThreadId: params.sourceThreadId,
1515
+ connectionFingerprint: params.connectionFingerprint,
1516
+ ...params.lastTurnId ? { lastTurnId: params.lastTurnId } : {}
1517
+ };
1518
+ if (!await reclaimCurrentCodexSessionGeneration({
1519
+ bindingStore: params.bindingStore,
1520
+ identity: params.identity,
1521
+ config: params.config
1522
+ })) throw new Error(`failed to claim the OpenClaw session generation for ${params.sourceThreadId}`);
1523
+ const existing = await params.bindingStore.read(params.identity);
1524
+ if (existing) {
1525
+ if (matchesPendingAdoptionBinding(existing, params)) return;
1526
+ throw new Error(`OpenClaw session is already bound to Codex thread ${existing.threadId}`);
1527
+ }
1528
+ const binding = {
1529
+ threadId: params.sourceThreadId,
1530
+ connectionScope: "supervision",
1531
+ supervisionSourceThreadId: params.sourceThreadId,
1532
+ cwd: params.cwd,
1533
+ historyCoveredThrough: (/* @__PURE__ */ new Date()).toISOString(),
1534
+ conversationSourceTransferComplete: true,
1535
+ preserveNativeModel: true,
1536
+ pendingSupervisionBranch: pending
1537
+ };
1538
+ let stored;
1539
+ try {
1540
+ stored = await params.bindingStore.mutate(params.identity, {
1541
+ kind: "set",
1542
+ if: { kind: "absent" },
1543
+ binding
1544
+ });
1545
+ } catch (error) {
1546
+ if (matchesPendingAdoptionBinding(await params.bindingStore.read(params.identity), params)) return;
1547
+ throw error;
1548
+ }
1549
+ if (stored) return;
1550
+ if (!matchesPendingAdoptionBinding(await params.bindingStore.read(params.identity), params)) throw new Error(`failed to bind OpenClaw session to Codex thread ${params.sourceThreadId}`);
1551
+ }
1552
+ async function createOrReuseAdoptedSession(params) {
1553
+ const runtime = params.api.runtime;
1554
+ const lookup = {
1555
+ ...params,
1556
+ runtime,
1557
+ threadId: params.sourceThread.id
1558
+ };
1559
+ const existing = await findAdoptedSessionEntry(lookup);
1560
+ if (existing) return existing;
1561
+ let createdBindingIdentity;
1562
+ let createdPendingBinding;
1563
+ try {
1564
+ const spawnedCwd = params.sourceThread.cwd?.trim() || void 0;
1565
+ const pendingLastTurnId = codexLastTerminalTurnId(params.sourceThread, boundCatalogSessionId);
1566
+ const marker = {
1567
+ sourceThreadId: params.sourceThread.id,
1568
+ ...params.sourceHomeId ? { sourceHomeId: params.sourceHomeId } : {}
1569
+ };
1570
+ const created = await createImportedCodexSession({
1571
+ runtime: params.api.runtime,
1572
+ config: params.config,
1573
+ key: adoptionSessionKey(params.sourceThread.id, params.sourceHomeId),
1574
+ agentId: params.agentId,
1575
+ displayName: params.sourceThread.name ?? void 0,
1576
+ thread: params.sourceThread,
1577
+ throughTurnId: pendingLastTurnId ?? null,
1578
+ recoverMatchingInitialEntry: true,
1579
+ initialEntry: {
1580
+ agentHarnessId: "codex",
1581
+ modelSelectionLocked: true,
1582
+ pluginExtensions: { codex: { supervision: {
1583
+ ...marker,
1584
+ initializing: true,
1585
+ modelLocked: true
1586
+ } } }
1587
+ },
1588
+ afterImport: async (entry) => {
1589
+ createdBindingIdentity = sessionBindingIdentity({
1590
+ sessionId: entry.sessionId,
1591
+ sessionKey: entry.key,
1592
+ config: params.config
1701
1593
  });
1702
- const marker = readMarker(probe);
1703
- if (page.data.length === 0 && marker) {
1704
- try {
1705
- await pinned.readThread(threadId, false);
1706
- } catch (error) {
1707
- if (isCodexThreadGoneError(error)) activities.push({
1708
- kind: "missing",
1709
- sessionKey: probe.sessionKey
1710
- });
1711
- }
1712
- continue;
1713
- }
1714
- const activity = classifyCodexUpstreamTurns({
1715
- probe,
1716
- turns: page.data
1594
+ createdPendingBinding = {
1595
+ sourceThreadId: params.sourceThread.id,
1596
+ connectionFingerprint: params.connectionFingerprint,
1597
+ ...pendingLastTurnId ? { lastTurnId: pendingLastTurnId } : {}
1598
+ };
1599
+ await ensurePendingAdoptionBinding({
1600
+ bindingStore: params.bindingStore,
1601
+ config: params.config,
1602
+ identity: createdBindingIdentity,
1603
+ sourceThreadId: params.sourceThread.id,
1604
+ connectionFingerprint: params.connectionFingerprint,
1605
+ cwd: spawnedCwd ?? "",
1606
+ ...pendingLastTurnId ? { lastTurnId: pendingLastTurnId } : {}
1717
1607
  });
1718
- if (activity) activities.push(activity);
1719
- } catch {}
1608
+ return { pluginExtensions: { codex: { supervision: {
1609
+ ...marker,
1610
+ modelLocked: true
1611
+ } } } };
1612
+ }
1613
+ });
1614
+ return {
1615
+ key: created.key,
1616
+ sessionId: created.sessionId,
1617
+ agentId: created.agentId,
1618
+ boundThreadId: params.sourceThread.id
1619
+ };
1620
+ } catch (error) {
1621
+ let raced = await findAdoptedSessionEntry(lookup);
1622
+ if (raced) return raced;
1623
+ if (createdBindingIdentity && createdPendingBinding) {
1624
+ await clearCreatedAdoptionBinding({
1625
+ bindingStore: params.bindingStore,
1626
+ identity: createdBindingIdentity,
1627
+ sourceThreadId: params.sourceThread.id,
1628
+ expectedPending: createdPendingBinding,
1629
+ cause: error
1630
+ });
1631
+ raced = await findAdoptedSessionEntry(lookup);
1632
+ if (raced) return raced;
1720
1633
  }
1721
- return activities;
1722
- });
1634
+ throw error;
1635
+ }
1723
1636
  }
1724
- function createChecker(params) {
1725
- return async (probes) => await checkCodexUpstreamActivity(probes, params.control, async (probe) => {
1726
- const config = params.getRuntimeConfig();
1727
- const sessionId = params.api.runtime.agent.session.getSessionEntry({
1728
- agentId: probe.agentId,
1729
- sessionKey: probe.sessionKey,
1730
- readConsistency: "latest"
1731
- })?.sessionId?.trim();
1732
- if (!sessionId) return probe.threadId;
1733
- const binding = await params.bindingStore.read(sessionBindingIdentity({
1734
- sessionId,
1735
- sessionKey: probe.sessionKey,
1736
- config
1737
- }));
1738
- return binding?.connectionScope === "supervision" && binding.supervisionSourceThreadId === probe.threadId ? binding.threadId : probe.threadId;
1637
+ async function continueLocalCodexSessionInner(params) {
1638
+ await requireCatalogEligibleThread(params.control, params.threadId);
1639
+ const existing = await findAdoptedSessionEntry({
1640
+ ...params,
1641
+ runtime: params.api.runtime
1739
1642
  });
1740
- }
1741
- //#endregion
1742
- //#region extensions/codex/src/session-catalog.ts
1743
- const boundCatalogSessionId = (value) => boundedCatalogString(value, 256);
1744
- const CODEX_SUPERVISION_SESSION_KEY_PREFIX = "harness:codex:supervision:";
1745
- const CODEX_SESSION_CATALOG_LIST_TTL_MS = 32e3;
1746
- const CODEX_SESSION_CATALOG_LIST_CACHE_MAX_ENTRIES = 32;
1747
- function codexCatalogPageCacheKey(params) {
1748
- return JSON.stringify([
1749
- params.cursor ?? null,
1750
- params.limit ?? null,
1751
- params.searchTerm?.trim().toLocaleLowerCase() || null,
1752
- params.cwd?.trim() || null
1753
- ]);
1754
- }
1755
- function createCodexSessionCatalogControlFromRequests(params) {
1756
- return {
1757
- ...params.clientId ? { clientId: params.clientId } : {},
1758
- ...params.connectionFingerprint ? { connectionFingerprint: params.connectionFingerprint } : {},
1759
- withPinnedConnection: params.withPinnedConnection,
1760
- async listPage(pageParams) {
1761
- const limit = normalizeLimit(pageParams.limit, "limit");
1762
- const search = pageParams.searchTerm?.trim().toLocaleLowerCase() || void 0;
1763
- const cwd = pageParams.cwd?.trim() || void 0;
1764
- const maxPages = search ? 20 : 1;
1765
- const sessions = [];
1766
- let cursor = readControlCursor(pageParams.cursor, "request");
1767
- let nextCursor;
1768
- let backwardsCursor;
1769
- const seenCursors = new Set(cursor ? [cursor] : []);
1770
- const requests = params.createRequestSnapshot();
1771
- const deadline = params.now() + requests.requestTimeoutMs;
1772
- for (let pageIndex = 0; pageIndex < maxPages; pageIndex += 1) {
1773
- const remaining = limit - sessions.length;
1774
- const remainingTimeoutMs = Math.ceil(deadline - params.now());
1775
- if (remainingTimeoutMs <= 0) throw new Error("Codex session catalog listing timed out");
1776
- const response = await requests.listThreads({
1777
- archived: false,
1778
- limit: remaining,
1779
- modelProviders: [],
1780
- sortKey: "updated_at",
1781
- sortDirection: "desc",
1782
- ...cwd ? { cwd } : {},
1783
- ...cursor ? { cursor } : {}
1784
- }, remainingTimeoutMs);
1785
- if (pageIndex === 0) backwardsCursor = readControlCursor(response.backwardsCursor, "backwards response");
1786
- sessions.push(...response.data.flatMap((thread) => {
1787
- const session = toCatalogSession(thread, false);
1788
- return session ? [session] : [];
1789
- }).filter((session) => !search || session.name?.toLocaleLowerCase().includes(search)));
1790
- nextCursor = readControlCursor(response.nextCursor, "next response");
1791
- if (!nextCursor || sessions.length >= limit) break;
1792
- if (seenCursors.has(nextCursor)) throw new Error("Codex session catalog returned a repeated search cursor");
1793
- seenCursors.add(nextCursor);
1794
- cursor = nextCursor;
1643
+ if (existing) {
1644
+ const boundThreadId = requireBoundThread(existing);
1645
+ const boundThread = await params.control.readThread(boundThreadId, true);
1646
+ if (boundThread.id !== boundThreadId) throw new Error("Codex app-server returned a different thread than requested");
1647
+ const changedError = () => new CatalogParamsError("Codex OpenClaw session changed before it could be opened. Retry.");
1648
+ if (!await params.api.runtime.agent.session.patchSessionEntry({
1649
+ sessionKey: existing.key,
1650
+ readConsistency: "latest",
1651
+ preserveActivity: true,
1652
+ update: (entry) => {
1653
+ if (entry.sessionId?.trim() !== existing.sessionId || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true) throw changedError();
1654
+ return { archivedAt: void 0 };
1795
1655
  }
1796
- return {
1797
- sessions,
1798
- ...nextCursor ? { nextCursor } : {},
1799
- ...backwardsCursor ? { backwardsCursor } : {}
1800
- };
1801
- },
1802
- async listDescendantPage(listParams) {
1803
- const requests = params.createRequestSnapshot();
1804
- return await requests.listThreads(listParams, requests.requestTimeoutMs);
1805
- },
1806
- async readThread(threadId, includeTurns = false) {
1807
- return await params.createRequestSnapshot().readThread(threadId, includeTurns);
1808
- },
1809
- async listTurnPage(listParams) {
1810
- return await params.createRequestSnapshot().listThreadTurns(listParams);
1811
- },
1812
- async forkThread(forkParams) {
1813
- return await params.createRequestSnapshot().forkThread(forkParams);
1814
- },
1815
- async archiveThread(threadId) {
1816
- await params.createRequestSnapshot().archiveThread(threadId);
1817
- }
1818
- };
1819
- }
1820
- /** Builds the passive catalog over the Codex plugin's canonical shared client. */
1821
- function createCodexSessionCatalogControl(params) {
1822
- const now = params.now ?? Date.now;
1823
- const getPluginConfig = () => params.getPluginConfig();
1824
- const requestOptionsByConfig = /* @__PURE__ */ new WeakMap();
1825
- const catalogPagesByConfig = /* @__PURE__ */ new WeakMap();
1826
- const resolveRequestOptions = (startOptions) => {
1827
- const runtimeConfig = params.getRuntimeConfig();
1828
- if (!runtimeConfig) return {
1829
- config: void 0,
1830
- startOptions: structuredClone(startOptions)
1831
- };
1832
- const cached = requestOptionsByConfig.get(runtimeConfig);
1833
- if (cached) return cached;
1834
- const resolved = {
1835
- config: structuredClone(runtimeConfig),
1836
- startOptions: structuredClone(startOptions)
1837
- };
1838
- requestOptionsByConfig.set(runtimeConfig, resolved);
1839
- return resolved;
1840
- };
1841
- const createRequestSnapshot = () => {
1842
- const pluginConfig = getPluginConfig();
1843
- const runtime = resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig });
1844
- const requestOptions = resolveRequestOptions(runtime.start);
1656
+ })) throw changedError();
1657
+ const connectionFingerprint = params.control.connectionFingerprint;
1658
+ if (connectionFingerprint) params.onContinued?.({
1659
+ connectionFingerprint,
1660
+ ...codexUpstreamBaseline(boundThread, boundCatalogSessionId)
1661
+ });
1845
1662
  return {
1846
- requestTimeoutMs: runtime.requestTimeoutMs,
1847
- listThreads: async (listParams, timeoutMs) => await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreads, listParams, {
1848
- ...requestOptions,
1849
- timeoutMs
1850
- }),
1851
- readThread: async (threadId, includeTurns) => (await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.readThread, {
1852
- threadId,
1853
- includeTurns
1854
- }, requestOptions)).thread,
1855
- listThreadTurns: async (listParams) => await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreadTurns, listParams, requestOptions),
1856
- forkThread: async (forkParams) => await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.forkThread, assertCodexThreadForkParams(forkParams), requestOptions),
1857
- archiveThread: async (threadId) => {
1858
- await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.archiveThread, { threadId }, requestOptions);
1859
- }
1663
+ sessionKey: existing.key,
1664
+ disposition: "existing"
1860
1665
  };
1861
- };
1862
- const withPinnedConnection = async (run) => {
1863
- const runtime = resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig: getPluginConfig() });
1864
- const { config: runtimeConfig, startOptions } = resolveRequestOptions(runtime.start);
1865
- const client = await getLeasedSharedCodexAppServerClient({
1866
- config: runtimeConfig,
1867
- startOptions,
1868
- timeoutMs: runtime.requestTimeoutMs
1869
- });
1870
- try {
1871
- const requests = {
1872
- requestTimeoutMs: runtime.requestTimeoutMs,
1873
- listThreads: async (listParams, timeoutMs) => await requestCodexAppServerClientJson({
1874
- client,
1875
- method: CODEX_CONTROL_METHODS.listThreads,
1876
- requestParams: listParams,
1877
- config: runtimeConfig,
1878
- timeoutMs
1879
- }),
1880
- readThread: async (threadId, includeTurns) => (await requestCodexAppServerClientJson({
1881
- client,
1882
- method: CODEX_CONTROL_METHODS.readThread,
1883
- requestParams: {
1884
- threadId,
1885
- includeTurns
1886
- },
1887
- config: runtimeConfig,
1888
- timeoutMs: runtime.requestTimeoutMs
1889
- })).thread,
1890
- listThreadTurns: async (listParams) => await requestCodexAppServerClientJson({
1891
- client,
1892
- method: CODEX_CONTROL_METHODS.listThreadTurns,
1893
- requestParams: listParams,
1894
- config: runtimeConfig,
1895
- timeoutMs: runtime.requestTimeoutMs
1896
- }),
1897
- forkThread: async (forkParams) => await requestCodexAppServerClientJson({
1898
- client,
1899
- method: CODEX_CONTROL_METHODS.forkThread,
1900
- requestParams: assertCodexThreadForkParams(forkParams),
1901
- config: runtimeConfig,
1902
- timeoutMs: runtime.requestTimeoutMs
1903
- }),
1904
- archiveThread: async (threadId) => {
1905
- await requestCodexAppServerClientJson({
1906
- client,
1907
- method: CODEX_CONTROL_METHODS.archiveThread,
1908
- requestParams: { threadId },
1909
- config: runtimeConfig,
1910
- timeoutMs: runtime.requestTimeoutMs
1911
- });
1912
- }
1913
- };
1914
- const pinnedControl = createCodexSessionCatalogControlFromRequests({
1915
- clientId: resolveCodexAppServerClientInstanceId(client),
1916
- connectionFingerprint: buildCodexAppServerConnectionFingerprint(runtime, resolveDefaultAgentDir(runtimeConfig ?? {})),
1917
- createRequestSnapshot: () => requests,
1918
- now,
1919
- withPinnedConnection: async (nestedRun) => await nestedRun(pinnedControl)
1920
- });
1921
- return await run(pinnedControl);
1922
- } finally {
1923
- releaseLeasedSharedCodexAppServerClient(client);
1924
- }
1925
- };
1926
- const control = createCodexSessionCatalogControlFromRequests({
1927
- createRequestSnapshot,
1928
- now,
1929
- withPinnedConnection
1666
+ }
1667
+ const sourceThread = await params.control.readThread(params.threadId, true);
1668
+ if (sourceThread.id !== params.threadId) throw new Error("Codex app-server returned a different thread than requested");
1669
+ if (sourceThread.status?.type !== "notLoaded") requireIdleThread(sourceThread, "continue");
1670
+ const connectionFingerprint = params.control.connectionFingerprint;
1671
+ if (!connectionFingerprint) throw new Error("Codex Continue requires a pinned app-server connection");
1672
+ const adopted = await createOrReuseAdoptedSession({
1673
+ ...params,
1674
+ sourceThread,
1675
+ connectionFingerprint
1676
+ });
1677
+ const boundThreadId = requireBoundThread(adopted);
1678
+ const baselineThread = boundThreadId === sourceThread.id ? sourceThread : await params.control.readThread(boundThreadId, true);
1679
+ if (baselineThread.id !== boundThreadId) throw new Error("Codex app-server returned a different thread than requested");
1680
+ params.onContinued?.({
1681
+ connectionFingerprint,
1682
+ ...codexUpstreamBaseline(baselineThread, boundCatalogSessionId)
1930
1683
  });
1931
1684
  return {
1932
- ...control,
1933
- async listPage(pageParams) {
1934
- const runtimeConfig = params.getRuntimeConfig();
1935
- if (!runtimeConfig) return await control.listPage(pageParams);
1936
- let cache = catalogPagesByConfig.get(runtimeConfig);
1937
- if (!cache) {
1938
- cache = /* @__PURE__ */ new Map();
1939
- catalogPagesByConfig.set(runtimeConfig, cache);
1940
- }
1941
- const key = codexCatalogPageCacheKey(pageParams);
1942
- const cached = cache.get(key);
1943
- if (pageParams.forceRefresh !== true && cached) {
1944
- cache.delete(key);
1945
- cache.set(key, cached);
1946
- if (cached.stalePage) return await cached.stalePage;
1947
- if (cached.expiresAt > now()) return await cached.page;
1948
- const stalePage = cached.settledPage;
1949
- if (!stalePage) return await cached.page;
1950
- const page = control.listPage(pageParams);
1951
- const entry = {
1952
- expiresAt: Number.POSITIVE_INFINITY,
1953
- page,
1954
- settledPage: stalePage,
1955
- stalePage
1956
- };
1957
- cache.set(key, entry);
1958
- page.then(() => {
1959
- if (cache.get(key) === entry) {
1960
- delete entry.stalePage;
1961
- entry.settledPage = page;
1962
- entry.expiresAt = now() + CODEX_SESSION_CATALOG_LIST_TTL_MS;
1963
- }
1964
- }, async () => {
1965
- let stale;
1966
- try {
1967
- stale = await stalePage;
1968
- } catch {}
1969
- if (cache.get(key) !== entry) return;
1970
- if (stale) {
1971
- cache.delete(key);
1972
- const settledPage = Promise.resolve(stale);
1973
- cache.set(key, {
1974
- expiresAt: now(),
1975
- page: settledPage,
1976
- settledPage
1977
- });
1978
- } else cache.delete(key);
1979
- });
1980
- return await stalePage;
1981
- }
1982
- if (cached) cache.delete(key);
1983
- const serveStaleOnError = pageParams.forceRefresh !== true;
1984
- const page = control.listPage(pageParams);
1985
- const stalePage = cached?.settledPage;
1986
- const entry = {
1987
- expiresAt: Number.POSITIVE_INFINITY,
1988
- page,
1989
- ...stalePage ? {
1990
- stalePage,
1991
- settledPage: stalePage
1992
- } : {}
1993
- };
1994
- cache.set(key, entry);
1995
- while (cache.size > CODEX_SESSION_CATALOG_LIST_CACHE_MAX_ENTRIES) {
1996
- const oldest = cache.keys().next();
1997
- if (oldest.done) break;
1998
- cache.delete(oldest.value);
1999
- }
2000
- try {
2001
- const result = await page;
2002
- if (cache.get(key) === entry) {
2003
- delete entry.stalePage;
2004
- entry.settledPage = page;
2005
- entry.expiresAt = now() + CODEX_SESSION_CATALOG_LIST_TTL_MS;
2006
- }
2007
- return result;
2008
- } catch (error) {
2009
- if (stalePage) {
2010
- let stale;
2011
- try {
2012
- stale = await stalePage;
2013
- } catch {}
2014
- if (stale) {
2015
- if (cache.get(key) === entry) {
2016
- cache.delete(key);
2017
- const settledPage = Promise.resolve(stale);
2018
- cache.set(key, {
2019
- expiresAt: now(),
2020
- page: settledPage,
2021
- settledPage
2022
- });
2023
- }
2024
- if (serveStaleOnError) return stale;
2025
- }
2026
- }
2027
- if (cache.get(key) === entry) cache.delete(key);
2028
- throw error;
2029
- }
2030
- }
1685
+ sessionKey: adopted.key,
1686
+ disposition: "forked"
2031
1687
  };
2032
1688
  }
2033
- async function listGatewayHost(params) {
1689
+ /** Creates one locked OpenClaw branch whose first harness run forks the Codex source. */
1690
+ async function continueLocalCodexSession(params) {
1691
+ const sourceKey = sessionCatalogAdoptedSourceKey(params.hostId ?? "gateway:local", params.threadId);
1692
+ const operationKey = sessionCatalogAdoptedSourceKey(params.agentId, sourceKey);
1693
+ const current = continueOperations.get(operationKey);
1694
+ if (current) return await current;
1695
+ const run = async (control) => await continueLocalCodexSessionInner({
1696
+ ...params,
1697
+ control
1698
+ });
1699
+ const operation = runSessionActionExclusive(sourceKey, async () => params.control.withPinnedConnection(run));
1700
+ continueOperations.set(operationKey, operation);
2034
1701
  try {
2035
- const page = parseCatalogPage(await params.control.listPage({
2036
- limit: params.query.limitPerHost,
2037
- ...params.query.cursors?.["gateway:local"] ? { cursor: params.query.cursors[CODEX_LOCAL_SESSION_HOST_ID] } : {},
2038
- ...params.query.search ? { searchTerm: params.query.search } : {}
2039
- }));
2040
- const adoptedSessions = await listAdoptedSessionEntries({
2041
- bindingStore: params.bindingStore,
2042
- config: params.config,
2043
- runtime: params.runtime,
2044
- sessionEntries: params.sessionEntries
2045
- });
2046
- return {
2047
- hostId: CODEX_LOCAL_SESSION_HOST_ID,
2048
- label: "Local Codex",
2049
- kind: "gateway",
2050
- connected: true,
2051
- ...page,
2052
- sessions: page.sessions.map((session) => {
2053
- const adopted = adoptedSessions.get(session.threadId);
2054
- return adopted ? Object.assign({}, session, { sessionKey: adopted.key }) : session;
2055
- })
2056
- };
2057
- } catch (error) {
2058
- return {
2059
- hostId: CODEX_LOCAL_SESSION_HOST_ID,
2060
- label: "Local Codex",
2061
- kind: "gateway",
2062
- connected: false,
2063
- sessions: [],
2064
- error: catalogError("APP_SERVER_UNAVAILABLE", error)
2065
- };
1702
+ return await operation;
1703
+ } finally {
1704
+ if (continueOperations.get(operationKey) === operation) continueOperations.delete(operationKey);
2066
1705
  }
2067
1706
  }
2068
- /** Lists Gateway-local and paired-node Codex sessions with per-host failures. */
2069
- async function listCodexSessionCatalog(params) {
2070
- const query = readGatewayParams(params.query);
2071
- const requestedHostIds = query.hostIds ? new Set(query.hostIds) : void 0;
2072
- const localHosts = params.includeLocal !== false && (!requestedHostIds || requestedHostIds.has("gateway:local")) ? [listGatewayHost({
2073
- bindingStore: params.bindingStore,
2074
- config: params.config,
2075
- control: params.control,
2076
- query,
2077
- runtime: params.runtime,
2078
- sessionEntries: params.sessionEntries
2079
- })] : [];
2080
- for (const host of localHosts) if (params.onHost) host.then(params.onHost).catch(() => void 0);
2081
- if (!(!requestedHostIds || query.hostIds?.some((hostId) => hostId.startsWith("node:")))) return { hosts: await Promise.all(localHosts) };
2082
- let nodes;
2083
- try {
2084
- nodes = (await (params.listNodes?.() ?? params.runtime.nodes.list())).nodes.filter((node) => node.gatewayLocal !== true && node.commands?.includes("codex.appServer.threads.list.v1") && (!requestedHostIds || requestedHostIds.has(`node:${node.nodeId}`))).slice(0, 100 - localHosts.length);
2085
- } catch (error) {
2086
- const registryHost = {
2087
- hostId: "node:registry",
2088
- label: "Paired nodes",
2089
- kind: "node",
2090
- connected: false,
2091
- sessions: [],
2092
- error: catalogError("NODE_LIST_FAILED", error)
2093
- };
2094
- params.onHost?.(registryHost);
2095
- return { hosts: [...await Promise.all(localHosts), registryHost] };
1707
+ //#endregion
1708
+ //#region extensions/codex/src/session-catalog-archive.ts
1709
+ async function assertNoPendingSupervisionBranch(params) {
1710
+ const adoptedEntries = [params.agentId, ...listAgentIds(params.config).filter((agentId) => agentId !== params.agentId)].flatMap((agentId) => params.runtime.agent.session.listSessionEntries({
1711
+ agentId,
1712
+ readOnly: true
1713
+ })).filter((candidate) => isAdoptionSessionKeyForThread(candidate.sessionKey, params.threadId, params.sourceHomeId) || params.sourceHomeId !== void 0 && params.allowLegacy === true && isAdoptionSessionKeyForThread(candidate.sessionKey, params.threadId));
1714
+ for (const adopted of adoptedEntries) {
1715
+ if (adopted.entry.initializationPending === true) throw new CatalogParamsError("Codex session cannot be archived while its OpenClaw branch is initializing");
1716
+ const sessionId = adopted.entry.sessionId?.trim();
1717
+ if (!sessionId) continue;
1718
+ const binding = await params.bindingStore.read(sessionBindingIdentity({
1719
+ sessionId,
1720
+ sessionKey: adopted.sessionKey,
1721
+ config: params.config
1722
+ }));
1723
+ if (binding?.connectionScope === "supervision" && binding.supervisionSourceThreadId === params.threadId && binding.pendingSupervisionBranch?.sourceThreadId === params.threadId) throw new CatalogParamsError("Codex session cannot be archived until its OpenClaw branch starts");
2096
1724
  }
2097
- const adoptedNodeSessions = listNodeAdoptedSessionEntries({
2098
- config: params.config,
2099
- runtime: params.runtime,
2100
- sessionEntries: params.sessionEntries
2101
- });
2102
- const nodeHosts = nodes.toSorted(compareNodeLabels).map(async (node) => {
2103
- const host = await listPairedNode({
2104
- runtime: params.runtime,
2105
- node,
2106
- query,
2107
- adoptedSessions: adoptedNodeSessions,
2108
- ...params.onHost ? { onHost: params.onHost } : {}
2109
- });
2110
- return Object.assign(host, codexNodeTerminalCapability(node));
2111
- });
2112
- return { hosts: await Promise.all([...localHosts, ...nodeHosts]) };
2113
1725
  }
2114
- /** Builds the node-local read-only Codex app-server catalog command. */
2115
- function createCodexSessionCatalogNodeHostCommands(control, configSources) {
2116
- return [
2117
- {
2118
- command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
2119
- cap: CODEX_APP_SERVER_THREADS_CAPABILITY,
2120
- dangerous: false,
2121
- handle: async (paramsJSON) => {
2122
- const pageParams = readPageParams(parseJsonParams(paramsJSON));
2123
- try {
2124
- const page = filterCatalogPageByTitle(parseCatalogPage(await control.listPage(pageParams)), pageParams.searchTerm);
2125
- return JSON.stringify(page);
2126
- } catch {
2127
- throw new Error("Codex app-server catalog is unavailable");
2128
- }
2129
- }
2130
- },
2131
- {
2132
- command: CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
2133
- cap: CODEX_APP_SERVER_THREADS_CAPABILITY,
2134
- dangerous: false,
2135
- handle: async (paramsJSON) => {
2136
- const action = readNodeTranscriptParams(parseJsonParams(paramsJSON));
2137
- try {
2138
- await requireCatalogEligibleThread(control, action.threadId);
2139
- const page = parseTranscriptPage(await control.listTurnPage({
2140
- threadId: action.threadId,
2141
- limit: action.limit,
2142
- sortDirection: "desc",
2143
- itemsView: "full",
2144
- ...action.cursor ? { cursor: action.cursor } : {}
2145
- }));
2146
- return JSON.stringify(page);
2147
- } catch (error) {
2148
- if (error instanceof CatalogParamsError) throw error;
2149
- throw new Error("Codex app-server transcript is unavailable", { cause: error });
2150
- }
2151
- }
2152
- },
2153
- createCodexTerminalNodeHostCommand(control, configSources)
2154
- ];
1726
+ /** Archives one inactive Gateway-local Codex thread after a fresh status read. */
1727
+ async function archiveLocalCodexSession(params) {
1728
+ return await runSessionActionExclusive(sessionCatalogAdoptedSourceKey(params.hostId ?? "gateway:local", params.threadId), async () => {
1729
+ return await params.bindingStore.withThreadArchiveFence(async () => {
1730
+ const run = async (control) => {
1731
+ await requireCatalogEligibleThread(control, params.threadId);
1732
+ await assertNoPendingSupervisionBranch(params);
1733
+ const thread = await control.readThread(params.threadId, false);
1734
+ if (thread.id !== params.threadId) throw new Error("Codex app-server returned a different thread than requested");
1735
+ requireIdleThread(thread, "archive");
1736
+ if (await params.bindingStore.hasOtherThreadOwner(params.threadId)) throw new CatalogParamsError("Codex session cannot be archived while it is attached to an OpenClaw session");
1737
+ await assertCodexArchiveDescendantsUnowned({
1738
+ bindingStore: params.bindingStore,
1739
+ threadId: params.threadId,
1740
+ listPage: (request) => control.listDescendantPage(request),
1741
+ assertDescendantIdle: async (descendantThreadId) => {
1742
+ const descendant = await control.readThread(descendantThreadId, false);
1743
+ if (descendant.id !== descendantThreadId) throw new Error("Codex app-server returned a different descendant than requested");
1744
+ requireIdleThread(descendant, "archive");
1745
+ }
1746
+ });
1747
+ await control.archiveThread(params.threadId);
1748
+ return { archived: true };
1749
+ };
1750
+ return await params.control.withPinnedConnection(run);
1751
+ });
1752
+ });
2155
1753
  }
2156
- function readNodeTranscriptParams(value) {
2157
- if (!isRecord(value)) throw new CatalogParamsError("Codex session read parameters must be an object");
2158
- requireOnlyKeys(value, /* @__PURE__ */ new Set([
2159
- "threadId",
2160
- "cursor",
2161
- "limit"
2162
- ]));
2163
- const threadId = readBoundedOptionalString(value, "threadId", 256);
2164
- if (!threadId) throw new CatalogParamsError("threadId is required");
2165
- const cursor = readBoundedOptionalString(value, "cursor", MAX_CURSOR_LENGTH);
2166
- return {
2167
- threadId,
2168
- limit: readBoundedLimit(value.limit, "limit", 20, 50),
2169
- ...cursor ? { cursor } : {}
1754
+ //#endregion
1755
+ //#region extensions/codex/src/session-catalog-create.ts
1756
+ const CODEX_AGENT_RUNTIME_ID = "codex";
1757
+ const CODEX_CATALOG_DEFAULT_MODEL_REF = "openai/gpt-5.6-sol";
1758
+ function resolveCodexCatalogCreateSession(config, requestedAgentId) {
1759
+ if (!config) return;
1760
+ const agentId = requestedAgentId ?? resolveDefaultAgentId(config);
1761
+ const defaultModel = resolveDefaultModelForAgent({
1762
+ cfg: config,
1763
+ agentId
1764
+ });
1765
+ return "error" in resolveAllowedModelRef({
1766
+ cfg: config,
1767
+ catalog: [],
1768
+ raw: CODEX_CATALOG_DEFAULT_MODEL_REF,
1769
+ defaultProvider: defaultModel.provider,
1770
+ defaultModel: defaultModel.model,
1771
+ agentId
1772
+ }) ? void 0 : {
1773
+ model: CODEX_CATALOG_DEFAULT_MODEL_REF,
1774
+ agentRuntime: CODEX_AGENT_RUNTIME_ID
2170
1775
  };
2171
1776
  }
2172
- function readBoundedLimit(value, key, fallback, max) {
2173
- if (value === void 0) return fallback;
2174
- if (!Number.isInteger(value) || value < 1 || value > max) throw new CatalogParamsError(`${key} must be an integer from 1 to ${max}`);
2175
- return value;
1777
+ //#endregion
1778
+ //#region extensions/codex/src/session-catalog-node-continue.ts
1779
+ const CODEX_NODE_CONTINUE_COMMANDS = [
1780
+ CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1781
+ CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
1782
+ CODEX_CLI_SESSION_RESUME_COMMAND
1783
+ ];
1784
+ const NODE_CATALOG_LIST_RESPONSE_TIMEOUT_MS = 8e3;
1785
+ const continueNodeAdoption = createSessionCatalogAdoptionCoordinator();
1786
+ function nodeLabel(node) {
1787
+ return node.displayName?.trim() || node.remoteIp?.trim() || node.nodeId;
2176
1788
  }
2177
- function flattenTranscriptPageDesc(page) {
2178
- return page.data.flatMap((turn) => turn.items.toReversed());
1789
+ function compareNodeLabels(left, right) {
1790
+ const leftLabel = nodeLabel(left);
1791
+ const rightLabel = nodeLabel(right);
1792
+ if (leftLabel < rightLabel) return -1;
1793
+ if (leftLabel > rightLabel) return 1;
1794
+ return 0;
2179
1795
  }
2180
- /** Reads the persisted transcript for a Gateway-local or paired-node Codex session. */
2181
- async function readCodexSessionTranscript(params) {
2182
- if (params.hostId === "gateway:local") {
2183
- await requireCatalogEligibleThread(params.control, params.threadId);
2184
- const page = parseTranscriptPage(await params.control.listTurnPage({
2185
- threadId: params.threadId,
2186
- limit: params.limit,
2187
- sortDirection: "desc",
2188
- itemsView: "full",
2189
- ...params.cursor ? { cursor: params.cursor } : {}
2190
- }));
1796
+ function canContinueCodexOnNode(node) {
1797
+ return node.connected === true && CODEX_NODE_CONTINUE_COMMANDS.every((command) => node.commands?.includes(command) === true && node.invocableCommands?.includes(command) === true);
1798
+ }
1799
+ async function listPairedNode(params) {
1800
+ const hostId = `node:${params.node.nodeId}`;
1801
+ const common = {
1802
+ hostId,
1803
+ label: nodeLabel(params.node),
1804
+ kind: "node",
1805
+ nodeId: params.node.nodeId,
1806
+ canContinueCodex: canContinueCodexOnNode(params.node)
1807
+ };
1808
+ if (params.node.connected !== true) {
1809
+ const host = {
1810
+ ...common,
1811
+ connected: false,
1812
+ sessions: [],
1813
+ error: {
1814
+ code: "NODE_OFFLINE",
1815
+ message: "Paired node is offline"
1816
+ }
1817
+ };
1818
+ params.onHost?.(host);
1819
+ return host;
1820
+ }
1821
+ const eventualHost = Promise.resolve().then(async () => {
1822
+ const page = filterCatalogPageByTitle(parseCatalogPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1823
+ nodeId: params.node.nodeId,
1824
+ command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1825
+ params: {
1826
+ agentId: params.agentId,
1827
+ cursor: params.query.cursors?.[hostId],
1828
+ limit: params.query.limitPerHost,
1829
+ searchTerm: params.query.search
1830
+ },
1831
+ timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1832
+ scopes: ["operator.write"]
1833
+ }))), params.query.search);
2191
1834
  return {
2192
- hostId: params.hostId,
2193
- label: "Local Codex",
2194
- threadId: params.threadId,
2195
- items: flattenTranscriptPageDesc(page),
2196
- ...page.nextCursor ? { nextCursor: page.nextCursor } : {},
2197
- ...page.backwardsCursor ? { backwardsCursor: page.backwardsCursor } : {}
1835
+ ...common,
1836
+ connected: true,
1837
+ ...page,
1838
+ sessions: page.sessions.map((session) => {
1839
+ const adopted = params.adoptedSessions.get(sessionCatalogAdoptedSourceKey(hostId, session.threadId));
1840
+ return adopted ? Object.assign({}, session, { sessionKey: adopted.key }) : session;
1841
+ })
1842
+ };
1843
+ }).catch((error) => ({
1844
+ ...common,
1845
+ connected: true,
1846
+ sessions: [],
1847
+ error: catalogError("NODE_INVOKE_FAILED", error)
1848
+ }));
1849
+ if (params.onHost) eventualHost.then(params.onHost).catch(() => void 0);
1850
+ try {
1851
+ return await withTimeout(eventualHost, NODE_CATALOG_LIST_RESPONSE_TIMEOUT_MS, "paired node Codex session catalog timed out");
1852
+ } catch (error) {
1853
+ return {
1854
+ ...common,
1855
+ connected: true,
1856
+ sessions: [],
1857
+ error: catalogError("NODE_INVOKE_FAILED", error)
2198
1858
  };
2199
1859
  }
2200
- const nodeId = params.hostId.slice(5);
2201
- const node = (await params.runtime.nodes.list()).nodes.find((candidate) => candidate.nodeId === nodeId && candidate.connected === true && candidate.commands?.includes("codex.appServer.thread.turns.list.v1"));
2202
- if (!node) throw new CatalogParamsError("paired-node Codex session host is offline or unavailable");
1860
+ }
1861
+ async function requireNodeForCodexContinue(params) {
1862
+ const nodeId = params.hostId.slice(5).trim();
1863
+ if (!nodeId || params.hostId !== `node:${nodeId}`) throw new CatalogParamsError("Codex session catalog hostId is invalid");
1864
+ const node = (await params.runtime.nodes.list()).nodes.find((candidate) => candidate.nodeId === nodeId);
1865
+ if (!node || !canContinueCodexOnNode(node)) throw new CatalogParamsError("paired node does not permit Codex session continuation");
1866
+ return {
1867
+ node,
1868
+ nodeId
1869
+ };
1870
+ }
1871
+ async function resolveNodeCodexRecord(params) {
1872
+ let cursor;
1873
+ const seenCursors = /* @__PURE__ */ new Set();
1874
+ for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
1875
+ const page = parseCatalogPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
1876
+ nodeId: params.nodeId,
1877
+ command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
1878
+ params: {
1879
+ agentId: params.agentId,
1880
+ limit: 100,
1881
+ ...cursor ? { cursor } : {}
1882
+ },
1883
+ timeoutMs: NODE_INVOKE_TIMEOUT_MS,
1884
+ scopes: ["operator.write"]
1885
+ })));
1886
+ const record = page.sessions.find((candidate) => candidate.threadId === params.threadId);
1887
+ if (record) return record;
1888
+ const nextCursor = page.nextCursor?.trim();
1889
+ if (!nextCursor) break;
1890
+ if (seenCursors.has(nextCursor)) throw new CatalogParamsError("Codex session eligibility could not be verified");
1891
+ seenCursors.add(nextCursor);
1892
+ cursor = nextCursor;
1893
+ }
1894
+ throw new CatalogParamsError("Codex session is unavailable on the paired node");
1895
+ }
1896
+ function requireContinuableNodeRecord(record) {
1897
+ if (record.archived) throw new CatalogParamsError("Codex session is archived on the paired node");
1898
+ if (!isInteractiveThreadSource(record.source)) throw new CatalogParamsError("Codex session is not a non-archived interactive Codex session");
1899
+ if (record.status === "idle" || record.status === "notLoaded") return;
1900
+ if (record.status === "active") throw new CatalogParamsError("Codex session is active on the paired node; wait for it to finish before continuing");
1901
+ throw new CatalogParamsError("Codex session cannot be continued in its current state");
1902
+ }
1903
+ async function readNodeCodexHistory(params) {
2203
1904
  const page = parseTranscriptPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
2204
- nodeId,
1905
+ nodeId: params.nodeId,
2205
1906
  command: CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
2206
1907
  params: {
2207
- threadId: params.threadId,
2208
- limit: params.limit,
2209
- ...params.cursor ? { cursor: params.cursor } : {}
1908
+ agentId: params.agentId,
1909
+ threadId: params.record.threadId,
1910
+ limit: 50
2210
1911
  },
2211
1912
  timeoutMs: NODE_INVOKE_TIMEOUT_MS,
2212
1913
  scopes: ["operator.write"]
2213
1914
  })));
1915
+ const thread = {
1916
+ id: params.record.threadId,
1917
+ createdAt: params.record.createdAt ?? 0,
1918
+ modelProvider: params.record.modelProvider ?? "openai",
1919
+ projectId: null,
1920
+ turns: page.data.toReversed()
1921
+ };
2214
1922
  return {
2215
- hostId: params.hostId,
2216
- label: nodeLabel(node),
2217
- threadId: params.threadId,
2218
- items: flattenTranscriptPageDesc(page),
2219
- ...page.nextCursor ? { nextCursor: page.nextCursor } : {},
2220
- ...page.backwardsCursor ? { backwardsCursor: page.backwardsCursor } : {}
1923
+ thread,
1924
+ throughTurnId: codexLastTerminalTurnId(thread, (value) => boundedCatalogString(value, 256)) ?? null
2221
1925
  };
2222
1926
  }
2223
- function requireIdleThread(thread, action) {
2224
- if (thread.status?.type === "idle" || action === "archive" && thread.status?.type === "notLoaded") return;
2225
- if (thread.status?.type === "active") throw new CatalogParamsError(`Codex session is active in this App Server; wait for it to finish before ${action === "continue" ? "starting a branch" : "archiving"}`);
2226
- throw new CatalogParamsError(action === "archive" ? "Codex session cannot be archived in its current state" : "Codex session cannot start a branch in its current state");
2227
- }
2228
- function adoptionSessionKey(threadId) {
2229
- const digest = createHash("sha256").update(threadId).digest("hex");
2230
- return `${CODEX_SUPERVISION_SESSION_KEY_PREFIX}${digest}`;
2231
- }
2232
- function isAdoptionSessionKeyForThread(sessionKey, threadId) {
2233
- return adoptionSessionKeyRest(sessionKey) === adoptionSessionKey(threadId);
2234
- }
2235
- async function listAdoptedSessionEntries(params) {
2236
- const adopted = /* @__PURE__ */ new Map();
2237
- for (const { agentId, entry, sessionKey } of listSessionCatalogEntries({
2238
- config: params.config ?? {},
2239
- runtime: params.runtime,
2240
- sessionEntries: params.sessionEntries
2241
- })) {
2242
- const sessionKeyRest = adoptionSessionKeyRest(sessionKey);
2243
- if (!sessionKeyRest.startsWith(CODEX_SUPERVISION_SESSION_KEY_PREFIX) || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true) continue;
2244
- const sessionId = entry.sessionId?.trim();
2245
- if (!sessionId) continue;
2246
- const binding = await params.bindingStore.read(sessionBindingIdentity({
2247
- sessionId,
2248
- sessionKey,
2249
- config: params.config
2250
- }));
2251
- const sourceThreadId = binding?.supervisionSourceThreadId?.trim();
2252
- const boundThreadId = binding?.threadId.trim();
2253
- if (binding?.connectionScope !== "supervision" || !sourceThreadId || !boundThreadId || sessionKeyRest !== adoptionSessionKey(sourceThreadId)) continue;
2254
- if (adopted.has(sourceThreadId)) throw new Error(`multiple OpenClaw sessions adopt Codex thread ${sourceThreadId}`);
2255
- adopted.set(sourceThreadId, {
2256
- key: sessionKey,
2257
- sessionId,
2258
- agentId,
2259
- boundThreadId
1927
+ async function continueNodeCodexSessionInner(params) {
1928
+ const { nodeId } = await requireNodeForCodexContinue({
1929
+ runtime: params.api.runtime,
1930
+ hostId: params.hostId
1931
+ });
1932
+ const record = await resolveNodeCodexRecord({
1933
+ agentId: params.agentId,
1934
+ runtime: params.api.runtime,
1935
+ nodeId,
1936
+ threadId: params.threadId
1937
+ });
1938
+ requireContinuableNodeRecord(record);
1939
+ const existing = findNodeAdoptedSessionEntry({
1940
+ agentId: params.agentId,
1941
+ config: params.config,
1942
+ runtime: params.api.runtime,
1943
+ hostId: params.hostId,
1944
+ threadId: params.threadId,
1945
+ includeInitializing: true
1946
+ });
1947
+ let adopted;
1948
+ let disposition;
1949
+ if (existing) {
1950
+ adopted = existing;
1951
+ disposition = "existing";
1952
+ } else {
1953
+ const history = await readNodeCodexHistory({
1954
+ agentId: params.agentId,
1955
+ runtime: params.api.runtime,
1956
+ nodeId,
1957
+ record
2260
1958
  });
2261
- }
2262
- return adopted;
2263
- }
2264
- async function findAdoptedSessionEntry(params) {
2265
- return (await listAdoptedSessionEntries(params)).get(params.threadId);
2266
- }
2267
- async function clearCreatedAdoptionBinding(params) {
2268
- let cleared = false;
2269
- let clearError;
2270
- try {
2271
- cleared = await params.bindingStore.mutate(params.identity, {
2272
- kind: "clear",
2273
- threadId: params.sourceThreadId,
2274
- expectedPendingSupervisionBranch: params.expectedPending
1959
+ adopted = await createOrReuseNodeAdoptedSession({
1960
+ agentId: params.agentId,
1961
+ api: params.api,
1962
+ config: params.config,
1963
+ hostId: params.hostId,
1964
+ nodeId,
1965
+ record,
1966
+ history
2275
1967
  });
2276
- } catch (error) {
2277
- clearError = error;
2278
- }
2279
- if (cleared) return;
2280
- let current;
2281
- try {
2282
- current = await params.bindingStore.read(params.identity);
2283
- } catch (readError) {
2284
- throw new AggregateError([
2285
- params.cause,
2286
- ...clearError ? [clearError] : [],
2287
- readError
2288
- ], `OpenClaw session creation failed and the Codex binding could not be verified for ${params.sourceThreadId}`, { cause: readError });
1968
+ disposition = "forked";
2289
1969
  }
2290
- if (!matchesPendingSupervisionOwner(current, params.expectedPending)) return;
2291
- throw new AggregateError([params.cause, ...clearError ? [clearError] : []], `OpenClaw session creation failed and the Codex binding could not be cleared for ${params.sourceThreadId}`, { cause: params.cause });
2292
- }
2293
- function matchesPendingAdoptionBinding(binding, expected) {
2294
- const historyCoveredThrough = binding?.historyCoveredThrough;
2295
- return binding?.threadId === expected.sourceThreadId && binding.connectionScope === "supervision" && binding.supervisionSourceThreadId === expected.sourceThreadId && binding.cwd === expected.cwd && binding.conversationSourceTransferComplete === true && binding.preserveNativeModel === true && binding.pendingSupervisionBranch?.sourceThreadId === expected.sourceThreadId && binding.pendingSupervisionBranch.connectionFingerprint === expected.connectionFingerprint && binding.pendingSupervisionBranch.lastTurnId === expected.lastTurnId && (binding.pendingSupervisionBranch.cleanupThreadIds?.length ?? 0) === 0 && typeof historyCoveredThrough === "string" && Number.isFinite(Date.parse(historyCoveredThrough));
1970
+ const marker = nodeSessionMarker({
1971
+ hostId: params.hostId,
1972
+ threadId: params.threadId,
1973
+ nodeId
1974
+ });
1975
+ return {
1976
+ sessionKey: adopted.key,
1977
+ disposition,
1978
+ conversationBinding: {
1979
+ summary: "Continue this Codex session on its paired node.",
1980
+ detachHint: "Start a new chat to leave the paired-node Codex session.",
1981
+ data: createCodexCliNodeConversationBindingData({
1982
+ nodeId,
1983
+ sessionId: params.threadId,
1984
+ agentId: adopted.agentId,
1985
+ cwd: record.cwd
1986
+ })
1987
+ },
1988
+ afterConversationBound: async () => await finalizeNodeAdoptedSession({
1989
+ api: params.api,
1990
+ adopted,
1991
+ marker
1992
+ })
1993
+ };
2296
1994
  }
2297
- function matchesPendingSupervisionOwner(binding, expected) {
2298
- const pending = binding?.pendingSupervisionBranch;
2299
- const cleanupThreadIds = pending?.cleanupThreadIds ?? [];
2300
- const expectedCleanupThreadIds = expected.cleanupThreadIds ?? [];
2301
- return binding?.threadId === expected.sourceThreadId && binding.connectionScope === "supervision" && binding.supervisionSourceThreadId === expected.sourceThreadId && pending?.sourceThreadId === expected.sourceThreadId && pending.connectionFingerprint === expected.connectionFingerprint && pending.lastTurnId === expected.lastTurnId && cleanupThreadIds.length === expectedCleanupThreadIds.length && cleanupThreadIds.every((threadId, index) => threadId === expectedCleanupThreadIds[index]);
1995
+ async function continueNodeCodexSession(params) {
1996
+ if (params.clientScopes?.includes("operator.admin") !== true) throw new CatalogParamsError("continuing a paired-node Codex session requires operator.admin");
1997
+ const nodeId = params.hostId.slice(5).trim();
1998
+ if (!nodeId || params.hostId !== `node:${nodeId}`) throw new CatalogParamsError("Codex session catalog hostId is invalid");
1999
+ const agentId = resolveSessionAgentIdsStrict({
2000
+ config: params.config,
2001
+ agentId: params.agentId
2002
+ }).sessionAgentId;
2003
+ const sourceKey = sessionCatalogAdoptedSourceKey(`node:${nodeId}`, params.threadId);
2004
+ const operationKey = sessionCatalogAdoptedSourceKey(agentId, sourceKey);
2005
+ return await continueNodeAdoption({
2006
+ sourceKey: operationKey,
2007
+ findExisting: () => void 0,
2008
+ create: () => runSessionActionExclusive(sourceKey, async () => continueNodeCodexSessionInner({
2009
+ ...params,
2010
+ agentId
2011
+ })),
2012
+ complete: async (continued) => continued
2013
+ });
2302
2014
  }
2303
- async function ensurePendingAdoptionBinding(params) {
2304
- const pending = {
2305
- sourceThreadId: params.sourceThreadId,
2306
- connectionFingerprint: params.connectionFingerprint,
2307
- ...params.lastTurnId ? { lastTurnId: params.lastTurnId } : {}
2308
- };
2309
- if (!await reclaimCurrentCodexSessionGeneration({
2310
- bindingStore: params.bindingStore,
2311
- identity: params.identity,
2312
- config: params.config
2313
- })) throw new Error(`failed to claim the OpenClaw session generation for ${params.sourceThreadId}`);
2314
- const existing = await params.bindingStore.read(params.identity);
2315
- if (existing) {
2316
- if (matchesPendingAdoptionBinding(existing, params)) return;
2317
- throw new Error(`OpenClaw session is already bound to Codex thread ${existing.threadId}`);
2015
+ //#endregion
2016
+ //#region extensions/codex/src/session-catalog-listing.ts
2017
+ async function listVisiblePage(params) {
2018
+ const excluded = params.excludedThreadIds;
2019
+ const sessions = [];
2020
+ let cursor = params.cursor;
2021
+ let nextCursor;
2022
+ let backwardsCursor;
2023
+ const seenCursors = /* @__PURE__ */ new Set();
2024
+ for (let pageIndex = 0; pageIndex < 20; pageIndex += 1) {
2025
+ let excludedFromPage = false;
2026
+ const rawPage = await params.control.listPage({
2027
+ limit: params.limit - sessions.length,
2028
+ ...cursor ? { cursor } : {},
2029
+ ...params.searchTerm ? { searchTerm: params.searchTerm } : {},
2030
+ ...params.cwd ? { cwd: params.cwd } : {}
2031
+ });
2032
+ const page = filterCatalogPageByTitle(parseCatalogPage(rawPage), params.searchTerm);
2033
+ if (pageIndex === 0) backwardsCursor = page.backwardsCursor;
2034
+ for (const managed of rawPage.managedThreads ?? []) {
2035
+ excludedFromPage = true;
2036
+ await params.onExcludedThread?.(managed);
2037
+ }
2038
+ for (const session of page.sessions) {
2039
+ if (!excluded?.has(session.threadId)) {
2040
+ sessions.push(session);
2041
+ continue;
2042
+ }
2043
+ excludedFromPage = true;
2044
+ await params.onExcludedThread?.({ threadId: session.threadId });
2045
+ }
2046
+ nextCursor = page.nextCursor;
2047
+ if (!nextCursor || sessions.length >= params.limit || !excludedFromPage) break;
2048
+ if (seenCursors.has(nextCursor)) throw new Error("Codex session catalog returned a repeated exclusion cursor");
2049
+ seenCursors.add(nextCursor);
2050
+ cursor = nextCursor;
2318
2051
  }
2319
- const binding = {
2320
- threadId: params.sourceThreadId,
2321
- connectionScope: "supervision",
2322
- supervisionSourceThreadId: params.sourceThreadId,
2323
- cwd: params.cwd,
2324
- historyCoveredThrough: (/* @__PURE__ */ new Date()).toISOString(),
2325
- conversationSourceTransferComplete: true,
2326
- preserveNativeModel: true,
2327
- pendingSupervisionBranch: pending
2052
+ return {
2053
+ sessions: sessions.slice(0, params.limit),
2054
+ ...nextCursor ? { nextCursor } : {},
2055
+ ...backwardsCursor ? { backwardsCursor } : {}
2328
2056
  };
2329
- let stored;
2057
+ }
2058
+ async function listGatewayHost(params) {
2059
+ const hostId = params.source?.hostId ?? "gateway:local";
2060
+ const label = params.source?.label ?? "Local Codex";
2061
+ const sourceHomeId = params.source?.sourceHomeId ?? "gateway:local";
2330
2062
  try {
2331
- stored = await params.bindingStore.mutate(params.identity, {
2332
- kind: "set",
2333
- if: { kind: "absent" },
2334
- binding
2063
+ const page = await listVisiblePage({
2064
+ control: params.control,
2065
+ cursor: params.query.cursors?.[hostId],
2066
+ excludedThreadIds: params.excludedThreadIds,
2067
+ limit: params.query.limitPerHost,
2068
+ onExcludedThread: params.onExcludedThread,
2069
+ searchTerm: params.query.search
2070
+ });
2071
+ const adoptedSessions = await listAdoptedSessionEntries({
2072
+ agentId: params.agentId,
2073
+ bindingStore: params.bindingStore,
2074
+ config: params.config,
2075
+ runtime: params.runtime,
2076
+ sessionEntries: params.sessionEntries
2335
2077
  });
2078
+ return {
2079
+ hostId,
2080
+ label,
2081
+ kind: "gateway",
2082
+ connected: true,
2083
+ ...page,
2084
+ sessions: page.sessions.map((session) => {
2085
+ const adopted = adoptedSessions.get(sessionCatalogAdoptedSourceKey(sourceHomeId, session.threadId)) ?? (hostId === "gateway:local" ? adoptedSessions.get(sessionCatalogAdoptedSourceKey("gateway:local", session.threadId)) : void 0);
2086
+ const sourced = params.source ? Object.assign({}, session, { sourceHomeId: params.source.sourceHomeId }) : session;
2087
+ return adopted ? Object.assign({}, sourced, { sessionKey: adopted.key }) : sourced;
2088
+ })
2089
+ };
2336
2090
  } catch (error) {
2337
- if (matchesPendingAdoptionBinding(await params.bindingStore.read(params.identity), params)) return;
2338
- throw error;
2091
+ return {
2092
+ hostId,
2093
+ label,
2094
+ kind: "gateway",
2095
+ connected: false,
2096
+ sessions: [],
2097
+ error: catalogError("APP_SERVER_UNAVAILABLE", error)
2098
+ };
2339
2099
  }
2340
- if (stored) return;
2341
- if (!matchesPendingAdoptionBinding(await params.bindingStore.read(params.identity), params)) throw new Error(`failed to bind OpenClaw session to Codex thread ${params.sourceThreadId}`);
2342
2100
  }
2343
- async function createOrReuseAdoptedSession(params) {
2344
- const existing = await findAdoptedSessionEntry({
2345
- bindingStore: params.bindingStore,
2346
- config: params.config,
2347
- runtime: params.api.runtime,
2348
- threadId: params.sourceThread.id
2349
- });
2350
- if (existing) return existing;
2351
- let createdBindingIdentity;
2352
- let createdPendingBinding;
2353
- try {
2354
- const spawnedCwd = params.sourceThread.cwd?.trim() || void 0;
2355
- const pendingLastTurnId = codexLastTerminalTurnId(params.sourceThread, boundCatalogSessionId);
2356
- const marker = { sourceThreadId: params.sourceThread.id };
2357
- const created = await createImportedCodexSession({
2358
- runtime: params.api.runtime,
2101
+ /** Lists Gateway-local and paired-node Codex sessions with per-host failures. */
2102
+ async function listCodexSessionCatalog(params) {
2103
+ const agentId = resolveSessionAgentIdsStrict({
2104
+ config: params.config ?? {},
2105
+ agentId: params.agentId
2106
+ }).sessionAgentId;
2107
+ const query = readGatewayParams(params.query);
2108
+ const requestedHostIds = query.hostIds ? new Set(query.hostIds) : void 0;
2109
+ const localSources = params.localHomes?.filter((source) => !requestedHostIds || requestedHostIds.has(source.hostId)) ?? (params.includeLocal !== false && (!requestedHostIds || requestedHostIds.has("gateway:local")) ? [void 0] : []);
2110
+ const managedThreads = await params.bindingStore.managedThreads?.snapshot();
2111
+ const fallbackSource = params.control.homesForAgent(agentId)[0];
2112
+ const localHosts = localSources.map((source) => (() => {
2113
+ const ownershipSource = source ?? fallbackSource;
2114
+ const managedThreadIds = ownershipSource ? managedThreads?.get(ownershipSource.sourceHomeId) : void 0;
2115
+ return listGatewayHost({
2116
+ agentId,
2117
+ bindingStore: params.bindingStore,
2359
2118
  config: params.config,
2360
- key: adoptionSessionKey(params.sourceThread.id),
2361
- agentId: resolveDefaultAgentId(params.config),
2362
- thread: params.sourceThread,
2363
- throughTurnId: pendingLastTurnId ?? null,
2364
- recoverMatchingInitialEntry: true,
2365
- initialEntry: {
2366
- agentHarnessId: "codex",
2367
- modelSelectionLocked: true,
2368
- pluginExtensions: { codex: { supervision: {
2369
- ...marker,
2370
- initializing: true,
2371
- modelLocked: true
2372
- } } }
2373
- },
2374
- afterImport: async (entry) => {
2375
- createdBindingIdentity = sessionBindingIdentity({
2376
- sessionId: entry.sessionId,
2377
- sessionKey: entry.key,
2378
- config: params.config
2379
- });
2380
- createdPendingBinding = {
2381
- sourceThreadId: params.sourceThread.id,
2382
- connectionFingerprint: params.connectionFingerprint,
2383
- ...pendingLastTurnId ? { lastTurnId: pendingLastTurnId } : {}
2384
- };
2385
- await ensurePendingAdoptionBinding({
2386
- bindingStore: params.bindingStore,
2387
- config: params.config,
2388
- identity: createdBindingIdentity,
2389
- sourceThreadId: params.sourceThread.id,
2390
- connectionFingerprint: params.connectionFingerprint,
2391
- cwd: spawnedCwd ?? "",
2392
- ...pendingLastTurnId ? { lastTurnId: pendingLastTurnId } : {}
2119
+ control: params.control.forRequest(agentId, ownershipSource),
2120
+ query,
2121
+ runtime: params.runtime,
2122
+ sessionEntries: params.sessionEntries,
2123
+ excludedThreadIds: managedThreadIds,
2124
+ ...ownershipSource && params.bindingStore.managedThreads ? { onExcludedThread: async ({ threadId, rolloutPath }) => {
2125
+ if (!managedThreadIds?.has(threadId)) await params.bindingStore.managedThreads?.mark({
2126
+ sourceHomeId: ownershipSource.sourceHomeId,
2127
+ threadId,
2128
+ ...rolloutPath ? { rolloutPath } : {}
2393
2129
  });
2394
- return { pluginExtensions: { codex: { supervision: {
2395
- ...marker,
2396
- modelLocked: true
2397
- } } } };
2398
- }
2130
+ } } : {},
2131
+ ...source ? { source } : {}
2399
2132
  });
2400
- return {
2401
- key: created.key,
2402
- sessionId: created.sessionId,
2403
- agentId: created.agentId,
2404
- boundThreadId: params.sourceThread.id
2405
- };
2133
+ })());
2134
+ for (const host of localHosts) if (params.onHost) host.then(params.onHost).catch(() => void 0);
2135
+ if (!(!requestedHostIds || query.hostIds?.some((hostId) => hostId.startsWith("node:")))) return { hosts: await Promise.all(localHosts) };
2136
+ let nodes;
2137
+ try {
2138
+ nodes = (await (params.listNodes?.() ?? params.runtime.nodes.list())).nodes.filter((node) => node.gatewayLocal !== true && node.commands?.includes("codex.appServer.threads.list.v1") && (!requestedHostIds || requestedHostIds.has(`node:${node.nodeId}`))).slice(0, 100 - localHosts.length);
2406
2139
  } catch (error) {
2407
- let raced = await findAdoptedSessionEntry({
2408
- bindingStore: params.bindingStore,
2409
- config: params.config,
2410
- runtime: params.api.runtime,
2411
- threadId: params.sourceThread.id
2412
- });
2413
- if (raced) return raced;
2414
- if (createdBindingIdentity && createdPendingBinding) {
2415
- await clearCreatedAdoptionBinding({
2416
- bindingStore: params.bindingStore,
2417
- identity: createdBindingIdentity,
2418
- sourceThreadId: params.sourceThread.id,
2419
- expectedPending: createdPendingBinding,
2420
- cause: error
2421
- });
2422
- raced = await findAdoptedSessionEntry({
2423
- bindingStore: params.bindingStore,
2424
- config: params.config,
2425
- runtime: params.api.runtime,
2426
- threadId: params.sourceThread.id
2427
- });
2428
- if (raced) return raced;
2429
- }
2430
- throw error;
2140
+ const registryHost = {
2141
+ hostId: "node:registry",
2142
+ label: "Paired nodes",
2143
+ kind: "node",
2144
+ connected: false,
2145
+ sessions: [],
2146
+ error: catalogError("NODE_LIST_FAILED", error)
2147
+ };
2148
+ params.onHost?.(registryHost);
2149
+ return { hosts: [...await Promise.all(localHosts), registryHost] };
2431
2150
  }
2432
- }
2433
- async function continueLocalCodexSessionInner(params) {
2434
- await requireCatalogEligibleThread(params.control, params.threadId);
2435
- const existing = await findAdoptedSessionEntry({
2436
- bindingStore: params.bindingStore,
2151
+ const adoptedNodeSessions = listNodeAdoptedSessionEntries({
2152
+ agentId,
2437
2153
  config: params.config,
2438
- runtime: params.api.runtime,
2439
- threadId: params.threadId
2154
+ runtime: params.runtime,
2155
+ sessionEntries: params.sessionEntries
2440
2156
  });
2441
- if (existing) {
2442
- const boundThreadId = requireBoundThread(existing);
2443
- const boundThread = await params.control.readThread(boundThreadId, true);
2444
- if (boundThread.id !== boundThreadId) throw new Error("Codex app-server returned a different thread than requested");
2445
- const changedError = () => new CatalogParamsError("Codex OpenClaw session changed before it could be opened. Retry.");
2446
- if (!await params.api.runtime.agent.session.patchSessionEntry({
2447
- sessionKey: existing.key,
2448
- readConsistency: "latest",
2449
- preserveActivity: true,
2450
- update: (entry) => {
2451
- if (entry.sessionId?.trim() !== existing.sessionId || entry.initializationPending === true || entry.agentHarnessId !== "codex" || entry.modelSelectionLocked !== true) throw changedError();
2452
- return { archivedAt: void 0 };
2453
- }
2454
- })) throw changedError();
2455
- const connectionFingerprint = params.control.connectionFingerprint;
2456
- if (connectionFingerprint) params.onContinued?.({
2457
- connectionFingerprint,
2458
- ...codexUpstreamBaseline(boundThread, boundCatalogSessionId)
2157
+ const nodeHosts = nodes.toSorted(compareNodeLabels).map(async (node) => {
2158
+ const host = await listPairedNode({
2159
+ agentId,
2160
+ runtime: params.runtime,
2161
+ node,
2162
+ query,
2163
+ adoptedSessions: adoptedNodeSessions,
2164
+ ...params.onHost ? { onHost: params.onHost } : {}
2459
2165
  });
2166
+ return Object.assign(host, codexNodeTerminalCapability(node));
2167
+ });
2168
+ return { hosts: await Promise.all([...localHosts, ...nodeHosts]) };
2169
+ }
2170
+ /** Builds the node-local read-only Codex app-server catalog command. */
2171
+ function createCodexSessionCatalogNodeHostCommands(controlFactory, configSources, bindingStore) {
2172
+ const bindRequest = (paramsJSON) => {
2173
+ const parsed = parseJsonParams(paramsJSON);
2174
+ if (!isRecord(parsed)) throw new CatalogParamsError("Codex session catalog parameters must be an object");
2175
+ const requestedAgentId = readBoundedOptionalString(parsed, "agentId", 256);
2176
+ const config = configSources.getRuntimeConfig() ?? {};
2177
+ const agentId = resolveSessionAgentIdsStrict({
2178
+ config,
2179
+ agentId: requestedAgentId
2180
+ }).sessionAgentId;
2181
+ if (!listAgentIds(config).includes(agentId)) throw new CatalogParamsError(`unknown Codex session catalog agent: ${agentId}`);
2182
+ const request = { ...parsed };
2183
+ delete request.agentId;
2184
+ const source = controlFactory.homesForAgent(agentId)[0];
2460
2185
  return {
2461
- sessionKey: existing.key,
2462
- disposition: "existing"
2186
+ agentId,
2187
+ control: controlFactory.forRequest(agentId, source),
2188
+ sourceHomeId: source?.sourceHomeId,
2189
+ params: request,
2190
+ paramsJSON: JSON.stringify(request)
2191
+ };
2192
+ };
2193
+ return [
2194
+ {
2195
+ command: CODEX_APP_SERVER_THREADS_LIST_COMMAND,
2196
+ cap: CODEX_APP_SERVER_THREADS_CAPABILITY,
2197
+ dangerous: false,
2198
+ handle: async (paramsJSON) => {
2199
+ const request = bindRequest(paramsJSON);
2200
+ const pageParams = readPageParams(request.params);
2201
+ try {
2202
+ const managedThreads = await bindingStore?.managedThreads?.snapshot();
2203
+ const sourceHomeId = request.sourceHomeId;
2204
+ const managedThreadIds = sourceHomeId ? managedThreads?.get(sourceHomeId) : void 0;
2205
+ const page = await listVisiblePage({
2206
+ control: request.control,
2207
+ cursor: pageParams.cursor,
2208
+ cwd: pageParams.cwd,
2209
+ excludedThreadIds: managedThreadIds,
2210
+ limit: pageParams.limit,
2211
+ ...sourceHomeId && bindingStore?.managedThreads ? { onExcludedThread: async ({ threadId, rolloutPath }) => {
2212
+ if (!managedThreadIds?.has(threadId)) await bindingStore.managedThreads?.mark({
2213
+ sourceHomeId,
2214
+ threadId,
2215
+ ...rolloutPath ? { rolloutPath } : {}
2216
+ });
2217
+ } } : {},
2218
+ searchTerm: pageParams.searchTerm
2219
+ });
2220
+ return JSON.stringify(page);
2221
+ } catch {
2222
+ throw new Error("Codex app-server catalog is unavailable");
2223
+ }
2224
+ }
2225
+ },
2226
+ {
2227
+ command: CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
2228
+ cap: CODEX_APP_SERVER_THREADS_CAPABILITY,
2229
+ dangerous: false,
2230
+ handle: async (paramsJSON) => {
2231
+ const request = bindRequest(paramsJSON);
2232
+ const action = readNodeTranscriptParams(request.params);
2233
+ try {
2234
+ await requireCatalogEligibleThread(request.control, action.threadId);
2235
+ const page = parseTranscriptPage(await request.control.listTurnPage({
2236
+ threadId: action.threadId,
2237
+ limit: action.limit,
2238
+ sortDirection: "desc",
2239
+ itemsView: "full",
2240
+ ...action.cursor ? { cursor: action.cursor } : {}
2241
+ }));
2242
+ return JSON.stringify(page);
2243
+ } catch (error) {
2244
+ if (error instanceof CatalogParamsError) throw error;
2245
+ throw new Error("Codex app-server transcript is unavailable", { cause: error });
2246
+ }
2247
+ }
2248
+ },
2249
+ createCodexTerminalNodeHostCommand(bindRequest, configSources)
2250
+ ];
2251
+ }
2252
+ function readNodeTranscriptParams(value) {
2253
+ if (!isRecord(value)) throw new CatalogParamsError("Codex session read parameters must be an object");
2254
+ requireOnlyKeys(value, /* @__PURE__ */ new Set([
2255
+ "threadId",
2256
+ "cursor",
2257
+ "limit"
2258
+ ]));
2259
+ const threadId = readBoundedOptionalString(value, "threadId", 256);
2260
+ if (!threadId) throw new CatalogParamsError("threadId is required");
2261
+ const cursor = readBoundedOptionalString(value, "cursor", MAX_CURSOR_LENGTH);
2262
+ return {
2263
+ threadId,
2264
+ limit: readBoundedLimit(value.limit, "limit", 20, 50),
2265
+ ...cursor ? { cursor } : {}
2266
+ };
2267
+ }
2268
+ function readBoundedLimit(value, key, fallback, max) {
2269
+ if (value === void 0) return fallback;
2270
+ if (!Number.isInteger(value) || value < 1 || value > max) throw new CatalogParamsError(`${key} must be an integer from 1 to ${max}`);
2271
+ return value;
2272
+ }
2273
+ function flattenTranscriptPageDesc(page) {
2274
+ return page.data.flatMap((turn) => turn.items.toReversed());
2275
+ }
2276
+ /** Reads the persisted transcript for a Gateway-local or paired-node Codex session. */
2277
+ async function readCodexSessionTranscript(params) {
2278
+ if (params.source || params.hostId === "gateway:local") {
2279
+ await requireCatalogEligibleThread(params.control, params.threadId);
2280
+ const listParams = {
2281
+ threadId: params.threadId,
2282
+ limit: params.limit,
2283
+ sortDirection: "desc",
2284
+ itemsView: "full",
2285
+ ...params.cursor ? { cursor: params.cursor } : {}
2286
+ };
2287
+ const page = parseTranscriptPage(await params.control.listTurnPage(listParams));
2288
+ return {
2289
+ hostId: params.hostId,
2290
+ label: params.source?.label ?? "Local Codex",
2291
+ threadId: params.threadId,
2292
+ items: flattenTranscriptPageDesc(page),
2293
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {},
2294
+ ...page.backwardsCursor ? { backwardsCursor: page.backwardsCursor } : {}
2463
2295
  };
2464
2296
  }
2465
- const sourceThread = await params.control.readThread(params.threadId, true);
2466
- if (sourceThread.id !== params.threadId) throw new Error("Codex app-server returned a different thread than requested");
2467
- if (sourceThread.status?.type !== "notLoaded") requireIdleThread(sourceThread, "continue");
2468
- const connectionFingerprint = params.control.connectionFingerprint;
2469
- if (!connectionFingerprint) throw new Error("Codex Continue requires a pinned app-server connection");
2470
- const adopted = await createOrReuseAdoptedSession({
2471
- api: params.api,
2472
- bindingStore: params.bindingStore,
2473
- config: params.config,
2474
- sourceThread,
2475
- connectionFingerprint
2476
- });
2477
- const boundThreadId = requireBoundThread(adopted);
2478
- const baselineThread = boundThreadId === sourceThread.id ? sourceThread : await params.control.readThread(boundThreadId, true);
2479
- if (baselineThread.id !== boundThreadId) throw new Error("Codex app-server returned a different thread than requested");
2480
- params.onContinued?.({
2481
- connectionFingerprint,
2482
- ...codexUpstreamBaseline(baselineThread, boundCatalogSessionId)
2297
+ const nodeId = params.hostId.slice(5);
2298
+ const node = (await params.runtime.nodes.list()).nodes.find((candidate) => candidate.nodeId === nodeId && candidate.connected === true && candidate.commands?.includes("codex.appServer.thread.turns.list.v1"));
2299
+ if (!node) throw new CatalogParamsError("paired-node Codex session host is offline or unavailable");
2300
+ const page = parseTranscriptPage(unwrapNodeInvokePayload(await params.runtime.nodes.invoke({
2301
+ nodeId,
2302
+ command: CODEX_APP_SERVER_THREAD_TURNS_LIST_COMMAND,
2303
+ params: {
2304
+ agentId: params.agentId,
2305
+ threadId: params.threadId,
2306
+ limit: params.limit,
2307
+ ...params.cursor ? { cursor: params.cursor } : {}
2308
+ },
2309
+ timeoutMs: NODE_INVOKE_TIMEOUT_MS,
2310
+ scopes: ["operator.write"]
2311
+ })));
2312
+ return {
2313
+ hostId: params.hostId,
2314
+ label: nodeLabel(node),
2315
+ threadId: params.threadId,
2316
+ items: flattenTranscriptPageDesc(page),
2317
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {},
2318
+ ...page.backwardsCursor ? { backwardsCursor: page.backwardsCursor } : {}
2319
+ };
2320
+ }
2321
+ //#endregion
2322
+ //#region extensions/codex/src/session-catalog-transcript-item.ts
2323
+ const CODEX_MESSAGE_TYPES = /* @__PURE__ */ new Map([
2324
+ ["userMessage", "userMessage"],
2325
+ ["agentMessage", "agentMessage"],
2326
+ ["reasoning", "reasoning"]
2327
+ ]);
2328
+ const CODEX_TOOL_TYPES = /* @__PURE__ */ new Set([
2329
+ "commandExecution",
2330
+ "fileChange",
2331
+ "mcpToolCall",
2332
+ "dynamicToolCall",
2333
+ "collabAgentToolCall",
2334
+ "webSearch",
2335
+ "imageView",
2336
+ "imageGeneration"
2337
+ ]);
2338
+ function toGenericTranscriptItem(item) {
2339
+ let type = CODEX_MESSAGE_TYPES.get(item.type);
2340
+ if (!type && CODEX_TOOL_TYPES.has(item.type)) type = item.result !== void 0 || Boolean(item.aggregatedOutput) ? "toolResult" : "toolCall";
2341
+ type ??= "other";
2342
+ const fallback = item.title ?? item.name ?? item.tool ?? item.command ?? item.query ?? void 0;
2343
+ const resultText = item.aggregatedOutput || (item.result === void 0 ? void 0 : JSON.stringify(item.result, null, 2));
2344
+ const changesText = Array.isArray(item.changes) ? item.changes.map((change) => `${change.kind}: ${change.path}`).join("\n") || void 0 : void 0;
2345
+ const text = item.type === "userMessage" ? projectCodexUserItemText(item) : item.text || resultText || changesText || fallback;
2346
+ return {
2347
+ id: item.id,
2348
+ type,
2349
+ ...text ? { text } : {},
2350
+ raw: item
2351
+ };
2352
+ }
2353
+ //#endregion
2354
+ //#region extensions/codex/src/session-upstream-activity.ts
2355
+ const CODEX_UPSTREAM_TURN_LIMIT = 100;
2356
+ const CODEX_APP_SERVER_INVALID_REQUEST_CODE = -32600;
2357
+ const CODEX_THREAD_NOT_LOADED_MESSAGE_PREFIX = "thread not loaded:";
2358
+ function isCodexThreadGoneError(error) {
2359
+ return error instanceof CodexAppServerRpcError && error.code === CODEX_APP_SERVER_INVALID_REQUEST_CODE && error.message.startsWith(CODEX_THREAD_NOT_LOADED_MESSAGE_PREFIX);
2360
+ }
2361
+ function readMarker(probe) {
2362
+ if (!isRecord(probe.marker)) return;
2363
+ const turnId = probe.marker.turnId;
2364
+ if (turnId !== null && typeof turnId !== "string") return;
2365
+ const count = probe.marker.userMessageCount;
2366
+ if (count !== void 0 && (!Number.isSafeInteger(count) || count < 0)) return;
2367
+ return {
2368
+ turnId,
2369
+ ...count === void 0 ? {} : { userMessageCount: count }
2370
+ };
2371
+ }
2372
+ function upstreamConnectionFingerprint(probe) {
2373
+ return isRecord(probe.upstreamRef) && typeof probe.upstreamRef.connectionFingerprint === "string" ? probe.upstreamRef.connectionFingerprint : void 0;
2374
+ }
2375
+ function classifyCodexUpstreamTurns(params) {
2376
+ const marker = readMarker(params.probe);
2377
+ if (!marker) return;
2378
+ const newest = params.turns[0];
2379
+ if (!newest?.id) return;
2380
+ const markerIndex = marker.turnId === null ? -1 : params.turns.findIndex((turn) => turn.id === marker.turnId);
2381
+ const candidateTurns = markerIndex < 0 ? params.turns : params.turns.slice(0, markerIndex + 1);
2382
+ const newestUserMessageCount = countUserMessages(newest);
2383
+ if (!(marker.turnId !== newest.id || marker.userMessageCount === void 0 || newestUserMessageCount > marker.userMessageCount)) return;
2384
+ const ownTexts = new Set(params.probe.ownRecentUserTexts);
2385
+ let humanTurns = 0;
2386
+ let occurredAt;
2387
+ for (const turn of candidateTurns) {
2388
+ const userMessages = turn.items.filter((item) => item.type === "userMessage");
2389
+ const alreadySeen = turn.id === marker.turnId ? marker.userMessageCount ?? userMessages.length : 0;
2390
+ for (const item of userMessages.slice(alreadySeen)) {
2391
+ const texts = normalizeUserMessageTexts(item);
2392
+ if (ownTexts.has(texts.join(" ")) || texts.length > 1 && texts.every((text) => ownTexts.has(text))) continue;
2393
+ humanTurns += 1;
2394
+ if (occurredAt === void 0) {
2395
+ const timestampSeconds = turn.completedAt ?? turn.startedAt;
2396
+ occurredAt = typeof timestampSeconds === "number" && Number.isFinite(timestampSeconds) ? timestampSeconds * 1e3 : params.now ?? Date.now();
2397
+ }
2398
+ }
2399
+ }
2400
+ const activityId = `${newest.id}:${newestUserMessageCount}`;
2401
+ return {
2402
+ kind: "activity",
2403
+ sessionKey: params.probe.sessionKey,
2404
+ humanTurns,
2405
+ nextMarker: {
2406
+ turnId: newest.id,
2407
+ userMessageCount: newestUserMessageCount
2408
+ },
2409
+ ...humanTurns > 0 ? {
2410
+ occurredAt: occurredAt ?? params.now ?? Date.now(),
2411
+ dedupeId: activityId
2412
+ } : {}
2413
+ };
2414
+ }
2415
+ function countUserMessages(turn) {
2416
+ return turn.items.filter((item) => item.type === "userMessage").length;
2417
+ }
2418
+ function normalizeUserMessageTexts(item) {
2419
+ const typed = item;
2420
+ const contentTexts = typed.content?.filter((input) => input.type === "text").map((input) => input.text.trim().replace(/\s+/g, " ")).filter(Boolean);
2421
+ return contentTexts?.length ? contentTexts : [(typed.text ?? "").trim().replace(/\s+/g, " ")];
2422
+ }
2423
+ async function checkCodexUpstreamActivity(probes, control, resolveThreadId = async (probe) => probe.threadId) {
2424
+ return await control.withPinnedConnection(async (pinned) => {
2425
+ const activities = [];
2426
+ for (const probe of probes) {
2427
+ const fingerprint = upstreamConnectionFingerprint(probe);
2428
+ if (probe.upstreamKind !== "codex-app-server" || !fingerprint || fingerprint !== pinned.connectionFingerprint) continue;
2429
+ try {
2430
+ const threadId = await resolveThreadId(probe);
2431
+ const page = await pinned.listTurnPage({
2432
+ threadId,
2433
+ limit: CODEX_UPSTREAM_TURN_LIMIT,
2434
+ sortDirection: "desc",
2435
+ itemsView: "full"
2436
+ });
2437
+ const marker = readMarker(probe);
2438
+ if (page.data.length === 0 && marker) {
2439
+ try {
2440
+ await pinned.readThread(threadId, false);
2441
+ } catch (error) {
2442
+ if (isCodexThreadGoneError(error)) activities.push({
2443
+ kind: "missing",
2444
+ sessionKey: probe.sessionKey
2445
+ });
2446
+ }
2447
+ continue;
2448
+ }
2449
+ const activity = classifyCodexUpstreamTurns({
2450
+ probe,
2451
+ turns: page.data
2452
+ });
2453
+ if (activity) activities.push(activity);
2454
+ } catch {}
2455
+ }
2456
+ return activities;
2483
2457
  });
2484
- return {
2485
- sessionKey: adopted.key,
2486
- disposition: "forked"
2458
+ }
2459
+ function createChecker(params) {
2460
+ const resolveThreadId = async (probe) => {
2461
+ const config = params.getRuntimeConfig();
2462
+ const sessionId = params.api.runtime.agent.session.getSessionEntry({
2463
+ agentId: probe.agentId,
2464
+ sessionKey: probe.sessionKey,
2465
+ readConsistency: "latest"
2466
+ })?.sessionId?.trim();
2467
+ if (!sessionId) return probe.threadId;
2468
+ const binding = await params.bindingStore.read(sessionBindingIdentity({
2469
+ sessionId,
2470
+ sessionKey: probe.sessionKey,
2471
+ config
2472
+ }));
2473
+ return binding?.connectionScope === "supervision" && binding.supervisionSourceThreadId === probe.threadId ? binding.threadId : probe.threadId;
2474
+ };
2475
+ return async (probes) => {
2476
+ const groups = /* @__PURE__ */ new Map();
2477
+ for (const probe of probes) {
2478
+ const fingerprint = upstreamConnectionFingerprint(probe);
2479
+ if (!fingerprint) continue;
2480
+ const control = params.control.forUpstream(probe.agentId, fingerprint);
2481
+ if (!control) continue;
2482
+ const key = `${probe.agentId}\0${fingerprint}`;
2483
+ const group = groups.get(key) ?? {
2484
+ control,
2485
+ probes: []
2486
+ };
2487
+ group.probes.push(probe);
2488
+ groups.set(key, group);
2489
+ }
2490
+ return (await Promise.all([...groups.values()].map((group) => checkCodexUpstreamActivity(group.probes, group.control, resolveThreadId)))).flat();
2487
2491
  };
2488
2492
  }
2489
- /** Creates one locked OpenClaw branch whose first harness run forks the Codex source. */
2490
- async function continueLocalCodexSession(params) {
2491
- const sourceKey = adoptedSourceKey(CODEX_LOCAL_SESSION_HOST_ID, params.threadId);
2492
- const current = continueOperations.get(sourceKey);
2493
- if (current) return await current;
2494
- const operation = runSessionActionExclusive(sourceKey, async () => params.control.withPinnedConnection(async (control) => continueLocalCodexSessionInner({
2495
- ...params,
2496
- control
2497
- })));
2498
- continueOperations.set(sourceKey, operation);
2493
+ //#endregion
2494
+ //#region extensions/codex/src/session-catalog-homes.ts
2495
+ function existingCatalogHomeCandidates(value, label) {
2496
+ const codexHome = canonicalCodexCatalogHome(value);
2499
2497
  try {
2500
- return await operation;
2501
- } finally {
2502
- if (continueOperations.get(sourceKey) === operation) continueOperations.delete(sourceKey);
2498
+ if (!fsSync.statSync(codexHome).isDirectory()) return [];
2499
+ } catch {
2500
+ return [];
2503
2501
  }
2502
+ return [{
2503
+ codexHome,
2504
+ label: `Local Codex · ${label ?? path.basename(codexHome)}`
2505
+ }];
2504
2506
  }
2505
- async function assertNoPendingSupervisionBranch(params) {
2506
- const adoptedEntries = listSupervisionAgentIds(params.config).flatMap((agentId) => params.runtime.agent.session.listSessionEntries({
2507
- agentId,
2508
- readOnly: true
2509
- })).filter((candidate) => isAdoptionSessionKeyForThread(candidate.sessionKey, params.threadId));
2510
- for (const adopted of adoptedEntries) {
2511
- if (adopted.entry.initializationPending === true) throw new CatalogParamsError("Codex session cannot be archived while its OpenClaw branch is initializing");
2512
- const sessionId = adopted.entry.sessionId?.trim();
2513
- if (!sessionId) continue;
2514
- const binding = await params.bindingStore.read(sessionBindingIdentity({
2515
- sessionId,
2516
- sessionKey: adopted.sessionKey,
2517
- config: params.config
2507
+ /** Resolves every local Codex store the operator already owns, without path disclosure. */
2508
+ function resolveCodexCatalogHomes(params) {
2509
+ const { config, env, ownerAgentId, pluginConfig } = params;
2510
+ const ownerAgentDir = resolveAgentDir$1(config, ownerAgentId, env);
2511
+ const configuredHomes = readCodexPluginConfig(pluginConfig).sessionCatalog?.homes ?? [];
2512
+ const base = resolveCodexSupervisionAppServerRuntimeOptions({
2513
+ pluginConfig,
2514
+ env,
2515
+ agentDir: ownerAgentDir,
2516
+ config
2517
+ });
2518
+ const primaryCodexHome = canonicalCodexCatalogHome(resolveCodexAppServerLocalHomeDir(base.start, ownerAgentDir, env));
2519
+ const processUserHome = canonicalCodexCatalogHome(resolveCodexAppServerUserHomeDir(env));
2520
+ const processHomeConfigured = Boolean(env.CODEX_HOME?.trim());
2521
+ const candidates = [{
2522
+ codexHome: primaryCodexHome,
2523
+ label: "Local Codex",
2524
+ usesProcessHomeFallback: base.start.transport === "stdio" && base.start.homeScope === "user" && !processHomeConfigured
2525
+ }];
2526
+ if (base.start.transport === "stdio") {
2527
+ candidates.push({
2528
+ codexHome: processUserHome,
2529
+ label: "Local Codex · user",
2530
+ usesProcessHomeFallback: !processHomeConfigured
2531
+ });
2532
+ const agentIds = listAgentIds(config).toSorted((left, right) => left === ownerAgentId ? -1 : right === ownerAgentId ? 1 : left.localeCompare(right));
2533
+ candidates.push(...agentIds.flatMap((agentId) => existingCatalogHomeCandidates(resolveCodexAppServerHomeDir(resolveAgentDir$1(config, agentId, env)), agentId)), ...configuredHomes.flatMap((entry) => {
2534
+ const { path: home, label } = typeof entry === "string" ? { path: entry } : entry;
2535
+ return existingCatalogHomeCandidates(home, label);
2518
2536
  }));
2519
- if (binding?.connectionScope === "supervision" && binding.supervisionSourceThreadId === params.threadId && binding.pendingSupervisionBranch?.sourceThreadId === params.threadId) throw new CatalogParamsError("Codex session cannot be archived until its OpenClaw branch starts");
2520
2537
  }
2538
+ const seen = /* @__PURE__ */ new Set();
2539
+ const homes = [];
2540
+ for (const candidate of candidates) {
2541
+ if (seen.has(candidate.codexHome)) continue;
2542
+ seen.add(candidate.codexHome);
2543
+ const sourceHomeId = codexCatalogHomeId(candidate.codexHome);
2544
+ const primary = homes.length === 0;
2545
+ homes.push({
2546
+ sourceHomeId,
2547
+ hostId: primary ? CODEX_LOCAL_SESSION_HOST_ID : `${CODEX_LOCAL_SESSION_HOST_ID}:${sourceHomeId}`,
2548
+ label: candidate.label,
2549
+ agentDir: ownerAgentDir,
2550
+ appServer: primary ? base : {
2551
+ ...base,
2552
+ start: {
2553
+ ...base.start,
2554
+ homeScope: "user",
2555
+ env: {
2556
+ ...base.start.env,
2557
+ CODEX_HOME: candidate.codexHome
2558
+ }
2559
+ }
2560
+ },
2561
+ ...base.connectionClass === "remote" ? {} : { localSessionsRoot: path.join(candidate.codexHome, "sessions") },
2562
+ usesProcessHomeFallback: candidate.usesProcessHomeFallback ?? false
2563
+ });
2564
+ if (homes.length >= 100) break;
2565
+ }
2566
+ return homes;
2567
+ }
2568
+ /** Discovers Codex homes once per immutable Gateway config generation. */
2569
+ function createCodexCatalogHomeResolver(params) {
2570
+ const env = params.env ?? process.env;
2571
+ const homesByConfig = /* @__PURE__ */ new WeakMap();
2572
+ const buildSnapshot = (config) => {
2573
+ const pluginConfig = params.getPluginConfig();
2574
+ const homesByAgent = new Map(listAgentIds(config).map((agentId) => [agentId, resolveCodexCatalogHomes({
2575
+ config,
2576
+ pluginConfig,
2577
+ ownerAgentId: agentId,
2578
+ env
2579
+ })]));
2580
+ replaceCodexCatalogConnectionHomes([...homesByAgent.values()].flatMap((homes) => homes.filter((home) => home.appServer.start.transport === "stdio").map((home) => ({
2581
+ agentDir: home.agentDir,
2582
+ fingerprint: buildCodexAppServerConnectionFingerprint(home.appServer, home.agentDir),
2583
+ codexHome: resolveCodexAppServerLocalHomeDir(home.appServer.start, home.agentDir, env)
2584
+ }))));
2585
+ homesByConfig.set(config, homesByAgent);
2586
+ return homesByAgent;
2587
+ };
2588
+ let lastSnapshot = buildSnapshot(params.config);
2589
+ return { forAgent(agentId) {
2590
+ const config = params.getRuntimeConfig();
2591
+ if (!config) return lastSnapshot.get(agentId) ?? [];
2592
+ const cached = homesByConfig.get(config);
2593
+ if (cached) return cached.get(agentId) ?? [];
2594
+ lastSnapshot = buildSnapshot(config);
2595
+ return lastSnapshot.get(agentId) ?? [];
2596
+ } };
2521
2597
  }
2522
- /** Archives one inactive Gateway-local Codex thread after a fresh status read. */
2523
- async function archiveLocalCodexSession(params) {
2524
- return await runSessionActionExclusive(adoptedSourceKey(CODEX_LOCAL_SESSION_HOST_ID, params.threadId), async () => {
2525
- return await params.bindingStore.withThreadArchiveFence(async () => {
2526
- return await params.control.withPinnedConnection(async (control) => {
2527
- await requireCatalogEligibleThread(control, params.threadId);
2528
- await assertNoPendingSupervisionBranch(params);
2529
- const thread = await control.readThread(params.threadId, false);
2530
- if (thread.id !== params.threadId) throw new Error("Codex app-server returned a different thread than requested");
2531
- requireIdleThread(thread, "archive");
2532
- if (await params.bindingStore.hasOtherThreadOwner(params.threadId)) throw new CatalogParamsError("Codex session cannot be archived while it is attached to an OpenClaw session");
2533
- await assertCodexArchiveDescendantsUnowned({
2534
- bindingStore: params.bindingStore,
2535
- threadId: params.threadId,
2536
- listPage: (request) => control.listDescendantPage(request),
2537
- assertDescendantIdle: async (descendantThreadId) => {
2538
- const descendant = await control.readThread(descendantThreadId, false);
2539
- if (descendant.id !== descendantThreadId) throw new Error("Codex app-server returned a different descendant than requested");
2540
- requireIdleThread(descendant, "archive");
2598
+ //#endregion
2599
+ //#region extensions/codex/src/session-catalog-control.ts
2600
+ const CODEX_SESSION_CATALOG_LIST_TTL_MS = 32e3;
2601
+ const CODEX_SESSION_CATALOG_LIST_CACHE_MAX_ENTRIES = 32;
2602
+ function codexCatalogPageCacheKey(params, agentId, source) {
2603
+ return JSON.stringify([
2604
+ agentId,
2605
+ source?.sourceHomeId ?? null,
2606
+ params.cursor ?? null,
2607
+ params.limit ?? null,
2608
+ params.searchTerm?.trim().toLocaleLowerCase() || null,
2609
+ params.cwd?.trim() || null
2610
+ ]);
2611
+ }
2612
+ function createCodexCatalogRequestSnapshot(requestTimeoutMs, request) {
2613
+ return {
2614
+ requestTimeoutMs,
2615
+ listThreads: (params, timeoutMs) => request(CODEX_CONTROL_METHODS.listThreads, params, timeoutMs),
2616
+ listThreadTurns: (params) => request(CODEX_CONTROL_METHODS.listThreadTurns, params),
2617
+ forkThread: (params) => request(CODEX_CONTROL_METHODS.forkThread, assertCodexThreadForkParams(params)),
2618
+ readThread: async (threadId, includeTurns) => (await request(CODEX_CONTROL_METHODS.readThread, {
2619
+ threadId,
2620
+ includeTurns
2621
+ })).thread,
2622
+ archiveThread: async (threadId) => {
2623
+ await request(CODEX_CONTROL_METHODS.archiveThread, { threadId });
2624
+ }
2625
+ };
2626
+ }
2627
+ function createCodexSessionCatalogControlFromRequests(params) {
2628
+ return {
2629
+ ...params.clientId ? { clientId: params.clientId } : {},
2630
+ ...params.connectionFingerprint ? { connectionFingerprint: params.connectionFingerprint } : {},
2631
+ withPinnedConnection: params.withPinnedConnection,
2632
+ async listPage(pageParams) {
2633
+ const limit = normalizeLimit(pageParams.limit, "limit");
2634
+ const search = pageParams.searchTerm?.trim().toLocaleLowerCase() || void 0;
2635
+ const cwd = pageParams.cwd?.trim() || void 0;
2636
+ const maxPages = search ? 20 : 1;
2637
+ const sessions = [];
2638
+ const managedThreads = [];
2639
+ let cursor = readControlCursor(pageParams.cursor, "request");
2640
+ let nextCursor;
2641
+ let backwardsCursor;
2642
+ const seenCursors = new Set(cursor ? [cursor] : []);
2643
+ const requests = params.createRequestSnapshot();
2644
+ const deadline = params.now() + requests.requestTimeoutMs;
2645
+ for (let pageIndex = 0; pageIndex < maxPages; pageIndex += 1) {
2646
+ const remainingTimeoutMs = Math.ceil(deadline - params.now());
2647
+ if (remainingTimeoutMs <= 0) throw new Error("Codex session catalog listing timed out");
2648
+ const response = await requests.listThreads({
2649
+ archived: false,
2650
+ limit: limit - sessions.length,
2651
+ modelProviders: [],
2652
+ sortKey: "updated_at",
2653
+ sortDirection: "desc",
2654
+ ...cwd ? { cwd } : {},
2655
+ ...cursor ? { cursor } : {}
2656
+ }, remainingTimeoutMs);
2657
+ if (pageIndex === 0) backwardsCursor = readControlCursor(response.backwardsCursor, "backwards response");
2658
+ for (const thread of response.data) {
2659
+ if (await isOpenClawManagedCodexThread(thread, params.localSessionsRoot)) {
2660
+ const rolloutPath = typeof thread.path === "string" ? thread.path.trim() : "";
2661
+ managedThreads.push({
2662
+ threadId: thread.id,
2663
+ ...rolloutPath ? { rolloutPath } : {}
2664
+ });
2665
+ continue;
2541
2666
  }
2542
- });
2543
- await control.archiveThread(params.threadId);
2544
- return { archived: true };
2667
+ const session = toCatalogSession(thread, false);
2668
+ if (session && (!search || (session.name ?? session.fallbackName)?.toLocaleLowerCase().includes(search))) sessions.push(session);
2669
+ }
2670
+ nextCursor = readControlCursor(response.nextCursor, "next response");
2671
+ if (!nextCursor || sessions.length >= limit) break;
2672
+ if (seenCursors.has(nextCursor)) throw new Error("Codex session catalog returned a repeated search cursor");
2673
+ seenCursors.add(nextCursor);
2674
+ cursor = nextCursor;
2675
+ }
2676
+ return {
2677
+ sessions,
2678
+ ...managedThreads.length > 0 ? { managedThreads } : {},
2679
+ ...nextCursor ? { nextCursor } : {},
2680
+ ...backwardsCursor ? { backwardsCursor } : {}
2681
+ };
2682
+ },
2683
+ async listDescendantPage(listParams) {
2684
+ const requests = params.createRequestSnapshot();
2685
+ return await requests.listThreads(listParams, requests.requestTimeoutMs);
2686
+ },
2687
+ async readThread(threadId, includeTurns = false) {
2688
+ return await params.createRequestSnapshot().readThread(threadId, includeTurns);
2689
+ },
2690
+ async listTurnPage(listParams) {
2691
+ return await params.createRequestSnapshot().listThreadTurns(listParams);
2692
+ },
2693
+ async forkThread(forkParams) {
2694
+ return await params.createRequestSnapshot().forkThread(forkParams);
2695
+ },
2696
+ async archiveThread(threadId) {
2697
+ await params.createRequestSnapshot().archiveThread(threadId);
2698
+ }
2699
+ };
2700
+ }
2701
+ /** Builds the passive catalog over the Codex plugin's canonical shared client. */
2702
+ function createCodexSessionCatalogControl(params) {
2703
+ const now = params.now ?? Date.now;
2704
+ const getPluginConfig = () => params.getPluginConfig();
2705
+ const homeResolver = createCodexCatalogHomeResolver({
2706
+ config: params.getRuntimeConfig() ?? params.config ?? {},
2707
+ getRuntimeConfig: params.getRuntimeConfig,
2708
+ getPluginConfig: params.getPluginConfig,
2709
+ ...params.env ? { env: params.env } : {}
2710
+ });
2711
+ const requestOptionsByConfig = /* @__PURE__ */ new WeakMap();
2712
+ const catalogPagesByConfig = /* @__PURE__ */ new WeakMap();
2713
+ const resolveRequestOptions = (startOptions, agentId, source) => {
2714
+ const runtimeConfig = params.getRuntimeConfig();
2715
+ const agentDir = source?.agentDir ?? resolveAgentDir$1(runtimeConfig ?? {}, agentId);
2716
+ const resolvedStartOptions = source?.appServer.start ?? startOptions;
2717
+ if (!runtimeConfig) return {
2718
+ agentDir,
2719
+ config: void 0,
2720
+ startOptions: structuredClone(resolvedStartOptions)
2721
+ };
2722
+ let byAgent = requestOptionsByConfig.get(runtimeConfig);
2723
+ const cacheKey = `${agentId ?? ""}\0${source?.sourceHomeId ?? ""}`;
2724
+ const cached = byAgent?.get(cacheKey);
2725
+ if (cached) return cached;
2726
+ const resolved = {
2727
+ agentDir,
2728
+ config: structuredClone(runtimeConfig),
2729
+ startOptions: structuredClone(resolvedStartOptions)
2730
+ };
2731
+ if (!byAgent) {
2732
+ byAgent = /* @__PURE__ */ new Map();
2733
+ requestOptionsByConfig.set(runtimeConfig, byAgent);
2734
+ }
2735
+ byAgent.set(cacheKey, resolved);
2736
+ return resolved;
2737
+ };
2738
+ const createRequestSnapshot = (agentId, source) => {
2739
+ const pluginConfig = getPluginConfig();
2740
+ const runtime = source?.appServer ?? resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig });
2741
+ const requestOptions = resolveRequestOptions(runtime.start, agentId, source);
2742
+ return createCodexCatalogRequestSnapshot(runtime.requestTimeoutMs, async (method, requestParams, timeoutMs) => await codexControlRequest(pluginConfig, method, requestParams, {
2743
+ ...requestOptions,
2744
+ ...timeoutMs === void 0 ? {} : { timeoutMs }
2745
+ }));
2746
+ };
2747
+ const forRequest = (agentId, source) => {
2748
+ const withPinnedConnection = async (run) => {
2749
+ const pluginConfig = getPluginConfig();
2750
+ const runtime = source?.appServer ?? resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig });
2751
+ const { agentDir, config: runtimeConfig, startOptions } = resolveRequestOptions(runtime.start, agentId, source);
2752
+ const client = await getLeasedSharedCodexAppServerClient({
2753
+ agentDir,
2754
+ config: runtimeConfig,
2755
+ startOptions,
2756
+ timeoutMs: runtime.requestTimeoutMs
2545
2757
  });
2758
+ try {
2759
+ const requests = createCodexCatalogRequestSnapshot(runtime.requestTimeoutMs, async (method, requestParams, timeoutMs) => await requestCodexAppServerClientJson({
2760
+ client,
2761
+ method,
2762
+ requestParams,
2763
+ config: runtimeConfig,
2764
+ timeoutMs: timeoutMs ?? runtime.requestTimeoutMs
2765
+ }));
2766
+ const pinnedControl = createCodexSessionCatalogControlFromRequests({
2767
+ clientId: resolveCodexAppServerClientInstanceId(client),
2768
+ connectionFingerprint: buildCodexAppServerConnectionFingerprint(runtime, agentDir),
2769
+ createRequestSnapshot: () => requests,
2770
+ ...source?.localSessionsRoot ? { localSessionsRoot: source.localSessionsRoot } : {},
2771
+ now,
2772
+ withPinnedConnection: async (nestedRun) => await nestedRun(pinnedControl)
2773
+ });
2774
+ return await run(pinnedControl);
2775
+ } finally {
2776
+ releaseLeasedSharedCodexAppServerClient(client);
2777
+ }
2778
+ };
2779
+ const control = createCodexSessionCatalogControlFromRequests({
2780
+ createRequestSnapshot: () => createRequestSnapshot(agentId, source),
2781
+ ...source?.localSessionsRoot ? { localSessionsRoot: source.localSessionsRoot } : {},
2782
+ now,
2783
+ withPinnedConnection
2546
2784
  });
2547
- });
2785
+ return {
2786
+ ...control,
2787
+ async listPage(pageParams) {
2788
+ const runtimeConfig = params.getRuntimeConfig();
2789
+ if (!runtimeConfig) return await control.listPage(pageParams);
2790
+ let cache = catalogPagesByConfig.get(runtimeConfig);
2791
+ if (!cache) {
2792
+ cache = /* @__PURE__ */ new Map();
2793
+ catalogPagesByConfig.set(runtimeConfig, cache);
2794
+ }
2795
+ const key = codexCatalogPageCacheKey(pageParams, agentId, source);
2796
+ const cached = cache.get(key);
2797
+ if (pageParams.forceRefresh !== true && cached) {
2798
+ cache.delete(key);
2799
+ cache.set(key, cached);
2800
+ if (cached.expiresAt > now()) return cached.value ?? await cached.page;
2801
+ }
2802
+ if (cached) cache.delete(key);
2803
+ const page = control.listPage(pageParams);
2804
+ const staleValue = cached?.value;
2805
+ const entry = {
2806
+ expiresAt: Number.POSITIVE_INFINITY,
2807
+ page,
2808
+ ...staleValue ? { value: staleValue } : {}
2809
+ };
2810
+ cache.set(key, entry);
2811
+ pruneMapToMaxSize(cache, CODEX_SESSION_CATALOG_LIST_CACHE_MAX_ENTRIES);
2812
+ const settle = (value) => {
2813
+ if (cache.get(key) === entry) {
2814
+ entry.value = value;
2815
+ entry.expiresAt = now() + CODEX_SESSION_CATALOG_LIST_TTL_MS;
2816
+ }
2817
+ return value;
2818
+ };
2819
+ const restore = () => {
2820
+ if (cache.get(key) !== entry) return;
2821
+ if (staleValue) cache.set(key, {
2822
+ expiresAt: now(),
2823
+ page: Promise.resolve(staleValue),
2824
+ value: staleValue
2825
+ });
2826
+ else cache.delete(key);
2827
+ };
2828
+ if (pageParams.forceRefresh !== true && staleValue) {
2829
+ page.then(settle, restore);
2830
+ return staleValue;
2831
+ }
2832
+ try {
2833
+ return settle(await page);
2834
+ } catch (error) {
2835
+ restore();
2836
+ throw error;
2837
+ }
2838
+ }
2839
+ };
2840
+ };
2841
+ const homesForAgent = (agentId) => homeResolver.forAgent(agentId);
2842
+ const forUpstream = (agentId, connectionFingerprint) => {
2843
+ const source = homesForAgent(agentId).find((home) => buildCodexAppServerConnectionFingerprint(home.appServer, home.agentDir) === connectionFingerprint);
2844
+ return source ? forRequest(agentId, source) : void 0;
2845
+ };
2846
+ return {
2847
+ forRequest,
2848
+ forUpstream,
2849
+ homesForAgent
2850
+ };
2548
2851
  }
2852
+ //#endregion
2853
+ //#region extensions/codex/src/session-catalog.ts
2549
2854
  /** Allows read-only catalog and transcript commands on supported paired-node platforms. */
2550
2855
  function createCodexSessionCatalogNodeInvokePolicies() {
2551
2856
  return [{
@@ -2563,7 +2868,7 @@ function createCodexSessionCatalogNodeInvokePolicies() {
2563
2868
  }];
2564
2869
  }
2565
2870
  function toGenericCatalogHost(host, localTerminalAvailable) {
2566
- const local = host.hostId === CODEX_LOCAL_SESSION_HOST_ID;
2871
+ const local = isLocalCodexCatalogHost(host.hostId);
2567
2872
  return {
2568
2873
  hostId: host.hostId,
2569
2874
  label: host.label,
@@ -2578,6 +2883,7 @@ function toGenericCatalogHost(host, localTerminalAvailable) {
2578
2883
  const name = session.name ?? session.fallbackName;
2579
2884
  return {
2580
2885
  threadId: session.threadId,
2886
+ ...session.sourceHomeId ? { sourceHomeId: session.sourceHomeId } : {},
2581
2887
  ...name ? { name } : {},
2582
2888
  ...session.cwd ? { cwd: session.cwd } : {},
2583
2889
  status: session.status,
@@ -2599,13 +2905,44 @@ function toGenericCatalogHost(host, localTerminalAvailable) {
2599
2905
  ...host.error ? { error: host.error } : {}
2600
2906
  };
2601
2907
  }
2908
+ function isLocalCodexCatalogHost(hostId) {
2909
+ return hostId === "gateway:local" || hostId.startsWith(`gateway:local:`);
2910
+ }
2911
+ function resolveLocalCatalogHomeForThread(params) {
2912
+ if (params.homes.length === 0) throw new CatalogParamsError("local Codex sessions are unavailable in isolated state");
2913
+ const exact = params.sourceHomeId ? params.homes.filter((home) => home.sourceHomeId === params.sourceHomeId) : params.homes.filter((home) => home.hostId === params.hostId);
2914
+ if (exact.length === 0 || params.sourceHomeId && exact[0]?.hostId !== params.hostId) throw new CatalogParamsError("Codex session source home is unavailable");
2915
+ return exact[0];
2916
+ }
2602
2917
  function registerCodexSessionCatalog(params) {
2603
- const usesProcessHomeFallback = () => {
2604
- const start = resolveCodexSupervisionAppServerRuntimeOptions({ pluginConfig: params.getPluginConfig() }).start;
2605
- return start.transport === "stdio" && start.homeScope === "user" && !process.env.CODEX_HOME?.trim();
2918
+ const catalogHomes = (agentId, allowProcessHomeFallback) => {
2919
+ const homes = params.control.homesForAgent(agentId);
2920
+ return allowProcessHomeFallback === false ? homes.filter((home) => !home.usesProcessHomeFallback) : homes;
2921
+ };
2922
+ const resolveRequestAgentId = (agentId) => resolveSessionAgentIdsStrict({
2923
+ config: params.getRuntimeConfig() ?? params.api.config,
2924
+ agentId
2925
+ }).sessionAgentId;
2926
+ const bindRequest = (request) => {
2927
+ const agentId = resolveRequestAgentId(request.agentId);
2928
+ const source = isLocalCodexCatalogHost(request.hostId) ? resolveLocalCatalogHomeForThread({
2929
+ homes: [...catalogHomes(agentId, request.allowProcessHomeFallback)],
2930
+ hostId: request.hostId,
2931
+ ...request.sourceHomeId ? { sourceHomeId: request.sourceHomeId } : {}
2932
+ }) : void 0;
2933
+ return {
2934
+ agentId,
2935
+ source,
2936
+ control: params.control.forRequest(agentId, source)
2937
+ };
2606
2938
  };
2607
- const assertLocalAccess = (hostId, allowProcessHomeFallback) => {
2608
- if (hostId === "gateway:local" && allowProcessHomeFallback === false && usesProcessHomeFallback()) throw new CatalogParamsError("local Codex sessions are unavailable in isolated state");
2939
+ const bindLocalRequest = (request) => {
2940
+ const bound = bindRequest(request);
2941
+ if (!bound.source) throw new CatalogParamsError("Codex session catalog hostId is invalid");
2942
+ return {
2943
+ ...bound,
2944
+ source: bound.source
2945
+ };
2609
2946
  };
2610
2947
  const checkUpstreamActivity = createChecker(params);
2611
2948
  params.api.registerSessionCatalog({
@@ -2615,9 +2952,12 @@ function registerCodexSessionCatalog(params) {
2615
2952
  resolveCreateSession: ({ agentId }) => resolveCodexCatalogCreateSession(params.getRuntimeConfig() ?? params.api.config, agentId),
2616
2953
  list: async (query) => {
2617
2954
  const localTerminalAvailable = resolveLocalCodexTerminalExecutable() !== void 0;
2618
- const { allowProcessHomeFallback, listNodes, onHost, sessionEntries, ...gatewayQuery } = query;
2955
+ const { agentId: requestedAgentId, allowProcessHomeFallback, listNodes, onHost, sessionEntries, ...gatewayQuery } = query;
2956
+ const agentId = resolveRequestAgentId(requestedAgentId);
2619
2957
  const mapHost = (host) => toGenericCatalogHost(host, localTerminalAvailable);
2958
+ const localHomes = [...catalogHomes(agentId, allowProcessHomeFallback)];
2620
2959
  return (await listCodexSessionCatalog({
2960
+ agentId,
2621
2961
  bindingStore: params.bindingStore,
2622
2962
  config: params.getRuntimeConfig(),
2623
2963
  runtime: params.api.runtime,
@@ -2625,20 +2965,21 @@ function registerCodexSessionCatalog(params) {
2625
2965
  query: gatewayQuery,
2626
2966
  listNodes,
2627
2967
  sessionEntries,
2628
- includeLocal: allowProcessHomeFallback !== false || !usesProcessHomeFallback(),
2968
+ localHomes,
2629
2969
  ...onHost ? { onHost: (host) => onHost(mapHost(host)) } : {}
2630
2970
  })).hosts.map(mapHost);
2631
2971
  },
2632
2972
  read: async (request) => {
2633
- const { allowProcessHomeFallback, ...catalogRequest } = request;
2634
- assertLocalAccess(catalogRequest.hostId, allowProcessHomeFallback);
2973
+ const { agentId, source, control } = bindRequest(request);
2635
2974
  const page = await readCodexSessionTranscript({
2975
+ agentId,
2636
2976
  runtime: params.api.runtime,
2637
- control: params.control,
2638
- hostId: catalogRequest.hostId,
2639
- threadId: catalogRequest.threadId,
2640
- cursor: catalogRequest.cursor,
2641
- limit: catalogRequest.limit ?? 20
2977
+ control,
2978
+ hostId: request.hostId,
2979
+ threadId: request.threadId,
2980
+ cursor: request.cursor,
2981
+ limit: request.limit ?? 20,
2982
+ ...source ? { source } : {}
2642
2983
  });
2643
2984
  return {
2644
2985
  ...page,
@@ -2646,58 +2987,69 @@ function registerCodexSessionCatalog(params) {
2646
2987
  };
2647
2988
  },
2648
2989
  continueSession: async (request) => {
2649
- assertLocalAccess(request.hostId, request.allowProcessHomeFallback);
2650
2990
  const config = params.getRuntimeConfig();
2651
2991
  if (!config) throw new Error("OpenClaw runtime config is unavailable");
2652
2992
  if (request.hostId.startsWith("node:")) return await continueNodeCodexSession({
2993
+ agentId: resolveRequestAgentId(request.agentId),
2653
2994
  api: params.api,
2654
2995
  config,
2655
2996
  hostId: request.hostId,
2656
2997
  threadId: request.threadId,
2657
2998
  clientScopes: request.clientScopes
2658
2999
  });
2659
- if (request.hostId !== "gateway:local") throw new CatalogParamsError("Codex session catalog hostId is invalid");
3000
+ if (!isLocalCodexCatalogHost(request.hostId)) throw new CatalogParamsError("Codex session catalog hostId is invalid");
3001
+ const { agentId, source, control } = bindLocalRequest(request);
2660
3002
  let upstreamBaseline;
2661
3003
  return codexUpstreamContinueResult((await continueLocalCodexSession({
3004
+ agentId,
2662
3005
  api: params.api,
2663
3006
  bindingStore: params.bindingStore,
2664
3007
  config,
2665
- control: params.control,
3008
+ control,
2666
3009
  threadId: request.threadId,
3010
+ hostId: source.hostId,
3011
+ sourceHomeId: source.sourceHomeId,
3012
+ ...source.hostId === "gateway:local" ? { allowLegacy: true } : {},
2667
3013
  onContinued: (baseline) => {
2668
3014
  upstreamBaseline = baseline;
2669
3015
  }
2670
3016
  })).sessionKey, request.threadId, upstreamBaseline);
2671
3017
  },
2672
- checkUpstreamActivity: (probes, policy) => checkUpstreamActivity(probes.filter((probe) => probe.hostId !== "gateway:local" || policy?.allowProcessHomeFallback !== false || !usesProcessHomeFallback())),
3018
+ checkUpstreamActivity: (probes, policy) => checkUpstreamActivity(probes.filter((probe) => !isLocalCodexCatalogHost(probe.hostId) || policy?.allowProcessHomeFallback !== false || catalogHomes(probe.agentId, false).some((home) => home.hostId === probe.hostId))),
2673
3019
  archive: async (request) => {
2674
- assertLocalAccess(request.hostId, request.allowProcessHomeFallback);
2675
3020
  if (request.confirmNoOtherRunner !== true) throw new CatalogParamsError("archive requires confirmation that no other runner is active");
2676
- if (request.hostId !== "gateway:local") throw new CatalogParamsError("paired-node Codex sessions are view-only");
3021
+ if (!isLocalCodexCatalogHost(request.hostId)) throw new CatalogParamsError("paired-node Codex sessions are view-only");
2677
3022
  const config = params.getRuntimeConfig();
2678
3023
  if (!config) throw new Error("OpenClaw runtime config is unavailable");
3024
+ const { agentId, source, control } = bindLocalRequest(request);
2679
3025
  await archiveLocalCodexSession({
3026
+ agentId,
2680
3027
  bindingStore: params.bindingStore,
2681
3028
  config,
2682
- control: params.control,
3029
+ control,
2683
3030
  runtime: params.api.runtime,
2684
- threadId: request.threadId
3031
+ threadId: request.threadId,
3032
+ hostId: source.hostId,
3033
+ sourceHomeId: source.sourceHomeId,
3034
+ ...source.hostId === "gateway:local" ? { allowLegacy: true } : {}
2685
3035
  });
2686
3036
  return { ok: true };
2687
3037
  },
2688
3038
  openTerminal: async (request) => {
2689
- assertLocalAccess(request.hostId, request.allowProcessHomeFallback);
3039
+ const { agentId, source, control } = bindRequest(request);
2690
3040
  return await openCodexCatalogTerminal({
2691
3041
  api: params.api,
2692
- control: params.control,
3042
+ control,
2693
3043
  getPluginConfig: params.getPluginConfig,
2694
3044
  getRuntimeConfig: params.getRuntimeConfig,
2695
3045
  parseCatalogPage,
2696
- ...request
3046
+ ...source ? { source } : {},
3047
+ ...request,
3048
+ agentId
2697
3049
  });
2698
3050
  },
2699
3051
  startTerminalSession: async (request) => {
2700
- assertLocalAccess(request.nodeId ? `node:${request.nodeId}` : CODEX_LOCAL_SESSION_HOST_ID, request.allowProcessHomeFallback);
3052
+ if (!request.nodeId && catalogHomes(request.agentId, request.allowProcessHomeFallback).length === 0) throw new CatalogParamsError("local Codex sessions are unavailable in isolated state");
2701
3053
  return await startCodexCatalogTerminal({
2702
3054
  getPluginConfig: params.getPluginConfig,
2703
3055
  getRuntimeConfig: params.getRuntimeConfig,
@@ -2715,4 +3067,4 @@ const codexSessionCatalogRuntime = {
2715
3067
  archiveLocal: archiveLocalCodexSession
2716
3068
  };
2717
3069
  //#endregion
2718
- export { CODEX_LOCAL_SESSION_HOST_ID as a, readCodexStatusProbes as c, createCodexCliSessionNodeHostCommands as d, createCodexCliSessionNodeInvokePolicies as f, resumeCodexCliSessionOnNode as g, resolveCodexCliSessionForBindingOnNode as h, createCodexSessionCatalogNodeInvokePolicies as i, requestOptions as l, listCodexCliSessionsOnNode as m, createCodexSessionCatalogControl as n, assertCodexArchiveDescendantsUnowned as o, formatCodexCliSessions as p, createCodexSessionCatalogNodeHostCommands as r, codexControlRequest as s, codexSessionCatalogRuntime as t, safeCodexControlRequest as u };
3070
+ export { CODEX_LOCAL_SESSION_HOST_ID as a, readCodexStatusProbes as c, createCodexCliSessionNodeHostCommands as d, createCodexCliSessionNodeInvokePolicies as f, resumeCodexCliSessionOnNode as g, resolveCodexCliSessionForBindingOnNode as h, createCodexSessionCatalogNodeHostCommands as i, requestOptions as l, listCodexCliSessionsOnNode as m, createCodexSessionCatalogNodeInvokePolicies as n, assertCodexArchiveDescendantsUnowned as o, formatCodexCliSessions as p, createCodexSessionCatalogControl as r, codexControlRequest as s, codexSessionCatalogRuntime as t, safeCodexControlRequest as u };