@openclaw/codex 2026.7.2-beta.3 → 2026.7.2-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/{app-server-policy-DUMF2Xyr.js → app-server-policy-BmDgseo_.js} +1 -1
  2. package/dist/attempt-client-cleanup-Ia52yRWc.js +127 -0
  3. package/dist/{attempt-notifications-Ecjyzufe.js → attempt-notifications-CMR2_fmR.js} +1 -1
  4. package/dist/{media-understanding-provider-IPNNQre6.js → bounded-turn-FEbUXoYT.js} +71 -188
  5. package/dist/capabilities-BHRSI6Cn.js +36 -0
  6. package/dist/cli-metadata.js +1 -1
  7. package/dist/{command-formatters-NCTL7A2q.js → command-formatters-ico4ZFnk.js} +3 -37
  8. package/dist/{command-handlers-DtFbZiba.js → command-handlers-CRmnwI2N.js} +1233 -1221
  9. package/dist/{compact-Kgy26gIM.js → compact-dEN49UH6.js} +8 -7
  10. package/dist/{computer-use-C7x4ZaqR.js → computer-use-BjGylO-E.js} +16 -6
  11. package/dist/{conversation-control-B0s3aIjT.js → conversation-control-DE7d9DOj.js} +6 -5
  12. package/dist/doctor-contract-api.js +24 -86
  13. package/dist/{dynamic-tools-D_vSVmv9.js → dynamic-tools-LIt9ojep.js} +135 -2696
  14. package/dist/event-projector-assistant-message-DTgnPPVF.js +101 -0
  15. package/dist/event-projector-usage-tvgb1NCX.js +69 -0
  16. package/dist/harness.js +31 -11
  17. package/dist/incognito-session-ET_ufDH-.js +2 -0
  18. package/dist/index.js +250 -275
  19. package/dist/media-understanding-provider.js +173 -1
  20. package/dist/{models-DD5zBdX5.js → models-BA01NEez.js} +2 -2
  21. package/dist/{notification-correlation-C8DVQ_Bq.js → notification-correlation-Bj_Ak_jI.js} +3 -3
  22. package/dist/{plugin-app-cache-key-DJbhLYSK.js → plugin-app-cache-key-DSXX-rXO.js} +5 -5
  23. package/dist/{transcript-mirror-D9rTxx2P.js → protocol-validators-Dm4eXZmt.js} +335 -595
  24. package/dist/{rate-limits-CqhWQ2R9.js → rate-limits-S_PXs0ul.js} +1 -1
  25. package/dist/realtime-voice-api-BPcA1uN9.js +494 -0
  26. package/dist/realtime-voice-api.js +2 -0
  27. package/dist/{request-9yf9pZjf.js → request-CXpPennK.js} +1 -1
  28. package/dist/{run-attempt-BIoO8ISr.js → run-attempt-C5GTPJOE.js} +3111 -387
  29. package/dist/{session-binding-Bmt20HfB.js → session-binding-DlVQ-Pxs.js} +957 -890
  30. package/dist/{session-catalog-CA7R-hX2.js → session-catalog-CP2oCcEb.js} +98 -87
  31. package/dist/{session-cli-Bxk3-Wqz.js → session-cli-CqZ0Fp2a.js} +2 -2
  32. package/dist/session-upstream-marker-QE-m5QFG.js +82 -0
  33. package/dist/settled-turn-finalizer-CA_2QXIE.js +303 -0
  34. package/dist/{shared-client-CSXLtCLE.js → shared-client-BVKE40c-.js} +1870 -1694
  35. package/dist/{side-question-DtzquMOb.js → side-question-CO12sHxo.js} +24 -16
  36. package/dist/{thread-lifecycle-CQ_9mAa1.js → thread-lifecycle-DRg_FdzC.js} +127 -153
  37. package/dist/transcript-mirror-uKA-A4tB.js +550 -0
  38. package/dist/upstream-session-fork-_Vrz4WJr.js +320 -0
  39. package/dist/{usage-CuPpFgTc.js → usage-UpVbdX5G.js} +3 -3
  40. package/dist/{web-search-provider.runtime-_x5XTeqh.js → web-search-provider.runtime-CaQMYDbp.js} +3 -3
  41. package/package.json +7 -8
  42. package/npm-shrinkwrap.json +0 -202
  43. package/dist/{auth-binding-C1czuHPA.js → auth-binding-B4FfbcCg.js} +1 -1
@@ -1,22 +1,23 @@
1
- import { S as resolveCodexAppServerAuthProfileIdForAgent, Z as isJsonObject } from "./shared-client-CSXLtCLE.js";
2
- import { _ as isCodexFastServiceTier, c as sessionBindingIdentity, n as assertCodexBindingMayBeReplaced, o as normalizeCodexAppServerBindingModelProvider, r as bindingStoreKey, s as reclaimCurrentCodexSessionGeneration } from "./session-binding-Bmt20HfB.js";
3
- import { u as assertCodexThreadResumeResponse } from "./transcript-mirror-D9rTxx2P.js";
4
- import { t as listAllCodexAppServerModels } from "./models-DD5zBdX5.js";
5
- import { a as formatComputerUseStatus, c as formatSkills, d as CODEX_CONTROL_METHODS, i as formatCodexStatus, l as formatThreads, n as formatAccount, o as formatList, r as formatCodexDisplayText, s as formatModels, t as buildHelp, u as readString$1 } from "./command-formatters-NCTL7A2q.js";
6
- import { s as summarizeCodexAccountUsage } from "./rate-limits-CqhWQ2R9.js";
1
+ import { N as resolveCodexAppServerAuthProfileIdForAgent, V as isJsonObject } from "./shared-client-BVKE40c-.js";
2
+ import { O as isCodexFastServiceTier, c as reclaimCurrentCodexSessionGeneration, i as createCodexSessionGenerationSupersededError, l as sessionBindingIdentity, n as assertCodexBindingMayBeReplaced, r as bindingStoreKey, s as normalizeCodexAppServerBindingModelProvider } from "./session-binding-DlVQ-Pxs.js";
3
+ import { r as assertCodexThreadResumeResponse } from "./protocol-validators-Dm4eXZmt.js";
4
+ import { t as listAllCodexAppServerModels } from "./models-BA01NEez.js";
5
+ import { t as CODEX_CONTROL_METHODS } from "./capabilities-BHRSI6Cn.js";
6
+ import { s as summarizeCodexAccountUsage } from "./rate-limits-S_PXs0ul.js";
7
+ import { a as formatComputerUseStatus, c as formatSkills, i as formatCodexStatus, l as formatThreads, n as formatAccount, o as formatList, r as formatCodexDisplayText, s as formatModels, t as buildHelp, u as readString$1 } from "./command-formatters-ico4ZFnk.js";
7
8
  import { n as resolveCodexNativeExecutionBlock, r as resolveCodexNativeSandboxBlock } from "./sandbox-guard-D1Wky__S.js";
8
- import { a as setCodexConversationFastMode, c as steerCodexConversationTurn, d as CODEX_NATIVE_EXECUTION_AUTH_ERROR, f as canMutateCodexHost, i as readCodexConversationActiveTurn, l as stopCodexConversationTurn, n as parseCodexFastModeArg, o as setCodexConversationModel, r as parseCodexPermissionsModeArg, s as setCodexConversationPermissions, t as formatPermissionsMode } from "./conversation-control-B0s3aIjT.js";
9
+ import { a as setCodexConversationFastMode, c as steerCodexConversationTurn, d as CODEX_NATIVE_EXECUTION_AUTH_ERROR, f as canMutateCodexHost, i as readCodexConversationActiveTurn, l as stopCodexConversationTurn, n as parseCodexFastModeArg, o as setCodexConversationModel, r as parseCodexPermissionsModeArg, s as setCodexConversationPermissions, t as formatPermissionsMode } from "./conversation-control-DE7d9DOj.js";
9
10
  import { i as readCodexConversationBindingData, n as createCodexCliNodeConversationBindingData, o as resolveCodexDefaultWorkspaceDir, r as createCodexConversationBindingData } from "./conversation-binding-data-BHRpYbX3.js";
10
- import { a as resolveCodexBindingAppServerConnection } from "./notification-correlation-C8DVQ_Bq.js";
11
- import { c as requestOptions, l as safeCodexControlRequest, o as codexControlRequest, p as formatCodexCliSessions, s as readCodexStatusProbes } from "./session-catalog-CA7R-hX2.js";
12
- import { i as readCodexComputerUseStatus, n as installCodexComputerUse } from "./computer-use-C7x4ZaqR.js";
11
+ import { a as resolveCodexBindingAppServerConnection } from "./notification-correlation-Bj_Ak_jI.js";
12
+ import { c as requestOptions, l as safeCodexControlRequest, o as codexControlRequest, p as formatCodexCliSessions, s as readCodexStatusProbes } from "./session-catalog-CP2oCcEb.js";
13
+ import { i as readCodexComputerUseStatus, n as installCodexComputerUse } from "./computer-use-BjGylO-E.js";
13
14
  import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
14
- import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
15
15
  import crypto from "node:crypto";
16
16
  import { ensureAuthProfileStore, findNormalizedProviderValue, resolveAgentDir, resolveAuthProfileEligibility, resolveAuthProfileOrder, resolveProfileUnusableUntilForDisplay, resolveSessionAgentIds } from "openclaw/plugin-sdk/agent-runtime";
17
17
  import { parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
18
18
  import { normalizeOptionalString, normalizeUniqueStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
19
19
  import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
20
+ import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
20
21
  import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
21
22
  import { MODEL_SELECTION_LOCKED_MESSAGE, isModelSelectionLocked } from "openclaw/plugin-sdk/model-session-runtime";
22
23
  //#region extensions/codex/src/command-account.ts
@@ -349,210 +350,8 @@ function buildCodexCommandPickerPresentation(title, prompt, buttons) {
349
350
  };
350
351
  }
351
352
  //#endregion
352
- //#region extensions/codex/src/command-plugins-management.ts
353
- const POLICY_REFRESH_HINT = "New Codex conversations pick this up automatically. Use /new or /reset to refresh the current one.";
354
- async function handleCodexPluginsSubcommand(ctx, rest, io) {
355
- const [verb = "list", ...args] = rest;
356
- const normalized = verb.toLowerCase();
357
- if (normalized === "menu") {
358
- if (args.length > 0) return { text: "Usage: /codex plugins menu" };
359
- return buildPluginsMenuReply();
360
- }
361
- if (normalized === "help") {
362
- if (args.length > 0) return { text: "Usage: /codex plugins help" };
363
- return { text: buildPluginsHelp() };
364
- }
365
- if (normalized === "list") {
366
- if (args.length > 0) return { text: "Usage: /codex plugins list" };
367
- const current = await io.readConfig();
368
- return { text: formatPluginList(current.plugins ?? {}, { globalEnabled: current.enabled === true }) };
369
- }
370
- const target = args[0];
371
- if (normalized === "enable" || normalized === "disable") {
372
- if (args.length === 0) return buildPluginNamePickerReply(normalized, await io.readConfig());
373
- if (!target || args.length > 1) return { text: `Usage: /codex plugins ${normalized} <name>` };
374
- if (!canMutateCodexHost(ctx)) return { text: `Only an owner or operator.admin gateway client can run /codex plugins ${normalized}.` };
375
- const wantEnabled = normalized === "enable";
376
- if (!((await io.readConfig()).plugins ?? {})[target]) return { text: `Codex sub-plugin '${formatCodexDisplayText(target)}' is not configured. Run '/codex plugins list' to see configured plugins.` };
377
- await io.mutate((block) => {
378
- if (wantEnabled) block.enabled = true;
379
- block.plugins ??= {};
380
- block.plugins[target] = {
381
- ...block.plugins[target],
382
- enabled: wantEnabled
383
- };
384
- });
385
- return { text: `${formatCodexDisplayText(target)}: ${wantEnabled ? "enabled" : "disabled"} in openclaw.json. ${POLICY_REFRESH_HINT}` };
386
- }
387
- return { text: `Unknown /codex plugins subcommand: ${formatCodexDisplayText(verb)}\n\n${buildPluginsHelp()}` };
388
- }
389
- function buildPluginsMenuReply() {
390
- return {
391
- text: [
392
- "Codex sub-plugins. Pick a sub-action or type:",
393
- "",
394
- " 1. /codex plugins list",
395
- " 2. /codex plugins enable",
396
- " 3. /codex plugins disable",
397
- " 4. /codex plugins help",
398
- "",
399
- "Type '/codex' to go back to the main menu."
400
- ].join("\n"),
401
- presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", "Pick a Codex sub-plugin action:", [
402
- {
403
- label: "list",
404
- command: "/codex plugins list"
405
- },
406
- {
407
- label: "enable",
408
- command: "/codex plugins enable"
409
- },
410
- {
411
- label: "disable",
412
- command: "/codex plugins disable"
413
- },
414
- {
415
- label: "help",
416
- command: "/codex plugins help"
417
- },
418
- {
419
- label: "back",
420
- command: "/codex"
421
- }
422
- ])
423
- };
424
- }
425
- function buildPluginNamePickerReply(verb, current) {
426
- const globalEnabled = current.enabled === true;
427
- const eligible = Object.entries(current.plugins ?? {}).toSorted(([left], [right]) => left.localeCompare(right)).filter(([, entry]) => {
428
- const effectivelyEnabled = globalEnabled && entry.enabled !== false;
429
- return verb === "disable" ? effectivelyEnabled : !effectivelyEnabled;
430
- });
431
- if (eligible.length === 0) return {
432
- text: [
433
- `No configured ${verb === "enable" ? "disabled" : "enabled"} Codex sub-plugins found.`,
434
- "",
435
- "Type '/codex plugins list' to inspect configured sub-plugins.",
436
- "Type '/codex plugins menu' to go back to the plugins menu."
437
- ].join("\n"),
438
- presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", "Pick another Codex sub-plugin action:", [{
439
- label: "list",
440
- command: "/codex plugins list"
441
- }, {
442
- label: "back",
443
- command: "/codex plugins menu"
444
- }])
445
- };
446
- const buttons = [...eligible.map(([key]) => ({
447
- label: formatCodexDisplayText(key),
448
- command: `/codex plugins ${verb} ${key}`
449
- })), {
450
- label: "back",
451
- command: "/codex plugins menu"
452
- }];
453
- return {
454
- text: [
455
- `Codex sub-plugins to ${verb}. Pick one or type:`,
456
- "",
457
- ...eligible.map(([key], index) => ` ${index + 1}. /codex plugins ${verb} ${key}`),
458
- "",
459
- ...verb === "enable" && !globalEnabled ? ["Global codexPlugins.enabled is off; enabling one configured sub-plugin turns it on.", ""] : [],
460
- "Type '/codex plugins menu' to go back to the plugins menu."
461
- ].join("\n"),
462
- presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", `Pick a Codex sub-plugin to ${verb}:`, buttons)
463
- };
464
- }
465
- function buildPluginsHelp() {
466
- return [
467
- "Codex sub-plugin management (writes only to ~/.openclaw/openclaw.json, never to ~/.codex/config.toml):",
468
- "- /codex plugins (alias for list)",
469
- "- /codex plugins list show all configured Codex sub-plugins",
470
- "- /codex plugins enable <name> enable a configured sub-plugin",
471
- "- /codex plugins disable <name> disable a configured sub-plugin"
472
- ].join("\n");
473
- }
474
- function formatPluginList(plugins, options = {}) {
475
- const globalEnabled = options.globalEnabled === true;
476
- const keys = Object.keys(plugins).toSorted();
477
- if (keys.length === 0) return "No Codex sub-plugins configured under plugins.entries.codex.config.codexPlugins.plugins";
478
- const rows = keys.map((key) => {
479
- const entry = plugins[key] ?? {};
480
- const state = globalEnabled && entry.enabled !== false ? "ON " : "OFF";
481
- return {
482
- displayKey: formatCodexDisplayText(key),
483
- state,
484
- pluginName: formatCodexDisplayText(entry.pluginName ?? key),
485
- marketplace: formatCodexDisplayText(entry.marketplaceName ?? "?")
486
- };
487
- });
488
- const keyW = Math.max(...rows.map((r) => r.displayKey.length));
489
- const pluginW = Math.max(...rows.map((r) => r.pluginName.length));
490
- return [
491
- "Codex sub-plugins in Openclaw config (~/.openclaw/openclaw.json):",
492
- "",
493
- ...rows.map((r) => ` ${r.state} ${r.displayKey.padEnd(keyW)} ${r.pluginName.padEnd(pluginW)} [${r.marketplace}]`),
494
- "",
495
- ...globalEnabled ? [] : ["Global codexPlugins.enabled is off; configured sub-plugins are inactive.", ""],
496
- "New Codex conversations pick up policy changes automatically; /new or /reset to refresh the current one."
497
- ].join("\n");
498
- }
499
- //#endregion
500
- //#region extensions/codex/src/command-handlers.ts
501
- const defaultCodexCommandDeps = {
502
- codexControlRequest,
503
- listCodexAppServerModels: listAllCodexAppServerModels,
504
- readCodexStatusProbes,
505
- requestOptions,
506
- safeCodexControlRequest,
507
- readCodexComputerUseStatus,
508
- installCodexComputerUse,
509
- resolveCodexDefaultWorkspaceDir,
510
- readCodexConversationActiveTurn,
511
- setCodexConversationFastMode,
512
- setCodexConversationModel,
513
- setCodexConversationPermissions,
514
- steerCodexConversationTurn,
515
- stopCodexConversationTurn,
516
- listCodexCliSessionsOnNode: async () => {
517
- throw new Error("Codex CLI node sessions require Gateway node runtime.");
518
- },
519
- resolveCodexCliSessionForBindingOnNode: async () => {
520
- throw new Error("Codex CLI node sessions require Gateway node runtime.");
521
- }
522
- };
523
- const CODEX_DIAGNOSTICS_SOURCE = "openclaw-diagnostics";
524
- const CODEX_DIAGNOSTICS_REASON_MAX_CHARS = 2048;
525
- const CODEX_DIAGNOSTICS_COOLDOWN_MS = 6e4;
526
- const CODEX_DIAGNOSTICS_ERROR_MAX_CHARS = 500;
527
- const CODEX_DIAGNOSTICS_COOLDOWN_MAX_THREADS = 100;
528
- const CODEX_DIAGNOSTICS_COOLDOWN_MAX_SCOPES = 100;
529
- const CODEX_DIAGNOSTICS_CONFIRMATION_TTL_MS = 5 * 6e4;
530
- const CODEX_DIAGNOSTICS_CONFIRMATION_MAX_REQUESTS_PER_SCOPE = 100;
531
- const CODEX_DIAGNOSTICS_CONFIRMATION_MAX_SCOPES = 100;
532
- const CODEX_DIAGNOSTICS_SCOPE_FIELD_MAX_CHARS = 128;
533
- const CODEX_RESUME_SAFE_THREAD_ID_PATTERN = /^[A-Za-z0-9._:-]+$/;
534
- const CODEX_NATIVE_EXECUTION_SUBCOMMANDS = /* @__PURE__ */ new Set([
535
- "bind",
536
- "resume",
537
- "steer",
538
- "model",
539
- "fast",
540
- "permissions",
541
- "compact",
542
- "review",
543
- "goal"
544
- ]);
545
- const CODEX_NATIVE_CONTROL_SUBCOMMANDS = /* @__PURE__ */ new Set([
546
- ...CODEX_NATIVE_EXECUTION_SUBCOMMANDS,
547
- "detach",
548
- "unbind",
549
- "stop"
550
- ]);
551
- const { lastUploadByThread: lastCodexDiagnosticsUploadByThread, lastUploadByScope: lastCodexDiagnosticsUploadByScope, pendingConfirmations: pendingCodexDiagnosticsConfirmations, pendingTokensByScope: pendingCodexDiagnosticsConfirmationTokensByScope } = codexDiagnosticsFeedbackState;
552
- /**
553
- * No-arg `/codex` picker. Codex owns the command tree; channels render the
554
- * portable command actions as inline controls when their transport can.
555
- */
353
+ //#region extensions/codex/src/command-handler-args.ts
354
+ /** No-arg `/codex` picker. */
556
355
  function buildCodexSubcommandPickerReply() {
557
356
  const verbs = [
558
357
  {
@@ -595,7 +394,6 @@ function buildCodexSubcommandPickerReply() {
595
394
  presentation: buildCodexCommandPickerPresentation("Codex commands", "Pick a Codex subcommand:", verbs)
596
395
  };
597
396
  }
598
- /** Sub-picker for `/codex fast menu` (on / off / status). */
599
397
  function buildCodexFastMenuReply() {
600
398
  const modes = [
601
399
  "on",
@@ -620,7 +418,6 @@ function buildCodexFastMenuReply() {
620
418
  presentation: buildCodexCommandPickerPresentation("Codex fast mode", "Pick a Codex fast mode:", buttons)
621
419
  };
622
420
  }
623
- /** Sub-picker for `/codex permissions menu` (default / yolo / status). */
624
421
  function buildCodexPermissionsMenuReply() {
625
422
  const modes = [
626
423
  "default",
@@ -645,7 +442,6 @@ function buildCodexPermissionsMenuReply() {
645
442
  presentation: buildCodexCommandPickerPresentation("Codex permissions", "Pick a Codex permissions mode:", buttons)
646
443
  };
647
444
  }
648
- /** Sub-picker for `/codex computer-use menu` (status / install). */
649
445
  function buildCodexComputerUseMenuReply() {
650
446
  const actions = ["status", "install"];
651
447
  const buttons = [...actions.map((action) => ({
@@ -668,503 +464,595 @@ function buildCodexComputerUseMenuReply() {
668
464
  presentation: buildCodexCommandPickerPresentation("Codex computer-use", "Pick a Codex computer-use action:", buttons)
669
465
  };
670
466
  }
671
- /** Returns true when the rest-args are exactly `["menu"]` (case-insensitive). */
672
467
  function isMenuVerb(rest) {
673
468
  return rest.length === 1 && (rest[0] ?? "").trim().toLowerCase() === "menu";
674
469
  }
675
- async function handleCodexSubcommand(ctx, options) {
676
- const deps = {
677
- ...defaultCodexCommandDeps,
678
- ...options.deps
679
- };
680
- const args = splitArgs(ctx.args);
681
- if (args.length === 0) return buildCodexSubcommandPickerReply();
682
- const [subcommand = "status", ...rest] = args;
683
- const normalized = subcommand.toLowerCase();
684
- if (normalized === "help") return { text: buildHelp() };
685
- if (CODEX_NATIVE_CONTROL_SUBCOMMANDS.has(normalized) && !returnsBeforeNativeCodexExecution(normalized, rest) && !isReadOnlyCodexGoalCommand(normalized, rest) && !canMutateCodexHost(ctx)) return { text: CODEX_NATIVE_EXECUTION_AUTH_ERROR };
686
- const sandboxBlock = resolveCodexNativeCommandSandboxBlock(ctx, normalized, rest);
687
- if (sandboxBlock) return { text: sandboxBlock };
688
- if (normalized === "plugins") {
689
- if (!deps.codexPluginsManagementIo) return { text: "Codex sub-plugin management is not wired up (codexPluginsManagementIo dep is undefined). Edit ~/.openclaw/openclaw.json or use `openclaw config patch` until the runtime exposes the IO." };
690
- return await handleCodexPluginsSubcommand(ctx, rest, deps.codexPluginsManagementIo);
470
+ function splitArgs(value) {
471
+ const input = value ?? "";
472
+ const args = [];
473
+ let current = "";
474
+ let quote;
475
+ let escaping = false;
476
+ let tokenStarted = false;
477
+ for (const char of input) {
478
+ if (escaping) {
479
+ current += char;
480
+ escaping = false;
481
+ tokenStarted = true;
482
+ continue;
483
+ }
484
+ if (char === "\\" && quote !== "'") {
485
+ escaping = true;
486
+ tokenStarted = true;
487
+ continue;
488
+ }
489
+ if (quote) {
490
+ if (char === quote) quote = void 0;
491
+ else current += char;
492
+ tokenStarted = true;
493
+ continue;
494
+ }
495
+ if (char === "\"" || char === "'") {
496
+ quote = char;
497
+ tokenStarted = true;
498
+ continue;
499
+ }
500
+ if (/\s/.test(char)) {
501
+ if (tokenStarted) {
502
+ args.push(current);
503
+ current = "";
504
+ tokenStarted = false;
505
+ }
506
+ continue;
507
+ }
508
+ current += char;
509
+ tokenStarted = true;
691
510
  }
692
- if (normalized === "status") {
693
- if (rest.length > 0) return { text: "Usage: /codex status" };
694
- const { agentDir } = resolveCodexConversationControlScope(ctx);
695
- return { text: formatCodexStatus(await deps.readCodexStatusProbes(options.pluginConfig, ctx.config, agentDir)) };
696
- }
697
- if (normalized === "models") {
698
- if (rest.length > 0) return { text: "Usage: /codex models" };
699
- const { agentDir } = resolveCodexConversationControlScope(ctx);
700
- return { text: formatModels(await deps.listCodexAppServerModels(deps.requestOptions(options.pluginConfig, 100, ctx.config, agentDir))) };
701
- }
702
- if (normalized === "threads") return { text: await buildThreads(deps, ctx, options.pluginConfig, rest.join(" ")) };
703
- if (normalized === "goal") return { text: await handleNativeGoal(deps, ctx, options.pluginConfig, rest) };
704
- if (normalized === "sessions") return { text: await buildCodexCliSessions(deps, rest) };
705
- if (normalized === "resume") return { text: await resumeThread(deps, ctx, options.pluginConfig, rest) };
706
- if (normalized === "bind") return await bindConversation(deps, ctx, options.pluginConfig, rest);
707
- if (normalized === "detach" || normalized === "unbind") {
708
- if (rest.length > 0) return { text: "Usage: /codex detach" };
709
- return { text: await detachConversation(deps, ctx) };
710
- }
711
- if (normalized === "binding") {
712
- if (rest.length > 0) return { text: "Usage: /codex binding" };
713
- return { text: await describeConversationBinding(deps, ctx) };
714
- }
715
- if (normalized === "stop") {
716
- if (rest.length > 0) return { text: "Usage: /codex stop" };
717
- return { text: await stopConversationTurn(deps, ctx, options.pluginConfig) };
718
- }
719
- if (normalized === "steer") return { text: await steerConversationTurn(deps, ctx, options.pluginConfig, rest.join(" ")) };
720
- if (normalized === "model") return { text: await setConversationModel(deps, ctx, options.pluginConfig, rest) };
721
- if (normalized === "fast") {
722
- if (isMenuVerb(rest)) return buildCodexFastMenuReply();
723
- return { text: await setConversationFastMode(deps, ctx, rest) };
511
+ if (escaping) current += "\\";
512
+ if (tokenStarted) args.push(current);
513
+ return args;
514
+ }
515
+ function parseBindArgs(args) {
516
+ const parsed = {};
517
+ for (let index = 0; index < args.length; index += 1) {
518
+ const arg = expectDefined(args[index], "current Codex bind argument");
519
+ if (arg === "--help" || arg === "-h") {
520
+ parsed.help = true;
521
+ continue;
522
+ }
523
+ if (arg === "--cwd") {
524
+ const value = readRequiredOptionValue(args, index);
525
+ if (!value || parsed.cwd !== void 0) {
526
+ parsed.help = true;
527
+ continue;
528
+ }
529
+ parsed.cwd = value;
530
+ index += 1;
531
+ continue;
532
+ }
533
+ if (arg === "--model") {
534
+ const value = readRequiredOptionValue(args, index);
535
+ if (!value || parsed.model !== void 0) {
536
+ parsed.help = true;
537
+ continue;
538
+ }
539
+ parsed.model = value;
540
+ index += 1;
541
+ continue;
542
+ }
543
+ if (arg === "--provider" || arg === "--model-provider") {
544
+ const value = readRequiredOptionValue(args, index);
545
+ if (!value || parsed.provider !== void 0) {
546
+ parsed.help = true;
547
+ continue;
548
+ }
549
+ parsed.provider = value;
550
+ index += 1;
551
+ continue;
552
+ }
553
+ if (!arg.startsWith("-") && !parsed.threadId) {
554
+ parsed.threadId = arg;
555
+ continue;
556
+ }
557
+ parsed.help = true;
724
558
  }
725
- if (normalized === "permissions") {
726
- if (isMenuVerb(rest)) return buildCodexPermissionsMenuReply();
727
- return { text: await setConversationPermissions(deps, ctx, rest) };
559
+ parsed.threadId = normalizeOptionalString(parsed.threadId);
560
+ parsed.cwd = normalizeOptionalString(parsed.cwd);
561
+ parsed.model = normalizeOptionalString(parsed.model);
562
+ parsed.provider = normalizeOptionalString(parsed.provider);
563
+ return parsed;
564
+ }
565
+ function parseCodexCliSessionsArgs(args) {
566
+ const parsed = { filter: "" };
567
+ const filter = [];
568
+ for (let index = 0; index < args.length; index += 1) {
569
+ const arg = expectDefined(args[index], "current Codex sessions argument");
570
+ if (arg === "--help" || arg === "-h") {
571
+ parsed.help = true;
572
+ continue;
573
+ }
574
+ if (arg === "--host" || arg === "--node") {
575
+ const value = readRequiredOptionValue(args, index);
576
+ if (!value || parsed.host !== void 0) {
577
+ parsed.help = true;
578
+ continue;
579
+ }
580
+ parsed.host = value;
581
+ index += 1;
582
+ continue;
583
+ }
584
+ if (arg === "--limit") {
585
+ const parsedLimit = parseStrictPositiveInteger(readRequiredOptionValue(args, index));
586
+ if (parsedLimit === void 0) {
587
+ parsed.help = true;
588
+ continue;
589
+ }
590
+ parsed.limit = parsedLimit;
591
+ index += 1;
592
+ continue;
593
+ }
594
+ if (arg.startsWith("-")) {
595
+ parsed.help = true;
596
+ continue;
597
+ }
598
+ filter.push(arg);
728
599
  }
729
- if (normalized === "compact") return { text: await startThreadAction(deps, ctx, options.pluginConfig, "compact", rest) };
730
- if (normalized === "review") return { text: await startThreadAction(deps, ctx, options.pluginConfig, "review", rest) };
731
- if (normalized === "diagnostics") return await handleCodexDiagnosticsFeedback(deps, ctx, options.pluginConfig, rest.join(" "), "/codex diagnostics");
732
- if (normalized === "computer-use" || normalized === "computeruse") {
733
- if (isMenuVerb(rest)) return buildCodexComputerUseMenuReply();
734
- return { text: await handleComputerUseCommand(deps, ctx, options.pluginConfig, rest) };
600
+ parsed.host = normalizeOptionalString(parsed.host);
601
+ parsed.filter = filter.join(" ").trim();
602
+ return parsed;
603
+ }
604
+ function parseResumeArgs(args) {
605
+ const parsed = {};
606
+ for (let index = 0; index < args.length; index += 1) {
607
+ const arg = expectDefined(args[index], "current Codex resume argument");
608
+ if (arg === "--help" || arg === "-h") {
609
+ parsed.help = true;
610
+ continue;
611
+ }
612
+ if (arg === "--host" || arg === "--node") {
613
+ const value = readRequiredOptionValue(args, index);
614
+ if (!value || parsed.host !== void 0) {
615
+ parsed.help = true;
616
+ continue;
617
+ }
618
+ parsed.host = value;
619
+ index += 1;
620
+ continue;
621
+ }
622
+ if (arg === "--bind") {
623
+ if (readRequiredOptionValue(args, index) !== "here" || parsed.bindHere !== void 0) {
624
+ parsed.help = true;
625
+ continue;
626
+ }
627
+ parsed.bindHere = true;
628
+ index += 1;
629
+ continue;
630
+ }
631
+ if (!arg.startsWith("-") && !parsed.threadId) {
632
+ parsed.threadId = arg;
633
+ continue;
634
+ }
635
+ parsed.help = true;
735
636
  }
736
- if (normalized === "mcp") {
737
- if (rest.length > 0) return { text: "Usage: /codex mcp" };
738
- const scope = await resolveCommandAppServerScope(deps, ctx, options.pluginConfig);
739
- return { text: formatList(await deps.codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listMcpServers, { limit: 100 }, {
740
- config: ctx.config,
741
- ...scope
742
- }), "MCP servers") };
637
+ parsed.threadId = normalizeOptionalString(parsed.threadId);
638
+ parsed.host = normalizeOptionalString(parsed.host);
639
+ return parsed;
640
+ }
641
+ function parseComputerUseArgs(args) {
642
+ const parsed = {
643
+ action: "status",
644
+ overrides: {},
645
+ hasOverrides: false,
646
+ persistentIdentity: {}
647
+ };
648
+ let sawAction = false;
649
+ for (let index = 0; index < args.length; index += 1) {
650
+ const arg = args[index];
651
+ if (arg === "--help" || arg === "-h") {
652
+ parsed.help = true;
653
+ continue;
654
+ }
655
+ if (arg === "status" || arg === "install") {
656
+ if (sawAction) {
657
+ parsed.help = true;
658
+ continue;
659
+ }
660
+ sawAction = true;
661
+ parsed.action = arg;
662
+ continue;
663
+ }
664
+ if (arg === "--source" || arg === "--marketplace-source") {
665
+ const value = readRequiredOptionValue(args, index);
666
+ if (!value || parsed.overrides.marketplaceSource !== void 0) {
667
+ parsed.help = true;
668
+ continue;
669
+ }
670
+ parsed.overrides.marketplaceSource = value;
671
+ index += 1;
672
+ continue;
673
+ }
674
+ if (arg === "--marketplace-path" || arg === "--path") {
675
+ const value = readRequiredOptionValue(args, index);
676
+ if (!value || parsed.overrides.marketplacePath !== void 0) {
677
+ parsed.help = true;
678
+ continue;
679
+ }
680
+ parsed.overrides.marketplacePath = value;
681
+ index += 1;
682
+ continue;
683
+ }
684
+ if (arg === "--marketplace") {
685
+ const value = readRequiredOptionValue(args, index);
686
+ if (!value || parsed.overrides.marketplaceName !== void 0) {
687
+ parsed.help = true;
688
+ continue;
689
+ }
690
+ parsed.overrides.marketplaceName = value;
691
+ index += 1;
692
+ continue;
693
+ }
694
+ if (arg === "--plugin" || arg === "--server" || arg === "--mcp-server") {
695
+ const value = readRequiredOptionValue(args, index);
696
+ const configKey = arg === "--plugin" ? "pluginName" : "mcpServerName";
697
+ if (!value || parsed.persistentIdentity[configKey] !== void 0) {
698
+ parsed.help = true;
699
+ continue;
700
+ }
701
+ parsed.persistentIdentity[configKey] = value.trim();
702
+ index += 1;
703
+ continue;
704
+ }
705
+ parsed.help = true;
743
706
  }
744
- if (normalized === "skills") {
745
- if (rest.length > 0) return { text: "Usage: /codex skills" };
746
- const scope = await resolveCommandAppServerScope(deps, ctx, options.pluginConfig);
747
- return { text: formatSkills(await deps.codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listSkills, {}, {
748
- config: ctx.config,
749
- ...scope
750
- })) };
707
+ parsed.overrides = normalizeComputerUseStringOverrides(parsed.overrides);
708
+ parsed.hasOverrides = Object.values(parsed.overrides).some(Boolean);
709
+ return parsed;
710
+ }
711
+ function formatComputerUsePersistentIdentityMigration(parsed) {
712
+ const configPrefix = "plugins.entries.codex.config.computerUse";
713
+ const settings = [parsed.persistentIdentity.pluginName ? `${configPrefix}.pluginName = ${JSON.stringify(parsed.persistentIdentity.pluginName)}` : void 0, parsed.persistentIdentity.mcpServerName ? `${configPrefix}.mcpServerName = ${JSON.stringify(parsed.persistentIdentity.mcpServerName)}` : void 0].filter((setting) => Boolean(setting));
714
+ const retryArgs = [
715
+ `/codex computer-use ${parsed.action}`,
716
+ parsed.overrides.marketplaceSource ? `--source ${JSON.stringify(parsed.overrides.marketplaceSource)}` : void 0,
717
+ parsed.overrides.marketplacePath ? `--marketplace-path ${JSON.stringify(parsed.overrides.marketplacePath)}` : void 0,
718
+ parsed.overrides.marketplaceName ? `--marketplace ${JSON.stringify(parsed.overrides.marketplaceName)}` : void 0
719
+ ].filter((arg) => Boolean(arg));
720
+ return ["One-off Computer Use plugin/server overrides are no longer supported.", `Set ${settings.join(" and ")} persistently, then rerun ${retryArgs.join(" ")}.`].join(" ");
721
+ }
722
+ function readRequiredOptionValue(args, index) {
723
+ const value = args[index + 1];
724
+ const normalized = value?.trim();
725
+ if (!normalized || normalized.startsWith("-")) return;
726
+ return value;
727
+ }
728
+ function normalizeComputerUseStringOverrides(overrides) {
729
+ const normalized = {};
730
+ const marketplaceSource = normalizeOptionalString(overrides.marketplaceSource);
731
+ if (marketplaceSource) normalized.marketplaceSource = marketplaceSource;
732
+ const marketplacePath = normalizeOptionalString(overrides.marketplacePath);
733
+ if (marketplacePath) normalized.marketplacePath = marketplacePath;
734
+ const marketplaceName = normalizeOptionalString(overrides.marketplaceName);
735
+ if (marketplaceName) normalized.marketplaceName = marketplaceName;
736
+ return normalized;
737
+ }
738
+ //#endregion
739
+ //#region extensions/codex/src/command-diagnostics-support.ts
740
+ const CODEX_DIAGNOSTICS_SOURCE = "openclaw-diagnostics";
741
+ const CODEX_DIAGNOSTICS_REASON_MAX_CHARS = 2048;
742
+ const CODEX_DIAGNOSTICS_COOLDOWN_MS = 6e4;
743
+ const CODEX_DIAGNOSTICS_ERROR_MAX_CHARS = 500;
744
+ const CODEX_DIAGNOSTICS_COOLDOWN_MAX_THREADS = 100;
745
+ const CODEX_DIAGNOSTICS_COOLDOWN_MAX_SCOPES = 100;
746
+ const CODEX_DIAGNOSTICS_CONFIRMATION_TTL_MS = 5 * 6e4;
747
+ const CODEX_DIAGNOSTICS_CONFIRMATION_MAX_REQUESTS_PER_SCOPE = 100;
748
+ const CODEX_DIAGNOSTICS_CONFIRMATION_MAX_SCOPES = 100;
749
+ const CODEX_DIAGNOSTICS_SCOPE_FIELD_MAX_CHARS = 128;
750
+ const CODEX_RESUME_SAFE_THREAD_ID_PATTERN = /^[A-Za-z0-9._:-]+$/;
751
+ const { lastUploadByThread: lastCodexDiagnosticsUploadByThread, lastUploadByScope: lastCodexDiagnosticsUploadByScope, pendingConfirmations: pendingCodexDiagnosticsConfirmations, pendingTokensByScope: pendingCodexDiagnosticsConfirmationTokensByScope } = codexDiagnosticsFeedbackState;
752
+ function normalizeDiagnosticsReason(note) {
753
+ const normalized = normalizeOptionalString(note);
754
+ return normalized ? truncateUtf16Safe(normalized, CODEX_DIAGNOSTICS_REASON_MAX_CHARS) : void 0;
755
+ }
756
+ function parseDiagnosticsArgs(args) {
757
+ const [action, token, ...extra] = splitArgs(args);
758
+ const normalizedAction = action?.toLowerCase();
759
+ if ((normalizedAction === "confirm" || normalizedAction === "--confirm") && token && extra.length === 0) return {
760
+ action: "confirm",
761
+ token
762
+ };
763
+ if ((normalizedAction === "cancel" || normalizedAction === "--cancel") && token && extra.length === 0) return {
764
+ action: "cancel",
765
+ token
766
+ };
767
+ if (normalizedAction === "confirm" || normalizedAction === "--confirm" || normalizedAction === "cancel" || normalizedAction === "--cancel") return { action: "usage" };
768
+ return {
769
+ action: "request",
770
+ note: args
771
+ };
772
+ }
773
+ function formatDiagnosticsUsage(commandPrefix) {
774
+ return [
775
+ `Usage: ${commandPrefix} [note]`,
776
+ `Usage: ${commandPrefix} confirm <token>`,
777
+ `Usage: ${commandPrefix} cancel <token>`
778
+ ].join("\n");
779
+ }
780
+ function createCodexDiagnosticsConfirmation(params) {
781
+ prunePendingCodexDiagnosticsConfirmations(params.now);
782
+ if (!pendingCodexDiagnosticsConfirmationTokensByScope.has(params.scopeKey) && pendingCodexDiagnosticsConfirmationTokensByScope.size >= CODEX_DIAGNOSTICS_CONFIRMATION_MAX_SCOPES) {
783
+ const oldestScopeKey = pendingCodexDiagnosticsConfirmationTokensByScope.keys().next().value;
784
+ if (typeof oldestScopeKey === "string") deletePendingCodexDiagnosticsConfirmationScope(oldestScopeKey);
751
785
  }
752
- if (normalized === "account") {
753
- if (rest.length > 0) return { text: "Usage: /codex account" };
754
- const scope = await resolveCommandAppServerScope(deps, ctx, options.pluginConfig);
755
- const requestScope = {
756
- config: ctx.config,
757
- ...scope
758
- };
759
- const [account, limits] = await Promise.all([deps.safeCodexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.account, { refreshToken: false }, requestScope), deps.safeCodexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.rateLimits, void 0, requestScope)]);
760
- return { text: formatAccount(account, limits, await readCodexAccountAuthOverview({
761
- ctx,
762
- agentDir: scope.agentDir,
763
- pluginConfig: options.pluginConfig,
764
- safeCodexControlRequest: deps.safeCodexControlRequest,
765
- account,
766
- limits
767
- })) };
786
+ const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(params.scopeKey) ?? [];
787
+ while (scopeTokens.length >= CODEX_DIAGNOSTICS_CONFIRMATION_MAX_REQUESTS_PER_SCOPE) {
788
+ const oldestToken = scopeTokens.shift();
789
+ if (!oldestToken) break;
790
+ pendingCodexDiagnosticsConfirmations.delete(oldestToken);
768
791
  }
769
- return { text: `Unknown Codex command: ${formatCodexDisplayText(subcommand)}\n\n${buildHelp()}` };
792
+ const token = crypto.randomBytes(6).toString("hex");
793
+ scopeTokens.push(token);
794
+ pendingCodexDiagnosticsConfirmationTokensByScope.set(params.scopeKey, scopeTokens);
795
+ pendingCodexDiagnosticsConfirmations.set(token, {
796
+ token,
797
+ targets: params.targets,
798
+ note: params.note,
799
+ senderId: params.senderId,
800
+ channel: params.channel,
801
+ accountId: params.accountId,
802
+ channelId: params.channelId,
803
+ messageThreadId: params.messageThreadId,
804
+ threadParentId: params.threadParentId,
805
+ sessionKey: params.sessionKey,
806
+ scopeKey: params.scopeKey,
807
+ ...params.privateRouted === void 0 ? {} : { privateRouted: params.privateRouted },
808
+ createdAt: params.now
809
+ });
810
+ return token;
770
811
  }
771
- function isCurrentSessionModelSelectionLocked(ctx) {
772
- const sessionKey = ctx.sessionKey?.trim();
773
- if (!sessionKey) return false;
774
- return isModelSelectionLocked(getSessionEntry({
775
- storePath: resolveStorePath(ctx.config.session?.store, { agentId: ctx.agentId }),
776
- sessionKey,
777
- hydrateSkillPromptRefs: false,
778
- readConsistency: "latest"
779
- }));
812
+ function readCodexDiagnosticsConfirmationScope(ctx) {
813
+ return {
814
+ accountId: normalizeCodexDiagnosticsScopeField(ctx.accountId),
815
+ channelId: normalizeCodexDiagnosticsScopeField(ctx.channelId),
816
+ messageThreadId: typeof ctx.messageThreadId === "string" || typeof ctx.messageThreadId === "number" ? normalizeCodexDiagnosticsScopeField(String(ctx.messageThreadId)) : void 0,
817
+ threadParentId: normalizeCodexDiagnosticsScopeField(ctx.threadParentId),
818
+ sessionKey: normalizeCodexDiagnosticsScopeField(ctx.sessionKey)
819
+ };
780
820
  }
781
- function resolveCodexNativeCommandSandboxBlock(ctx, subcommand, args) {
782
- if (isReadOnlyCodexGoalCommand(subcommand, args)) return;
783
- if (!CODEX_NATIVE_EXECUTION_SUBCOMMANDS.has(subcommand)) return;
784
- if (returnsBeforeNativeCodexExecution(subcommand, args)) return;
785
- if (isCodexCliNodeResumeBind(subcommand, args)) return resolveCodexNativeSandboxBlock({
786
- config: ctx.config,
787
- sessionKey: ctx.sessionKey,
788
- sessionId: ctx.sessionId,
789
- surface: `/${["codex", subcommand].join(" ")}`
790
- });
791
- return resolveCodexNativeExecutionBlock({
792
- config: ctx.config,
793
- agentId: ctx.agentId,
794
- sessionKey: ctx.sessionKey,
795
- sessionId: ctx.sessionId,
796
- surface: `/${["codex", subcommand].join(" ")}`
797
- });
821
+ function readCodexDiagnosticsScopeMismatch(pending, ctx) {
822
+ const current = readCodexDiagnosticsConfirmationScope(ctx);
823
+ if (pending.accountId !== current.accountId) return {
824
+ confirmMessage: "This Codex diagnostics confirmation belongs to a different account.",
825
+ cancelMessage: "This Codex diagnostics confirmation belongs to a different account."
826
+ };
827
+ if (pending.privateRouted) return;
828
+ if (pending.channelId !== current.channelId) return {
829
+ confirmMessage: "This Codex diagnostics confirmation belongs to a different channel instance.",
830
+ cancelMessage: "This Codex diagnostics confirmation belongs to a different channel instance."
831
+ };
832
+ if (pending.messageThreadId !== current.messageThreadId) return {
833
+ confirmMessage: "This Codex diagnostics confirmation belongs to a different thread.",
834
+ cancelMessage: "This Codex diagnostics confirmation belongs to a different thread."
835
+ };
836
+ if (pending.threadParentId !== current.threadParentId) return {
837
+ confirmMessage: "This Codex diagnostics confirmation belongs to a different parent thread.",
838
+ cancelMessage: "This Codex diagnostics confirmation belongs to a different parent thread."
839
+ };
840
+ if (pending.sessionKey !== current.sessionKey) return {
841
+ confirmMessage: "This Codex diagnostics confirmation belongs to a different session.",
842
+ cancelMessage: "This Codex diagnostics confirmation belongs to a different session."
843
+ };
798
844
  }
799
- function isReadOnlyCodexGoalCommand(subcommand, args) {
800
- if (subcommand !== "goal" || args.length > 1) return false;
801
- const action = (args[0] ?? "status").toLowerCase();
802
- return action === "status" || action === "get";
845
+ function readPendingCodexDiagnosticsConfirmation(token, now) {
846
+ prunePendingCodexDiagnosticsConfirmations(now);
847
+ return pendingCodexDiagnosticsConfirmations.get(token);
803
848
  }
804
- function returnsBeforeNativeCodexExecution(subcommand, args) {
805
- switch (subcommand) {
806
- case "bind": return parseBindArgs([...args]).help === true;
807
- case "resume": return returnsBeforeNativeCodexResume(args);
808
- case "steer": return args.join(" ").trim() === "";
809
- case "model": return args.length === 0 || args.length > 1;
810
- case "fast": return args.length === 0 || args.length > 1 || parseCodexFastModeArg(args[0]) === void 0;
811
- case "permissions": return args.length === 0 || args.length > 1 || parseCodexPermissionsModeArg(args[0]) === void 0;
812
- case "compact":
813
- case "review":
814
- case "detach":
815
- case "unbind":
816
- case "stop": return args.length > 0;
817
- default: return false;
849
+ function deletePendingCodexDiagnosticsConfirmation(token) {
850
+ const pending = pendingCodexDiagnosticsConfirmations.get(token);
851
+ pendingCodexDiagnosticsConfirmations.delete(token);
852
+ if (!pending) return;
853
+ const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(pending.scopeKey);
854
+ if (!scopeTokens) return;
855
+ const tokenIndex = scopeTokens.indexOf(token);
856
+ if (tokenIndex >= 0) scopeTokens.splice(tokenIndex, 1);
857
+ if (scopeTokens.length === 0) pendingCodexDiagnosticsConfirmationTokensByScope.delete(pending.scopeKey);
858
+ }
859
+ function prunePendingCodexDiagnosticsConfirmations(now) {
860
+ for (const [token, pending] of pendingCodexDiagnosticsConfirmations) if (now - pending.createdAt >= CODEX_DIAGNOSTICS_CONFIRMATION_TTL_MS) deletePendingCodexDiagnosticsConfirmation(token);
861
+ }
862
+ function deletePendingCodexDiagnosticsConfirmationScope(scopeKey) {
863
+ const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(scopeKey) ?? [];
864
+ for (const token of scopeTokens) pendingCodexDiagnosticsConfirmations.delete(token);
865
+ pendingCodexDiagnosticsConfirmationTokensByScope.delete(scopeKey);
866
+ }
867
+ function codexDiagnosticsTargetsMatch(expected, actual) {
868
+ const fingerprint = (target) => JSON.stringify([
869
+ bindingStoreKey(target.identity),
870
+ target.threadId,
871
+ target.connectionScope ?? null,
872
+ target.pendingSupervisionBranch?.connectionFingerprint ?? target.appServerRuntimeFingerprint ?? null,
873
+ target.authProfileId ?? null
874
+ ]);
875
+ const expectedTargets = expected.map(fingerprint).toSorted();
876
+ const actualTargets = actual.map(fingerprint).toSorted();
877
+ return expectedTargets.length === actualTargets.length && expectedTargets.every((target, index) => target === actualTargets[index]);
878
+ }
879
+ function formatCodexDiagnosticsUploadResult(sent, failed) {
880
+ const lines = [];
881
+ if (sent.length > 0) {
882
+ lines.push("Codex diagnostics sent to OpenAI servers:");
883
+ lines.push(...formatCodexDiagnosticsTargetLines(sent));
884
+ lines.push("Included Codex logs and spawned Codex subthreads when available.");
885
+ }
886
+ if (failed.length > 0) {
887
+ if (lines.length > 0) lines.push("");
888
+ lines.push("Could not send Codex diagnostics:");
889
+ lines.push(...failed.map(({ target, error }) => `${formatCodexDiagnosticsTargetLine(target)}: ${formatCodexErrorForDisplay(error)}`));
890
+ lines.push("Inspect locally:");
891
+ lines.push(...failed.map(({ target }) => `- ${formatCodexResumeCommandForDisplay(target.threadId)}`));
818
892
  }
893
+ return lines.join("\n");
819
894
  }
820
- function isCodexCliNodeResumeBind(subcommand, args) {
821
- if (subcommand !== "resume") return false;
822
- const parsed = parseResumeArgs([...args]);
823
- return Boolean(parsed.host && parsed.threadId && parsed.bindHere === true && !parsed.help);
895
+ function formatCodexDiagnosticsTargetLines(targets) {
896
+ return targets.flatMap((target, index) => {
897
+ const lines = formatCodexDiagnosticsTargetBlock(target, index);
898
+ return index < targets.length - 1 ? [...lines, ""] : lines;
899
+ });
824
900
  }
825
- function returnsBeforeNativeCodexResume(args) {
826
- const parsed = parseResumeArgs([...args]);
827
- const normalizedThreadId = parsed.threadId?.trim();
828
- if (parsed.help) return true;
829
- if (parsed.host) return !normalizedThreadId || parsed.bindHere !== true;
830
- return !normalizedThreadId || args.length !== 1;
901
+ function formatCodexDiagnosticsTargetBlock(target, index) {
902
+ const lines = [`Session ${index + 1}`];
903
+ if (target.channel) lines.push(`Channel: ${formatCodexValueForDisplay(target.channel)}`);
904
+ if (target.sessionKey) lines.push(`OpenClaw session key: ${formatCodexCopyableValueForDisplay(target.sessionKey)}`);
905
+ if (target.sessionId) lines.push(`OpenClaw session id: ${formatCodexCopyableValueForDisplay(target.sessionId)}`);
906
+ lines.push(`Codex thread id: ${formatCodexCopyableValueForDisplay(target.threadId)}`);
907
+ lines.push(`Inspect locally: ${formatCodexResumeCommandForDisplay(target.threadId)}`);
908
+ return lines;
831
909
  }
832
- async function handleComputerUseCommand(deps, ctx, pluginConfig, args) {
833
- const parsed = parseComputerUseArgs(args);
834
- if (parsed.help) return ["Usage: /codex computer-use [status|install] [--source <marketplace-source>] [--marketplace-path <path>] [--marketplace <name>]", "Checks or installs the configured Codex Computer Use plugin through app-server."].join("\n");
835
- if (Object.keys(parsed.persistentIdentity).length > 0) return formatComputerUsePersistentIdentityMigration(parsed);
836
- if (parsed.action === "install" && !canMutateCodexHost(ctx)) return "Only an owner or operator.admin gateway client can configure Codex Computer Use.";
837
- const { agentDir } = resolveCodexConversationControlScope(ctx);
838
- const params = {
839
- pluginConfig,
840
- config: ctx.config,
841
- agentDir,
842
- forceEnable: parsed.action === "install" || parsed.hasOverrides,
843
- ...Object.keys(parsed.overrides).length > 0 ? { overrides: parsed.overrides } : {}
844
- };
845
- if (parsed.action === "install") return formatComputerUseStatus(await deps.installCodexComputerUse(params));
846
- return formatComputerUseStatus(await deps.readCodexComputerUseStatus(params));
910
+ function formatCodexDiagnosticsTargetLine(target) {
911
+ const parts = [];
912
+ if (target.channel) parts.push(`channel ${formatCodexValueForDisplay(target.channel)}`);
913
+ const sessionLabel = target.sessionId || target.sessionKey;
914
+ if (sessionLabel) parts.push(`OpenClaw session ${formatCodexValueForDisplay(sessionLabel)}`);
915
+ parts.push(`Codex thread ${formatCodexThreadIdForDisplay(target.threadId)}`);
916
+ return `- ${parts.join(", ")}`;
847
917
  }
848
- async function bindConversation(deps, ctx, pluginConfig, args) {
849
- const parsed = parseBindArgs(args);
850
- if (parsed.help) return { text: "Usage: /codex bind [thread-id] [--cwd <path>] [--model <model>] [--provider <provider>]" };
851
- if (isCurrentSessionModelSelectionLocked(ctx)) return { text: MODEL_SELECTION_LOCKED_MESSAGE };
852
- const scope = resolveCodexConversationControlScope(ctx);
853
- const workspaceDir = parsed.cwd ?? deps.resolveCodexDefaultWorkspaceDir(pluginConfig);
854
- const currentConversation = await ctx.getCurrentConversationBinding();
855
- const currentConversationData = readCodexConversationBindingData(currentConversation);
856
- const bindingId = currentConversationData?.kind === "codex-app-server-session" ? currentConversationData.bindingId : currentConversation ? `conversation-${currentConversation.bindingId}` : void 0;
857
- const sessionOwner = ctx.sessionId ? sessionBindingIdentity({
858
- sessionId: ctx.sessionId,
859
- sessionKey: ctx.sessionKey,
860
- agentId: scope.agentId,
861
- config: ctx.config
862
- }) : void 0;
863
- const currentOwner = currentConversationData?.kind === "codex-app-server-session" ? conversationBindingIdentity(currentConversationData.bindingId) : sessionOwner;
864
- const existingBinding = currentOwner ? await deps.bindingStore.read(currentOwner) : void 0;
865
- assertCodexBindingMayBeReplaced(existingBinding, "binding this conversation to another thread");
866
- const sessionSource = sessionOwner && existingBinding ? {
867
- agentId: sessionOwner.agentId,
868
- sessionId: sessionOwner.sessionId,
869
- threadId: existingBinding.threadId,
870
- ...sessionOwner.sessionKey ? { sessionKey: sessionOwner.sessionKey } : {}
871
- } : void 0;
872
- const authProfileId = existingBinding?.authProfileId;
873
- const data = createCodexConversationBindingData({
874
- bindingId,
875
- workspaceDir,
876
- agentId: scope.agentId,
877
- agentDir: scope.agentDir,
878
- source: currentConversationData?.kind === "codex-app-server-session" ? currentConversationData.source : sessionSource,
879
- start: {
880
- id: crypto.randomUUID(),
881
- threadId: parsed.threadId,
882
- model: parsed.model,
883
- modelProvider: parsed.provider,
884
- authProfileId
918
+ function escapeCodexChatText(value) {
919
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("@", "@").replaceAll("`", "`").replaceAll("[", "[").replaceAll("]", "]").replaceAll("(", "(").replaceAll(")", ")").replaceAll("*", "∗").replaceAll("_", "_").replaceAll("~", "~").replaceAll("|", "|");
920
+ }
921
+ function formatCodexTextForDisplay(value) {
922
+ let safe = "";
923
+ for (const character of value) {
924
+ const codePoint = character.codePointAt(0);
925
+ safe += codePoint != null && isUnsafeDisplayCodePoint(codePoint) ? "?" : character;
926
+ }
927
+ safe = safe.trim();
928
+ return safe || "<unknown>";
929
+ }
930
+ function readCodexDiagnosticsTargetsCooldownMessage(targets, ctx, now, options = {}) {
931
+ for (const target of targets) {
932
+ const cooldownMs = readCodexDiagnosticsCooldownMs(target.threadId, now);
933
+ if (cooldownMs > 0) {
934
+ if (options.includeThreadId === false) return `Codex diagnostics were already sent for one of these Codex threads recently. Try again in ${Math.ceil(cooldownMs / 1e3)}s.`;
935
+ return `Codex diagnostics were already sent for thread ${formatCodexThreadIdForDisplay(target.threadId)} recently. Try again in ${Math.ceil(cooldownMs / 1e3)}s.`;
885
936
  }
886
- });
887
- const threadLabel = parsed.threadId ?? "a new thread";
888
- const request = await ctx.requestConversationBinding({
889
- summary: `Codex app-server thread ${formatCodexDisplayText(threadLabel)} in ${formatCodexDisplayText(workspaceDir)}`,
890
- detachHint: "/codex detach",
891
- data
892
- });
893
- if (request.status === "pending") return request.reply;
894
- if (request.status === "error") return { text: formatCodexDisplayText(request.message) };
895
- return { text: `Bound this conversation to ${formatCodexDisplayText(threadLabel)} in ${formatCodexDisplayText(workspaceDir)}. The next message will initialize it.` };
937
+ }
938
+ const scopeCooldownMs = readCodexDiagnosticsScopeCooldownMs(options.cooldownScope ?? readCodexDiagnosticsCooldownScope(ctx), now);
939
+ if (scopeCooldownMs > 0) return `Codex diagnostics were already sent for this account or channel recently. Try again in ${Math.ceil(scopeCooldownMs / 1e3)}s.`;
940
+ }
941
+ function recordCodexDiagnosticsUpload(threadId, ctx, now, cooldownScope) {
942
+ pruneCodexDiagnosticsCooldowns(now);
943
+ recordBoundedCodexDiagnosticsCooldown(lastCodexDiagnosticsUploadByScope, cooldownScope ?? readCodexDiagnosticsCooldownScope(ctx), CODEX_DIAGNOSTICS_COOLDOWN_MAX_SCOPES, now);
944
+ recordBoundedCodexDiagnosticsCooldown(lastCodexDiagnosticsUploadByThread, threadId, CODEX_DIAGNOSTICS_COOLDOWN_MAX_THREADS, now);
945
+ }
946
+ function readCodexDiagnosticsCooldownScope(ctx) {
947
+ const scope = readCodexDiagnosticsConfirmationScope(ctx);
948
+ const payload = JSON.stringify({
949
+ accountId: scope.accountId ?? null,
950
+ channelId: scope.channelId ?? null,
951
+ sessionKey: scope.sessionKey ?? null,
952
+ messageThreadId: scope.messageThreadId ?? null,
953
+ threadParentId: scope.threadParentId ?? null,
954
+ senderId: normalizeCodexDiagnosticsScopeField(ctx.senderId) ?? null,
955
+ channel: normalizeCodexDiagnosticsScopeField(ctx.channel) ?? ""
956
+ });
957
+ return crypto.createHash("sha256").update(payload).digest("hex");
896
958
  }
897
- async function detachConversation(deps, ctx) {
898
- if (isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
899
- const data = readCodexConversationBindingData(await ctx.getCurrentConversationBinding());
900
- if (data?.kind === "codex-app-server-session") assertCodexBindingMayBeReplaced(await deps.bindingStore.read(conversationBindingIdentity(data.bindingId)), "detaching its conversation binding");
901
- const detached = await ctx.detachConversationBinding();
902
- if (data?.kind === "codex-app-server-session") await deps.bindingStore.mutate(conversationBindingIdentity(data.bindingId), { kind: "clear" });
903
- return detached.removed ? "Detached this conversation from Codex." : "No Codex conversation binding was attached.";
959
+ function buildDiagnosticsTags(ctx) {
960
+ const tags = { source: CODEX_DIAGNOSTICS_SOURCE };
961
+ addTag(tags, "channel", ctx.channel);
962
+ return tags;
904
963
  }
905
- async function describeConversationBinding(deps, ctx) {
906
- const current = await ctx.getCurrentConversationBinding();
907
- const data = readCodexConversationBindingData(current);
908
- if (!current || !data) return "No Codex conversation binding is attached.";
909
- if (data.kind === "codex-cli-node-session") return [
910
- "Codex conversation binding:",
911
- "- Mode: Codex CLI node session",
912
- `- Node: ${formatCodexDisplayText(data.nodeId)}`,
913
- `- Session: ${formatCodexDisplayText(data.sessionId)}`,
914
- `- Workspace: ${formatCodexDisplayText(data.cwd ?? "unknown")}`,
915
- "- Active run: not tracked"
916
- ].join("\n");
917
- const identity = conversationBindingIdentity(data.bindingId);
918
- const threadBinding = await deps.bindingStore.read(identity);
919
- const active = deps.readCodexConversationActiveTurn(identity);
920
- return [
921
- "Codex conversation binding:",
922
- `- Thread: ${formatCodexDisplayText(threadBinding?.threadId ?? "unknown")}`,
923
- `- Workspace: ${formatCodexDisplayText(data.workspaceDir)}`,
924
- `- Model: ${formatCodexDisplayText(threadBinding?.model ?? "default")}`,
925
- `- Fast: ${isCodexFastServiceTier(threadBinding?.serviceTier) ? "on" : "off"}`,
926
- `- Permissions: ${threadBinding ? formatPermissionsMode(threadBinding) : "default"}`,
927
- `- Active run: ${formatCodexDisplayText(active ? active.turnId : "none")}`,
928
- `- Binding: ${formatCodexDisplayText(data.bindingId)}`
929
- ].join("\n");
964
+ function addTag(tags, key, value) {
965
+ if (typeof value === "string" && value.trim()) tags[key] = value.trim();
930
966
  }
931
- async function buildThreads(deps, ctx, pluginConfig, filter) {
932
- const scope = await resolveCommandAppServerScope(deps, ctx, pluginConfig);
933
- return formatThreads(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreads, {
934
- limit: 10,
935
- ...filter.trim() ? { searchTerm: filter.trim() } : {}
936
- }, {
937
- config: ctx.config,
938
- ...scope
939
- }));
967
+ function formatCodexThreadIdForDisplay(threadId) {
968
+ return escapeCodexChatText(formatCodexTextForDisplay(threadId));
940
969
  }
941
- async function handleNativeGoal(deps, ctx, pluginConfig, args) {
942
- const action = (args[0] ?? "status").toLowerCase();
943
- const objective = args.slice(1).join(" ").trim();
944
- const target = await resolveControlTarget(ctx);
945
- if (!target) return "Cannot manage the Codex goal because this command has no stable binding identity.";
946
- const binding = await deps.bindingStore.read(target.identity);
947
- if (!binding?.threadId) return "No Codex thread is attached to this OpenClaw session yet.";
948
- const connection = resolveCodexBindingAppServerConnection({
949
- binding,
950
- authProfileId: binding.authProfileId,
951
- pluginConfig
952
- });
953
- const goalRequestOptions = {
954
- agentDir: target.agentDir,
955
- authProfileId: connection.clientAuthProfileId,
956
- config: ctx.config,
957
- ...connection.usesSupervisionConnection ? { startOptions: connection.appServer.start } : {}
958
- };
959
- if (action === "status" || action === "get") {
960
- if (args.length > 1) return "Usage: /codex goal [status]";
961
- return formatNativeGoal(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.getThreadGoal, { threadId: binding.threadId }, goalRequestOptions));
962
- }
963
- if (action === "clear") {
964
- if (args.length > 1) return "Usage: /codex goal clear";
965
- const response = await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.clearThreadGoal, { threadId: binding.threadId }, goalRequestOptions);
966
- return isJsonObject(response) && response.cleared === true ? "Cleared the Codex goal." : "No Codex goal was active.";
967
- }
968
- const requestedStatus = action === "pause" ? "paused" : action === "resume" ? "active" : action === "block" ? "blocked" : action === "complete" ? "complete" : void 0;
969
- const isObjectiveUpdate = action === "set";
970
- if (!requestedStatus && !isObjectiveUpdate || isObjectiveUpdate && !objective) return "Usage: /codex goal [status|set <objective>|pause|resume|block|complete|clear]";
971
- if (requestedStatus && args.length > 1) return `Usage: /codex goal ${action}`;
972
- return formatNativeGoal(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.setThreadGoal, {
973
- threadId: binding.threadId,
974
- ...objective ? { objective } : {},
975
- ...requestedStatus ? { status: requestedStatus } : {}
976
- }, goalRequestOptions));
970
+ function formatCodexValueForDisplay(value) {
971
+ return escapeCodexChatText(formatCodexTextForDisplay(value));
977
972
  }
978
- function formatNativeGoal(response) {
979
- const goal = isJsonObject(response) && isJsonObject(response.goal) ? response.goal : void 0;
980
- if (!goal) return "No Codex goal is active.";
981
- const objective = readString$1(goal, "objective") ?? "unknown";
982
- const status = readString$1(goal, "status") ?? "unknown";
983
- const tokensUsed = typeof goal.tokensUsed === "number" ? goal.tokensUsed : 0;
984
- const tokenBudget = typeof goal.tokenBudget === "number" ? goal.tokenBudget : void 0;
985
- return [
986
- `Codex goal: ${formatCodexDisplayText(objective)}`,
987
- `- Status: ${formatCodexDisplayText(status)}`,
988
- `- Tokens: ${tokensUsed}${tokenBudget === void 0 ? "" : ` / ${tokenBudget}`}`
989
- ].join("\n");
973
+ function formatCodexCopyableValueForDisplay(value) {
974
+ const safe = formatCodexTextForDisplay(value);
975
+ if (CODEX_RESUME_SAFE_THREAD_ID_PATTERN.test(safe)) return `\`${safe}\``;
976
+ return escapeCodexChatText(safe);
990
977
  }
991
- async function buildCodexCliSessions(deps, args) {
992
- const parsed = parseCodexCliSessionsArgs(args);
993
- if (parsed.help || !parsed.host) return "Usage: /codex sessions --host <node> [filter] [--limit <n>]";
994
- return formatCodexCliSessions(await deps.listCodexCliSessionsOnNode({
995
- requestedNode: parsed.host,
996
- filter: parsed.filter,
997
- limit: parsed.limit
998
- }));
978
+ function readCodexDiagnosticsCooldownMs(threadId, now) {
979
+ const lastSentAt = lastCodexDiagnosticsUploadByThread.get(threadId);
980
+ if (!lastSentAt) return 0;
981
+ const remainingMs = Math.max(0, CODEX_DIAGNOSTICS_COOLDOWN_MS - (now - lastSentAt));
982
+ if (remainingMs === 0) lastCodexDiagnosticsUploadByThread.delete(threadId);
983
+ return remainingMs;
999
984
  }
1000
- async function resumeThread(deps, ctx, pluginConfig, args) {
1001
- const parsed = parseResumeArgs(args);
1002
- const normalizedThreadId = parsed.threadId?.trim();
1003
- if (parsed.help) return args.includes("--help") || args.includes("-h") || parsed.host ? "Usage: /codex resume <thread-id>\nUsage: /codex resume <session-id> --host <node> --bind here" : "Usage: /codex resume <thread-id>";
1004
- if (parsed.host) return await bindCodexCliNodeSession(deps, ctx, parsed);
1005
- if (!normalizedThreadId || args.length !== 1) return "Usage: /codex resume <thread-id>";
1006
- if (isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
1007
- if (!ctx.sessionId) return "Cannot attach a Codex thread because this command did not include an OpenClaw session id.";
1008
- const scope = resolveCodexConversationControlScope(ctx);
1009
- const identity = sessionBindingIdentity({
1010
- sessionId: ctx.sessionId,
1011
- sessionKey: ctx.sessionKey,
1012
- agentId: scope.agentId,
1013
- config: ctx.config
1014
- });
1015
- return await deps.bindingStore.withLease(identity, async () => {
1016
- if (!await reclaimCurrentCodexSessionGeneration({
1017
- bindingStore: deps.bindingStore,
1018
- identity,
1019
- config: ctx.config
1020
- })) throw new Error(`Codex session generation is no longer current: ${identity.sessionId}`);
1021
- const currentBinding = await deps.bindingStore.read(identity);
1022
- assertCodexBindingMayBeReplaced(currentBinding, "attaching a different resumed thread");
1023
- const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
1024
- authProfileId: currentBinding?.authProfileId,
1025
- agentDir: scope.agentDir,
1026
- config: ctx.config
1027
- });
1028
- const response = assertCodexThreadResumeResponse(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.resumeThread, {
1029
- threadId: normalizedThreadId,
1030
- excludeTurns: true
1031
- }, {
1032
- config: ctx.config,
1033
- agentDir: scope.agentDir,
1034
- authProfileId,
1035
- sessionKey: ctx.sessionKey,
1036
- sessionId: ctx.sessionId
1037
- }));
1038
- const effectiveThreadId = response.thread.id;
1039
- if (effectiveThreadId !== normalizedThreadId) throw new Error(`Codex thread/resume returned ${effectiveThreadId} for ${normalizedThreadId}`);
1040
- const resumedCwd = response.thread.cwd;
1041
- if (typeof resumedCwd !== "string") throw new Error(`Codex thread/resume returned no cwd for ${normalizedThreadId}`);
1042
- const modelProvider = normalizeCodexAppServerBindingModelProvider({
1043
- authProfileId,
1044
- modelProvider: response.modelProvider ?? void 0,
1045
- agentDir: scope.agentDir,
1046
- config: ctx.config
1047
- });
1048
- assertCodexBindingMayBeReplaced(await deps.bindingStore.read(identity), "committing a different resumed thread");
1049
- if (!await deps.bindingStore.mutate(identity, {
1050
- kind: "set",
1051
- binding: {
1052
- threadId: effectiveThreadId,
1053
- cwd: resumedCwd,
1054
- authProfileId,
1055
- model: response.model,
1056
- modelProvider,
1057
- historyCoveredThrough: (/* @__PURE__ */ new Date()).toISOString()
1058
- }
1059
- })) throw new Error("Codex thread binding changed while attaching the resumed thread.");
1060
- return `Attached this OpenClaw session to Codex thread ${formatCodexDisplayText(effectiveThreadId)}.`;
1061
- });
985
+ function readCodexDiagnosticsScopeCooldownMs(scope, now) {
986
+ const lastSentAt = lastCodexDiagnosticsUploadByScope.get(scope);
987
+ if (!lastSentAt) return 0;
988
+ const remainingMs = Math.max(0, CODEX_DIAGNOSTICS_COOLDOWN_MS - (now - lastSentAt));
989
+ if (remainingMs === 0) lastCodexDiagnosticsUploadByScope.delete(scope);
990
+ return remainingMs;
1062
991
  }
1063
- async function bindCodexCliNodeSession(deps, ctx, parsed) {
1064
- if (!parsed.threadId || !parsed.host || parsed.bindHere !== true) return "Usage: /codex resume <session-id> --host <node> --bind here";
1065
- if (isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
1066
- if (ctx.sessionId) {
1067
- const scope = resolveCodexConversationControlScope(ctx);
1068
- assertCodexBindingMayBeReplaced(await deps.bindingStore.read(sessionBindingIdentity({
1069
- sessionId: ctx.sessionId,
1070
- sessionKey: ctx.sessionKey,
1071
- agentId: scope.agentId,
1072
- config: ctx.config
1073
- })), "binding a Codex CLI node session");
992
+ function recordBoundedCodexDiagnosticsCooldown(map, key, maxSize, now) {
993
+ if (!map.has(key)) while (map.size >= maxSize) {
994
+ const oldestKey = map.keys().next().value;
995
+ if (typeof oldestKey !== "string") break;
996
+ map.delete(oldestKey);
1074
997
  }
1075
- const resolved = await deps.resolveCodexCliSessionForBindingOnNode({
1076
- requestedNode: parsed.host,
1077
- sessionId: parsed.threadId
1078
- });
1079
- if (!resolved.session) return `No Codex CLI session ${formatCodexDisplayText(parsed.threadId)} was found on ${formatCodexDisplayText(parsed.host)}.`;
1080
- const nodeId = resolved.node.nodeId;
1081
- if (!nodeId) return "Cannot bind Codex CLI session because the selected node did not include a node id.";
1082
- const scope = resolveCodexConversationControlScope(ctx);
1083
- const data = createCodexCliNodeConversationBindingData({
1084
- nodeId,
1085
- sessionId: parsed.threadId,
1086
- agentId: scope.agentId,
1087
- cwd: resolved.session?.cwd
1088
- });
1089
- const summary = `Codex CLI session ${formatCodexDisplayText(parsed.threadId)} on ${formatCodexDisplayText(nodeId)}`;
1090
- const request = await ctx.requestConversationBinding({
1091
- summary,
1092
- detachHint: "/codex detach",
1093
- data
1094
- });
1095
- if (request.status === "bound") return `Bound this conversation to Codex CLI session ${formatCodexDisplayText(parsed.threadId)} on ${formatCodexDisplayText(nodeId)}.`;
1096
- if (request.status === "pending") return request.reply.text ?? "Codex CLI session binding is pending approval.";
1097
- return formatCodexDisplayText(request.message);
998
+ map.set(key, now);
1098
999
  }
1099
- async function stopConversationTurn(deps, ctx, pluginConfig) {
1100
- const target = await resolveControlTarget(ctx);
1101
- if (!target) return "Cannot stop Codex because this command did not include a stable binding identity.";
1102
- return (await deps.stopCodexConversationTurn({
1103
- identity: target.identity,
1104
- bindingStore: deps.bindingStore,
1105
- pluginConfig,
1106
- agentDir: target.agentDir,
1107
- config: ctx.config
1108
- })).message;
1000
+ function pruneCodexDiagnosticsCooldowns(now) {
1001
+ pruneCodexDiagnosticsCooldownMap(lastCodexDiagnosticsUploadByThread, now);
1002
+ pruneCodexDiagnosticsCooldownMap(lastCodexDiagnosticsUploadByScope, now);
1109
1003
  }
1110
- async function steerConversationTurn(deps, ctx, pluginConfig, message) {
1111
- const target = await resolveControlTarget(ctx);
1112
- if (!target) return "Cannot steer Codex because this command did not include a stable binding identity.";
1113
- return (await deps.steerCodexConversationTurn({
1114
- identity: target.identity,
1115
- bindingStore: deps.bindingStore,
1116
- message,
1117
- pluginConfig,
1118
- agentDir: target.agentDir,
1119
- config: ctx.config
1120
- })).message;
1004
+ function pruneCodexDiagnosticsCooldownMap(map, now) {
1005
+ for (const [key, lastSentAt] of map) if (now - lastSentAt >= CODEX_DIAGNOSTICS_COOLDOWN_MS) map.delete(key);
1006
+ }
1007
+ function formatCodexErrorForDisplay(error) {
1008
+ return escapeCodexChatText(truncateUtf16Safe(formatCodexTextForDisplay(error), CODEX_DIAGNOSTICS_ERROR_MAX_CHARS)) || "unknown error";
1009
+ }
1010
+ function formatCodexResumeCommandForDisplay(threadId) {
1011
+ const safeThreadId = formatCodexTextForDisplay(threadId);
1012
+ if (!CODEX_RESUME_SAFE_THREAD_ID_PATTERN.test(safeThreadId)) return "run codex resume and paste the thread id shown above";
1013
+ return `\`codex resume ${safeThreadId}\``;
1121
1014
  }
1122
- async function setConversationModel(deps, ctx, pluginConfig, args) {
1123
- if (args.length > 1) return "Usage: /codex model <model>";
1124
- const [model = ""] = args;
1125
- const normalized = model.trim();
1126
- if (normalized && isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
1127
- const target = await resolveControlTarget(ctx);
1128
- if (!target) return "Cannot set Codex model because this command did not include a stable binding identity.";
1129
- if (!normalized) {
1130
- const binding = await deps.bindingStore.read(target.identity);
1131
- return binding?.model ? `Codex model: ${formatCodexDisplayText(binding.model)}` : "Usage: /codex model <model>";
1132
- }
1133
- return await deps.setCodexConversationModel({
1134
- identity: target.identity,
1135
- bindingStore: deps.bindingStore,
1136
- pluginConfig,
1137
- model: normalized,
1138
- agentDir: target.agentDir,
1139
- config: ctx.config
1140
- });
1015
+ function isUnsafeDisplayCodePoint(codePoint) {
1016
+ return codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint === 173 || codePoint === 1564 || codePoint === 6158 || codePoint >= 8203 && codePoint <= 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8288 && codePoint <= 8303 || codePoint === 65279 || codePoint >= 65529 && codePoint <= 65531 || codePoint >= 917504 && codePoint <= 917631;
1141
1017
  }
1142
- async function setConversationFastMode(deps, ctx, args) {
1143
- if (args.length > 1) return "Usage: /codex fast [on|off|status]";
1144
- const target = await resolveControlTarget(ctx);
1145
- if (!target) return "Cannot set Codex fast mode because this command did not include a stable binding identity.";
1146
- const value = args[0];
1147
- const parsed = parseCodexFastModeArg(value);
1148
- if (value && parsed == null && value.trim().toLowerCase() !== "status") return "Usage: /codex fast [on|off|status]";
1149
- return await deps.setCodexConversationFastMode({
1150
- identity: target.identity,
1151
- bindingStore: deps.bindingStore,
1152
- enabled: parsed
1153
- });
1018
+ function normalizeCodexDiagnosticsScopeField(value) {
1019
+ const normalized = normalizeOptionalString(value);
1020
+ if (!normalized) return;
1021
+ if (normalized.length <= CODEX_DIAGNOSTICS_SCOPE_FIELD_MAX_CHARS) return normalized;
1022
+ return `sha256:${crypto.createHash("sha256").update(normalized).digest("hex")}`;
1154
1023
  }
1155
- async function setConversationPermissions(deps, ctx, args) {
1156
- if (args.length > 1) return "Usage: /codex permissions [default|yolo|status]";
1157
- const target = await resolveControlTarget(ctx);
1158
- if (!target) return "Cannot set Codex permissions because this command did not include a stable binding identity.";
1159
- const value = args[0];
1160
- const parsed = parseCodexPermissionsModeArg(value);
1161
- if (value && !parsed && value.trim().toLowerCase() !== "status") return "Usage: /codex permissions [default|yolo|status]";
1162
- return await deps.setCodexConversationPermissions({
1163
- identity: target.identity,
1164
- bindingStore: deps.bindingStore,
1165
- mode: parsed
1166
- });
1024
+ //#endregion
1025
+ //#region extensions/codex/src/command-handler-deps.ts
1026
+ const defaultCodexCommandDeps = {
1027
+ codexControlRequest,
1028
+ listCodexAppServerModels: listAllCodexAppServerModels,
1029
+ readCodexStatusProbes,
1030
+ requestOptions,
1031
+ safeCodexControlRequest,
1032
+ readCodexComputerUseStatus,
1033
+ installCodexComputerUse,
1034
+ resolveCodexDefaultWorkspaceDir,
1035
+ readCodexConversationActiveTurn,
1036
+ setCodexConversationFastMode,
1037
+ setCodexConversationModel,
1038
+ setCodexConversationPermissions,
1039
+ steerCodexConversationTurn,
1040
+ stopCodexConversationTurn,
1041
+ listCodexCliSessionsOnNode: async () => {
1042
+ throw new Error("Codex CLI node sessions require Gateway node runtime.");
1043
+ },
1044
+ resolveCodexCliSessionForBindingOnNode: async () => {
1045
+ throw new Error("Codex CLI node sessions require Gateway node runtime.");
1046
+ }
1047
+ };
1048
+ function resolveCodexCommandDeps(overrides) {
1049
+ return {
1050
+ ...defaultCodexCommandDeps,
1051
+ ...overrides
1052
+ };
1167
1053
  }
1054
+ //#endregion
1055
+ //#region extensions/codex/src/command-handler-scope.ts
1168
1056
  async function resolveControlTarget(ctx) {
1169
1057
  const data = readCodexConversationBindingData(await ctx.getCurrentConversationBinding());
1170
1058
  const scope = resolveCodexConversationControlScope(ctx);
@@ -1225,6 +1113,8 @@ function resolveCodexConversationControlScope(ctx) {
1225
1113
  agentDir: resolveAgentDir(ctx.config, sessionAgentId)
1226
1114
  };
1227
1115
  }
1116
+ //#endregion
1117
+ //#region extensions/codex/src/command-diagnostics.ts
1228
1118
  async function handleCodexDiagnosticsFeedback(deps, ctx, pluginConfig, args, commandPrefix) {
1229
1119
  if (ctx.senderIsOwner !== true) return { text: "Only an owner can send Codex diagnostics." };
1230
1120
  const parsed = parseDiagnosticsArgs(args);
@@ -1447,317 +1337,464 @@ async function resolveCodexDiagnosticsTargets(deps, ctx) {
1447
1337
  targets.push(resolveCodexDiagnosticsTarget(candidate, binding, ctx.config));
1448
1338
  }
1449
1339
  return targets;
1450
- }
1451
- async function resolvePendingCodexDiagnosticsTargets(deps, targets, config) {
1452
- const resolved = [];
1453
- for (const target of targets) {
1454
- const binding = await deps.bindingStore.read(target.identity);
1455
- if (!binding?.threadId) continue;
1456
- resolved.push(resolveCodexDiagnosticsTarget(target, binding, config));
1457
- }
1458
- return resolved;
1459
- }
1460
- function resolveCodexDiagnosticsTarget(target, binding, config) {
1461
- const candidate = {
1462
- identity: target.identity,
1463
- agentDir: target.agentDir,
1464
- sessionKey: target.sessionKey,
1465
- sessionId: target.sessionId,
1466
- channel: target.channel,
1467
- channelId: target.channelId,
1468
- accountId: target.accountId,
1469
- messageThreadId: target.messageThreadId,
1470
- threadParentId: target.threadParentId
1471
- };
1472
- if (binding.connectionScope === "supervision") return {
1473
- ...candidate,
1474
- threadId: binding.threadId,
1475
- connectionScope: binding.connectionScope,
1476
- appServerRuntimeFingerprint: binding.appServerRuntimeFingerprint,
1477
- pendingSupervisionBranch: binding.pendingSupervisionBranch
1478
- };
1479
- const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
1480
- authProfileId: binding.authProfileId,
1481
- agentDir: target.agentDir,
1482
- config
1483
- });
1484
- return {
1485
- ...candidate,
1486
- threadId: binding.threadId,
1487
- authProfileId
1488
- };
1489
- }
1490
- function codexDiagnosticsTargetsMatch(expected, actual) {
1491
- const fingerprint = (target) => JSON.stringify([
1492
- bindingStoreKey(target.identity),
1493
- target.threadId,
1494
- target.connectionScope ?? null,
1495
- target.pendingSupervisionBranch?.connectionFingerprint ?? target.appServerRuntimeFingerprint ?? null,
1496
- target.authProfileId ?? null
1497
- ]);
1498
- const expectedTargets = expected.map(fingerprint).toSorted();
1499
- const actualTargets = actual.map(fingerprint).toSorted();
1500
- return expectedTargets.length === actualTargets.length && expectedTargets.every((target, index) => target === actualTargets[index]);
1501
- }
1502
- function formatCodexDiagnosticsUploadResult(sent, failed) {
1503
- const lines = [];
1504
- if (sent.length > 0) {
1505
- lines.push("Codex diagnostics sent to OpenAI servers:");
1506
- lines.push(...formatCodexDiagnosticsTargetLines(sent));
1507
- lines.push("Included Codex logs and spawned Codex subthreads when available.");
1508
- }
1509
- if (failed.length > 0) {
1510
- if (lines.length > 0) lines.push("");
1511
- lines.push("Could not send Codex diagnostics:");
1512
- lines.push(...failed.map(({ target, error }) => `${formatCodexDiagnosticsTargetLine(target)}: ${formatCodexErrorForDisplay(error)}`));
1513
- lines.push("Inspect locally:");
1514
- lines.push(...failed.map(({ target }) => `- ${formatCodexResumeCommandForDisplay(target.threadId)}`));
1515
- }
1516
- return lines.join("\n");
1517
- }
1518
- function formatCodexDiagnosticsTargetLines(targets) {
1519
- return targets.flatMap((target, index) => {
1520
- const lines = formatCodexDiagnosticsTargetBlock(target, index);
1521
- return index < targets.length - 1 ? [...lines, ""] : lines;
1522
- });
1523
- }
1524
- function formatCodexDiagnosticsTargetBlock(target, index) {
1525
- const lines = [`Session ${index + 1}`];
1526
- if (target.channel) lines.push(`Channel: ${formatCodexValueForDisplay(target.channel)}`);
1527
- if (target.sessionKey) lines.push(`OpenClaw session key: ${formatCodexCopyableValueForDisplay(target.sessionKey)}`);
1528
- if (target.sessionId) lines.push(`OpenClaw session id: ${formatCodexCopyableValueForDisplay(target.sessionId)}`);
1529
- lines.push(`Codex thread id: ${formatCodexCopyableValueForDisplay(target.threadId)}`);
1530
- lines.push(`Inspect locally: ${formatCodexResumeCommandForDisplay(target.threadId)}`);
1531
- return lines;
1532
- }
1533
- function formatCodexDiagnosticsTargetLine(target) {
1534
- const parts = [];
1535
- if (target.channel) parts.push(`channel ${formatCodexValueForDisplay(target.channel)}`);
1536
- const sessionLabel = target.sessionId || target.sessionKey;
1537
- if (sessionLabel) parts.push(`OpenClaw session ${formatCodexValueForDisplay(sessionLabel)}`);
1538
- parts.push(`Codex thread ${formatCodexThreadIdForDisplay(target.threadId)}`);
1539
- return `- ${parts.join(", ")}`;
1540
- }
1541
- function normalizeDiagnosticsReason(note) {
1542
- const normalized = normalizeOptionalString(note);
1543
- return normalized ? truncateUtf16Safe(normalized, CODEX_DIAGNOSTICS_REASON_MAX_CHARS) : void 0;
1544
- }
1545
- function parseDiagnosticsArgs(args) {
1546
- const [action, token, ...extra] = splitArgs(args);
1547
- const normalizedAction = action?.toLowerCase();
1548
- if ((normalizedAction === "confirm" || normalizedAction === "--confirm") && token && extra.length === 0) return {
1549
- action: "confirm",
1550
- token
1551
- };
1552
- if ((normalizedAction === "cancel" || normalizedAction === "--cancel") && token && extra.length === 0) return {
1553
- action: "cancel",
1554
- token
1555
- };
1556
- if (normalizedAction === "confirm" || normalizedAction === "--confirm" || normalizedAction === "cancel" || normalizedAction === "--cancel") return { action: "usage" };
1557
- return {
1558
- action: "request",
1559
- note: args
1560
- };
1561
- }
1562
- function formatDiagnosticsUsage(commandPrefix) {
1563
- return [
1564
- `Usage: ${commandPrefix} [note]`,
1565
- `Usage: ${commandPrefix} confirm <token>`,
1566
- `Usage: ${commandPrefix} cancel <token>`
1567
- ].join("\n");
1568
- }
1569
- function createCodexDiagnosticsConfirmation(params) {
1570
- prunePendingCodexDiagnosticsConfirmations(params.now);
1571
- if (!pendingCodexDiagnosticsConfirmationTokensByScope.has(params.scopeKey) && pendingCodexDiagnosticsConfirmationTokensByScope.size >= CODEX_DIAGNOSTICS_CONFIRMATION_MAX_SCOPES) {
1572
- const oldestScopeKey = pendingCodexDiagnosticsConfirmationTokensByScope.keys().next().value;
1573
- if (typeof oldestScopeKey === "string") deletePendingCodexDiagnosticsConfirmationScope(oldestScopeKey);
1574
- }
1575
- const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(params.scopeKey) ?? [];
1576
- while (scopeTokens.length >= CODEX_DIAGNOSTICS_CONFIRMATION_MAX_REQUESTS_PER_SCOPE) {
1577
- const oldestToken = scopeTokens.shift();
1578
- if (!oldestToken) break;
1579
- pendingCodexDiagnosticsConfirmations.delete(oldestToken);
1580
- }
1581
- const token = crypto.randomBytes(6).toString("hex");
1582
- scopeTokens.push(token);
1583
- pendingCodexDiagnosticsConfirmationTokensByScope.set(params.scopeKey, scopeTokens);
1584
- pendingCodexDiagnosticsConfirmations.set(token, {
1585
- token,
1586
- targets: params.targets,
1587
- note: params.note,
1588
- senderId: params.senderId,
1589
- channel: params.channel,
1590
- accountId: params.accountId,
1591
- channelId: params.channelId,
1592
- messageThreadId: params.messageThreadId,
1593
- threadParentId: params.threadParentId,
1594
- sessionKey: params.sessionKey,
1595
- scopeKey: params.scopeKey,
1596
- ...params.privateRouted === void 0 ? {} : { privateRouted: params.privateRouted },
1597
- createdAt: params.now
1598
- });
1599
- return token;
1600
- }
1601
- function readCodexDiagnosticsConfirmationScope(ctx) {
1602
- return {
1603
- accountId: normalizeCodexDiagnosticsScopeField(ctx.accountId),
1604
- channelId: normalizeCodexDiagnosticsScopeField(ctx.channelId),
1605
- messageThreadId: typeof ctx.messageThreadId === "string" || typeof ctx.messageThreadId === "number" ? normalizeCodexDiagnosticsScopeField(String(ctx.messageThreadId)) : void 0,
1606
- threadParentId: normalizeCodexDiagnosticsScopeField(ctx.threadParentId),
1607
- sessionKey: normalizeCodexDiagnosticsScopeField(ctx.sessionKey)
1608
- };
1609
- }
1610
- function readCodexDiagnosticsScopeMismatch(pending, ctx) {
1611
- const current = readCodexDiagnosticsConfirmationScope(ctx);
1612
- if (pending.accountId !== current.accountId) return {
1613
- confirmMessage: "This Codex diagnostics confirmation belongs to a different account.",
1614
- cancelMessage: "This Codex diagnostics confirmation belongs to a different account."
1615
- };
1616
- if (pending.privateRouted) return;
1617
- if (pending.channelId !== current.channelId) return {
1618
- confirmMessage: "This Codex diagnostics confirmation belongs to a different channel instance.",
1619
- cancelMessage: "This Codex diagnostics confirmation belongs to a different channel instance."
1620
- };
1621
- if (pending.messageThreadId !== current.messageThreadId) return {
1622
- confirmMessage: "This Codex diagnostics confirmation belongs to a different thread.",
1623
- cancelMessage: "This Codex diagnostics confirmation belongs to a different thread."
1340
+ }
1341
+ async function resolvePendingCodexDiagnosticsTargets(deps, targets, config) {
1342
+ const resolved = [];
1343
+ for (const target of targets) {
1344
+ const binding = await deps.bindingStore.read(target.identity);
1345
+ if (!binding?.threadId) continue;
1346
+ resolved.push(resolveCodexDiagnosticsTarget(target, binding, config));
1347
+ }
1348
+ return resolved;
1349
+ }
1350
+ function resolveCodexDiagnosticsTarget(target, binding, config) {
1351
+ const candidate = {
1352
+ identity: target.identity,
1353
+ agentDir: target.agentDir,
1354
+ sessionKey: target.sessionKey,
1355
+ sessionId: target.sessionId,
1356
+ channel: target.channel,
1357
+ channelId: target.channelId,
1358
+ accountId: target.accountId,
1359
+ messageThreadId: target.messageThreadId,
1360
+ threadParentId: target.threadParentId
1624
1361
  };
1625
- if (pending.threadParentId !== current.threadParentId) return {
1626
- confirmMessage: "This Codex diagnostics confirmation belongs to a different parent thread.",
1627
- cancelMessage: "This Codex diagnostics confirmation belongs to a different parent thread."
1362
+ if (binding.connectionScope === "supervision") return {
1363
+ ...candidate,
1364
+ threadId: binding.threadId,
1365
+ connectionScope: binding.connectionScope,
1366
+ appServerRuntimeFingerprint: binding.appServerRuntimeFingerprint,
1367
+ pendingSupervisionBranch: binding.pendingSupervisionBranch
1628
1368
  };
1629
- if (pending.sessionKey !== current.sessionKey) return {
1630
- confirmMessage: "This Codex diagnostics confirmation belongs to a different session.",
1631
- cancelMessage: "This Codex diagnostics confirmation belongs to a different session."
1369
+ const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
1370
+ authProfileId: binding.authProfileId,
1371
+ agentDir: target.agentDir,
1372
+ config
1373
+ });
1374
+ return {
1375
+ ...candidate,
1376
+ threadId: binding.threadId,
1377
+ authProfileId
1632
1378
  };
1633
1379
  }
1634
- function readPendingCodexDiagnosticsConfirmation(token, now) {
1635
- prunePendingCodexDiagnosticsConfirmations(now);
1636
- return pendingCodexDiagnosticsConfirmations.get(token);
1637
- }
1638
- function prunePendingCodexDiagnosticsConfirmations(now) {
1639
- for (const [token, pending] of pendingCodexDiagnosticsConfirmations) if (now - pending.createdAt >= CODEX_DIAGNOSTICS_CONFIRMATION_TTL_MS) deletePendingCodexDiagnosticsConfirmation(token);
1640
- }
1641
- function deletePendingCodexDiagnosticsConfirmation(token) {
1642
- const pending = pendingCodexDiagnosticsConfirmations.get(token);
1643
- pendingCodexDiagnosticsConfirmations.delete(token);
1644
- if (!pending) return;
1645
- const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(pending.scopeKey);
1646
- if (!scopeTokens) return;
1647
- const tokenIndex = scopeTokens.indexOf(token);
1648
- if (tokenIndex >= 0) scopeTokens.splice(tokenIndex, 1);
1649
- if (scopeTokens.length === 0) pendingCodexDiagnosticsConfirmationTokensByScope.delete(pending.scopeKey);
1380
+ //#endregion
1381
+ //#region extensions/codex/src/command-handler-bindings.ts
1382
+ function isCurrentSessionModelSelectionLocked(ctx) {
1383
+ const sessionKey = ctx.sessionKey?.trim();
1384
+ if (!sessionKey) return false;
1385
+ const { agentId } = resolveCodexConversationControlScope(ctx);
1386
+ return isModelSelectionLocked(getSessionEntry({
1387
+ storePath: resolveStorePath(ctx.config.session?.store, { agentId }),
1388
+ sessionKey,
1389
+ hydrateSkillPromptRefs: false,
1390
+ readConsistency: "latest"
1391
+ }));
1650
1392
  }
1651
- function deletePendingCodexDiagnosticsConfirmationScope(scopeKey) {
1652
- const scopeTokens = pendingCodexDiagnosticsConfirmationTokensByScope.get(scopeKey) ?? [];
1653
- for (const token of scopeTokens) pendingCodexDiagnosticsConfirmations.delete(token);
1654
- pendingCodexDiagnosticsConfirmationTokensByScope.delete(scopeKey);
1393
+ async function bindConversation(deps, ctx, pluginConfig, args) {
1394
+ const parsed = parseBindArgs(args);
1395
+ if (parsed.help) return { text: "Usage: /codex bind [thread-id] [--cwd <path>] [--model <model>] [--provider <provider>]" };
1396
+ if (isCurrentSessionModelSelectionLocked(ctx)) return { text: MODEL_SELECTION_LOCKED_MESSAGE };
1397
+ const scope = resolveCodexConversationControlScope(ctx);
1398
+ const workspaceDir = parsed.cwd ?? deps.resolveCodexDefaultWorkspaceDir(pluginConfig);
1399
+ const currentConversation = await ctx.getCurrentConversationBinding();
1400
+ const currentConversationData = readCodexConversationBindingData(currentConversation);
1401
+ const bindingId = currentConversationData?.kind === "codex-app-server-session" ? currentConversationData.bindingId : currentConversation ? `conversation-${currentConversation.bindingId}` : void 0;
1402
+ const sessionOwner = ctx.sessionId ? sessionBindingIdentity({
1403
+ sessionId: ctx.sessionId,
1404
+ sessionKey: ctx.sessionKey,
1405
+ agentId: scope.agentId,
1406
+ config: ctx.config
1407
+ }) : void 0;
1408
+ const currentOwner = currentConversationData?.kind === "codex-app-server-session" ? conversationBindingIdentity(currentConversationData.bindingId) : sessionOwner;
1409
+ const existingBinding = currentOwner ? await deps.bindingStore.read(currentOwner) : void 0;
1410
+ assertCodexBindingMayBeReplaced(existingBinding, "binding this conversation to another thread");
1411
+ const sessionSource = sessionOwner && existingBinding ? {
1412
+ agentId: sessionOwner.agentId,
1413
+ sessionId: sessionOwner.sessionId,
1414
+ threadId: existingBinding.threadId,
1415
+ ...sessionOwner.sessionKey ? { sessionKey: sessionOwner.sessionKey } : {}
1416
+ } : void 0;
1417
+ const authProfileId = existingBinding?.authProfileId;
1418
+ const data = createCodexConversationBindingData({
1419
+ bindingId,
1420
+ workspaceDir,
1421
+ agentId: scope.agentId,
1422
+ agentDir: scope.agentDir,
1423
+ source: currentConversationData?.kind === "codex-app-server-session" ? currentConversationData.source : sessionSource,
1424
+ start: {
1425
+ id: crypto.randomUUID(),
1426
+ threadId: parsed.threadId,
1427
+ model: parsed.model,
1428
+ modelProvider: parsed.provider,
1429
+ authProfileId
1430
+ }
1431
+ });
1432
+ const threadLabel = parsed.threadId ?? "a new thread";
1433
+ const request = await ctx.requestConversationBinding({
1434
+ summary: `Codex app-server thread ${formatCodexDisplayText(threadLabel)} in ${formatCodexDisplayText(workspaceDir)}`,
1435
+ detachHint: "/codex detach",
1436
+ data
1437
+ });
1438
+ if (request.status === "pending") return request.reply;
1439
+ if (request.status === "error") return { text: formatCodexDisplayText(request.message) };
1440
+ return { text: `Bound this conversation to ${formatCodexDisplayText(threadLabel)} in ${formatCodexDisplayText(workspaceDir)}. The next message will initialize it.` };
1655
1441
  }
1656
- function buildDiagnosticsTags(ctx) {
1657
- const tags = { source: CODEX_DIAGNOSTICS_SOURCE };
1658
- addTag(tags, "channel", ctx.channel);
1659
- return tags;
1442
+ async function detachConversation(deps, ctx) {
1443
+ if (isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
1444
+ const data = readCodexConversationBindingData(await ctx.getCurrentConversationBinding());
1445
+ if (data?.kind === "codex-app-server-session") assertCodexBindingMayBeReplaced(await deps.bindingStore.read(conversationBindingIdentity(data.bindingId)), "detaching its conversation binding");
1446
+ const detached = await ctx.detachConversationBinding();
1447
+ if (data?.kind === "codex-app-server-session") await deps.bindingStore.mutate(conversationBindingIdentity(data.bindingId), { kind: "clear" });
1448
+ return detached.removed ? "Detached this conversation from Codex." : "No Codex conversation binding was attached.";
1660
1449
  }
1661
- function addTag(tags, key, value) {
1662
- if (typeof value === "string" && value.trim()) tags[key] = value.trim();
1450
+ async function describeConversationBinding(deps, ctx) {
1451
+ const current = await ctx.getCurrentConversationBinding();
1452
+ const data = readCodexConversationBindingData(current);
1453
+ if (!current || !data) return "No Codex conversation binding is attached.";
1454
+ if (data.kind === "codex-cli-node-session") return [
1455
+ "Codex conversation binding:",
1456
+ "- Mode: Codex CLI node session",
1457
+ `- Node: ${formatCodexDisplayText(data.nodeId)}`,
1458
+ `- Session: ${formatCodexDisplayText(data.sessionId)}`,
1459
+ `- Workspace: ${formatCodexDisplayText(data.cwd ?? "unknown")}`,
1460
+ "- Active run: not tracked"
1461
+ ].join("\n");
1462
+ const identity = conversationBindingIdentity(data.bindingId);
1463
+ const threadBinding = await deps.bindingStore.read(identity);
1464
+ const active = deps.readCodexConversationActiveTurn(identity);
1465
+ return [
1466
+ "Codex conversation binding:",
1467
+ `- Thread: ${formatCodexDisplayText(threadBinding?.threadId ?? "unknown")}`,
1468
+ `- Workspace: ${formatCodexDisplayText(data.workspaceDir)}`,
1469
+ `- Model: ${formatCodexDisplayText(threadBinding?.model ?? "default")}`,
1470
+ `- Fast: ${isCodexFastServiceTier(threadBinding?.serviceTier) ? "on" : "off"}`,
1471
+ `- Permissions: ${threadBinding ? formatPermissionsMode(threadBinding) : "default"}`,
1472
+ `- Active run: ${formatCodexDisplayText(active ? active.turnId : "none")}`,
1473
+ `- Binding: ${formatCodexDisplayText(data.bindingId)}`
1474
+ ].join("\n");
1663
1475
  }
1664
- function formatCodexThreadIdForDisplay(threadId) {
1665
- return escapeCodexChatText(formatCodexTextForDisplay(threadId));
1476
+ async function buildThreads(deps, ctx, pluginConfig, filter) {
1477
+ const scope = await resolveCommandAppServerScope(deps, ctx, pluginConfig);
1478
+ return formatThreads(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreads, {
1479
+ limit: 10,
1480
+ ...filter.trim() ? { searchTerm: filter.trim() } : {}
1481
+ }, {
1482
+ config: ctx.config,
1483
+ ...scope
1484
+ }));
1666
1485
  }
1667
- function formatCodexValueForDisplay(value) {
1668
- return escapeCodexChatText(formatCodexTextForDisplay(value));
1486
+ async function buildCodexCliSessions(deps, args) {
1487
+ const parsed = parseCodexCliSessionsArgs(args);
1488
+ if (parsed.help || !parsed.host) return "Usage: /codex sessions --host <node> [filter] [--limit <n>]";
1489
+ return formatCodexCliSessions(await deps.listCodexCliSessionsOnNode({
1490
+ requestedNode: parsed.host,
1491
+ filter: parsed.filter,
1492
+ limit: parsed.limit
1493
+ }));
1669
1494
  }
1670
- function formatCodexCopyableValueForDisplay(value) {
1671
- const safe = formatCodexTextForDisplay(value);
1672
- if (CODEX_RESUME_SAFE_THREAD_ID_PATTERN.test(safe)) return `\`${safe}\``;
1673
- return escapeCodexChatText(safe);
1495
+ async function resumeThread(deps, ctx, pluginConfig, args) {
1496
+ const parsed = parseResumeArgs(args);
1497
+ const normalizedThreadId = parsed.threadId?.trim();
1498
+ if (parsed.help) return args.includes("--help") || args.includes("-h") || parsed.host ? "Usage: /codex resume <thread-id>\nUsage: /codex resume <session-id> --host <node> --bind here" : "Usage: /codex resume <thread-id>";
1499
+ if (parsed.host) return await bindCodexCliNodeSession(deps, ctx, parsed);
1500
+ if (!normalizedThreadId || args.length !== 1) return "Usage: /codex resume <thread-id>";
1501
+ if (isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
1502
+ if (!ctx.sessionId) return "Cannot attach a Codex thread because this command did not include an OpenClaw session id.";
1503
+ const scope = resolveCodexConversationControlScope(ctx);
1504
+ const identity = sessionBindingIdentity({
1505
+ sessionId: ctx.sessionId,
1506
+ sessionKey: ctx.sessionKey,
1507
+ agentId: scope.agentId,
1508
+ config: ctx.config
1509
+ });
1510
+ return await deps.bindingStore.withLease(identity, async () => {
1511
+ if (!await reclaimCurrentCodexSessionGeneration({
1512
+ bindingStore: deps.bindingStore,
1513
+ identity,
1514
+ config: ctx.config
1515
+ })) throw createCodexSessionGenerationSupersededError(identity.sessionId);
1516
+ const currentBinding = await deps.bindingStore.read(identity);
1517
+ assertCodexBindingMayBeReplaced(currentBinding, "attaching a different resumed thread");
1518
+ const authProfileId = resolveCodexAppServerAuthProfileIdForAgent({
1519
+ authProfileId: currentBinding?.authProfileId,
1520
+ agentDir: scope.agentDir,
1521
+ config: ctx.config
1522
+ });
1523
+ const response = assertCodexThreadResumeResponse(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.resumeThread, {
1524
+ threadId: normalizedThreadId,
1525
+ excludeTurns: true
1526
+ }, {
1527
+ config: ctx.config,
1528
+ agentDir: scope.agentDir,
1529
+ authProfileId,
1530
+ sessionKey: ctx.sessionKey,
1531
+ sessionId: ctx.sessionId
1532
+ }));
1533
+ const effectiveThreadId = response.thread.id;
1534
+ if (effectiveThreadId !== normalizedThreadId) throw new Error(`Codex thread/resume returned ${effectiveThreadId} for ${normalizedThreadId}`);
1535
+ const resumedCwd = response.thread.cwd;
1536
+ if (typeof resumedCwd !== "string") throw new Error(`Codex thread/resume returned no cwd for ${normalizedThreadId}`);
1537
+ const modelProvider = normalizeCodexAppServerBindingModelProvider({
1538
+ authProfileId,
1539
+ modelProvider: response.modelProvider ?? void 0,
1540
+ agentDir: scope.agentDir,
1541
+ config: ctx.config
1542
+ });
1543
+ assertCodexBindingMayBeReplaced(await deps.bindingStore.read(identity), "committing a different resumed thread");
1544
+ if (!await deps.bindingStore.mutate(identity, {
1545
+ kind: "set",
1546
+ binding: {
1547
+ threadId: effectiveThreadId,
1548
+ cwd: resumedCwd,
1549
+ authProfileId,
1550
+ model: response.model,
1551
+ modelProvider,
1552
+ historyCoveredThrough: (/* @__PURE__ */ new Date()).toISOString()
1553
+ }
1554
+ })) throw new Error("Codex thread binding changed while attaching the resumed thread.");
1555
+ return `Attached this OpenClaw session to Codex thread ${formatCodexDisplayText(effectiveThreadId)}.`;
1556
+ });
1674
1557
  }
1675
- function formatCodexTextForDisplay(value) {
1676
- let safe = "";
1677
- for (const character of value) {
1678
- const codePoint = character.codePointAt(0);
1679
- safe += codePoint != null && isUnsafeDisplayCodePoint(codePoint) ? "?" : character;
1558
+ async function bindCodexCliNodeSession(deps, ctx, parsed) {
1559
+ if (!parsed.threadId || !parsed.host || parsed.bindHere !== true) return "Usage: /codex resume <session-id> --host <node> --bind here";
1560
+ if (isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
1561
+ if (ctx.sessionId) {
1562
+ const scope = resolveCodexConversationControlScope(ctx);
1563
+ assertCodexBindingMayBeReplaced(await deps.bindingStore.read(sessionBindingIdentity({
1564
+ sessionId: ctx.sessionId,
1565
+ sessionKey: ctx.sessionKey,
1566
+ agentId: scope.agentId,
1567
+ config: ctx.config
1568
+ })), "binding a Codex CLI node session");
1680
1569
  }
1681
- safe = safe.trim();
1682
- return safe || "<unknown>";
1570
+ const resolved = await deps.resolveCodexCliSessionForBindingOnNode({
1571
+ requestedNode: parsed.host,
1572
+ sessionId: parsed.threadId
1573
+ });
1574
+ if (!resolved.session) return `No Codex CLI session ${formatCodexDisplayText(parsed.threadId)} was found on ${formatCodexDisplayText(parsed.host)}.`;
1575
+ const nodeId = resolved.node.nodeId;
1576
+ if (!nodeId) return "Cannot bind Codex CLI session because the selected node did not include a node id.";
1577
+ const scope = resolveCodexConversationControlScope(ctx);
1578
+ const data = createCodexCliNodeConversationBindingData({
1579
+ nodeId,
1580
+ sessionId: parsed.threadId,
1581
+ agentId: scope.agentId,
1582
+ cwd: resolved.session?.cwd
1583
+ });
1584
+ const summary = `Codex CLI session ${formatCodexDisplayText(parsed.threadId)} on ${formatCodexDisplayText(nodeId)}`;
1585
+ const request = await ctx.requestConversationBinding({
1586
+ summary,
1587
+ detachHint: "/codex detach",
1588
+ data
1589
+ });
1590
+ if (request.status === "bound") return `Bound this conversation to Codex CLI session ${formatCodexDisplayText(parsed.threadId)} on ${formatCodexDisplayText(nodeId)}.`;
1591
+ if (request.status === "pending") return request.reply.text ?? "Codex CLI session binding is pending approval.";
1592
+ return formatCodexDisplayText(request.message);
1683
1593
  }
1684
- function escapeCodexChatText(value) {
1685
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("@", "@").replaceAll("`", "`").replaceAll("[", "[").replaceAll("]", "]").replaceAll("(", "(").replaceAll(")", ")").replaceAll("*", "∗").replaceAll("_", "_").replaceAll("~", "~").replaceAll("|", "|");
1594
+ //#endregion
1595
+ //#region extensions/codex/src/command-handler-actions.ts
1596
+ const CODEX_NATIVE_EXECUTION_SUBCOMMANDS = /* @__PURE__ */ new Set([
1597
+ "bind",
1598
+ "resume",
1599
+ "steer",
1600
+ "model",
1601
+ "fast",
1602
+ "permissions",
1603
+ "compact",
1604
+ "review",
1605
+ "goal"
1606
+ ]);
1607
+ const CODEX_NATIVE_CONTROL_SUBCOMMANDS = /* @__PURE__ */ new Set([
1608
+ ...CODEX_NATIVE_EXECUTION_SUBCOMMANDS,
1609
+ "detach",
1610
+ "unbind",
1611
+ "stop"
1612
+ ]);
1613
+ function resolveCodexNativeCommandSandboxBlock(ctx, subcommand, args) {
1614
+ if (isReadOnlyCodexGoalCommand(subcommand, args)) return;
1615
+ if (!CODEX_NATIVE_EXECUTION_SUBCOMMANDS.has(subcommand)) return;
1616
+ if (returnsBeforeNativeCodexExecution(subcommand, args)) return;
1617
+ if (isCodexCliNodeResumeBind(subcommand, args)) return resolveCodexNativeSandboxBlock({
1618
+ config: ctx.config,
1619
+ sessionKey: ctx.sessionKey,
1620
+ sessionId: ctx.sessionId,
1621
+ surface: `/${["codex", subcommand].join(" ")}`
1622
+ });
1623
+ return resolveCodexNativeExecutionBlock({
1624
+ config: ctx.config,
1625
+ agentId: ctx.agentId,
1626
+ sessionKey: ctx.sessionKey,
1627
+ sessionId: ctx.sessionId,
1628
+ surface: `/${["codex", subcommand].join(" ")}`
1629
+ });
1686
1630
  }
1687
- function readCodexDiagnosticsCooldownMs(threadId, now) {
1688
- const lastSentAt = lastCodexDiagnosticsUploadByThread.get(threadId);
1689
- if (!lastSentAt) return 0;
1690
- const remainingMs = Math.max(0, CODEX_DIAGNOSTICS_COOLDOWN_MS - (now - lastSentAt));
1691
- if (remainingMs === 0) lastCodexDiagnosticsUploadByThread.delete(threadId);
1692
- return remainingMs;
1631
+ function isReadOnlyCodexGoalCommand(subcommand, args) {
1632
+ if (subcommand !== "goal" || args.length > 1) return false;
1633
+ const action = (args[0] ?? "status").toLowerCase();
1634
+ return action === "status" || action === "get";
1693
1635
  }
1694
- function readCodexDiagnosticsTargetsCooldownMessage(targets, ctx, now, options = {}) {
1695
- for (const target of targets) {
1696
- const cooldownMs = readCodexDiagnosticsCooldownMs(target.threadId, now);
1697
- if (cooldownMs > 0) {
1698
- if (options.includeThreadId === false) return `Codex diagnostics were already sent for one of these Codex threads recently. Try again in ${Math.ceil(cooldownMs / 1e3)}s.`;
1699
- return `Codex diagnostics were already sent for thread ${formatCodexThreadIdForDisplay(target.threadId)} recently. Try again in ${Math.ceil(cooldownMs / 1e3)}s.`;
1700
- }
1636
+ function returnsBeforeNativeCodexExecution(subcommand, args) {
1637
+ switch (subcommand) {
1638
+ case "bind": return parseBindArgs([...args]).help === true;
1639
+ case "resume": return returnsBeforeNativeCodexResume(args);
1640
+ case "steer": return args.join(" ").trim() === "";
1641
+ case "model": return args.length === 0 || args.length > 1;
1642
+ case "fast": return args.length === 0 || args.length > 1 || parseCodexFastModeArg(args[0]) === void 0;
1643
+ case "permissions": return args.length === 0 || args.length > 1 || parseCodexPermissionsModeArg(args[0]) === void 0;
1644
+ case "compact":
1645
+ case "review":
1646
+ case "detach":
1647
+ case "unbind":
1648
+ case "stop": return args.length > 0;
1649
+ default: return false;
1701
1650
  }
1702
- const scopeCooldownMs = readCodexDiagnosticsScopeCooldownMs(options.cooldownScope ?? readCodexDiagnosticsCooldownScope(ctx), now);
1703
- if (scopeCooldownMs > 0) return `Codex diagnostics were already sent for this account or channel recently. Try again in ${Math.ceil(scopeCooldownMs / 1e3)}s.`;
1704
1651
  }
1705
- function readCodexDiagnosticsScopeCooldownMs(scope, now) {
1706
- const lastSentAt = lastCodexDiagnosticsUploadByScope.get(scope);
1707
- if (!lastSentAt) return 0;
1708
- const remainingMs = Math.max(0, CODEX_DIAGNOSTICS_COOLDOWN_MS - (now - lastSentAt));
1709
- if (remainingMs === 0) lastCodexDiagnosticsUploadByScope.delete(scope);
1710
- return remainingMs;
1652
+ function isCodexCliNodeResumeBind(subcommand, args) {
1653
+ if (subcommand !== "resume") return false;
1654
+ const parsed = parseResumeArgs([...args]);
1655
+ return Boolean(parsed.host && parsed.threadId && parsed.bindHere === true && !parsed.help);
1711
1656
  }
1712
- function recordCodexDiagnosticsUpload(threadId, ctx, now, cooldownScope) {
1713
- pruneCodexDiagnosticsCooldowns(now);
1714
- recordBoundedCodexDiagnosticsCooldown(lastCodexDiagnosticsUploadByScope, cooldownScope ?? readCodexDiagnosticsCooldownScope(ctx), CODEX_DIAGNOSTICS_COOLDOWN_MAX_SCOPES, now);
1715
- recordBoundedCodexDiagnosticsCooldown(lastCodexDiagnosticsUploadByThread, threadId, CODEX_DIAGNOSTICS_COOLDOWN_MAX_THREADS, now);
1657
+ function returnsBeforeNativeCodexResume(args) {
1658
+ const parsed = parseResumeArgs([...args]);
1659
+ const normalizedThreadId = parsed.threadId?.trim();
1660
+ if (parsed.help) return true;
1661
+ if (parsed.host) return !normalizedThreadId || parsed.bindHere !== true;
1662
+ return !normalizedThreadId || args.length !== 1;
1716
1663
  }
1717
- function recordBoundedCodexDiagnosticsCooldown(map, key, maxSize, now) {
1718
- if (!map.has(key)) while (map.size >= maxSize) {
1719
- const oldestKey = map.keys().next().value;
1720
- if (typeof oldestKey !== "string") break;
1721
- map.delete(oldestKey);
1722
- }
1723
- map.set(key, now);
1664
+ async function handleComputerUseCommand(deps, ctx, pluginConfig, args) {
1665
+ const parsed = parseComputerUseArgs(args);
1666
+ if (parsed.help) return ["Usage: /codex computer-use [status|install] [--source <marketplace-source>] [--marketplace-path <path>] [--marketplace <name>]", "Checks or installs the configured Codex Computer Use plugin through app-server."].join("\n");
1667
+ if (Object.keys(parsed.persistentIdentity).length > 0) return formatComputerUsePersistentIdentityMigration(parsed);
1668
+ if (parsed.action === "install" && !canMutateCodexHost(ctx)) return "Only an owner or operator.admin gateway client can configure Codex Computer Use.";
1669
+ const { agentDir } = resolveCodexConversationControlScope(ctx);
1670
+ const params = {
1671
+ pluginConfig,
1672
+ config: ctx.config,
1673
+ agentDir,
1674
+ forceEnable: parsed.action === "install" || parsed.hasOverrides,
1675
+ ...Object.keys(parsed.overrides).length > 0 ? { overrides: parsed.overrides } : {}
1676
+ };
1677
+ if (parsed.action === "install") return formatComputerUseStatus(await deps.installCodexComputerUse(params));
1678
+ return formatComputerUseStatus(await deps.readCodexComputerUseStatus(params));
1724
1679
  }
1725
- function readCodexDiagnosticsCooldownScope(ctx) {
1726
- const scope = readCodexDiagnosticsConfirmationScope(ctx);
1727
- const payload = JSON.stringify({
1728
- accountId: scope.accountId ?? null,
1729
- channelId: scope.channelId ?? null,
1730
- sessionKey: scope.sessionKey ?? null,
1731
- messageThreadId: scope.messageThreadId ?? null,
1732
- threadParentId: scope.threadParentId ?? null,
1733
- senderId: normalizeCodexDiagnosticsScopeField(ctx.senderId) ?? null,
1734
- channel: normalizeCodexDiagnosticsScopeField(ctx.channel) ?? ""
1680
+ async function handleNativeGoal(deps, ctx, pluginConfig, args) {
1681
+ const action = (args[0] ?? "status").toLowerCase();
1682
+ const objective = args.slice(1).join(" ").trim();
1683
+ const target = await resolveControlTarget(ctx);
1684
+ if (!target) return "Cannot manage the Codex goal because this command has no stable binding identity.";
1685
+ const binding = await deps.bindingStore.read(target.identity);
1686
+ if (!binding?.threadId) return "No Codex thread is attached to this OpenClaw session yet.";
1687
+ const connection = resolveCodexBindingAppServerConnection({
1688
+ binding,
1689
+ authProfileId: binding.authProfileId,
1690
+ pluginConfig
1735
1691
  });
1736
- return crypto.createHash("sha256").update(payload).digest("hex");
1692
+ const goalRequestOptions = {
1693
+ agentDir: target.agentDir,
1694
+ authProfileId: connection.clientAuthProfileId,
1695
+ config: ctx.config,
1696
+ ...connection.usesSupervisionConnection ? { startOptions: connection.appServer.start } : {}
1697
+ };
1698
+ if (action === "status" || action === "get") {
1699
+ if (args.length > 1) return "Usage: /codex goal [status]";
1700
+ return formatNativeGoal(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.getThreadGoal, { threadId: binding.threadId }, goalRequestOptions));
1701
+ }
1702
+ if (action === "clear") {
1703
+ if (args.length > 1) return "Usage: /codex goal clear";
1704
+ const response = await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.clearThreadGoal, { threadId: binding.threadId }, goalRequestOptions);
1705
+ return isJsonObject(response) && response.cleared === true ? "Cleared the Codex goal." : "No Codex goal was active.";
1706
+ }
1707
+ const requestedStatus = action === "pause" ? "paused" : action === "resume" ? "active" : action === "block" ? "blocked" : action === "complete" ? "complete" : void 0;
1708
+ const isObjectiveUpdate = action === "set";
1709
+ if (!requestedStatus && !isObjectiveUpdate || isObjectiveUpdate && !objective) return "Usage: /codex goal [status|set <objective>|pause|resume|block|complete|clear]";
1710
+ if (requestedStatus && args.length > 1) return `Usage: /codex goal ${action}`;
1711
+ return formatNativeGoal(await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.setThreadGoal, {
1712
+ threadId: binding.threadId,
1713
+ ...objective ? { objective } : {},
1714
+ ...requestedStatus ? { status: requestedStatus } : {}
1715
+ }, goalRequestOptions));
1737
1716
  }
1738
- function pruneCodexDiagnosticsCooldowns(now) {
1739
- pruneCodexDiagnosticsCooldownMap(lastCodexDiagnosticsUploadByThread, now);
1740
- pruneCodexDiagnosticsCooldownMap(lastCodexDiagnosticsUploadByScope, now);
1717
+ function formatNativeGoal(response) {
1718
+ const goal = isJsonObject(response) && isJsonObject(response.goal) ? response.goal : void 0;
1719
+ if (!goal) return "No Codex goal is active.";
1720
+ const objective = readString$1(goal, "objective") ?? "unknown";
1721
+ const status = readString$1(goal, "status") ?? "unknown";
1722
+ const tokensUsed = typeof goal.tokensUsed === "number" ? goal.tokensUsed : 0;
1723
+ const tokenBudget = typeof goal.tokenBudget === "number" ? goal.tokenBudget : void 0;
1724
+ return [
1725
+ `Codex goal: ${formatCodexDisplayText(objective)}`,
1726
+ `- Status: ${formatCodexDisplayText(status)}`,
1727
+ `- Tokens: ${tokensUsed}${tokenBudget === void 0 ? "" : ` / ${tokenBudget}`}`
1728
+ ].join("\n");
1741
1729
  }
1742
- function pruneCodexDiagnosticsCooldownMap(map, now) {
1743
- for (const [key, lastSentAt] of map) if (now - lastSentAt >= CODEX_DIAGNOSTICS_COOLDOWN_MS) map.delete(key);
1730
+ async function stopConversationTurn(deps, ctx, pluginConfig) {
1731
+ const target = await resolveControlTarget(ctx);
1732
+ if (!target) return "Cannot stop Codex because this command did not include a stable binding identity.";
1733
+ return (await deps.stopCodexConversationTurn({
1734
+ identity: target.identity,
1735
+ bindingStore: deps.bindingStore,
1736
+ pluginConfig,
1737
+ agentDir: target.agentDir,
1738
+ config: ctx.config
1739
+ })).message;
1744
1740
  }
1745
- function formatCodexErrorForDisplay(error) {
1746
- return escapeCodexChatText(truncateUtf16Safe(formatCodexTextForDisplay(error), CODEX_DIAGNOSTICS_ERROR_MAX_CHARS)) || "unknown error";
1741
+ async function steerConversationTurn(deps, ctx, pluginConfig, message) {
1742
+ const target = await resolveControlTarget(ctx);
1743
+ if (!target) return "Cannot steer Codex because this command did not include a stable binding identity.";
1744
+ return (await deps.steerCodexConversationTurn({
1745
+ identity: target.identity,
1746
+ bindingStore: deps.bindingStore,
1747
+ message,
1748
+ pluginConfig,
1749
+ agentDir: target.agentDir,
1750
+ config: ctx.config
1751
+ })).message;
1747
1752
  }
1748
- function formatCodexResumeCommandForDisplay(threadId) {
1749
- const safeThreadId = formatCodexTextForDisplay(threadId);
1750
- if (!CODEX_RESUME_SAFE_THREAD_ID_PATTERN.test(safeThreadId)) return "run codex resume and paste the thread id shown above";
1751
- return `\`codex resume ${safeThreadId}\``;
1753
+ async function setConversationModel(deps, ctx, pluginConfig, args) {
1754
+ if (args.length > 1) return "Usage: /codex model <model>";
1755
+ const [model = ""] = args;
1756
+ const normalized = model.trim();
1757
+ if (normalized && isCurrentSessionModelSelectionLocked(ctx)) return MODEL_SELECTION_LOCKED_MESSAGE;
1758
+ const target = await resolveControlTarget(ctx);
1759
+ if (!target) return "Cannot set Codex model because this command did not include a stable binding identity.";
1760
+ if (!normalized) {
1761
+ const binding = await deps.bindingStore.read(target.identity);
1762
+ return binding?.model ? `Codex model: ${formatCodexDisplayText(binding.model)}` : "Usage: /codex model <model>";
1763
+ }
1764
+ return await deps.setCodexConversationModel({
1765
+ identity: target.identity,
1766
+ bindingStore: deps.bindingStore,
1767
+ pluginConfig,
1768
+ model: normalized,
1769
+ agentDir: target.agentDir,
1770
+ config: ctx.config
1771
+ });
1752
1772
  }
1753
- function isUnsafeDisplayCodePoint(codePoint) {
1754
- return codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint === 173 || codePoint === 1564 || codePoint === 6158 || codePoint >= 8203 && codePoint <= 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8288 && codePoint <= 8303 || codePoint === 65279 || codePoint >= 65529 && codePoint <= 65531 || codePoint >= 917504 && codePoint <= 917631;
1773
+ async function setConversationFastMode(deps, ctx, args) {
1774
+ if (args.length > 1) return "Usage: /codex fast [on|off|status]";
1775
+ const target = await resolveControlTarget(ctx);
1776
+ if (!target) return "Cannot set Codex fast mode because this command did not include a stable binding identity.";
1777
+ const value = args[0];
1778
+ const parsed = parseCodexFastModeArg(value);
1779
+ if (value && parsed == null && value.trim().toLowerCase() !== "status") return "Usage: /codex fast [on|off|status]";
1780
+ return await deps.setCodexConversationFastMode({
1781
+ identity: target.identity,
1782
+ bindingStore: deps.bindingStore,
1783
+ enabled: parsed
1784
+ });
1755
1785
  }
1756
- function normalizeCodexDiagnosticsScopeField(value) {
1757
- const normalized = normalizeOptionalString(value);
1758
- if (!normalized) return;
1759
- if (normalized.length <= CODEX_DIAGNOSTICS_SCOPE_FIELD_MAX_CHARS) return normalized;
1760
- return `sha256:${crypto.createHash("sha256").update(normalized).digest("hex")}`;
1786
+ async function setConversationPermissions(deps, ctx, args) {
1787
+ if (args.length > 1) return "Usage: /codex permissions [default|yolo|status]";
1788
+ const target = await resolveControlTarget(ctx);
1789
+ if (!target) return "Cannot set Codex permissions because this command did not include a stable binding identity.";
1790
+ const value = args[0];
1791
+ const parsed = parseCodexPermissionsModeArg(value);
1792
+ if (value && !parsed && value.trim().toLowerCase() !== "status") return "Usage: /codex permissions [default|yolo|status]";
1793
+ return await deps.setCodexConversationPermissions({
1794
+ identity: target.identity,
1795
+ bindingStore: deps.bindingStore,
1796
+ mode: parsed
1797
+ });
1761
1798
  }
1762
1799
  async function startThreadAction(deps, ctx, pluginConfig, kind, args) {
1763
1800
  const label = kind === "compact" ? "compaction" : "review";
@@ -1782,273 +1819,248 @@ async function startThreadAction(deps, ctx, pluginConfig, kind, args) {
1782
1819
  });
1783
1820
  return `Started Codex ${label} for thread ${formatCodexDisplayText(binding.threadId)}.`;
1784
1821
  }
1785
- function splitArgs(value) {
1786
- const input = value ?? "";
1787
- const args = [];
1788
- let current = "";
1789
- let quote;
1790
- let escaping = false;
1791
- let tokenStarted = false;
1792
- for (const char of input) {
1793
- if (escaping) {
1794
- current += char;
1795
- escaping = false;
1796
- tokenStarted = true;
1797
- continue;
1798
- }
1799
- if (char === "\\" && quote !== "'") {
1800
- escaping = true;
1801
- tokenStarted = true;
1802
- continue;
1803
- }
1804
- if (quote) {
1805
- if (char === quote) quote = void 0;
1806
- else current += char;
1807
- tokenStarted = true;
1808
- continue;
1809
- }
1810
- if (char === "\"" || char === "'") {
1811
- quote = char;
1812
- tokenStarted = true;
1813
- continue;
1814
- }
1815
- if (/\s/.test(char)) {
1816
- if (tokenStarted) {
1817
- args.push(current);
1818
- current = "";
1819
- tokenStarted = false;
1820
- }
1821
- continue;
1822
- }
1823
- current += char;
1824
- tokenStarted = true;
1822
+ //#endregion
1823
+ //#region extensions/codex/src/command-plugins-management.ts
1824
+ const POLICY_REFRESH_HINT = "New Codex conversations pick this up automatically. Use /new or /reset to refresh the current one.";
1825
+ async function handleCodexPluginsSubcommand(ctx, rest, io) {
1826
+ const [verb = "list", ...args] = rest;
1827
+ const normalized = verb.toLowerCase();
1828
+ if (normalized === "menu") {
1829
+ if (args.length > 0) return { text: "Usage: /codex plugins menu" };
1830
+ return buildPluginsMenuReply();
1825
1831
  }
1826
- if (escaping) current += "\\";
1827
- if (tokenStarted) args.push(current);
1828
- return args;
1829
- }
1830
- function parseBindArgs(args) {
1831
- const parsed = {};
1832
- for (let index = 0; index < args.length; index += 1) {
1833
- const arg = expectDefined(args[index], "current Codex bind argument");
1834
- if (arg === "--help" || arg === "-h") {
1835
- parsed.help = true;
1836
- continue;
1837
- }
1838
- if (arg === "--cwd") {
1839
- const value = readRequiredOptionValue(args, index);
1840
- if (!value || parsed.cwd !== void 0) {
1841
- parsed.help = true;
1842
- continue;
1843
- }
1844
- parsed.cwd = value;
1845
- index += 1;
1846
- continue;
1847
- }
1848
- if (arg === "--model") {
1849
- const value = readRequiredOptionValue(args, index);
1850
- if (!value || parsed.model !== void 0) {
1851
- parsed.help = true;
1852
- continue;
1853
- }
1854
- parsed.model = value;
1855
- index += 1;
1856
- continue;
1857
- }
1858
- if (arg === "--provider" || arg === "--model-provider") {
1859
- const value = readRequiredOptionValue(args, index);
1860
- if (!value || parsed.provider !== void 0) {
1861
- parsed.help = true;
1862
- continue;
1863
- }
1864
- parsed.provider = value;
1865
- index += 1;
1866
- continue;
1867
- }
1868
- if (!arg.startsWith("-") && !parsed.threadId) {
1869
- parsed.threadId = arg;
1870
- continue;
1871
- }
1872
- parsed.help = true;
1832
+ if (normalized === "help") {
1833
+ if (args.length > 0) return { text: "Usage: /codex plugins help" };
1834
+ return { text: buildPluginsHelp() };
1873
1835
  }
1874
- parsed.threadId = normalizeOptionalString(parsed.threadId);
1875
- parsed.cwd = normalizeOptionalString(parsed.cwd);
1876
- parsed.model = normalizeOptionalString(parsed.model);
1877
- parsed.provider = normalizeOptionalString(parsed.provider);
1878
- return parsed;
1879
- }
1880
- function parseCodexCliSessionsArgs(args) {
1881
- const parsed = { filter: "" };
1882
- const filter = [];
1883
- for (let index = 0; index < args.length; index += 1) {
1884
- const arg = expectDefined(args[index], "current Codex sessions argument");
1885
- if (arg === "--help" || arg === "-h") {
1886
- parsed.help = true;
1887
- continue;
1888
- }
1889
- if (arg === "--host" || arg === "--node") {
1890
- const value = readRequiredOptionValue(args, index);
1891
- if (!value || parsed.host !== void 0) {
1892
- parsed.help = true;
1893
- continue;
1894
- }
1895
- parsed.host = value;
1896
- index += 1;
1897
- continue;
1898
- }
1899
- if (arg === "--limit") {
1900
- const parsedLimit = parseStrictPositiveInteger(readRequiredOptionValue(args, index));
1901
- if (parsedLimit === void 0) {
1902
- parsed.help = true;
1903
- continue;
1904
- }
1905
- parsed.limit = parsedLimit;
1906
- index += 1;
1907
- continue;
1908
- }
1909
- if (arg.startsWith("-")) {
1910
- parsed.help = true;
1911
- continue;
1912
- }
1913
- filter.push(arg);
1836
+ if (normalized === "list") {
1837
+ if (args.length > 0) return { text: "Usage: /codex plugins list" };
1838
+ const current = await io.readConfig();
1839
+ return { text: formatPluginList(current.plugins ?? {}, { globalEnabled: current.enabled === true }) };
1914
1840
  }
1915
- parsed.host = normalizeOptionalString(parsed.host);
1916
- parsed.filter = filter.join(" ").trim();
1917
- return parsed;
1841
+ const target = args[0];
1842
+ if (normalized === "enable" || normalized === "disable") {
1843
+ if (args.length === 0) return buildPluginNamePickerReply(normalized, await io.readConfig());
1844
+ if (!target || args.length > 1) return { text: `Usage: /codex plugins ${normalized} <name>` };
1845
+ if (!canMutateCodexHost(ctx)) return { text: `Only an owner or operator.admin gateway client can run /codex plugins ${normalized}.` };
1846
+ const wantEnabled = normalized === "enable";
1847
+ if (!((await io.readConfig()).plugins ?? {})[target]) return { text: `Codex sub-plugin '${formatCodexDisplayText(target)}' is not configured. Run '/codex plugins list' to see configured plugins.` };
1848
+ await io.mutate((block) => {
1849
+ if (wantEnabled) block.enabled = true;
1850
+ block.plugins ??= {};
1851
+ block.plugins[target] = {
1852
+ ...block.plugins[target],
1853
+ enabled: wantEnabled
1854
+ };
1855
+ });
1856
+ return { text: `${formatCodexDisplayText(target)}: ${wantEnabled ? "enabled" : "disabled"} in openclaw.json. ${POLICY_REFRESH_HINT}` };
1857
+ }
1858
+ return { text: `Unknown /codex plugins subcommand: ${formatCodexDisplayText(verb)}\n\n${buildPluginsHelp()}` };
1918
1859
  }
1919
- function parseResumeArgs(args) {
1920
- const parsed = {};
1921
- for (let index = 0; index < args.length; index += 1) {
1922
- const arg = expectDefined(args[index], "current Codex resume argument");
1923
- if (arg === "--help" || arg === "-h") {
1924
- parsed.help = true;
1925
- continue;
1926
- }
1927
- if (arg === "--host" || arg === "--node") {
1928
- const value = readRequiredOptionValue(args, index);
1929
- if (!value || parsed.host !== void 0) {
1930
- parsed.help = true;
1931
- continue;
1932
- }
1933
- parsed.host = value;
1934
- index += 1;
1935
- continue;
1936
- }
1937
- if (arg === "--bind") {
1938
- if (readRequiredOptionValue(args, index) !== "here" || parsed.bindHere !== void 0) {
1939
- parsed.help = true;
1940
- continue;
1860
+ function buildPluginsMenuReply() {
1861
+ return {
1862
+ text: [
1863
+ "Codex sub-plugins. Pick a sub-action or type:",
1864
+ "",
1865
+ " 1. /codex plugins list",
1866
+ " 2. /codex plugins enable",
1867
+ " 3. /codex plugins disable",
1868
+ " 4. /codex plugins help",
1869
+ "",
1870
+ "Type '/codex' to go back to the main menu."
1871
+ ].join("\n"),
1872
+ presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", "Pick a Codex sub-plugin action:", [
1873
+ {
1874
+ label: "list",
1875
+ command: "/codex plugins list"
1876
+ },
1877
+ {
1878
+ label: "enable",
1879
+ command: "/codex plugins enable"
1880
+ },
1881
+ {
1882
+ label: "disable",
1883
+ command: "/codex plugins disable"
1884
+ },
1885
+ {
1886
+ label: "help",
1887
+ command: "/codex plugins help"
1888
+ },
1889
+ {
1890
+ label: "back",
1891
+ command: "/codex"
1941
1892
  }
1942
- parsed.bindHere = true;
1943
- index += 1;
1944
- continue;
1945
- }
1946
- if (!arg.startsWith("-") && !parsed.threadId) {
1947
- parsed.threadId = arg;
1948
- continue;
1949
- }
1950
- parsed.help = true;
1951
- }
1952
- parsed.threadId = normalizeOptionalString(parsed.threadId);
1953
- parsed.host = normalizeOptionalString(parsed.host);
1954
- return parsed;
1893
+ ])
1894
+ };
1955
1895
  }
1956
- function parseComputerUseArgs(args) {
1957
- const parsed = {
1958
- action: "status",
1959
- overrides: {},
1960
- hasOverrides: false,
1961
- persistentIdentity: {}
1896
+ function buildPluginNamePickerReply(verb, current) {
1897
+ const globalEnabled = current.enabled === true;
1898
+ const eligible = Object.entries(current.plugins ?? {}).toSorted(([left], [right]) => left.localeCompare(right)).filter(([, entry]) => {
1899
+ const effectivelyEnabled = globalEnabled && entry.enabled !== false;
1900
+ return verb === "disable" ? effectivelyEnabled : !effectivelyEnabled;
1901
+ });
1902
+ if (eligible.length === 0) return {
1903
+ text: [
1904
+ `No configured ${verb === "enable" ? "disabled" : "enabled"} Codex sub-plugins found.`,
1905
+ "",
1906
+ "Type '/codex plugins list' to inspect configured sub-plugins.",
1907
+ "Type '/codex plugins menu' to go back to the plugins menu."
1908
+ ].join("\n"),
1909
+ presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", "Pick another Codex sub-plugin action:", [{
1910
+ label: "list",
1911
+ command: "/codex plugins list"
1912
+ }, {
1913
+ label: "back",
1914
+ command: "/codex plugins menu"
1915
+ }])
1916
+ };
1917
+ const buttons = [...eligible.map(([key]) => ({
1918
+ label: formatCodexDisplayText(key),
1919
+ command: `/codex plugins ${verb} ${key}`
1920
+ })), {
1921
+ label: "back",
1922
+ command: "/codex plugins menu"
1923
+ }];
1924
+ return {
1925
+ text: [
1926
+ `Codex sub-plugins to ${verb}. Pick one or type:`,
1927
+ "",
1928
+ ...eligible.map(([key], index) => ` ${index + 1}. /codex plugins ${verb} ${key}`),
1929
+ "",
1930
+ ...verb === "enable" && !globalEnabled ? ["Global codexPlugins.enabled is off; enabling one configured sub-plugin turns it on.", ""] : [],
1931
+ "Type '/codex plugins menu' to go back to the plugins menu."
1932
+ ].join("\n"),
1933
+ presentation: buildCodexCommandPickerPresentation("Codex sub-plugins", `Pick a Codex sub-plugin to ${verb}:`, buttons)
1962
1934
  };
1963
- let sawAction = false;
1964
- for (let index = 0; index < args.length; index += 1) {
1965
- const arg = args[index];
1966
- if (arg === "--help" || arg === "-h") {
1967
- parsed.help = true;
1968
- continue;
1969
- }
1970
- if (arg === "status" || arg === "install") {
1971
- if (sawAction) {
1972
- parsed.help = true;
1973
- continue;
1974
- }
1975
- sawAction = true;
1976
- parsed.action = arg;
1977
- continue;
1978
- }
1979
- if (arg === "--source" || arg === "--marketplace-source") {
1980
- const value = readRequiredOptionValue(args, index);
1981
- if (!value || parsed.overrides.marketplaceSource !== void 0) {
1982
- parsed.help = true;
1983
- continue;
1984
- }
1985
- parsed.overrides.marketplaceSource = value;
1986
- index += 1;
1987
- continue;
1988
- }
1989
- if (arg === "--marketplace-path" || arg === "--path") {
1990
- const value = readRequiredOptionValue(args, index);
1991
- if (!value || parsed.overrides.marketplacePath !== void 0) {
1992
- parsed.help = true;
1993
- continue;
1994
- }
1995
- parsed.overrides.marketplacePath = value;
1996
- index += 1;
1997
- continue;
1998
- }
1999
- if (arg === "--marketplace") {
2000
- const value = readRequiredOptionValue(args, index);
2001
- if (!value || parsed.overrides.marketplaceName !== void 0) {
2002
- parsed.help = true;
2003
- continue;
2004
- }
2005
- parsed.overrides.marketplaceName = value;
2006
- index += 1;
2007
- continue;
2008
- }
2009
- if (arg === "--plugin" || arg === "--server" || arg === "--mcp-server") {
2010
- const value = readRequiredOptionValue(args, index);
2011
- const configKey = arg === "--plugin" ? "pluginName" : "mcpServerName";
2012
- if (!value || parsed.persistentIdentity[configKey] !== void 0) {
2013
- parsed.help = true;
2014
- continue;
2015
- }
2016
- parsed.persistentIdentity[configKey] = value.trim();
2017
- index += 1;
2018
- continue;
2019
- }
2020
- parsed.help = true;
2021
- }
2022
- parsed.overrides = normalizeComputerUseStringOverrides(parsed.overrides);
2023
- parsed.hasOverrides = Object.values(parsed.overrides).some(Boolean);
2024
- return parsed;
2025
1935
  }
2026
- function formatComputerUsePersistentIdentityMigration(parsed) {
2027
- const configPrefix = "plugins.entries.codex.config.computerUse";
2028
- const settings = [parsed.persistentIdentity.pluginName ? `${configPrefix}.pluginName = ${JSON.stringify(parsed.persistentIdentity.pluginName)}` : void 0, parsed.persistentIdentity.mcpServerName ? `${configPrefix}.mcpServerName = ${JSON.stringify(parsed.persistentIdentity.mcpServerName)}` : void 0].filter((setting) => Boolean(setting));
2029
- const retryArgs = [
2030
- `/codex computer-use ${parsed.action}`,
2031
- parsed.overrides.marketplaceSource ? `--source ${JSON.stringify(parsed.overrides.marketplaceSource)}` : void 0,
2032
- parsed.overrides.marketplacePath ? `--marketplace-path ${JSON.stringify(parsed.overrides.marketplacePath)}` : void 0,
2033
- parsed.overrides.marketplaceName ? `--marketplace ${JSON.stringify(parsed.overrides.marketplaceName)}` : void 0
2034
- ].filter((arg) => Boolean(arg));
2035
- return ["One-off Computer Use plugin/server overrides are no longer supported.", `Set ${settings.join(" and ")} persistently, then rerun ${retryArgs.join(" ")}.`].join(" ");
1936
+ function buildPluginsHelp() {
1937
+ return [
1938
+ "Codex sub-plugin management (writes only to ~/.openclaw/openclaw.json, never to ~/.codex/config.toml):",
1939
+ "- /codex plugins (alias for list)",
1940
+ "- /codex plugins list show all configured Codex sub-plugins",
1941
+ "- /codex plugins enable <name> enable a configured sub-plugin",
1942
+ "- /codex plugins disable <name> disable a configured sub-plugin"
1943
+ ].join("\n");
2036
1944
  }
2037
- function readRequiredOptionValue(args, index) {
2038
- const value = args[index + 1];
2039
- const normalized = value?.trim();
2040
- if (!normalized || normalized.startsWith("-")) return;
2041
- return value;
1945
+ function formatPluginList(plugins, options = {}) {
1946
+ const globalEnabled = options.globalEnabled === true;
1947
+ const keys = Object.keys(plugins).toSorted();
1948
+ if (keys.length === 0) return "No Codex sub-plugins configured under plugins.entries.codex.config.codexPlugins.plugins";
1949
+ const rows = keys.map((key) => {
1950
+ const entry = plugins[key] ?? {};
1951
+ const state = globalEnabled && entry.enabled !== false ? "ON " : "OFF";
1952
+ return {
1953
+ displayKey: formatCodexDisplayText(key),
1954
+ state,
1955
+ pluginName: formatCodexDisplayText(entry.pluginName ?? key),
1956
+ marketplace: formatCodexDisplayText(entry.marketplaceName ?? "?")
1957
+ };
1958
+ });
1959
+ const keyW = Math.max(...rows.map((r) => r.displayKey.length));
1960
+ const pluginW = Math.max(...rows.map((r) => r.pluginName.length));
1961
+ return [
1962
+ "Codex sub-plugins in Openclaw config (~/.openclaw/openclaw.json):",
1963
+ "",
1964
+ ...rows.map((r) => ` ${r.state} ${r.displayKey.padEnd(keyW)} ${r.pluginName.padEnd(pluginW)} [${r.marketplace}]`),
1965
+ "",
1966
+ ...globalEnabled ? [] : ["Global codexPlugins.enabled is off; configured sub-plugins are inactive.", ""],
1967
+ "New Codex conversations pick up policy changes automatically; /new or /reset to refresh the current one."
1968
+ ].join("\n");
2042
1969
  }
2043
- function normalizeComputerUseStringOverrides(overrides) {
2044
- const normalized = {};
2045
- const marketplaceSource = normalizeOptionalString(overrides.marketplaceSource);
2046
- if (marketplaceSource) normalized.marketplaceSource = marketplaceSource;
2047
- const marketplacePath = normalizeOptionalString(overrides.marketplacePath);
2048
- if (marketplacePath) normalized.marketplacePath = marketplacePath;
2049
- const marketplaceName = normalizeOptionalString(overrides.marketplaceName);
2050
- if (marketplaceName) normalized.marketplaceName = marketplaceName;
2051
- return normalized;
1970
+ //#endregion
1971
+ //#region extensions/codex/src/command-handlers.ts
1972
+ async function handleCodexSubcommand(ctx, options) {
1973
+ const deps = resolveCodexCommandDeps(options.deps);
1974
+ const args = splitArgs(ctx.args);
1975
+ if (args.length === 0) return buildCodexSubcommandPickerReply();
1976
+ const [subcommand = "status", ...rest] = args;
1977
+ const normalized = subcommand.toLowerCase();
1978
+ if (normalized === "help") return { text: buildHelp() };
1979
+ if (CODEX_NATIVE_CONTROL_SUBCOMMANDS.has(normalized) && !returnsBeforeNativeCodexExecution(normalized, rest) && !isReadOnlyCodexGoalCommand(normalized, rest) && !canMutateCodexHost(ctx)) return { text: CODEX_NATIVE_EXECUTION_AUTH_ERROR };
1980
+ const sandboxBlock = resolveCodexNativeCommandSandboxBlock(ctx, normalized, rest);
1981
+ if (sandboxBlock) return { text: sandboxBlock };
1982
+ if (normalized === "plugins") {
1983
+ if (!deps.codexPluginsManagementIo) return { text: "Codex sub-plugin management is not wired up (codexPluginsManagementIo dep is undefined). Edit ~/.openclaw/openclaw.json or use `openclaw config patch` until the runtime exposes the IO." };
1984
+ return await handleCodexPluginsSubcommand(ctx, rest, deps.codexPluginsManagementIo);
1985
+ }
1986
+ if (normalized === "status") {
1987
+ if (rest.length > 0) return { text: "Usage: /codex status" };
1988
+ const { agentDir } = resolveCodexConversationControlScope(ctx);
1989
+ return { text: formatCodexStatus(await deps.readCodexStatusProbes(options.pluginConfig, ctx.config, agentDir)) };
1990
+ }
1991
+ if (normalized === "models") {
1992
+ if (rest.length > 0) return { text: "Usage: /codex models" };
1993
+ const { agentDir } = resolveCodexConversationControlScope(ctx);
1994
+ return { text: formatModels(await deps.listCodexAppServerModels(deps.requestOptions(options.pluginConfig, 100, ctx.config, agentDir))) };
1995
+ }
1996
+ if (normalized === "threads") return { text: await buildThreads(deps, ctx, options.pluginConfig, rest.join(" ")) };
1997
+ if (normalized === "goal") return { text: await handleNativeGoal(deps, ctx, options.pluginConfig, rest) };
1998
+ if (normalized === "sessions") return { text: await buildCodexCliSessions(deps, rest) };
1999
+ if (normalized === "resume") return { text: await resumeThread(deps, ctx, options.pluginConfig, rest) };
2000
+ if (normalized === "bind") return await bindConversation(deps, ctx, options.pluginConfig, rest);
2001
+ if (normalized === "detach" || normalized === "unbind") {
2002
+ if (rest.length > 0) return { text: "Usage: /codex detach" };
2003
+ return { text: await detachConversation(deps, ctx) };
2004
+ }
2005
+ if (normalized === "binding") {
2006
+ if (rest.length > 0) return { text: "Usage: /codex binding" };
2007
+ return { text: await describeConversationBinding(deps, ctx) };
2008
+ }
2009
+ if (normalized === "stop") {
2010
+ if (rest.length > 0) return { text: "Usage: /codex stop" };
2011
+ return { text: await stopConversationTurn(deps, ctx, options.pluginConfig) };
2012
+ }
2013
+ if (normalized === "steer") return { text: await steerConversationTurn(deps, ctx, options.pluginConfig, rest.join(" ")) };
2014
+ if (normalized === "model") return { text: await setConversationModel(deps, ctx, options.pluginConfig, rest) };
2015
+ if (normalized === "fast") {
2016
+ if (isMenuVerb(rest)) return buildCodexFastMenuReply();
2017
+ return { text: await setConversationFastMode(deps, ctx, rest) };
2018
+ }
2019
+ if (normalized === "permissions") {
2020
+ if (isMenuVerb(rest)) return buildCodexPermissionsMenuReply();
2021
+ return { text: await setConversationPermissions(deps, ctx, rest) };
2022
+ }
2023
+ if (normalized === "compact") return { text: await startThreadAction(deps, ctx, options.pluginConfig, "compact", rest) };
2024
+ if (normalized === "review") return { text: await startThreadAction(deps, ctx, options.pluginConfig, "review", rest) };
2025
+ if (normalized === "diagnostics") return await handleCodexDiagnosticsFeedback(deps, ctx, options.pluginConfig, rest.join(" "), "/codex diagnostics");
2026
+ if (normalized === "computer-use" || normalized === "computeruse") {
2027
+ if (isMenuVerb(rest)) return buildCodexComputerUseMenuReply();
2028
+ return { text: await handleComputerUseCommand(deps, ctx, options.pluginConfig, rest) };
2029
+ }
2030
+ if (normalized === "mcp") {
2031
+ if (rest.length > 0) return { text: "Usage: /codex mcp" };
2032
+ const scope = await resolveCommandAppServerScope(deps, ctx, options.pluginConfig);
2033
+ return { text: formatList(await deps.codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listMcpServers, { limit: 100 }, {
2034
+ config: ctx.config,
2035
+ ...scope
2036
+ }), "MCP servers") };
2037
+ }
2038
+ if (normalized === "skills") {
2039
+ if (rest.length > 0) return { text: "Usage: /codex skills" };
2040
+ const scope = await resolveCommandAppServerScope(deps, ctx, options.pluginConfig);
2041
+ return { text: formatSkills(await deps.codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listSkills, {}, {
2042
+ config: ctx.config,
2043
+ ...scope
2044
+ })) };
2045
+ }
2046
+ if (normalized === "account") {
2047
+ if (rest.length > 0) return { text: "Usage: /codex account" };
2048
+ const scope = await resolveCommandAppServerScope(deps, ctx, options.pluginConfig);
2049
+ const requestScope = {
2050
+ config: ctx.config,
2051
+ ...scope
2052
+ };
2053
+ const [account, limits] = await Promise.all([deps.safeCodexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.account, { refreshToken: false }, requestScope), deps.safeCodexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.rateLimits, void 0, requestScope)]);
2054
+ return { text: formatAccount(account, limits, await readCodexAccountAuthOverview({
2055
+ ctx,
2056
+ agentDir: scope.agentDir,
2057
+ pluginConfig: options.pluginConfig,
2058
+ safeCodexControlRequest: deps.safeCodexControlRequest,
2059
+ account,
2060
+ limits
2061
+ })) };
2062
+ }
2063
+ return { text: `Unknown Codex command: ${formatCodexDisplayText(subcommand)}\n\n${buildHelp()}` };
2052
2064
  }
2053
2065
  //#endregion
2054
2066
  export { handleCodexSubcommand };