@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.
- package/dist/api.js +146 -0
- package/dist/{attempt-client-cleanup-DMLXUfa8.js → attempt-client-cleanup-CVPvp9dL.js} +47 -5
- package/dist/{auth-binding-B4FfbcCg.js → auth-binding-Ds_QVm-X.js} +18 -3
- package/dist/{binding-connection-I5Bo-AsX.js → binding-connection-CCQFs8lf.js} +15 -3
- package/dist/{bounded-turn-qgZmzy_4.js → bounded-turn-rq5tiSXk.js} +27 -26
- package/dist/{capabilities-CybGI1oz.js → capabilities-CGm_94eL.js} +1 -1
- package/dist/cli-metadata.js +1 -1
- package/dist/{command-formatters-B1AEz8ZA.js → command-formatters-C54Yg3eA.js} +2 -2
- package/dist/{command-handlers-Db8pu_ds.js → command-handlers-BvOAHTrA.js} +111 -67
- package/dist/{compact-LspQaGfl.js → compact-Hd7SAc0N.js} +35 -30
- package/dist/{session-binding-DPxIvMgE.js → config-Cxw3Cl3k.js} +361 -1161
- package/dist/context-compaction-activity-BOeJf7yo.js +58 -0
- package/dist/doctor-contract-api.js +128 -26
- package/dist/{dynamic-tools-BVdyWkUD.js → dynamic-tools-BFoUcRdx.js} +1341 -1464
- package/dist/{effective-mcp-catalog-DIIhAtOZ.js → effective-mcp-catalog-BTJHnDSx.js} +2 -2
- package/dist/elicitation-input-DMkeki8d.js +71 -0
- package/dist/{event-projector-assistant-message-BwxV4d6N.js → event-projector-assistant-message-C5-Ds8mA.js} +18 -1
- package/dist/{event-projector-usage-e5huIQWu.js → event-projector-usage-D3PHFFXh.js} +18 -18
- package/dist/harness-DVfUQBlQ.js +248 -0
- package/dist/harness.js +2 -226
- package/dist/index.js +405 -299
- package/dist/{isolated-completion-C58NbL40.js → isolated-completion-Cbbx_5Ed.js} +4 -4
- package/dist/launch-args-sXOAg0SX.js +84 -0
- package/dist/managed-binary-ZkGodW19.js +248 -0
- package/dist/media-understanding-provider.js +2 -2
- package/dist/model-catalog-o6Cgjoss.js +101 -0
- package/dist/{models-BFlpn1Ex.js → models-DgFWSuxA.js} +25 -28
- package/dist/{native-subagent-monitor-DyTGmTUx.js → native-subagent-monitor-CwFyoylz.js} +385 -94
- package/dist/node-exec-server.runtime-DXrqpw1c.js +195 -0
- package/dist/plugin-approval-roundtrip-D80UZiJ_.js +95 -0
- package/dist/{plugin-marketplace-discovery-CzrGJdCR.js → plugin-marketplace-discovery-mdmScYOj.js} +60 -55
- package/dist/{plugin-metadata-cache-CNJPj4_b.js → plugin-metadata-cache-CulOABwu.js} +164 -134
- package/dist/{protocol-validators-B4F7L8Rf.js → protocol-validators-Bg5rpNxk.js} +203 -7
- package/dist/{rate-limits-DZXmOL8-.js → rate-limits-hFtzt2Up.js} +1 -1
- package/dist/{sandbox-guard-Cmwc9eIk.js → request-CCm4LaED.js} +207 -20
- package/dist/{run-attempt-P8BdxC1I.js → run-attempt-BwNQDH2u.js} +1506 -836
- package/dist/{runtime-artifact-BVSPdd6g.js → runtime-artifact-DeBvleJw.js} +4 -7
- package/dist/session-binding-q2WSTVmC.js +947 -0
- package/dist/{session-catalog-xwNi_SDM.js → session-catalog-C2Tae4rI.js} +1698 -1346
- package/dist/session-catalog-home-id-DAKUrm3C.js +112 -0
- package/dist/{session-cli-7hgXZDV7.js → session-cli-BuDjm5Ou.js} +20 -7
- package/dist/session-permission-policy-gLm40Vtq.js +160 -0
- package/dist/session-retirement-CdkI0bvb.js +90 -0
- package/dist/{session-upstream-marker-CrIFX06J.js → session-upstream-marker-Bcy33kam.js} +2 -3
- package/dist/{settled-turn-finalizer-BRWBO4MO.js → settled-turn-finalizer-B8JJcxCi.js} +27 -22
- package/dist/{shared-client-kIBbGjk4.js → shared-client-kNkljY-R.js} +2023 -607
- package/dist/{side-question-9HAL26l9.js → side-question-GwD73tX6.js} +109 -57
- package/dist/{thread-lifecycle-Bu1BTDXr.js → thread-lifecycle-Ss-uwnC4.js} +1208 -497
- package/dist/{thread-ownership-6kOQHlyC.js → thread-ownership-CeYBqg_4.js} +18 -8
- package/dist/{plugin-app-cache-key-C3H2CVp6.js → thread-resume-DF54hagX.js} +64 -6
- package/dist/{transcript-mirror-B9gGgnJS.js → transcript-mirror-CkEQIna-.js} +158 -41
- package/dist/transport-stdio-C7-s9TLG.js +708 -0
- package/dist/{upstream-prompt-provenance-ClRBije0.js → upstream-prompt-provenance-CFmsKwr9.js} +12 -14
- package/dist/{upstream-session-fork-CE-_LdnP.js → upstream-session-fork-BACGlOFa.js} +86 -88
- package/dist/{usage-CHlV0e-o.js → usage-C2l8Mils.js} +3 -3
- package/dist/{web-search-provider.runtime-DRw2Vl42.js → web-search-provider.runtime-B2XgRdFv.js} +3 -3
- package/openclaw.plugin.json +50 -2
- package/package.json +9 -8
- package/dist/app-server-policy-zcBDc1JU.js +0 -49
- package/dist/incognito-session-ET_ufDH-.js +0 -2
- package/dist/request-BRl5_UfX.js +0 -157
- package/dist/session-retirement-DoMMJFKm.js +0 -36
- package/dist/thread-resume-CPrJNQVM.js +0 -30
- package/dist/transport-stdio-DIImwDVS.js +0 -99
|
@@ -1,32 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { n as
|
|
8
|
-
import { r as
|
|
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 {
|
|
11
|
-
import { i as createImportedCodexSession, n as codexUpstreamBaseline, r as codexUpstreamContinueResult, t as codexLastTerminalTurnId } from "./session-upstream-marker-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
291
|
-
"pipe",
|
|
292
|
-
"pipe",
|
|
293
|
-
"pipe"
|
|
294
|
-
],
|
|
297
|
+
input: params.prompt,
|
|
295
298
|
env: process$1.env,
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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 (
|
|
320
|
-
if (
|
|
321
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1050
|
-
return
|
|
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 =
|
|
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(
|
|
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:
|
|
1193
|
+
agentId: params.agentId,
|
|
1153
1194
|
recoverMatchingInitialEntry: true,
|
|
1154
|
-
|
|
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-
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
|
1207
|
-
|
|
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
|
|
1214
|
-
return
|
|
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
|
-
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1284
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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({
|
|
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-
|
|
1588
|
-
const
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
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
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
const
|
|
1631
|
-
|
|
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
|
-
|
|
1634
|
-
...
|
|
1436
|
+
sourceThreadId: sourceThreadId.trim(),
|
|
1437
|
+
...typeof sourceHomeId === "string" ? { sourceHomeId: sourceHomeId.trim() } : {}
|
|
1635
1438
|
};
|
|
1636
1439
|
}
|
|
1637
|
-
function
|
|
1638
|
-
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
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
|
-
|
|
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
|
|
1681
|
-
|
|
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
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
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
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
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
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
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
|
-
|
|
1719
|
-
|
|
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
|
-
|
|
1722
|
-
}
|
|
1634
|
+
throw error;
|
|
1635
|
+
}
|
|
1723
1636
|
}
|
|
1724
|
-
function
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
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
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
const
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
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
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
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
|
-
|
|
1847
|
-
|
|
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
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
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
|
-
|
|
1933
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
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
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
const
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
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
|
-
/**
|
|
2115
|
-
function
|
|
2116
|
-
return
|
|
2117
|
-
{
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
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
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
const
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
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
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
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
|
|
2178
|
-
|
|
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
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
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
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
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
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
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
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
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
|
-
|
|
2216
|
-
|
|
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
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
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
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
return
|
|
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
|
|
2298
|
-
|
|
2299
|
-
const
|
|
2300
|
-
|
|
2301
|
-
|
|
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
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
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
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
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
|
-
|
|
2338
|
-
|
|
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
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
config: params.config,
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
const
|
|
2356
|
-
const
|
|
2357
|
-
|
|
2358
|
-
|
|
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
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
...
|
|
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
|
-
|
|
2395
|
-
|
|
2396
|
-
modelLocked: true
|
|
2397
|
-
} } } };
|
|
2398
|
-
}
|
|
2130
|
+
} } : {},
|
|
2131
|
+
...source ? { source } : {}
|
|
2399
2132
|
});
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
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
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
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
|
-
|
|
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.
|
|
2439
|
-
|
|
2154
|
+
runtime: params.runtime,
|
|
2155
|
+
sessionEntries: params.sessionEntries
|
|
2440
2156
|
});
|
|
2441
|
-
|
|
2442
|
-
const
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
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
|
-
|
|
2462
|
-
|
|
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
|
|
2466
|
-
|
|
2467
|
-
if (
|
|
2468
|
-
const
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
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
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
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
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
const
|
|
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
|
|
2501
|
-
}
|
|
2502
|
-
|
|
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
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
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
|
-
|
|
2544
|
-
|
|
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
|
|
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
|
|
2604
|
-
const
|
|
2605
|
-
return
|
|
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
|
|
2608
|
-
|
|
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
|
-
|
|
2968
|
+
localHomes,
|
|
2629
2969
|
...onHost ? { onHost: (host) => onHost(mapHost(host)) } : {}
|
|
2630
2970
|
})).hosts.map(mapHost);
|
|
2631
2971
|
},
|
|
2632
2972
|
read: async (request) => {
|
|
2633
|
-
const {
|
|
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
|
|
2638
|
-
hostId:
|
|
2639
|
-
threadId:
|
|
2640
|
-
cursor:
|
|
2641
|
-
limit:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3039
|
+
const { agentId, source, control } = bindRequest(request);
|
|
2690
3040
|
return await openCodexCatalogTerminal({
|
|
2691
3041
|
api: params.api,
|
|
2692
|
-
control
|
|
3042
|
+
control,
|
|
2693
3043
|
getPluginConfig: params.getPluginConfig,
|
|
2694
3044
|
getRuntimeConfig: params.getRuntimeConfig,
|
|
2695
3045
|
parseCatalogPage,
|
|
2696
|
-
...
|
|
3046
|
+
...source ? { source } : {},
|
|
3047
|
+
...request,
|
|
3048
|
+
agentId
|
|
2697
3049
|
});
|
|
2698
3050
|
},
|
|
2699
3051
|
startTerminalSession: async (request) => {
|
|
2700
|
-
|
|
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,
|
|
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 };
|