@bitkyc08/opencodex 2.47.0 → 2.48.0

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.
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { ConfigMutationLockError, withConfigMutationLockSync } from "../config";
4
5
  import { atomicWriteFile } from "../config/atomic-write";
5
6
  import { getConfigDir } from "../config/paths";
6
7
  import { registerOptionalShutdownHook } from "../lib/optional-shutdown-hooks";
@@ -91,9 +92,9 @@ function readJournal(path: string): Journal {
91
92
  }
92
93
  }
93
94
 
94
- function writeJournal(path: string, journal: Journal): void {
95
+ function writeJournal(path: string, journal: Journal, now: number): void {
95
96
  // Keep only entries whose credit could still matter: settled ones older than a week are noise.
96
- const cutoff = Date.now() - 7 * 24 * 60 * 60_000;
97
+ const cutoff = now - 7 * 24 * 60 * 60_000;
97
98
  journal.entries = journal.entries.filter(e => e.state !== "settled" || e.updatedAt > cutoff);
98
99
  atomicWriteFile(path, JSON.stringify(journal, null, 2));
99
100
  }
@@ -112,6 +113,7 @@ export interface AutoRedeemDeps {
112
113
  now?: () => number;
113
114
  setTimer?: (fn: () => void, ms: number) => unknown;
114
115
  clearTimer?: (handle: unknown) => void;
116
+ /** Callers sharing an overridden journal must also share the OPENCODEX_HOME mutation coordinator. */
115
117
  journalFile?: string;
116
118
  log?: (line: string) => void;
117
119
  /** Upper bound on one sleep so a laptop sleep or clock jump re-checks rather than trusting a stale plan. */
@@ -155,15 +157,38 @@ export function createResetCreditAutoRedeemer(deps: AutoRedeemDeps): ResetCredit
155
157
  handle = setTimer(() => { handle = null; void tick(); }, Math.max(0, Math.min(ms, maxSleepMs)));
156
158
  };
157
159
 
160
+ const retryJournal = (error: unknown): void => {
161
+ const cause = error instanceof ConfigMutationLockError ? error.cause : error;
162
+ const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
163
+ const busy = code === "SQLITE_BUSY" || code === "SQLITE_LOCKED"
164
+ || (cause instanceof Error && /database (?:is|table is) locked/i.test(cause.message));
165
+ schedule(busy ? 1_000 : idleRecheckMs);
166
+ };
167
+
158
168
  const dispatch = async (plan: AutoRedeemPlan): Promise<AutoRedeemOutcome> => {
159
- const journal = readJournal(path);
160
- let entry = journal.entries.find(e => e.accountKey === accountKey && e.grantedAt === plan.grantedAt && e.expiresAt === plan.expiresAt);
161
- if (entry?.state === "settled") return { kind: "skipped", reason: "credit-gone" };
162
- if (!entry) {
163
- entry = { accountKey, grantedAt: plan.grantedAt, expiresAt: plan.expiresAt, redeemRequestId: randomUUID(), state: "dispatched", updatedAt: now() };
164
- journal.entries.push(entry);
165
- // Journal BEFORE the network call: a crash after this line replays the same request id.
166
- writeJournal(path, journal);
169
+ // Reserve under the shared config-mutation lock. `inFlight` only serializes ticks inside
170
+ // ONE process; two servers on the same config dir would otherwise both read a journal with
171
+ // no entry, each mint a different `redeem_request_id`, and spend two credits for one plan.
172
+ let entry: JournalEntry;
173
+ try {
174
+ entry = withConfigMutationLockSync(() => {
175
+ const journal = readJournal(path);
176
+ const existing = journal.entries.find(e => e.accountKey === accountKey && e.grantedAt === plan.grantedAt && e.expiresAt === plan.expiresAt);
177
+ if (existing) return existing;
178
+ const created: JournalEntry = { accountKey, grantedAt: plan.grantedAt, expiresAt: plan.expiresAt, redeemRequestId: randomUUID(), state: "dispatched", updatedAt: now() };
179
+ journal.entries.push(created);
180
+ // Journal BEFORE the network call: a crash after this line replays the same request id.
181
+ writeJournal(path, journal, created.updatedAt);
182
+ return created;
183
+ });
184
+ } catch (error) {
185
+ // Only contention gets a short retry; persistent storage failures must not spin.
186
+ retryJournal(error);
187
+ return { kind: "error", message: error instanceof Error ? error.message : "journal reservation failed" };
188
+ }
189
+ if (entry.state === "settled") {
190
+ schedule(idleRecheckMs);
191
+ return { kind: "skipped", reason: "credit-gone" };
167
192
  }
168
193
  log(`[opencodex] reset-credit auto-redeem: dispatching for account ${accountKey} (credit expires ${plan.expiresAt})`);
169
194
  let result: { code: string };
@@ -174,9 +199,25 @@ export function createResetCreditAutoRedeemer(deps: AutoRedeemDeps): ResetCredit
174
199
  schedule(60_000);
175
200
  return { kind: "ambiguous", redeemRequestId: entry.redeemRequestId };
176
201
  }
177
- entry.state = "settled";
178
- entry.updatedAt = now();
179
- writeJournal(path, journal);
202
+ // Re-read under the lock: a peer may have appended its own entries since the reservation,
203
+ // and writing a stale in-memory journal would drop them.
204
+ try {
205
+ withConfigMutationLockSync(() => {
206
+ const journal = readJournal(path);
207
+ const current = journal.entries.find(e => e.accountKey === accountKey && e.grantedAt === plan.grantedAt && e.expiresAt === plan.expiresAt);
208
+ if (!current || current.redeemRequestId !== entry.redeemRequestId) {
209
+ throw new Error("auto-redeem journal reservation changed before settlement");
210
+ }
211
+ current.state = "settled";
212
+ current.updatedAt = now();
213
+ writeJournal(path, journal, current.updatedAt);
214
+ });
215
+ } catch (error) {
216
+ // Upstream answered, but settlement could not be committed. Preserve any reservation;
217
+ // a later dispatch must reuse its request id. A vanished credit may never dispatch again.
218
+ retryJournal(error);
219
+ return { kind: "error", message: error instanceof Error ? error.message : "journal settlement failed" };
220
+ }
180
221
  log(`[opencodex] reset-credit auto-redeem: upstream answered ${result.code} for account ${accountKey}`);
181
222
  schedule(idleRecheckMs);
182
223
  return { kind: "dispatched", code: result.code, redeemRequestId: entry.redeemRequestId };
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { saveConfigPreservingClaudeCode } from "../config";
3
- import { isCodexAccountGenerationLive, readCodexAccountRecord } from "./account-store";
3
+ import { isCodexAccountGenerationLive, readCodexAccountRecord, type CodexRefreshProvenance } from "./account-store";
4
4
  import { codexAccountLogLabel } from "./account-label";
5
5
  import { NATIVE_RESERVE_MODEL } from "./catalog/native-models";
6
6
  import { isCodexAccountPaused } from "./account-pause";
@@ -642,6 +642,80 @@ export function claimDueCodexQuotaRecoveryProbes(
642
642
  });
643
643
  }
644
644
 
645
+ type CooldownRecoveryLease = Pick<CodexQuotaRecoveryProbeClaim,
646
+ "accountId" | "scope" | "leaseId" | "cooldownGeneration">;
647
+
648
+ export type ManualResetCooldownClaim =
649
+ | { kind: "pool"; probe: CodexQuotaRecoveryProbeClaim }
650
+ | { kind: "main"; probe: CooldownRecoveryLease };
651
+
652
+ function manualResetAccountEligible(config: OcxConfig, accountId: string): boolean {
653
+ return !isCodexAccountPaused(config, accountId) && !isAccountNeedsReauth(accountId)
654
+ && (accountId === MAIN_CODEX_ACCOUNT_ID
655
+ || (config.codexAccounts ?? []).some(account => account.id === accountId && isSelectableCodexPoolAccount(account)));
656
+ }
657
+
658
+ /** Explicit reset bypasses probe pacing, never another owner's lease or quota scope. */
659
+ export function claimManualResetCooldowns(
660
+ config: OcxConfig,
661
+ accountId: string,
662
+ now = Date.now(),
663
+ expectedPoolGeneration?: number,
664
+ ): ManualResetCooldownClaim[] {
665
+ if (!manualResetAccountEligible(config, accountId)) return [];
666
+ const record = accountId === MAIN_CODEX_ACCOUNT_ID ? undefined : readCodexAccountRecord(accountId);
667
+ if (accountId !== MAIN_CODEX_ACCOUNT_ID && (!record?.credential || record.deletedAt != null)) return [];
668
+ if (record && expectedPoolGeneration !== undefined && record.generation !== expectedPoolGeneration) return [];
669
+ const claims: ManualResetCooldownClaim[] = [];
670
+ for (const scope of [undefined, "shared"] as const) {
671
+ const health = scope ? scopedHealthFor(accountId, scope) : upstreamHealth.get(accountId);
672
+ if (!health || health.cooldownSource !== "reset-derived" || health.probeLeaseId !== undefined
673
+ || !Number.isFinite(health.cooldownUntil) || !(health.cooldownUntil! > now)) continue;
674
+ const leaseId = randomUUID();
675
+ const cooldownGeneration = health.cooldownGeneration ?? 0;
676
+ const next = { ...health, probeLeaseId: leaseId, probeLeaseGeneration: cooldownGeneration, lastProbeAt: now };
677
+ if (scope) setScopedHealth(accountId, scope, next);
678
+ else upstreamHealth.set(accountId, next);
679
+ const probe = { accountId, scope, leaseId, cooldownGeneration };
680
+ claims.push(record ? { kind: "pool", probe: {
681
+ ...probe, credentialGeneration: record.generation, credentialReplacedAt: record.replacedAt,
682
+ } } : { kind: "main", probe });
683
+ }
684
+ return claims;
685
+ }
686
+
687
+ export type ManualResetRefreshLineage = Readonly<{
688
+ fromGeneration: number;
689
+ toGeneration: number;
690
+ provenance: CodexRefreshProvenance;
691
+ }>;
692
+
693
+ type ManualResetQuotaProof = CodexQuotaRecoveryProbeProof & {
694
+ refreshLineage?: ManualResetRefreshLineage;
695
+ };
696
+
697
+ /** Main proof is checked by the already-owned auth operation, never by a Pool record. */
698
+ export function settleManualResetCooldown(
699
+ config: OcxConfig,
700
+ claim: ManualResetCooldownClaim,
701
+ recovered: boolean,
702
+ proof: ManualResetQuotaProof = {},
703
+ now = Date.now(),
704
+ ): boolean {
705
+ if (!recovered) return settleCooldownRecoveryLease(claim.probe, false, now);
706
+ const eligible = manualResetAccountEligible(config, claim.probe.accountId);
707
+ if (claim.kind === "main") return settleCooldownRecoveryLease(claim.probe, eligible, now);
708
+ const lineage = proof.refreshLineage;
709
+ // Equal wall-clock replacement stamps do not establish ancestry. Manual +1
710
+ // recovery additionally needs the actual forced-refresh result for this edge.
711
+ const ownedGeneration = proof.credentialGeneration === claim.probe.credentialGeneration
712
+ || (proof.credentialGeneration === claim.probe.credentialGeneration + 1
713
+ && lineage?.fromGeneration === claim.probe.credentialGeneration
714
+ && lineage.toGeneration === proof.credentialGeneration
715
+ && (lineage.provenance === "self-refresh" || lineage.provenance === "joined-lineage"));
716
+ return settleCodexQuotaRecoveryProbe(claim.probe, eligible && ownedGeneration, proof, now);
717
+ }
718
+
645
719
  /** Settle one background recovery claim without mutating account-wide outcome state. */
646
720
  export function settleCodexQuotaRecoveryProbe(
647
721
  claim: CodexQuotaRecoveryProbeClaim,
@@ -665,9 +739,16 @@ export function settleCodexQuotaRecoveryProbe(
665
739
  : proofGeneration === claim.credentialGeneration + 1
666
740
  && currentRecord?.replacedAt === claim.credentialReplacedAt
667
741
  && isCodexAccountGenerationLive(claim.accountId, proofGeneration));
668
- const fenced = (health.cooldownGeneration ?? 0) === claim.cooldownGeneration
669
- && (health.probeLeaseGeneration ?? 0) === claim.cooldownGeneration
670
- && generationFenced;
742
+ return settleCooldownRecoveryLease(claim, recovered && generationFenced, now);
743
+ }
744
+
745
+ function settleCooldownRecoveryLease(claim: CooldownRecoveryLease, recovered: boolean, now: number): boolean {
746
+ const health = claim.scope ? scopedHealthFor(claim.accountId, claim.scope) : upstreamHealth.get(claim.accountId);
747
+ if (!health || health.probeLeaseId !== claim.leaseId) return false;
748
+ const fenced = (claim.scope === undefined || claim.scope === "shared")
749
+ && health.cooldownSource === "reset-derived"
750
+ && (health.cooldownGeneration ?? 0) === claim.cooldownGeneration
751
+ && (health.probeLeaseGeneration ?? 0) === claim.cooldownGeneration;
671
752
  if (!recovered || !fenced) {
672
753
  const released = withProbeLeaseReleased(health, now);
673
754
  if (claim.scope) setScopedHealth(claim.accountId, claim.scope, released);
package/src/codex/shim.ts CHANGED
@@ -1069,6 +1069,7 @@ export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cl
1069
1069
  const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n");
1070
1070
  return `@echo off\r
1071
1071
  rem ${SHIM_MARKER}\r
1072
+ setlocal\r
1072
1073
  ${windowsBatchSet("OCX_REAL_CODEX", realCodexPath)}\r
1073
1074
  ${windowsBatchSet("OCX_BUN", bunPath)}\r
1074
1075
  ${windowsBatchSet("OCX_CLI", cliPath)}\r
@@ -1115,6 +1116,9 @@ export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath:
1115
1116
  const tokenFile = serviceApiTokenFilePath();
1116
1117
  return `#!/usr/bin/env pwsh
1117
1118
  # ${SHIM_MARKER}
1119
+ $hadApiAuthToken = Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN
1120
+ $priorApiAuthToken = $env:OPENCODEX_API_AUTH_TOKEN
1121
+ try {
1118
1122
  if (-not $env:OPENCODEX_API_AUTH_TOKEN -and (Test-Path -LiteralPath ${psString(tokenFile)})) {
1119
1123
  $env:OPENCODEX_API_AUTH_TOKEN = (Get-Content -Raw -LiteralPath ${psString(tokenFile)}).Trim()
1120
1124
  }
@@ -1147,7 +1151,12 @@ if (-not $skipEnsure) {
1147
1151
  }
1148
1152
  }
1149
1153
  & ${psString(realCodexPath)} @args
1150
- exit $LASTEXITCODE
1154
+ $codexExitCode = $LASTEXITCODE
1155
+ } finally {
1156
+ if ($hadApiAuthToken) { $env:OPENCODEX_API_AUTH_TOKEN = $priorApiAuthToken }
1157
+ else { Remove-Item Env:\\OPENCODEX_API_AUTH_TOKEN -ErrorAction SilentlyContinue }
1158
+ }
1159
+ exit $codexExitCode
1151
1160
  `;
1152
1161
  }
1153
1162
 
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  chmodSync,
3
3
  closeSync,
4
- constants,
5
4
  fchmodSync,
6
5
  fstatSync,
7
6
  lstatSync,
@@ -121,7 +120,7 @@ function writePrivateTempFile(
121
120
  timeoutMemoKey: string,
122
121
  onCreated: () => void,
123
122
  ): void {
124
- const descriptor = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
123
+ const descriptor = openSync(path, "wx", 0o600);
125
124
  onCreated();
126
125
  try {
127
126
  if (process.platform === "win32") {
@@ -142,7 +141,7 @@ async function writePrivateTempFileAsync(
142
141
  timeoutMemoKey: string,
143
142
  onCreated: () => void,
144
143
  ): Promise<void> {
145
- const descriptor = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
144
+ const descriptor = openSync(path, "wx", 0o600);
146
145
  onCreated();
147
146
  try {
148
147
  if (process.platform === "win32") {
@@ -1,5 +1,5 @@
1
1
  import {
2
- closeSync, constants, fchmodSync, fstatSync, linkSync, lstatSync,
2
+ closeSync, fchmodSync, fstatSync, linkSync, lstatSync,
3
3
  openSync, unlinkSync, writeFileSync,
4
4
  } from "node:fs";
5
5
  import { dirname } from "node:path";
@@ -15,10 +15,12 @@ export class InitialConfigPublicationError extends Error {
15
15
  readonly publication: PublicationState,
16
16
  readonly residualTemp: boolean,
17
17
  readonly hardLinkUnavailable: boolean,
18
- options?: ErrorOptions,
18
+ options?: ErrorOptions & { hardeningFailed?: boolean },
19
19
  ) {
20
- super(hardLinkUnavailable
21
- ? "Initial config requires hard-link publication; the filesystem or its permissions denied it."
20
+ super(options?.hardeningFailed
21
+ ? "Initial config permissions could not be secured. Choose an OPENCODEX_HOME location that supports private file permissions (NTFS ACLs on Windows), then rerun `ocx init`."
22
+ : hardLinkUnavailable
23
+ ? "Initial config requires hard-link publication; the filesystem or its permissions denied it. Inspect the config directory before retrying. Choose an OPENCODEX_HOME location that supports hard links and private file permissions, then rerun `ocx init`."
22
24
  : "Initial config publication did not finish.", options);
23
25
  this.name = "InitialConfigPublicationError";
24
26
  }
@@ -90,10 +92,13 @@ export function publishInitialConfigNoReplace(
90
92
  let failure: unknown;
91
93
  let failed = false;
92
94
  let hardLinkUnavailable = false;
95
+ let hardeningFailed = false;
93
96
  let residualTemp = false;
94
97
  try {
95
- fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
98
+ fd = openSync(temp, "wx", 0o600);
99
+ hardeningFailed = true;
96
100
  (io.harden ?? hardenInitialConfig)(fd, temp, target);
101
+ hardeningFailed = false;
97
102
  verifyPrivateTemp(fd, temp);
98
103
  (io.write ?? ((descriptor: number, value: string) => writeFileSync(descriptor, value, { encoding: "utf8" })))(fd, bytes);
99
104
  verifyPrivateTemp(fd, temp);
@@ -126,7 +131,7 @@ export function publishInitialConfigNoReplace(
126
131
  }
127
132
  }
128
133
  if (failed || residualTemp) {
129
- throw new InitialConfigPublicationError(publication, residualTemp, hardLinkUnavailable, { cause: failure });
134
+ throw new InitialConfigPublicationError(publication, residualTemp, hardLinkUnavailable, { cause: failure, hardeningFailed });
130
135
  }
131
136
  return !collided;
132
137
  }
@@ -10,7 +10,7 @@
10
10
  },
11
11
  {
12
12
  "path": "package.json",
13
- "sha256": "a2c26a283356c9af4cfa97cc0b716e338b17fde951d9c71fc9450944dde31a24"
13
+ "sha256": "8c97c97d74c4f42c86a4165fdcaeb5d3bce90b7d0bc2ed085593702f63cea20d"
14
14
  },
15
15
  {
16
16
  "path": "scripts/model-metadata.source.json",
@@ -366,11 +366,11 @@
366
366
  },
367
367
  {
368
368
  "path": "src/adapters/openai-responses.ts",
369
- "sha256": "db01d0ade13c4e241e4818091884f689d6dd85ab5d7656832265d6f04097ab55"
369
+ "sha256": "c1ede0d8538bbefdd60e85eac045992fc740971fa93b3529cc5417de2b091e40"
370
370
  },
371
371
  {
372
- "path": "src/adapters/opencode-go.ts",
373
- "sha256": "37b3aad6259736d1973eca1444f61184e0329404bbdbd1c4326953c76f401192"
372
+ "path": "src/adapters/opencode-go-additional-tools.ts",
373
+ "sha256": "42090cb27e20503d3d6ad0680b1a45b918b19d878c2e6db6ed031ca57f17c1ee"
374
374
  },
375
375
  {
376
376
  "path": "src/adapters/registry.ts",
@@ -384,6 +384,10 @@
384
384
  "path": "src/adapters/responses-tool-schema.ts",
385
385
  "sha256": "affeb179617d9abfb8cd18d47b132ff02d05c4438e5395b90103c7b03c184f55"
386
386
  },
387
+ {
388
+ "path": "src/adapters/routed-agent-messages.ts",
389
+ "sha256": "3617b10e10db2ebfd573a479f7c345c254c49f3b139cc2d0290011c007c05c8b"
390
+ },
387
391
  {
388
392
  "path": "src/adapters/run-turn-queue.ts",
389
393
  "sha256": "31b12f11c9c35f3e499c418bf943762f299755d8a68d7bba9fa9371fa5684a43"
@@ -506,7 +510,7 @@
506
510
  },
507
511
  {
508
512
  "path": "src/claude/inbound-content-options.ts",
509
- "sha256": "0c402f2034bd6a01f0d7a338e011bdf4edd404c5acc9406985db6d6f6beeb3f0"
513
+ "sha256": "5bd23dd382a52ed889ad5b42535f86cd214bc35617fdbf80c8daa8f34a4a763d"
510
514
  },
511
515
  {
512
516
  "path": "src/claude/inbound-debug.ts",
@@ -946,7 +950,7 @@
946
950
  },
947
951
  {
948
952
  "path": "src/codex/auth-api.ts",
949
- "sha256": "626192f1207e2721c78c181b2a436411577520126ae14ec324ec23e08cd5a0e7"
953
+ "sha256": "10ea42a877146d9d4adca8590c0207affa9a12c4dc429d14441cd256cd88e796"
950
954
  },
951
955
  {
952
956
  "path": "src/codex/auth-collision.ts",
@@ -1070,7 +1074,7 @@
1070
1074
  },
1071
1075
  {
1072
1076
  "path": "src/codex/features.ts",
1073
- "sha256": "a42c3891a7978d27b974d68a3b5abecc7c3650e7d4d6f17c9e96ff52536f0a1e"
1077
+ "sha256": "2d2e3ff6e5c0cddd31188fbe76f38c5fc50bce83b0800ff3a44fa36a2362aa7e"
1074
1078
  },
1075
1079
  {
1076
1080
  "path": "src/codex/forward-transport-headers.ts",
@@ -1208,6 +1212,10 @@
1208
1212
  "path": "src/codex/model-entitlements.ts",
1209
1213
  "sha256": "a21db9fc98bfad86aaaf6aff404c2ee1e3990c1b1598c7d9ab7af91af1c2b601"
1210
1214
  },
1215
+ {
1216
+ "path": "src/codex/multi-agent-mode-policy.ts",
1217
+ "sha256": "8559eddf8c697993997778665daf21060ae1971c289fbb70bc971e5bbef6084a"
1218
+ },
1211
1219
  {
1212
1220
  "path": "src/codex/native-main-admission.ts",
1213
1221
  "sha256": "1f4956299bf243980bb06c7ddcfdadf71497cd874d3c1c49cb28465f27f6c85f"
@@ -1366,7 +1374,7 @@
1366
1374
  },
1367
1375
  {
1368
1376
  "path": "src/codex/reset-credit-auto-redeem.ts",
1369
- "sha256": "3dd462d5e8c5ddccea10c999f2228a4cadb00424f2dc65586578d144c4b0c843"
1377
+ "sha256": "7d5a443e6fa6b4c36b8daa59d6d2ca8131ac7a086b3e85bc51d2b337ff43047d"
1370
1378
  },
1371
1379
  {
1372
1380
  "path": "src/codex/reset-credit-operation-ledger.ts",
@@ -1378,7 +1386,7 @@
1378
1386
  },
1379
1387
  {
1380
1388
  "path": "src/codex/routing.ts",
1381
- "sha256": "6ffbfef59a981164bda8fbc238f6155e89a06aaab13586d88d97c6c9f26ce1ec"
1389
+ "sha256": "31009533a9eeb8aab35f77bd626de9c01b15a74d5e06ee98282c5b07572cf2b9"
1382
1390
  },
1383
1391
  {
1384
1392
  "path": "src/codex/runtime.ts",
@@ -1386,7 +1394,7 @@
1386
1394
  },
1387
1395
  {
1388
1396
  "path": "src/codex/shim.ts",
1389
- "sha256": "16cbf109b0c0057d058a8eccc1114201baa29d0444547a22b45c53bf6f15b3bc"
1397
+ "sha256": "08c28d3daafaeee903ddab78ff396d6a6bf46118893640ae316ece111e8a3e38"
1390
1398
  },
1391
1399
  {
1392
1400
  "path": "src/codex/subagent-defaults.ts",
@@ -1470,11 +1478,11 @@
1470
1478
  },
1471
1479
  {
1472
1480
  "path": "src/config/atomic-write.ts",
1473
- "sha256": "96e61002678c6f425518aece2e3fb4fd3ae49e25c375d337eecd676ae5d859c6"
1481
+ "sha256": "e4e0e2c3bb3adc7a2e02c7411718d4faad8417870c27143742ba72f9d6f6ab3a"
1474
1482
  },
1475
1483
  {
1476
1484
  "path": "src/config/initialize.ts",
1477
- "sha256": "12e07b539b32fbd7b4d96f0a92c3ba8c520c46aaea41a15f604d2f965daddde4"
1485
+ "sha256": "eddee7d5cbef36444f4d73d193b5e16bbe623d5fbf408ef4a47b40bc9daefa3c"
1478
1486
  },
1479
1487
  {
1480
1488
  "path": "src/config/paths.ts",
@@ -2378,7 +2386,7 @@
2378
2386
  },
2379
2387
  {
2380
2388
  "path": "src/lib/state-store-registrations.ts",
2381
- "sha256": "f108d262fb54d753458631b9bb16c22474ea398181331b9a2cb87d9b487864c7"
2389
+ "sha256": "7aa40b75b379f4b8f4c450bf3a0eeea2d0d7619c919be09df3aba73b58f9f57d"
2382
2390
  },
2383
2391
  {
2384
2392
  "path": "src/lib/state-store-sweeper.ts",
@@ -2662,7 +2670,7 @@
2662
2670
  },
2663
2671
  {
2664
2672
  "path": "src/providers/default-aliases.ts",
2665
- "sha256": "f6a830d5144af65167ecffadfcfafe4c69530f866f720ed4f1690c80203aff5c"
2673
+ "sha256": "190c2b85e92ff3d686efefa4c7dac0db75a4897b670e5ee5ff3d3e8055d4eb35"
2666
2674
  },
2667
2675
  {
2668
2676
  "path": "src/providers/derive.ts",
@@ -2802,7 +2810,7 @@
2802
2810
  },
2803
2811
  {
2804
2812
  "path": "src/providers/registry.ts",
2805
- "sha256": "a77a52f5f62617b17a3a9dbbdbf06a8bfe4c8fc05bf1947e65d1b38ddb1607ea"
2813
+ "sha256": "e636da79c8dbabe1702fd00e9b47199bc00dde5e871b931a6e12598e970f7c20"
2806
2814
  },
2807
2815
  {
2808
2816
  "path": "src/providers/request-pacing.ts",
@@ -2978,7 +2986,7 @@
2978
2986
  },
2979
2987
  {
2980
2988
  "path": "src/router.ts",
2981
- "sha256": "efa46cc81115575bc3c47ce4910adf75b4ffd34674f27661b5d2f3d1e3c08363"
2989
+ "sha256": "9fca9c7d3736dcb4cd07b58af87ff3b19abe14280fff8e6ff9b053841c2e51b4"
2982
2990
  },
2983
2991
  {
2984
2992
  "path": "src/routing/analytics.ts",
@@ -3106,7 +3114,7 @@
3106
3114
  },
3107
3115
  {
3108
3116
  "path": "src/server/claude-messages.ts",
3109
- "sha256": "53dfb51007f96c1a3d037e7c6fe850fe726d5507d68d222b326427505111152c"
3117
+ "sha256": "09c50f4454e990ccd5161ec5acdc49da2c75bf3bc04c7534209081756eed2b31"
3110
3118
  },
3111
3119
  {
3112
3120
  "path": "src/server/direct-local-http.ts",
@@ -3162,7 +3170,7 @@
3162
3170
  },
3163
3171
  {
3164
3172
  "path": "src/server/live.ts",
3165
- "sha256": "6347a6dd273bb6d5d2c1ba9826497087edb59f0c61d4c10196ddfbd01f3b6205"
3173
+ "sha256": "5e68c7830a9a143f218bed2686dd794d0df7aa9a79cacc118c11b13690c630d4"
3166
3174
  },
3167
3175
  {
3168
3176
  "path": "src/server/local-management-read-client.ts",
@@ -3186,7 +3194,7 @@
3186
3194
  },
3187
3195
  {
3188
3196
  "path": "src/server/management/agent-settings-routes.ts",
3189
- "sha256": "1d91b0c0e58fde53bfe17b581d0fae125f86afbdad436b8a9c2ec9e4fff45a1c"
3197
+ "sha256": "a771aa9da4a4b2b0efebd4367f10b792347881baec056bc375ee8821373f8df8"
3190
3198
  },
3191
3199
  {
3192
3200
  "path": "src/server/management/api-access.ts",
@@ -3486,7 +3494,11 @@
3486
3494
  },
3487
3495
  {
3488
3496
  "path": "src/server/responses/collaboration.ts",
3489
- "sha256": "031603ccc5195ea7d74df7349464ad949739c0020f9cf179a4da36f566b5d921"
3497
+ "sha256": "5982f3f249f5e3e1da217029597c56e1d154a1226e816d884c29c31b28ae125e"
3498
+ },
3499
+ {
3500
+ "path": "src/server/responses/combo-session-recall.ts",
3501
+ "sha256": "4401cbfadaeaeff14dd14c8afb25f0b8e61cadc1009393b35fe9bd25c6796f20"
3490
3502
  },
3491
3503
  {
3492
3504
  "path": "src/server/responses/combo-stream-preflight.ts",
@@ -3494,7 +3506,7 @@
3494
3506
  },
3495
3507
  {
3496
3508
  "path": "src/server/responses/compact.ts",
3497
- "sha256": "7a154e9c46cb6ebe31ca2b5d9a649cc4b6b120925e6e8b4844d16635be276938"
3509
+ "sha256": "205d9eaf2ce0f7c3d1af3bd58f7e3167479deec2c3a99e3743747f4fcb205e91"
3498
3510
  },
3499
3511
  {
3500
3512
  "path": "src/server/responses/context-overflow.ts",
@@ -3502,7 +3514,7 @@
3502
3514
  },
3503
3515
  {
3504
3516
  "path": "src/server/responses/core.ts",
3505
- "sha256": "1e3f30886b003f96c3e1ca47de0a1196c55a46a9d1b2fc65f0090d2d0655af9c"
3517
+ "sha256": "2d90436a0b09a6455c6bf666d49b5e4bbd1236fd38f4375a83b8af6e3dfe84d4"
3506
3518
  },
3507
3519
  {
3508
3520
  "path": "src/server/responses/empty-completion-guard.ts",
@@ -3706,7 +3718,7 @@
3706
3718
  },
3707
3719
  {
3708
3720
  "path": "src/types/config.ts",
3709
- "sha256": "aa4ac106c1be6312e328be2b61d77a0c5ba6de969814a189f5a994bda662c301"
3721
+ "sha256": "ec0942e86fcbb8a7ac4e8fb2bd44e6b180b35782e3d0dc15c85c6061d4998bfa"
3710
3722
  },
3711
3723
  {
3712
3724
  "path": "src/types/provider.ts",
@@ -21,6 +21,7 @@ import {
21
21
  } from "../combos/failover";
22
22
  import { reconcileComboWarningMemos } from "../combos/request";
23
23
  import { reconcileComboRotationState } from "../combos/resolve";
24
+ import { reconcileComboRecall } from "../server/responses/combo-session-recall";
24
25
  import { listLiveComboTargetKeys } from "../combos/types";
25
26
  import {
26
27
  listLiveConfigOwnershipRoots,
@@ -111,6 +112,7 @@ export const STATE_STORE_REGISTRATIONS = [
111
112
  { name: "model-cache-history", reconcileGeneration: reconcileModelCacheGeneration },
112
113
  { name: "pool-rotation", reconcileGeneration: reconcilePoolRotationState },
113
114
  { name: "combo-rotation", reconcileGeneration: reconcileComboRotationState },
115
+ { name: "combo-session-recall", reconcileGeneration: reconcileComboRecall },
114
116
  { name: "guardian-backoff", reconcileGeneration: reconcileGuardianBackoff },
115
117
  { name: "codex-reauth", reconcileGeneration: reconcileCodexReauthState },
116
118
  { name: "oauth-reauth", reconcileGeneration: reconcileOAuthReauthState },
@@ -15,7 +15,10 @@ export function effectiveProviderAlias(
15
15
  if (config?.providers) {
16
16
  const lower = regAlias.toLowerCase();
17
17
  const claimedByOther = Object.entries(config.providers).some(([name, p]) =>
18
- name !== providerName && typeof p.alias === "string" && p.alias.trim().toLowerCase() === lower
18
+ name !== providerName && (
19
+ name.toLowerCase() === lower
20
+ || (typeof p.alias === "string" && p.alias.trim().toLowerCase() === lower)
21
+ )
19
22
  );
20
23
  if (claimedByOther) return undefined;
21
24
  }
@@ -1539,8 +1539,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1539
1539
  modelDiscovery: {
1540
1540
  // Resolves against effectiveBaseUrl (registry baseUrl .../v1) to the same
1541
1541
  // canonical endpoint https://inference-api.nousresearch.com/v1/models.
1542
+ // Nous returns a mixed paid/free catalog whose JSON can exceed 256 KiB;
1543
+ // keep the provider-specific limit below the process-wide 4 MiB ceiling.
1542
1544
  path: "models",
1543
- maxResponseBytes: 262_144,
1545
+ maxResponseBytes: 1_048_576,
1544
1546
  maxModels: 512,
1545
1547
  },
1546
1548
  note: "Nous Research subscription gateway. OAuth device login with your own Portal account; mixed paid + :free models discovered live (fallback seed 2026-08-10: tencent/hy3:free, poolside/laguna-s-2.1:free, stepfun/step-3.7-flash:free, poolside/laguna-xs-2.1:free).",
@@ -1674,6 +1676,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1674
1676
  // Zen Go can close a Chat stream after a fully assembled function call without sending
1675
1677
  // finish_reason or [DONE] (#2260). The adapter still rejects incomplete argument JSON.
1676
1678
  openaiChatEofTolerance: true,
1679
+ // Go rejects reasoning.encrypted_content with previous_response_id (#3838).
1680
+ // Use explicit replay history and the existing stateless Responses policy.
1681
+ statelessResponses: true,
1677
1682
  /* [Decision Log]
1678
1683
  - 목적과 의도: Route the exact models OpenCode Go documents on the Responses endpoint — GPT 5.6 Luna, Grok 4.6, and Muse Spark Contributor (#2617).
1679
1684
  - 기존 구현 및 제약 조건: The provider is mixed-wire but its provider-wide `openai-chat` adapter sent Luna to `/chat/completions`; explicit user `modelAdapters` entries must remain authoritative.
package/src/router.ts CHANGED
@@ -703,13 +703,16 @@ function routeModelInternal(
703
703
  throw new Error("provider alias '" + requestedProvider + "' is ambiguous: " + configuredMatches.map(([n]) => n).sort().join(", "));
704
704
  } else {
705
705
  // Pass 2: built-in registry aliases, only for providers that do NOT have an explicit alias override
706
- // and whose registry alias has not been claimed by another configured provider (#3531 review)
706
+ // and whose registry alias has not been claimed by another configured provider name or alias
707
707
  const registryMatches = Object.entries(config.providers).filter(([name, provider]) => {
708
708
  if (provider.alias !== undefined) return false;
709
709
  const regAlias = PROVIDER_REGISTRY.find(e => e.id === name)?.alias;
710
710
  if (!regAlias || regAlias.toLowerCase() !== requestedLower) return false;
711
711
  const claimedByOther = Object.entries(config.providers).some(([otherName, p]) =>
712
- otherName !== name && typeof p.alias === "string" && p.alias.trim().toLowerCase() === requestedLower
712
+ otherName !== name && (
713
+ otherName.toLowerCase() === requestedLower
714
+ || (typeof p.alias === "string" && p.alias.trim().toLowerCase() === requestedLower)
715
+ )
713
716
  );
714
717
  return !claimedByOther;
715
718
  });