@gajae-code/coding-agent 0.18.5 → 0.18.6

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 (91) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/dist/types/cli-commands.d.ts +10 -0
  3. package/dist/types/cli-ordinary.d.ts +13 -0
  4. package/dist/types/cli.d.ts +12 -3
  5. package/dist/types/commands/contribution-prep.d.ts +0 -3
  6. package/dist/types/commands/sdk.d.ts +3 -0
  7. package/dist/types/commands/update.d.ts +1 -0
  8. package/dist/types/config/model-profile-activation.d.ts +19 -0
  9. package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
  10. package/dist/types/edit/modes/replace.d.ts +3 -3
  11. package/dist/types/edit/streaming.d.ts +2 -2
  12. package/dist/types/exec/bash-shell-supervisor.d.ts +12 -0
  13. package/dist/types/sdk/broker/discovery.d.ts +8 -0
  14. package/dist/types/sdk/broker/ensure.d.ts +1 -0
  15. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +4 -0
  16. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  17. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  18. package/dist/types/sdk/bus/telegram-daemon.d.ts +1 -0
  19. package/dist/types/sdk/cli/diagnostics-cli.d.ts +18 -0
  20. package/dist/types/sdk/diagnostics/observe-broker.d.ts +185 -0
  21. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  22. package/dist/types/sdk/index.d.ts +1 -0
  23. package/dist/types/sdk/router/session-router.d.ts +2 -0
  24. package/dist/types/sdk/session.d.ts +13 -0
  25. package/dist/types/session/agent-session.d.ts +5 -1
  26. package/dist/types/tools/ask-contract.d.ts +75 -254
  27. package/dist/types/tools/bash.d.ts +1 -1
  28. package/package.json +7 -7
  29. package/scripts/build-sdk-package-smoke.ts +5 -0
  30. package/src/cli/public-command-entry.ts +5 -0
  31. package/src/cli/ssh-cli.ts +6 -3
  32. package/src/cli/web-search-cli.ts +9 -3
  33. package/src/cli-commands.ts +14 -0
  34. package/src/cli-ordinary.ts +197 -0
  35. package/src/cli.ts +25 -182
  36. package/src/commands/contribution-prep.ts +1 -3
  37. package/src/commands/sdk.ts +59 -0
  38. package/src/commands/update.ts +15 -1
  39. package/src/config/file-lock.ts +91 -2
  40. package/src/config/model-profile-activation.ts +70 -4
  41. package/src/config/model-profile-contract.ts +4 -1
  42. package/src/config/model-profiles.ts +23 -31
  43. package/src/config/model-resolver.ts +9 -1
  44. package/src/coordinator-mcp/event-webhook.ts +19 -14
  45. package/src/coordinator-mcp/policy.ts +14 -4
  46. package/src/coordinator-mcp/server.ts +142 -7
  47. package/src/coordinator-mcp/session-reaper.ts +9 -1
  48. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  49. package/src/edit/modes/replace.ts +6 -6
  50. package/src/edit/streaming.ts +15 -15
  51. package/src/exec/bash-shell-supervisor.ts +60 -3
  52. package/src/internal-urls/docs-index.generated.ts +10 -9
  53. package/src/memories/index.ts +8 -2
  54. package/src/modes/acp/acp-agent.ts +649 -367
  55. package/src/modes/components/tool-execution.ts +1 -1
  56. package/src/prompts/tools/ask.md +2 -0
  57. package/src/sdk/broker/broker.ts +46 -0
  58. package/src/sdk/broker/daemon-entry.ts +67 -6
  59. package/src/sdk/broker/discovery.ts +8 -0
  60. package/src/sdk/broker/ensure.ts +156 -20
  61. package/src/sdk/broker/lifecycle-ledger.ts +110 -12
  62. package/src/sdk/broker/lifecycle.ts +225 -341
  63. package/src/sdk/broker/runtime.ts +5 -1
  64. package/src/sdk/broker/transport.ts +5 -1
  65. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  66. package/src/sdk/bus/telegram-daemon-control.ts +15 -12
  67. package/src/sdk/bus/telegram-daemon.ts +34 -1
  68. package/src/sdk/cli/diagnostics-cli.ts +247 -0
  69. package/src/sdk/diagnostics/observe-broker.ts +654 -0
  70. package/src/sdk/host/control/dispatch.ts +52 -4
  71. package/src/sdk/host/control/operations.ts +1 -1
  72. package/src/sdk/host/session-runtime.test.ts +40 -2
  73. package/src/sdk/host/session-runtime.ts +13 -5
  74. package/src/sdk/index.ts +1 -0
  75. package/src/sdk/lifecycle-session.ts +1 -0
  76. package/src/sdk/protocol/operation-registry.ts +1 -1
  77. package/src/sdk/router/session-router.ts +7 -1
  78. package/src/sdk/session.ts +37 -8
  79. package/src/session/agent-session.ts +114 -39
  80. package/src/session/blob-store.ts +78 -33
  81. package/src/session/internal/managed-session-storage.ts +96 -3
  82. package/src/setup/hermes/templates/operator-instructions.v1.md +19 -1
  83. package/src/setup/hermes-setup.ts +4 -1
  84. package/src/slash-commands/builtin-registry.ts +8 -5
  85. package/src/tools/ask-contract.test.ts +76 -1
  86. package/src/tools/ask-contract.ts +57 -6
  87. package/src/tools/ask.ts +16 -7
  88. package/src/tools/bash.ts +38 -29
  89. package/src/tools/descriptors.test.ts +29 -0
  90. package/src/tools/tool-catalog.generated.ts +7 -4
  91. package/src/web/search/index.ts +2 -2
@@ -610,7 +610,7 @@ async function classifyFileLockRemovalTransition(
610
610
  if (!observation) return "active";
611
611
  const info = parseLockInfoBytes(observation.bytes);
612
612
  if (info) {
613
- const stale = await staleLockSnapshot(transitionPath, 0, ownerHostId, previousOwnerHostIds);
613
+ const stale = await staleLockSnapshot(transitionPath, 0, ownerHostId, previousOwnerHostIds, undefined, true);
614
614
  return stale.stale ? "abandoned" : "active";
615
615
  }
616
616
 
@@ -698,6 +698,74 @@ async function adoptOrphanedFileLockRemovalTransition(lockPath: string, orphanAg
698
698
  return false;
699
699
  }
700
700
 
701
+ async function adoptAbandonedFileLockRemovalTransition(
702
+ lockPath: string,
703
+ ownerHostId?: string,
704
+ previousOwnerHostIds: readonly string[] = [],
705
+ ): Promise<boolean> {
706
+ const transitionPath = fileLockRemovalTransitionPath(lockPath);
707
+ const stale = await staleLockSnapshot(transitionPath, 0, ownerHostId, previousOwnerHostIds, undefined, true);
708
+ if (!stale.stale || !stale.identity) return false;
709
+ if (!(await isNativeExactRemovalUsable())) return false;
710
+
711
+ let captured: NativeDirectoryTreeResult;
712
+ try {
713
+ captured = nativeFileLockBindings().snapshotDirectoryTree(transitionPath);
714
+ } catch (error) {
715
+ if (isTransientReleaseError(error)) return false;
716
+ throw error;
717
+ }
718
+ if (!captured.ok || !captured.snapshot) return false;
719
+ const snapshot = captured.snapshot;
720
+ const root = snapshot.entries.find(entry => entry.relativePath === "");
721
+ const infoEntry = snapshot.entries.find(entry => entry.relativePath === "info");
722
+ if (
723
+ root?.kind !== "directory" ||
724
+ infoEntry?.kind !== "file" ||
725
+ snapshot.entries.some(
726
+ entry =>
727
+ entry.relativePath !== "" &&
728
+ entry.relativePath !== "info" &&
729
+ entry.kind !== "directory" &&
730
+ (entry.kind !== "file" || entry.size !== "0"),
731
+ )
732
+ )
733
+ return false;
734
+ const currentIdentity = await captureFileLockDirIdentity(transitionPath);
735
+ if (!currentIdentity || !sameStableFileLockIdentity(currentIdentity, stale.identity)) return false;
736
+ if (
737
+ !nativeFileLockInfoMatchesStableIdentity(snapshot.rootDev, snapshot.rootIno, infoEntry, stale.identity) ||
738
+ !nativeFileLockInfoMatchesContentEvidence(infoEntry, stale.identity)
739
+ )
740
+ return false;
741
+
742
+ let removal: NativeExactUnlinkResult;
743
+ try {
744
+ removal = nativeFileLockBindings().exactRemoveDirectoryTree(transitionPath, snapshot);
745
+ } catch (error) {
746
+ if (isTransientReleaseError(error)) return false;
747
+ throw error;
748
+ }
749
+ if (removal.ok === true) return removal.code === undefined && Object.keys(removal).every(key => key === "ok");
750
+ if (
751
+ removal.ok === false &&
752
+ removal.code === "cleanup_pending" &&
753
+ removal.payloadDurable === true &&
754
+ removal.detachedPath !== undefined &&
755
+ path.resolve(removal.detachedPath) === path.resolve(transitionPath) &&
756
+ removal.retainedSuccessorPath === undefined &&
757
+ removal.retainedPlaceholderPath === undefined &&
758
+ removal.retainedUnknownPath === undefined &&
759
+ Object.keys(removal).every(key => ["ok", "code", "payloadDurable", "detachedPath"].includes(key))
760
+ )
761
+ return await removeDetachedLockQuarantineOnDisk(transitionPath, snapshot.rootDev, snapshot.rootIno);
762
+ return (
763
+ removal.ok === false &&
764
+ removal.code === "not_found" &&
765
+ Object.keys(removal).every(key => key === "ok" || key === "code")
766
+ );
767
+ }
768
+
701
769
  function sameFileLockTreeAfterPublication(
702
770
  staged: NativeDirectoryTreeSnapshot,
703
771
  published: NativeDirectoryTreeSnapshot,
@@ -1488,6 +1556,14 @@ async function staleLockSnapshot(
1488
1556
  ownerHostId?: string,
1489
1557
  previousOwnerHostIds: readonly string[] = [],
1490
1558
  startTimeCache?: Map<string, string | null>,
1559
+ // `.removing` transitions are internal cleanup state this codebase's own removal
1560
+ // machinery produced, never a live holder's public identity: a hostless transition
1561
+ // record predates host-id stamping and is reclaimable by local liveness proof alone,
1562
+ // regardless of the acquirer's own host id. Active `<file>.lock` holders keep the
1563
+ // stricter fail-closed rule (see `lockRecordIsForeignHost`) because that record can
1564
+ // belong to a genuinely foreign process; only the two transition-specific callers
1565
+ // below set this.
1566
+ hostlessTransitionIsLocal = false,
1491
1567
  ): Promise<LockStaleSnapshot> {
1492
1568
  // Capture the root and info inode BEFORE asking whether the owner is stale. A later
1493
1569
  // snapshot alone would let a copied successor inherit the stale verdict's authority.
@@ -1521,7 +1597,8 @@ async function staleLockSnapshot(
1521
1597
  // A host-qualified lock may only be reclaimed after proving that its owner is
1522
1598
  // local. Foreign and malformed host-qualified records fail closed: PID values
1523
1599
  // and clocks are not meaningful across hosts.
1524
- if (lockRecordIsForeignHost(info, ownerHostId, previousOwnerHostIds)) return { stale: false };
1600
+ const foreignCheckHostId = hostlessTransitionIsLocal && info.owner_host_id === undefined ? undefined : ownerHostId;
1601
+ if (lockRecordIsForeignHost(info, foreignCheckHostId, previousOwnerHostIds)) return { stale: false };
1525
1602
  if (ownerIncarnationChanged(info, startTimeCache)) {
1526
1603
  if (!judgedIdentity) return { stale: false };
1527
1604
  let currentIdentity: GenericFileLockDirIdentity | null;
@@ -1752,6 +1829,18 @@ async function tryAcquireLock(
1752
1829
  }
1753
1830
  if (transitionState === "orphan_transition")
1754
1831
  return { kind: "orphan_transition", path: fileLockRemovalTransitionPath(destinationPath) };
1832
+ if (transitionState === "abandoned") {
1833
+ if (await adoptAbandonedFileLockRemovalTransition(destinationPath, ownerHostId, previousOwnerHostIds)) {
1834
+ transitionState = await classifyFileLockRemovalTransition(
1835
+ destinationPath,
1836
+ orphanTransitionAgeMs,
1837
+ ownerHostId,
1838
+ previousOwnerHostIds,
1839
+ );
1840
+ } else {
1841
+ return null;
1842
+ }
1843
+ }
1755
1844
  if (transitionState !== null) return null;
1756
1845
  const staged = snapshotDirectoryTree(canonicalPendingPath);
1757
1846
  if (!staged.ok || !staged.snapshot) {
@@ -115,6 +115,7 @@ export interface PrepareModelProfileActivationOptions {
115
115
  >
116
116
  > & {
117
117
  getError?: ModelRegistry["getError"];
118
+ authStorage?: ModelRegistry["authStorage"];
118
119
  };
119
120
  settings: Pick<Settings, "get" | "getGlobal" | "getOverride">;
120
121
  profileName: string;
@@ -475,6 +476,53 @@ export class ModelProfileCredentialError extends Error {
475
476
  }
476
477
  }
477
478
 
479
+ interface SessionPinRegistry {
480
+ authStorage?: {
481
+ hasRuntimeApiKey(provider: string): boolean;
482
+ hasLiteralConfigApiKey(provider: string, owner?: object): boolean;
483
+ hasSessionCredentialUnavailable(provider: string, scopeId?: string): boolean;
484
+ };
485
+ getAuthStorageOwner?(): object;
486
+ }
487
+
488
+ /**
489
+ * Whether an unavailable session credential pin blocks `provider`.
490
+ *
491
+ * Mirrors `AuthStorage.getApiKey` precedence: a runtime `--api-key` override or a literal
492
+ * `models.yml` `apiKey` registered for this registry's owner is resolved before the
493
+ * unavailable-pin marker, so either one keeps the provider usable. An `apiKeyEnv` key does
494
+ * not: `getApiKey` prefers another stored api_key account over it, which would silently
495
+ * retarget the unavailable pin.
496
+ */
497
+ export function isSessionCredentialPinBlocking(
498
+ registry: SessionPinRegistry,
499
+ provider: string,
500
+ sessionId: string | undefined,
501
+ ): boolean {
502
+ const authStorage = registry.authStorage;
503
+ if (!authStorage) return false;
504
+ if (authStorage.hasRuntimeApiKey(provider)) return false;
505
+ if (authStorage.hasLiteralConfigApiKey(provider, registry.getAuthStorageOwner?.())) return false;
506
+ return authStorage.hasSessionCredentialUnavailable(provider, sessionId);
507
+ }
508
+
509
+ async function getProfileProviderApiKey(
510
+ registry: PrepareModelProfileActivationOptions["modelRegistry"],
511
+ provider: string,
512
+ sessionId: string,
513
+ profileLabel: string,
514
+ ): Promise<string | undefined> {
515
+ const pinBlocking = () => isSessionCredentialPinBlocking(registry, provider, sessionId);
516
+ if (pinBlocking()) throw new ModelProfileCredentialError(profileLabel, [provider]);
517
+ try {
518
+ return await registry.getApiKeyForProvider(provider, sessionId);
519
+ } catch (error) {
520
+ // OAuth selection can invalidate a pin while the credential probe is running.
521
+ if (pinBlocking()) throw new ModelProfileCredentialError(profileLabel, [provider]);
522
+ throw error;
523
+ }
524
+ }
525
+
478
526
  export function formatModelProfileCredentialError(profileLabel: string, providers: readonly string[]): string {
479
527
  return `Model profile "${profileLabel}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
480
528
  }
@@ -762,7 +810,12 @@ export async function resolveModelProfileDefaultChain(options: {
762
810
  ])) {
763
811
  let apiKey: string | undefined;
764
812
  try {
765
- apiKey = await options.modelRegistry.getApiKeyForProvider(provider, options.credentialSessionId);
813
+ apiKey = await getProfileProviderApiKey(
814
+ options.modelRegistry,
815
+ provider,
816
+ options.credentialSessionId,
817
+ profileLabel,
818
+ );
766
819
  } catch (error) {
767
820
  if (requiredProviderSet.has(provider) && !alternativeSet.has(provider)) throw error;
768
821
  continue;
@@ -787,7 +840,12 @@ export async function resolveModelProfileDefaultChain(options: {
787
840
  const proxyApiKey =
788
841
  proxyProvider === undefined
789
842
  ? undefined
790
- : await options.modelRegistry.getApiKeyForProvider(proxyProvider, options.credentialSessionId);
843
+ : await getProfileProviderApiKey(
844
+ options.modelRegistry,
845
+ proxyProvider,
846
+ options.credentialSessionId,
847
+ profileLabel,
848
+ );
791
849
  if (
792
850
  proxyProvider !== undefined &&
793
851
  !isModelProfileProxyConfigured(proxyProvider, configuredProviderIds, proxyApiKey === kNoAuth)
@@ -886,6 +944,8 @@ export async function resolveModelProfileDefaultChain(options: {
886
944
  aliasIntent: "preset-equivalent",
887
945
  canonicalSessionId: null,
888
946
  credentialSessionId: options.credentialSessionId,
947
+ isCredentialUnavailable: provider =>
948
+ isSessionCredentialPinBlocking(options.modelRegistry, provider, options.credentialSessionId),
889
949
  },
890
950
  );
891
951
  return { profileName, entries: defaultChain, ...resolution };
@@ -1083,6 +1143,8 @@ async function resolveAndClampSelectorValue(
1083
1143
  aliasIntent: options.aliasIntent,
1084
1144
  canonicalSessionId: options.sessionId,
1085
1145
  credentialSessionId: options.credentialSessionId,
1146
+ isCredentialUnavailable: provider =>
1147
+ isSessionCredentialPinBlocking(options.modelRegistry, provider, options.credentialSessionId),
1086
1148
  },
1087
1149
  );
1088
1150
  resolved = {
@@ -1213,6 +1275,8 @@ async function concretizeProfileSelectorValue(
1213
1275
  aliasIntent: "preset-equivalent",
1214
1276
  canonicalSessionId: prepared.session.sessionId,
1215
1277
  credentialSessionId,
1278
+ isCredentialUnavailable: provider =>
1279
+ isSessionCredentialPinBlocking(prepared.modelRegistry, provider, credentialSessionId),
1216
1280
  },
1217
1281
  )
1218
1282
  : resolveModelRoleValue(selector, candidates, {
@@ -1322,7 +1386,7 @@ export async function prepareModelProfileActivation(
1322
1386
  for (const provider of authenticationProbeProviders) {
1323
1387
  let apiKey: string | undefined;
1324
1388
  try {
1325
- apiKey = await options.modelRegistry.getApiKeyForProvider(provider, credentialSessionId);
1389
+ apiKey = await getProfileProviderApiKey(options.modelRegistry, provider, credentialSessionId, profileLabel);
1326
1390
  } catch (error) {
1327
1391
  if (requiredProviderSet.has(provider) && !alternativeSet.has(provider)) throw error;
1328
1392
  continue;
@@ -1357,7 +1421,7 @@ export async function prepareModelProfileActivation(
1357
1421
  const proxyApiKey =
1358
1422
  proxyProvider === undefined
1359
1423
  ? undefined
1360
- : await options.modelRegistry.getApiKeyForProvider(proxyProvider, credentialSessionId);
1424
+ : await getProfileProviderApiKey(options.modelRegistry, proxyProvider, credentialSessionId, profileLabel);
1361
1425
  if (proxyProvider !== undefined) {
1362
1426
  const configuredProxyProviders = options.modelRegistry.getConfiguredProviderIds?.();
1363
1427
  if (!isModelProfileProxyConfigured(proxyProvider, configuredProxyProviders, proxyApiKey === kNoAuth)) {
@@ -1462,6 +1526,8 @@ export async function prepareModelProfileActivation(
1462
1526
  aliasIntent: "preset-equivalent",
1463
1527
  canonicalSessionId: options.session.sessionId,
1464
1528
  credentialSessionId,
1529
+ isCredentialUnavailable: provider =>
1530
+ isSessionCredentialPinBlocking(options.modelRegistry, provider, credentialSessionId),
1465
1531
  },
1466
1532
  );
1467
1533
  const defaultModel = defaultResolution.model;
@@ -4,7 +4,10 @@ export const MODEL_PROFILE_DISCOVERY_QUERY = "models.profiles.list";
4
4
  export const MODEL_PROFILE_ERROR_DETAIL_MAX_BYTES = 2048;
5
5
  const REQUESTED_PROFILE_MAX_BYTES = 256;
6
6
 
7
- const LEGACY_MODEL_PROFILE_ALIASES: ReadonlyMap<string, string> = new Map([["codex-standard", "codex-medium"]]);
7
+ const LEGACY_MODEL_PROFILE_ALIASES: ReadonlyMap<string, string> = new Map([
8
+ ["codex-standard", "codex-medium"],
9
+ ["codex-sol61", "codex-pro"],
10
+ ]);
8
11
 
9
12
  export interface ModelProfileCatalogItem {
10
13
  id: string;
@@ -73,32 +73,25 @@ const profile = (
73
73
 
74
74
  export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
75
75
  profile("codex-eco", ["openai-codex"], {
76
- default: "openai-codex/gpt-5.6-terra:low",
76
+ default: "openai-codex/gpt-6-luna:low",
77
77
  executor: "openai-codex/gpt-6-luna:low",
78
78
  planner: "openai-codex/gpt-6-luna:high",
79
- critic: "openai-codex/gpt-5.6-terra:xhigh",
80
- architect: "openai-codex/gpt-5.6-terra:high",
79
+ critic: "openai-codex/gpt-6-luna:xhigh",
80
+ architect: "openai-codex/gpt-6-luna:high",
81
81
  }),
82
82
  profile("codex-medium", ["openai-codex"], {
83
- default: "openai-codex/gpt-6-sol:low",
84
- executor: "openai-codex/gpt-5.6-terra:low",
85
- planner: "openai-codex/gpt-5.6-terra:high",
86
- critic: "openai-codex/gpt-6-sol:xhigh",
87
- architect: "openai-codex/gpt-6-sol:high",
83
+ default: "openai-codex/gpt-6.1-sol:low",
84
+ executor: "openai-codex/gpt-6.1-sol:low",
85
+ planner: "openai-codex/gpt-6.1-sol:high",
86
+ critic: "openai-codex/gpt-6.1-sol:xhigh",
87
+ architect: "openai-codex/gpt-6.1-sol:high",
88
88
  }),
89
89
  profile("codex-pro", ["openai-codex"], {
90
- default: "openai-codex/gpt-6-sol:medium",
91
- executor: "openai-codex/gpt-5.6-terra:medium",
92
- planner: "openai-codex/gpt-6-sol:high",
93
- critic: "openai-codex/gpt-6-sol:max",
94
- architect: "openai-codex/gpt-6-sol:xhigh",
95
- }),
96
- profile("codex-sol61", ["openai-codex"], {
97
90
  default: "openai-codex/gpt-6.1-sol:medium",
98
91
  executor: "openai-codex/gpt-6.1-sol:medium",
99
92
  planner: "openai-codex/gpt-6.1-sol:high",
100
- critic: "openai-codex/gpt-6.1-sol:xhigh",
101
- architect: "openai-codex/gpt-6-astra:xhigh",
93
+ critic: "openai-codex/gpt-6.1-sol:max",
94
+ architect: "openai-codex/gpt-6.1-sol:xhigh",
102
95
  }),
103
96
  profile("lunamaxxing", ["openai-codex"], {
104
97
  default: "openai-codex/gpt-6-luna:medium",
@@ -111,20 +104,20 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
111
104
  default: "openai-codex/gpt-6-astra:medium",
112
105
  executor: "openai-codex/gpt-6-luna:xhigh",
113
106
  planner: "openai-codex/gpt-6-astra:xhigh",
114
- critic: "openai-codex/gpt-6-sol:high",
115
- architect: "openai-codex/gpt-5.6-terra:xhigh",
107
+ critic: "openai-codex/gpt-6.1-sol:high",
108
+ architect: "openai-codex/gpt-6.1-sol:xhigh",
116
109
  }),
117
110
  profile("astra-default", ["openai-codex"], {
118
111
  default: "openai-codex/gpt-6-astra:medium",
119
112
  executor: "openai-codex/gpt-6-luna:max",
120
113
  planner: "openai-codex/gpt-6-astra:xhigh",
121
- critic: "openai-codex/gpt-6-sol:xhigh",
122
- architect: "openai-codex/gpt-5.6-terra:xhigh",
114
+ critic: "openai-codex/gpt-6.1-sol:xhigh",
115
+ architect: "openai-codex/gpt-6.1-sol:xhigh",
123
116
  }),
124
117
  profile("astra-heavy", ["openai-codex"], {
125
118
  default: "openai-codex/gpt-6-astra:medium",
126
- executor: "openai-codex/gpt-6-sol:max",
127
- planner: "openai-codex/gpt-6-sol:max",
119
+ executor: "openai-codex/gpt-6.1-sol:max",
120
+ planner: "openai-codex/gpt-6.1-sol:max",
128
121
  critic: "openai-codex/gpt-6-astra:max",
129
122
  architect: "openai-codex/gpt-6-astra:max",
130
123
  }),
@@ -497,24 +490,24 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
497
490
  }),
498
491
  profile("opus-codex", ["anthropic", "openai-codex"], {
499
492
  default: "anthropic/claude-opus-5-5:medium",
500
- executor: "openai-codex/gpt-5.6-terra:low",
493
+ executor: "openai-codex/gpt-6.1-sol:low",
501
494
  planner: "anthropic/claude-sonnet-5-5",
502
- critic: "openai-codex/gpt-6-sol:xhigh",
503
- architect: "openai-codex/gpt-6-sol:high",
495
+ critic: "openai-codex/gpt-6.1-sol:xhigh",
496
+ architect: "openai-codex/gpt-6.1-sol:high",
504
497
  }),
505
498
  profile("codex-opencodego", ["openai-codex", "opencode-go"], {
506
- default: "openai-codex/gpt-6-sol:low",
499
+ default: "openai-codex/gpt-6.1-sol:low",
507
500
  executor: "opencode-go/deepseek-v4-pro",
508
501
  planner: "opencode-go/kimi-k3",
509
502
  critic: "opencode-go/mimo-v2.5-pro",
510
- architect: "openai-codex/gpt-6-sol:high",
503
+ architect: "openai-codex/gpt-6.1-sol:high",
511
504
  }),
512
505
  profile("fable-opus-codex", ["anthropic", "openai-codex"], {
513
506
  default: "anthropic/claude-fable-5-1:high",
514
- executor: "openai-codex/gpt-5.6-terra:medium",
507
+ executor: "openai-codex/gpt-6.1-sol:medium",
515
508
  planner: "anthropic/claude-opus-5-5:medium",
516
509
  critic: "anthropic/claude-opus-5-5:high",
517
- architect: "openai-codex/gpt-6-sol:xhigh",
510
+ architect: "openai-codex/gpt-6.1-sol:xhigh",
518
511
  }),
519
512
  profile("astra-fable", ["openai-codex", "anthropic"], {
520
513
  default: "openai-codex/gpt-6-astra:medium",
@@ -562,7 +555,6 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
562
555
  "codex-eco": { displayName: "Codex Eco", providerGroup: "CODEX" },
563
556
  "codex-medium": { displayName: "Codex Medium", providerGroup: "CODEX" },
564
557
  "codex-pro": { displayName: "Codex Pro", providerGroup: "CODEX" },
565
- "codex-sol61": { displayName: "Codex Sol 6.1", providerGroup: "CODEX" },
566
558
  lunamaxxing: { displayName: "LunaMaxxing", providerGroup: "CODEX" },
567
559
  "astra-lite": { displayName: "ASTRA-Lite", providerGroup: "CODEX" },
568
560
  "astra-default": { displayName: "ASTRA-Default", providerGroup: "CODEX" },
@@ -1189,7 +1189,15 @@ async function resolveModelChainEntries(
1189
1189
  skips.push({ selector, reason: "credential_unavailable" });
1190
1190
  break;
1191
1191
  }
1192
- const key = await modelRegistry.getApiKey(candidate.model, credentialSessionId, { signal: options?.signal });
1192
+ let key: string | undefined;
1193
+ try {
1194
+ key = await modelRegistry.getApiKey(candidate.model, credentialSessionId, { signal: options?.signal });
1195
+ } catch (error) {
1196
+ // OAuth selection can invalidate a pin after the preflight check.
1197
+ if (!options?.isCredentialUnavailable?.(candidate.model.provider)) throw error;
1198
+ skips.push({ selector, reason: "credential_unavailable" });
1199
+ break;
1200
+ }
1193
1201
  if (isAuthenticatedOrKeyless(key)) {
1194
1202
  return { ...candidate, activeIndex, skips };
1195
1203
  }
@@ -116,6 +116,17 @@ function webhookEnvValue(explicitEnv: NodeJS.ProcessEnv | undefined, name: strin
116
116
  return $credentialEnv(name)?.trim() || undefined;
117
117
  }
118
118
 
119
+ // Number.parseInt reads a leading digit prefix ("1.5" -> 1, "1e4" -> 1,
120
+ // "10s" -> 10), which would silently turn a timeout into one millisecond or a
121
+ // retry budget into a single attempt. Only plain decimal digits count; anything
122
+ // else falls back to the default, matching the coordinator policy env values.
123
+ function boundedWebhookEnvInt(raw: string | undefined, fallback: number, max: number): number {
124
+ if (raw === undefined || !/^\d+$/.test(raw)) return fallback;
125
+ const parsed = Number.parseInt(raw, 10);
126
+ // An oversized digit string still means "as large as allowed", so it caps.
127
+ return parsed > 0 ? Math.min(parsed, max) : fallback;
128
+ }
129
+
119
130
  export function parseEventWebhookConfig(explicitEnv?: NodeJS.ProcessEnv): EventWebhookConfig | null {
120
131
  const rawUrl = webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_URL");
121
132
  if (!rawUrl) return null;
@@ -139,22 +150,16 @@ export function parseEventWebhookConfig(explicitEnv?: NodeJS.ProcessEnv): EventW
139
150
  : null;
140
151
  const tokenFile = webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_TOKEN_FILE") ?? "";
141
152
  if (tokenFile !== "" && !path.isAbsolute(tokenFile)) throw new Error("coordinator_event_webhook_token_file_invalid");
142
- const timeoutRaw = Number.parseInt(
143
- webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_TIMEOUT_MS") ?? "",
144
- 10,
153
+ const timeoutMs = boundedWebhookEnvInt(
154
+ webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_TIMEOUT_MS"),
155
+ DEFAULT_EVENT_WEBHOOK_TIMEOUT_MS,
156
+ MAX_EVENT_WEBHOOK_TIMEOUT_MS,
145
157
  );
146
- const timeoutMs =
147
- Number.isFinite(timeoutRaw) && timeoutRaw > 0
148
- ? Math.min(timeoutRaw, MAX_EVENT_WEBHOOK_TIMEOUT_MS)
149
- : DEFAULT_EVENT_WEBHOOK_TIMEOUT_MS;
150
- const attemptsRaw = Number.parseInt(
151
- webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_MAX_ATTEMPTS") ?? "",
152
- 10,
158
+ const maxAttempts = boundedWebhookEnvInt(
159
+ webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_MAX_ATTEMPTS"),
160
+ DEFAULT_EVENT_WEBHOOK_MAX_ATTEMPTS,
161
+ MAX_EVENT_WEBHOOK_MAX_ATTEMPTS,
153
162
  );
154
- const maxAttempts =
155
- Number.isFinite(attemptsRaw) && attemptsRaw > 0
156
- ? Math.min(attemptsRaw, MAX_EVENT_WEBHOOK_MAX_ATTEMPTS)
157
- : DEFAULT_EVENT_WEBHOOK_MAX_ATTEMPTS;
158
163
  return { url: rawUrl, tokenFile: tokenFile || null, sessionIds, timeoutMs, maxAttempts };
159
164
  }
160
165
 
@@ -86,9 +86,19 @@ const LEGACY_MUTATION_CLASS_ALIASES = new Map<string, CoordinatorMutationClass>(
86
86
  ["report", "reports"],
87
87
  ]);
88
88
 
89
+ // Number.parseInt reads a leading digit prefix ("64KB" -> 64, "1e6" -> 1,
90
+ // "1.5" -> 1), which would silently shrink a cap or TTL to a tiny value. Only
91
+ // plain decimal digits count; anything else falls back to the default.
92
+ function parsePositiveInt(value: string | undefined): number | undefined {
93
+ const trimmed = (value ?? "").trim();
94
+ if (!/^\d+$/.test(trimmed)) return undefined;
95
+ const parsed = Number.parseInt(trimmed, 10);
96
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
97
+ }
98
+
89
99
  function parsePositiveIntMs(value: string | undefined, fallback: number, floor: number): number {
90
- const parsed = Number.parseInt((value ?? "").trim(), 10);
91
- if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
100
+ const parsed = parsePositiveInt(value);
101
+ if (parsed === undefined) return fallback;
92
102
  return Math.max(floor, parsed);
93
103
  }
94
104
 
@@ -133,8 +143,8 @@ function parseMutationClasses(value: string | undefined): Set<CoordinatorMutatio
133
143
  }
134
144
 
135
145
  function parseByteCap(value: string | undefined): number {
136
- const parsed = Number.parseInt(value ?? "", 10);
137
- if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_ARTIFACT_BYTE_CAP;
146
+ const parsed = parsePositiveInt(value);
147
+ if (parsed === undefined) return DEFAULT_ARTIFACT_BYTE_CAP;
138
148
  return Math.min(parsed, MAX_ARTIFACT_BYTE_CAP);
139
149
  }
140
150