@bitkyc08/opencodex 2.82.0 → 2.83.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.
Files changed (85) hide show
  1. package/gui/dist/assets/{App-Ds_qL7o8.js → App-Ba8uG696.js} +4 -4
  2. package/gui/dist/assets/{Tray-BqHF5Trd.js → Tray-IImbNPRy.js} +1 -1
  3. package/gui/dist/assets/{index-D3FBCNiP.js → index-BHRZdJuv.js} +12 -12
  4. package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-DiJO6sYY.js} +1 -1
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/cursor/catalog.ts +2 -2
  8. package/src/adapters/cursor/effort-map.ts +1 -1
  9. package/src/adapters/devin/live-models.ts +2 -2
  10. package/src/adapters/google-truncation.ts +22 -0
  11. package/src/adapters/google.ts +7 -3
  12. package/src/adapters/ollama-native.ts +18 -14
  13. package/src/adapters/openai-responses/passthrough.ts +2 -0
  14. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  15. package/src/adapters/openai-responses/reasoning.ts +11 -1
  16. package/src/bridge/errors.ts +7 -1
  17. package/src/claude/context-windows.ts +3 -3
  18. package/src/claude/desktop-picker.ts +1 -1
  19. package/src/cli/account-auth.ts +20 -12
  20. package/src/cli/resolve.ts +1 -1
  21. package/src/clients/config-export/model-metadata.ts +16 -0
  22. package/src/clients/config-export/zcode-store.ts +46 -11
  23. package/src/clients/config-export/zcode.ts +3 -5
  24. package/src/clients/config-export.ts +1 -1
  25. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  26. package/src/codex/auth-context.ts +8 -3
  27. package/src/codex/catalog/native-models.ts +1 -1
  28. package/src/codex/catalog/provider-models.ts +25 -0
  29. package/src/codex/desktop-app/darwin.ts +1 -1
  30. package/src/codex/desktop-app/handoff.ts +1 -1
  31. package/src/codex/desktop-app/linux.ts +1 -1
  32. package/src/codex/desktop-app/lock.ts +1 -1
  33. package/src/codex/desktop-app/types.ts +1 -1
  34. package/src/codex/desktop-app/windows.ts +1 -1
  35. package/src/codex/desktop-app-restart.ts +1 -1
  36. package/src/codex/main-account.ts +6 -7
  37. package/src/generated/compatibility-version.json +91 -75
  38. package/src/grok/inject.ts +245 -221
  39. package/src/grok/reset-coupon-ledger.ts +223 -57
  40. package/src/grok/reset-coupons.ts +13 -6
  41. package/src/integrations/cursor-detect.ts +1 -1
  42. package/src/lib/errors.ts +25 -0
  43. package/src/lib/redact.ts +2 -2
  44. package/src/oauth/index.ts +1 -1
  45. package/src/oauth/meta-muse-device.ts +1 -1
  46. package/src/oauth/types.ts +1 -1
  47. package/src/providers/anthropic-fast.ts +1 -1
  48. package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
  49. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  50. package/src/providers/devin-provider-merge-migration.ts +1 -1
  51. package/src/providers/github-copilot-auto.ts +169 -0
  52. package/src/providers/model-presets.ts +1 -1
  53. package/src/providers/muse-subscription-usage.ts +1 -1
  54. package/src/providers/registry/entries-core.ts +6 -6
  55. package/src/providers/registry/entries-extended.ts +4 -0
  56. package/src/providers/registry/model-seeds.ts +3 -3
  57. package/src/providers/xai-fast-model.ts +1 -1
  58. package/src/server/inbound-body-admission.ts +24 -7
  59. package/src/server/index/link-listener.ts +30 -17
  60. package/src/server/index/serve-options.ts +8 -2
  61. package/src/server/index.ts +1 -1
  62. package/src/server/lifecycle.ts +31 -4
  63. package/src/server/management/grok-coupon-routes.ts +277 -52
  64. package/src/server/management/link-routes.ts +42 -20
  65. package/src/server/management/route-registry.ts +2 -2
  66. package/src/server/relay-eager.ts +2 -0
  67. package/src/server/relay.ts +18 -7
  68. package/src/server/request-log-failure-attribution.ts +3 -0
  69. package/src/server/request-log-terminal-status.ts +186 -0
  70. package/src/server/request-log.ts +15 -118
  71. package/src/server/responses/adapter-continuation.ts +9 -1
  72. package/src/server/responses/adapter-dispatch.ts +65 -6
  73. package/src/server/responses/compact.ts +3 -2
  74. package/src/server/responses/core-opaque-recovery.ts +5 -5
  75. package/src/server/responses/core.ts +6 -6
  76. package/src/server/responses/empty-completion-guard.ts +18 -2
  77. package/src/server/responses/jev-model-invoke.ts +1 -1
  78. package/src/server/responses/passthrough-dispatch.ts +115 -60
  79. package/src/server/responses/passthrough-execution.ts +3 -3
  80. package/src/server/responses/request-transport.ts +125 -5
  81. package/src/server/responses/run-turn-execution.ts +13 -5
  82. package/src/server/responses/sidecar-execution.ts +5 -0
  83. package/src/server/responses/spend-storage-error.ts +33 -0
  84. package/src/types/request.ts +5 -0
  85. package/src/usage/expected-prices.ts +1 -1
@@ -6,6 +6,7 @@ import { warnIfCodexCatalogRefreshPending } from "./account-catalog-refresh";
6
6
  import { isCodexResetCreditOperationId } from "../codex/reset-credit-recovery";
7
7
  import { BROWSER_LAUNCH_FAILED_NOTICE } from "../lib/browser-launch-notice";
8
8
  import {
9
+ RuntimeApiError,
9
10
  CliUsageError,
10
11
  printData,
11
12
  readSecretLine,
@@ -465,18 +466,25 @@ async function grokResetCoupons(argv: string[], deps: RuntimeApiDeps): Promise<v
465
466
  }
466
467
  rejectArgs(args, USAGE);
467
468
  const accountId = rawId ? (rawId === "main" ? "__main__" : rawId) : undefined;
468
- const result = consume
469
- ? await runtimeRequest("/api/grok/reset-coupons/consume", {
470
- method: "POST",
471
- // Spread, not `operationId: undefined`: the server distinguishes an absent
472
- // key from a caller who asked for a stable idempotency identity.
473
- body: JSON.stringify({ accountId, tokenId, ...(operationId === undefined ? {} : { operationId }) }),
474
- }, deps)
475
- : await runtimeRequest(
476
- `/api/grok/reset-coupons${accountId ? `?accountId=${encodeURIComponent(accountId)}` : ""}`,
477
- {},
478
- deps,
479
- );
469
+ const effectiveOperationId = consume ? operationId ?? crypto.randomUUID() : undefined;
470
+ let result: unknown;
471
+ try {
472
+ result = consume
473
+ ? await runtimeRequest("/api/grok/reset-coupons/consume", {
474
+ method: "POST", body: JSON.stringify({ accountId, tokenId, operationId: effectiveOperationId }),
475
+ }, deps)
476
+ : await runtimeRequest(`/api/grok/reset-coupons${accountId ? `?accountId=${encodeURIComponent(accountId)}` : ""}`, {}, deps);
477
+ } catch (error) {
478
+ const apiError = error instanceof RuntimeApiError ? error : undefined;
479
+ const body = apiError?.body as { error?: { code?: string } } | null | undefined;
480
+ const code = body && typeof body === "object" ? body.error?.code : undefined;
481
+ if (consume && (!apiError || (!apiError.code && (
482
+ (!code && apiError.status >= 500) || ["attempt_unresolved", "attempt_in_progress", "attempt_reconcile_failed", "operation_state_changed", "redeem_failed"].includes(code ?? "")
483
+ )))) {
484
+ throw new RuntimeApiError(`Coupon redemption outcome is unconfirmed. Preserve --operation-id ${effectiveOperationId}; reuse it to inspect this attempt and do not create a replacement operation.`, apiError?.status ?? 503, apiError?.body ?? null);
485
+ }
486
+ throw error;
487
+ }
480
488
  printData(result, wantsJson);
481
489
  }
482
490
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `ocx resolve` — the machine-readable runtime resolution surface for an embedding shell.
3
3
  *
4
- * D5 of devlog/_plan/260921_app_runtime_ownership/: the desktop shell must stop resolving
4
+ * D5 of devlog/_fin/260921_app_runtime_ownership/: the desktop shell must stop resolving
5
5
  * the config home, the port and liveness itself. The tuned probe budgets in
6
6
  * src/server/proxy-liveness.ts exist because a shell-side reimplementation answered
7
7
  * "nobody listening" twice and started duplicate proxies; this verb exposes that module's
@@ -2,6 +2,7 @@
2
2
  import { getModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
3
3
  import { SCHEMA_REQUIRED_OUTPUT_BUDGET } from "./constants";
4
4
  import { expandFastExportModels } from "./fast-models";
5
+ import { sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
5
6
  import type { OpencodeCatalogModel, ExportModel, ExportClientId, ManagedContribution } from "./contracts";
6
7
  import type { OcxConfig } from "../../types";
7
8
  import { shouldInjectApiAuthHeader } from "../../codex/inject";
@@ -84,6 +85,21 @@ export function inputModalitiesForClient(
84
85
  return kept.length > 0 ? kept : null;
85
86
  }
86
87
 
88
+ /**
89
+ * Effort ladder a ZCode surface can offer as picker values, or undefined when the row
90
+ * has nothing selectable.
91
+ *
92
+ * Both ZCode exports — the legacy `v2/config.json` block's `reasoning.variants` and the
93
+ * `v2/provider_config.json` store's `optionSpecs.reasoningLevel.values` — carry the same
94
+ * Codex vocabulary: the client validates the chosen level against the emitted list and
95
+ * forwards it verbatim, and `/v1/responses` reads that vocabulary natively. `none` is a
96
+ * Codex omit-sentinel, not a picker option, so it never survives into the values.
97
+ */
98
+ export function zcodeSelectableEfforts(efforts: readonly string[] | undefined): string[] | undefined {
99
+ const selectable = sanitizeCodexReasoningEfforts(efforts)?.filter(effort => effort !== "none");
100
+ return selectable && selectable.length > 0 ? selectable : undefined;
101
+ }
102
+
87
103
  /**
88
104
  * Input modalities opencode's model schema accepts (opencode.ai/config.json, both
89
105
  * `modalities.input` and `modalities.output`). Wider than our internal `text | image | audio`
@@ -1,6 +1,6 @@
1
1
  // ZCode personal provider store export.
2
2
  import type { ExportContext, ManagedContribution, ManagedFragment } from "./contracts";
3
- import { authoritativeContextWindow, inputModalitiesForClient, normalizeExportModels } from "./model-metadata";
3
+ import { authoritativeContextWindow, inputModalitiesForClient, normalizeExportModels, zcodeSelectableEfforts } from "./model-metadata";
4
4
  import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
5
5
  import { formatSelectorConjunction } from "../../integrations/merge";
6
6
 
@@ -49,10 +49,30 @@ export interface ZcodeStoreProviderRule {
49
49
  };
50
50
  }
51
51
 
52
+ /**
53
+ * One model rule, as the client's own store loader accepts it.
54
+ *
55
+ * The capability fields are the client's schema, not ours: ZCode 3.14's CLI
56
+ * bundle parses each `providerModelRules` entry with a strict object whose
57
+ * `config.properties` admits `inputFormat.{supportsText..supportsPdf}` (booleans)
58
+ * and whose `config.optionSpecs` admits `reasoningLevel.values` (non-empty,
59
+ * deduplicated) plus an optional compiled `map`. With no `map`, the chosen level
60
+ * is validated against `values` and forwarded verbatim to the wire field the
61
+ * `openai-responses` api type uses — `reasoning.effort`, which is exactly the
62
+ * Codex vocabulary our ladder already carries, so no mapping is emitted.
63
+ */
52
64
  export interface ZcodeStoreModelRule {
53
65
  providerId: string;
54
66
  modelId: string;
55
- config: { properties: { contextWindow: number } };
67
+ config: {
68
+ properties: {
69
+ contextWindow?: number;
70
+ inputFormat?: { supportsImage: true };
71
+ };
72
+ optionSpecs?: {
73
+ reasoningLevel: { values: string[] };
74
+ };
75
+ };
56
76
  }
57
77
 
58
78
  /**
@@ -77,7 +97,7 @@ export function zcodeStoreSchemaEstablished(parsed: unknown): boolean {
77
97
  * carrying only the model would match another provider's rule for the same
78
98
  * model. The conjunction grammar reserves `,` and `]`, and a model id holding
79
99
  * either cannot be addressed unambiguously — that row ships without its
80
- * context window rather than with a selector that points somewhere else.
100
+ * model rule rather than with a selector that points somewhere else.
81
101
  */
82
102
  function modelRuleSelector(modelId: string): string | null {
83
103
  return formatSelectorConjunction([
@@ -128,12 +148,18 @@ export function buildZcodeStoreProviderRule(ctx: ExportContext): ZcodeStoreProvi
128
148
 
129
149
  /**
130
150
  * Everything this project owns inside the store: one provider rule, plus one
131
- * model rule per row that has an authoritative context window.
151
+ * model rule per row that has anything authoritative to assert.
132
152
  *
133
- * A row without one ships no model rule at all, which is the same
134
- * authoritative-window rule the legacy block follows — an emitted stand-in
135
- * would be a guessed capability. Reasoning ladders have no counterpart here
136
- * that this project has observed the client produce, so none is written.
153
+ * A model rule now exists when the row has any of: an authoritative context
154
+ * window, image input, or a selectable effort ladder. A row with none of the
155
+ * three ships no rule at all, which is the same no-guessed-capability rule the
156
+ * legacy block follows. Image input is asserted only when the catalog declares
157
+ * it — `supportsImage: true` is what gates the client's attachment UI, and a
158
+ * text-only row keeps the client's own defaults rather than being pinned to
159
+ * `false`. `reasoningLevel.values` carries the same ladder (minus the `none`
160
+ * sentinel) the legacy block's `reasoning.variants` carries, so both surfaces
161
+ * offer the picker the catalog declared instead of the store silently losing
162
+ * reasoning and vision the way #5348 lost the provider itself.
137
163
  */
138
164
  export function buildZcodeStoreContribution(ctx: ExportContext): ManagedContribution {
139
165
  const fragments: ManagedFragment[] = [{
@@ -141,15 +167,24 @@ export function buildZcodeStoreContribution(ctx: ExportContext): ManagedContribu
141
167
  value: buildZcodeStoreProviderRule(ctx),
142
168
  }];
143
169
  for (const model of normalizeExportModels(ctx.models)) {
144
- if (inputModalitiesForClient("pi", model.inputModalities) === null) continue;
170
+ const input = inputModalitiesForClient("pi", model.inputModalities);
171
+ if (input === null) continue;
145
172
  const contextWindow = authoritativeContextWindow(model.contextWindow);
146
- if (contextWindow === undefined) continue;
173
+ const supportsImage = input.includes("image");
174
+ const efforts = zcodeSelectableEfforts(model.reasoningEfforts);
175
+ if (contextWindow === undefined && !supportsImage && efforts === undefined) continue;
147
176
  const selector = modelRuleSelector(model.namespaced);
148
177
  if (selector === null) continue;
149
178
  const rule: ZcodeStoreModelRule = {
150
179
  providerId: OPENCODE_PROVIDER_ID,
151
180
  modelId: model.namespaced,
152
- config: { properties: { contextWindow } },
181
+ config: {
182
+ properties: {
183
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
184
+ ...(supportsImage ? { inputFormat: { supportsImage: true } } : {}),
185
+ },
186
+ ...(efforts !== undefined ? { optionSpecs: { reasoningLevel: { values: efforts } } } : {}),
187
+ },
153
188
  };
154
189
  fragments.push({ path: [...ZCODE_STORE_MODEL_RULES_PATH, selector], value: rule });
155
190
  }
@@ -1,7 +1,6 @@
1
1
  // ZCode config export.
2
2
  import type { ExportContext, ManagedContribution } from "./contracts";
3
- import { normalizeExportModels, inputModalitiesForClient, exportModelLabel, authoritativeContextWindow, singleFragment } from "./model-metadata";
4
- import { sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
3
+ import { normalizeExportModels, inputModalitiesForClient, exportModelLabel, authoritativeContextWindow, singleFragment, zcodeSelectableEfforts } from "./model-metadata";
5
4
  import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
6
5
 
7
6
 
@@ -86,9 +85,8 @@ export function buildZcodeClientConfig(ctx: ExportContext): ZcodeGeneratedConfig
86
85
  // `ultra` when present: ZCode forwards the selected variant to the wire field its
87
86
  // kind uses — `reasoning.effort` on `openai`, which is what `/v1/responses` reads
88
87
  // natively. Set `defaultVariant` only when it survives that filter.
89
- const efforts = sanitizeCodexReasoningEfforts(model.reasoningEfforts)
90
- ?.filter(effort => effort !== "none");
91
- if (efforts && efforts.length > 0) {
88
+ const efforts = zcodeSelectableEfforts(model.reasoningEfforts);
89
+ if (efforts !== undefined) {
92
90
  const defaultVariant = model.defaultReasoningEffort?.trim().toLowerCase();
93
91
  entry.reasoning = {
94
92
  enabled: true,
@@ -1367,7 +1367,7 @@ function buildAsideContribution(ctx: ExportContext): ManagedContribution {
1367
1367
  * the keyed `providers`, the `models` ARRAY whose identity is `id`, the
1368
1368
  * `openai-completions` dialect, the loopback placeholder, and the
1369
1369
  * `thinkingLevelMap` levels. Evidence:
1370
- * `devlog/_plan/260912_omo_client_integration/001_omo_contract.md`.
1370
+ * `devlog/_fin/260912_omo_client_integration/001_omo_contract.md`.
1371
1371
  *
1372
1372
  * The flag is passed HERE as well as in the spec's `build`, which is the one
1373
1373
  * thing Prime and Aside do not do. They pass the default on both paths, so they
@@ -88,14 +88,13 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
88
88
  const now = Date.now();
89
89
  const quota = getMainPolicyQuota();
90
90
  const observedAt = quota?.credits?.observedAt;
91
- // getMainPolicyQuota binds evidence to the observed physical identity. Only renew
92
- // valid formerly spendable funds; admission retains the actual five-minute clock.
91
+ // Positive evidence renews before expiry; unavailable evidence keeps discovery
92
+ // eligible under the same pacing. Admission still requires fresh spendable funds.
93
93
  return codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)
94
94
  && !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
95
95
  && codexUsageLimitResetAt(quota, undefined, now) !== undefined
96
- && observedAt !== undefined && observedAt > 0
97
- && now - observedAt >= MAIN_CREDIT_RENEWAL_AGE_MS
98
- && hasSpendableCodexCredits(quota, observedAt);
96
+ && (!hasSpendableCodexCredits(quota, now)
97
+ || (observedAt !== undefined && now - observedAt >= MAIN_CREDIT_RENEWAL_AGE_MS));
99
98
  };
100
99
  const status = getMainAccountHardLockStatus(config);
101
100
  if (status.state !== "blocked" && !creditRecoveryNeeded()) { mainHardLockRecoveryAttempt = undefined; return; }
@@ -1,5 +1,6 @@
1
1
  import { bindPoolCreditPolicy, poolCreditHoldResetAt, poolContextCreditHoldResetAt } from "./pool-credit-policy";
2
2
  import { hasSpendableCodexCredits } from "./quota-types";
3
+ import { nextQuotaQueryDelay } from "./quota-query-backoff";
3
4
  import { noteMainAccountActivity } from "./main-account-external-usage";
4
5
  import { codexAccountPriorityFailbackEnabled } from "./account-priority";
5
6
  import type { PoolQuotaWriter } from "./quota-types";
@@ -553,8 +554,11 @@ function assertPoolAccountCredits(accountId: string, resetAt: number | undefined
553
554
  export class CodexMainAccountCreditsOffError extends CodexAccountCooldownError {
554
555
  readonly resetAt?: number;
555
556
 
556
- constructor(resetAt?: number) {
557
- super(MAIN_CODEX_ACCOUNT_ID, resetAt ?? 0);
557
+ constructor(resetAt?: number, creditRecovery = false) {
558
+ // Keep the included reset as policy evidence, but let consented callers retry
559
+ // within the base metadata recovery interval even when backoff grows longer.
560
+ super(MAIN_CODEX_ACCOUNT_ID, creditRecovery
561
+ ? Math.min(resetAt ?? Infinity, Date.now() + nextQuotaQueryDelay()) : resetAt ?? 0);
558
562
  this.name = "CodexMainAccountCreditsOffError";
559
563
  this.resetAt = resetAt;
560
564
  this.message = "Codex main account reached its usage limit, and spending ChatGPT credits is off or no fresh spendable balance is available."
@@ -778,7 +782,8 @@ function assertMainAccountPolicy(
778
782
  const status = getMainAccountHardLockStatus(config);
779
783
  if (status.state === "blocked") throw new CodexMainAccountHardLockError(status.resetAt, status.thresholds);
780
784
  const creditsResetAt = mainCreditsHoldResetAt(config);
781
- if (creditsResetAt !== undefined) throw new CodexMainAccountCreditsOffError(creditsResetAt);
785
+ if (creditsResetAt !== undefined) throw new CodexMainAccountCreditsOffError(creditsResetAt,
786
+ codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID));
782
787
  }
783
788
  // Only an admitted request is opencodex's own use of the main account. Counting a refused one
784
789
  // would hide outside usage from the warning exactly while the lock is holding.
@@ -93,7 +93,7 @@ export const CONFIGURED_NATIVE_OPENAI_TEMPLATE_MODEL = NATIVE_GPT61_SOL_MODEL;
93
93
  *
94
94
  * `gpt-daybreak-blue-latest` stays gated. It has no shipped catalog row anywhere, so absence is
95
95
  * the only signal that exists for it, and the ungating decision was scoped to the flagships.
96
- * Evidence: devlog/_plan/260904_flagship_native_always_visible/.
96
+ * Evidence: devlog/_fin/260904_flagship_native_always_visible/.
97
97
  */
98
98
  export const ACCOUNT_GATED_NATIVE_OPENAI_MODELS: ReadonlySet<string> = new Set([
99
99
  NATIVE_DAYBREAK_BLUE_MODEL,
@@ -1,3 +1,6 @@
1
+ import { fetchCopilotAutoModels, copilotPickerModels, copilotRequiresAuto } from "../../providers/github-copilot-auto";
2
+ import { resolveGithubCopilotTransport } from "../../providers/github-copilot-transport";
3
+ import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
1
4
  import { isAnthropicOAuthInstance } from "../../providers/anthropic-instance";
2
5
  import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
3
6
  import { initialModelSelectionPending } from "../../providers/initial-model-selection";
@@ -164,6 +167,7 @@ export function observedModelsAuthResolver(
164
167
  },
165
168
  };
166
169
  }
170
+ /** Project captured discovery through config hints and retention; Auto-only Copilot exposes only Auto. */
167
171
  export async function fetchProviderModelsWithAuth(
168
172
  captured: CapturedProviderGather,
169
173
  ttlMs: number,
@@ -282,6 +286,27 @@ export async function fetchProviderModelsWithAuth(
282
286
  : { apiKey: await resolveModelsAuthToken(name, prov), observed: false }
283
287
  : resolveAuth.resolve(name, prov));
284
288
  const apiKey = auth.apiKey;
289
+ if (name === "github-copilot" && apiKey) {
290
+ const transport = resolveGithubCopilotTransport({ ...prov, apiKey }, prov.authMode === "oauth"
291
+ ? resolveCopilotApiBaseUrl(auth.oauthApiBaseUrl) : undefined);
292
+ try {
293
+ const models = await fetchCopilotAutoModels(transport, AbortSignal.timeout(8_000), undefined, ttlMs);
294
+ const picker = copilotPickerModels(prov, models).map(/** Apply configured hints to the permission-filtered account catalog without changing upstream metadata. */
295
+ model => applyProviderConfigHints(name, prov, {
296
+ id: model.id, provider: name,
297
+ ...catalogHintsFromModelsApiItem(name, model as ProviderModelsApiItem),
298
+ }, contextCap, metadataModelIdCaseFold, captured.effectiveAlias));
299
+ if (!isCurrentCacheGeneration()) return observed(configured, "degraded");
300
+ markProviderDiscoveryOk(name, picker.length);
301
+ // Retain callable manual/combo selectors, but do not advertise named selection
302
+ // for an Auto-only account even when its saved configuration still contains it.
303
+ return observed(copilotRequiresAuto(prov, models)
304
+ ? picker : withConfiguredRetention(picker, { warnDrops: true }), "authoritative");
305
+ } catch {
306
+ if (isCurrentCacheGeneration()) markProviderDiscoveryFailed(name, { reason: "provider" });
307
+ return observed(configured, "degraded");
308
+ }
309
+ }
285
310
  const maySendAnthropicDiscovery = () => {
286
311
  if (!isAnthropicOAuthInstance(name) || prov.authMode !== "oauth") return true;
287
312
  if (!mayResolveModelsOAuth(name, prov)) return false;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * macOS adapter for the Codex desktop-app restart.
3
3
  *
4
- * Measured shape (devlog/_plan/260913_cross_platform_desktop_app_restart/001_platform_topology.md):
4
+ * Measured shape (devlog/_fin/260913_cross_platform_desktop_app_restart/001_platform_topology.md):
5
5
  *
6
6
  * 15901 1 /Applications/ChatGPT.app/Contents/MacOS/ChatGPT
7
7
  * 16733 15901 /Applications/ChatGPT.app/Contents/Resources/codex ... app-server ...
@@ -23,7 +23,7 @@
23
23
  * it passes `allowHandoff: false` so it can only ever take the direct path or refuse.
24
24
  * Recursion is structurally impossible rather than merely unlikely.
25
25
  *
26
- * Design: devlog/_plan/260913_cross_platform_desktop_app_restart/020_phase2_detached_self_handoff.md
26
+ * Design: devlog/_fin/260913_cross_platform_desktop_app_restart/020_phase2_detached_self_handoff.md
27
27
  */
28
28
  import { spawn } from "node:child_process";
29
29
  import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeSync } from "node:fs";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Linux adapter for the Codex desktop-app restart.
3
3
  *
4
- * Measured shape (devlog/_plan/260913_cross_platform_desktop_app_restart/001_platform_topology.md):
4
+ * Measured shape (devlog/_fin/260913_cross_platform_desktop_app_restart/001_platform_topology.md):
5
5
  *
6
6
  * /usr/bin/chatgpt -> /usr/lib/chatgpt/codex-launcher (2-line sh script)
7
7
  * that execs /usr/lib/chatgpt/ChatGPT
@@ -22,7 +22,7 @@
22
22
  * itself. An unconditional unlink would let a late release destroy somebody else's
23
23
  * live lock, which is exactly the mutual exclusion this file exists to provide.
24
24
  *
25
- * Design: `devlog/_plan/260913_cross_platform_desktop_app_restart/020_phase2_detached_self_handoff.md` §4.1.
25
+ * Design: `devlog/_fin/260913_cross_platform_desktop_app_restart/020_phase2_detached_self_handoff.md` §4.1.
26
26
  */
27
27
  import { mkdirSync, openSync, closeSync, writeSync, readFileSync, unlinkSync, renameSync } from "node:fs";
28
28
  import { join, dirname } from "node:path";
@@ -7,7 +7,7 @@
7
7
  * PID-reuse and fail-closed reasoning from being re-derived three times, once per
8
8
  * operating system, with two of the three getting it subtly wrong.
9
9
  *
10
- * Design and audit history: `devlog/_plan/260913_cross_platform_desktop_app_restart/`.
10
+ * Design and audit history: `devlog/_fin/260913_cross_platform_desktop_app_restart/`.
11
11
  */
12
12
  import { sep } from "node:path";
13
13
 
@@ -7,7 +7,7 @@
7
7
  * AUMID. The shape is DesktopAppAdapter so the ladder, not this file, owns
8
8
  * PID-reuse re-verification and the fail-closed sequencing.
9
9
  *
10
- * Measured (devlog/_plan/260913_cross_platform_desktop_app_restart/001_platform_topology.md §3):
10
+ * Measured (devlog/_fin/260913_cross_platform_desktop_app_restart/001_platform_topology.md §3):
11
11
  * OpenAI.Codex MSIX, ChatGPT.exe, InstallLocation under WindowsApps.
12
12
  */
13
13
  import { execFileSync } from "node:child_process";
@@ -29,7 +29,7 @@
29
29
  * never reported as "nothing to do". A stale picker is a much smaller problem than a
30
30
  * wrongly killed process.
31
31
  *
32
- * Design and audit history: devlog/_plan/260913_cross_platform_desktop_app_restart/.
32
+ * Design and audit history: devlog/_fin/260913_cross_platform_desktop_app_restart/.
33
33
  */
34
34
  import {
35
35
  acquireDesktopRestartLock,
@@ -510,8 +510,9 @@ async function resolveMainAccountToken(
510
510
  }
511
511
  const refresh = dependencies.refreshToken ?? refreshNativeMainGrant;
512
512
  let refreshed: OAuthCredentials;
513
+ if (signal.aborted) throw signal.reason;
513
514
  try {
514
- refreshed = await refresh(locked.refreshToken, { signal });
515
+ refreshed = await refresh(locked.refreshToken, { signal: refreshTimeout });
515
516
  } catch (cause) {
516
517
  // A refusal already classified at the endpoint keeps that verdict. An injected refresh
517
518
  // threw an ordinary Error and has only its message, so prose still decides there --
@@ -535,16 +536,14 @@ async function resolveMainAccountToken(
535
536
  }
536
537
  throw new MainAccountTokenRefreshError(reason, { cause });
537
538
  }
538
- // The refresh may resolve after the caller went away (an implementation that does
539
- // not observe the signal, or an abort landing in the window between resolution and
540
- // commit). A cancelled request's late refresh must not rewrite auth.json on behalf
541
- // of a request that no longer exists -- the same fence the reauth twin applies
542
- // before its own commit above.
543
- if (dependencies.signal?.aborted) throw dependencies.signal.reason;
539
+ // Once the exchange starts, only its own timeout may abort it: a successful
540
+ // rotating grant retires the old refresh token. Save the only live grant before
541
+ // honoring caller cancellation, while retaining the external-writer guards.
544
542
  const result = persistRefreshedMainAuthJson(locked, refreshed);
545
543
  // A permitted in-flight success speaks only for its own profile and grant.
546
544
  clearMainRefreshGrantRejection(mainRefreshGrantKey(locked)!);
547
545
  if (dependencies.preserveReauth !== true) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
546
+ if (dependencies.signal?.aborted) throw dependencies.signal.reason;
548
547
  return result;
549
548
  }),
550
549
  { waitMs: 30_000, signal },