@bitkyc08/opencodex 2.50.0 → 2.51.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.
- package/bin/ocx.mjs +222 -71
- package/gui/dist/assets/{index-C39tnjXO.js → index-D7BdZpZm.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/qoder/adapter.ts +69 -1
- package/src/adapters/qoder/scaffold-guard.ts +233 -0
- package/src/claude/agents-inject.ts +29 -5
- package/src/claude/desktop-3p.ts +31 -3
- package/src/claude/gateway-cache.ts +12 -21
- package/src/cli/capabilities.ts +28 -0
- package/src/cli/claude-agent-startup-sync.ts +26 -1
- package/src/cli/claude.ts +138 -20
- package/src/cli/config-command.ts +67 -1
- package/src/cli/connect.ts +181 -14
- package/src/cli/dispatch.ts +53 -9
- package/src/cli/doctor.ts +9 -2
- package/src/cli/ensure-desired-integrations.ts +10 -0
- package/src/cli/gui-pair-client.ts +1 -12
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +367 -0
- package/src/cli/index.ts +94 -30
- package/src/cli/launcher-context.ts +1 -1
- package/src/cli/registry.ts +43 -3
- package/src/cli/status.ts +325 -5
- package/src/cli/version-skew.ts +4 -1
- package/src/cli.ts +2 -2
- package/src/client/catalog-compatibility.ts +192 -0
- package/src/client/connect.ts +31 -0
- package/src/client/hub-client.ts +52 -0
- package/src/client/hub-state.ts +214 -0
- package/src/codex/account-usability.ts +48 -12
- package/src/codex/auth-api.ts +49 -5
- package/src/codex/catalog/effort.ts +67 -8
- package/src/codex/catalog/sync.ts +85 -0
- package/src/codex/codex-write-lock.ts +11 -2
- package/src/codex/desired-state.ts +47 -1
- package/src/codex/inject-coordination.ts +10 -5
- package/src/codex/inject.ts +26 -10
- package/src/codex/loopback-target.ts +45 -0
- package/src/codex/routing.ts +48 -1
- package/src/codex/runtime.ts +37 -3
- package/src/codex/sync.ts +29 -9
- package/src/codex/warmup.ts +21 -4
- package/src/config/pending-teardown.ts +1 -1
- package/src/config.ts +126 -12
- package/src/generated/compatibility-version.json +136 -76
- package/src/grok/status.ts +9 -1
- package/src/integrations/config-io.ts +54 -1
- package/src/lib/bun-runtime.ts +1 -1
- package/src/lib/gui-pair-capability.ts +27 -0
- package/src/lib/local-destinations.ts +162 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/process-control.ts +130 -20
- package/src/lib/service-secrets.ts +28 -0
- package/src/lib/test-home-guard.ts +49 -0
- package/src/providers/opencode-go-transport.ts +9 -1
- package/src/providers/quota.ts +5 -1
- package/src/providers/registry.ts +34 -5
- package/src/remote/hub-state.ts +182 -0
- package/src/server/auth-cors.ts +5 -0
- package/src/server/chat-completions.ts +6 -3
- package/src/server/claude-messages.ts +7 -1
- package/src/server/hub-state.ts +98 -0
- package/src/server/index.ts +124 -6
- package/src/server/management/api-access.ts +14 -3
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/cursor-integration-routes.ts +13 -4
- package/src/server/proxy-liveness.ts +7 -1
- package/src/server/request-log-conversation.ts +41 -1
- package/src/server/responses/codex-auth-error.ts +18 -1
- package/src/server/responses/codex-ws-exchange.ts +36 -4
- package/src/server/responses/codex-ws-wire.ts +75 -4
- package/src/server/responses/compact.ts +20 -9
- package/src/server/responses/core.ts +57 -10
- package/src/server/responses/policy-fallback.ts +7 -1
- package/src/server/system-env-shell.ts +14 -2
- package/src/server/system-env.ts +106 -14
- package/src/service.ts +906 -94
- package/src/types/config.ts +57 -4
- package/src/update/badge.ts +3 -2
- package/src/update/index.ts +317 -64
- package/src/update/install-detection.d.mts +6 -0
- package/src/update/install-detection.mjs +73 -0
- package/src/update/job.ts +101 -49
- package/src/update/pnpm-global-install.d.mts +144 -0
- package/src/update/pnpm-global-install.mjs +591 -0
- package/src/update/pnpm-invocation.d.mts +43 -0
- package/src/update/pnpm-invocation.mjs +141 -0
- package/src/update/registry-integrity.d.mts +16 -0
- package/src/update/registry-integrity.mjs +37 -0
- package/src/update/transactional-install.d.mts +1 -1
- package/src/update/transactional-install.mjs +101 -7
- package/src/update/tray-update-plan.mjs +1 -1
- package/src/vision/plan.ts +13 -3
- package/src/vision/routed-describe.ts +51 -20
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
resolveCodexModelEntitlements,
|
|
43
43
|
type CodexModelEntitlementSnapshot,
|
|
44
44
|
} from "../model-entitlements";
|
|
45
|
+
import { isAccountNeedsReauth } from "../account-runtime-state";
|
|
46
|
+
import { codexAccountLogLabel, fallbackCodexAccountLogLabel } from "../account-label";
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, findSupportedNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readCodexCatalogPathForHome, readConfiguredAutoReviewModel, readNativeBaseline } from "./parsing";
|
|
@@ -1708,6 +1710,75 @@ export function finalizeAutoReviewModelOverride(
|
|
|
1708
1710
|
return applyAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), sourceModels);
|
|
1709
1711
|
}
|
|
1710
1712
|
|
|
1713
|
+
/**
|
|
1714
|
+
* Why an account-gated native model stopped being offered, but only when the answer is one the
|
|
1715
|
+
* operator can act on.
|
|
1716
|
+
*
|
|
1717
|
+
* Suppression is an omission: the row is never built, so there is no catalog entry for a reason
|
|
1718
|
+
* to ride on and no downstream consumer that could explain it later. #4212's reporter watched
|
|
1719
|
+
* their models disappear and reasonably concluded the proxy was broken, because every surface
|
|
1720
|
+
* that changed said nothing about the account that caused it.
|
|
1721
|
+
*
|
|
1722
|
+
* Returns `undefined` for the ordinary case — an account that is simply not entitled to a gated
|
|
1723
|
+
* model. That is the default state for most installations, it is not news, and warning about it
|
|
1724
|
+
* on every sync would bury the one case that matters. A credential the operator must repair is
|
|
1725
|
+
* the case that matters, so that is the only one this speaks up about.
|
|
1726
|
+
*
|
|
1727
|
+
* Accounts are named with the durable `p`-prefixed log label, the same identifier the dashboard
|
|
1728
|
+
* shows, never the raw pool id or the email.
|
|
1729
|
+
*/
|
|
1730
|
+
export function gatedNativeReauthSuppressionReason(args: {
|
|
1731
|
+
snapshot: CodexModelEntitlementSnapshot;
|
|
1732
|
+
slug: string;
|
|
1733
|
+
eligibleAccountIds?: ReadonlySet<string>;
|
|
1734
|
+
needsReauth: (accountId: string) => boolean;
|
|
1735
|
+
label: (accountId: string) => string;
|
|
1736
|
+
}): string | undefined {
|
|
1737
|
+
const observed = [...args.snapshot.modelsByAccount.keys()]
|
|
1738
|
+
.filter(accountId => !args.eligibleAccountIds || args.eligibleAccountIds.has(accountId))
|
|
1739
|
+
// Only accounts that could actually have served THIS model. An account upstream positively
|
|
1740
|
+
// denied is not why the model is missing, and blaming it would send the operator to repair a
|
|
1741
|
+
// credential that was never going to help. `unknown` has to stay in: an account whose roster
|
|
1742
|
+
// could not be confirmed reports `unknown` rather than `granted`, and a credential stuck on
|
|
1743
|
+
// a failed refresh is exactly that account.
|
|
1744
|
+
.filter(accountId => (
|
|
1745
|
+
codexModelEntitlementStateForAccount(args.snapshot, accountId, args.slug) !== "denied"
|
|
1746
|
+
));
|
|
1747
|
+
const stuck = observed.filter(accountId => args.needsReauth(accountId));
|
|
1748
|
+
if (stuck.length === 0) return undefined;
|
|
1749
|
+
const names = stuck.map(accountId => args.label(accountId)).sort().join(", ");
|
|
1750
|
+
return stuck.length === observed.length
|
|
1751
|
+
? `every Codex account that could serve it needs reauthentication (${names})`
|
|
1752
|
+
: `${stuck.length} of ${observed.length} Codex accounts that could serve it need reauthentication (${names})`;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/** Durable, operator-facing label for a pool account id; never the raw id or the email. */
|
|
1756
|
+
function gatedNativeAccountLabel(config: OcxConfig, accountId: string): string {
|
|
1757
|
+
// Direct mode narrows eligibility to the native main credential, so this is the account most
|
|
1758
|
+
// likely to be named here. `codexAuthContextLogLabel` calls it "main" everywhere else; hashing
|
|
1759
|
+
// it into a `p`-prefixed digest would name the one account the operator cannot look up.
|
|
1760
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) return "main";
|
|
1761
|
+
const account = (config.codexAccounts ?? []).find(candidate => candidate.id === accountId);
|
|
1762
|
+
return account ? codexAccountLogLabel(account) : fallbackCodexAccountLogLabel(accountId);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
const warnedGatedNativeSuppression = new Set<string>();
|
|
1766
|
+
|
|
1767
|
+
/** Test seam: the warn-once memory is process-global, so a case needs to be able to clear it. */
|
|
1768
|
+
export function resetGatedNativeSuppressionWarningsForTests(): void {
|
|
1769
|
+
warnedGatedNativeSuppression.clear();
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function warnGatedNativeSuppressedOnce(slug: string, reason: string): void {
|
|
1773
|
+
const signature = `${slug}\u0000${reason}`;
|
|
1774
|
+
if (warnedGatedNativeSuppression.has(signature)) return;
|
|
1775
|
+
warnedGatedNativeSuppression.add(signature);
|
|
1776
|
+
console.warn(
|
|
1777
|
+
`[opencodex] catalog sync: ${slug} is not being offered because ${reason}. `
|
|
1778
|
+
+ "Sign in again to restore it.",
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1711
1782
|
/**
|
|
1712
1783
|
* Mescla o catálogo retido com os modelos visíveis e as configurações atuais,
|
|
1713
1784
|
* incluindo os nomes nativos. Tenta preservar o backup original e usa a permissão
|
|
@@ -1777,6 +1848,20 @@ function writeRetainedCatalogSync({
|
|
|
1777
1848
|
const unavailableGatedNativeSlugs = new Set([...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(slug => (
|
|
1778
1849
|
!availableBareGatedNativeSlugs.has(slug)
|
|
1779
1850
|
)));
|
|
1851
|
+
// #4212: this set is the whole record of a model vanishing, and it is a set of strings that
|
|
1852
|
+
// nothing downstream ever asks a question of. Explain it here, while the entitlement snapshot
|
|
1853
|
+
// that produced it is still in scope, because after this point the model is simply absent and
|
|
1854
|
+
// no later surface can tell "never entitled" apart from "the account broke this morning".
|
|
1855
|
+
for (const slug of unavailableGatedNativeSlugs) {
|
|
1856
|
+
const reason = gatedNativeReauthSuppressionReason({
|
|
1857
|
+
snapshot: modelEntitlements,
|
|
1858
|
+
slug,
|
|
1859
|
+
eligibleAccountIds: bareEligibleAccountIds,
|
|
1860
|
+
needsReauth: isAccountNeedsReauth,
|
|
1861
|
+
label: accountId => gatedNativeAccountLabel(config, accountId),
|
|
1862
|
+
});
|
|
1863
|
+
if (reason) warnGatedNativeSuppressedOnce(slug, reason);
|
|
1864
|
+
}
|
|
1780
1865
|
const suppressedBareNativeSlugs = new Set([
|
|
1781
1866
|
...desktopAllowlistSuppressedNativeSlugs(config),
|
|
1782
1867
|
...unavailableGatedNativeSlugs,
|
|
@@ -66,7 +66,7 @@ export type CodexWriteLockRefusalReason =
|
|
|
66
66
|
|
|
67
67
|
export type CodexWriteLockResult<T> =
|
|
68
68
|
| { status: "acquired"; value: T; waitedMs: number; lockId: string }
|
|
69
|
-
| { status: "skipped"; reason:
|
|
69
|
+
| { status: "skipped"; reason: CodexWriteLockSkipReason; waitedMs: number }
|
|
70
70
|
| { status: "busy"; reason: "deadline" | "cancelled"; retryable: true; waitedMs: number; lockId: string }
|
|
71
71
|
| {
|
|
72
72
|
status: "refused";
|
|
@@ -127,9 +127,18 @@ export interface CodexWriteCommitContext {
|
|
|
127
127
|
readonly coordinator: CodexCoordinatorTransaction;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Why an under-lock policy re-read refused the write.
|
|
132
|
+
*
|
|
133
|
+
* `hub-gated` is not the user's switch: a hub declines to rewrite its own local clients, and
|
|
134
|
+
* reporting that as "integration is OFF" sent operators hunting for a toggle they never set
|
|
135
|
+
* (#4236).
|
|
136
|
+
*/
|
|
137
|
+
export type CodexWriteLockSkipReason = "desired_disabled" | "desired_enabled" | "hub-gated";
|
|
138
|
+
|
|
130
139
|
/** A synchronous under-lock policy re-read proved the requested apply stale. */
|
|
131
140
|
export class CodexWriteLockSkipped extends Error {
|
|
132
|
-
constructor(readonly reason:
|
|
141
|
+
constructor(readonly reason: CodexWriteLockSkipReason) {
|
|
133
142
|
super(reason);
|
|
134
143
|
this.name = "CodexWriteLockSkipped";
|
|
135
144
|
}
|
|
@@ -76,11 +76,57 @@ type LocalClientSyncConfig = Pick<
|
|
|
76
76
|
"clientIntegrations" | "runtimeRole" | "unauthenticatedLoopbackListener"
|
|
77
77
|
>;
|
|
78
78
|
|
|
79
|
-
function localClientSyncAllowed(config: LocalClientSyncConfig): boolean {
|
|
79
|
+
export function localClientSyncAllowed(config: LocalClientSyncConfig): boolean {
|
|
80
80
|
return config.runtimeRole !== "hub"
|
|
81
81
|
|| config.unauthenticatedLoopbackListener?.enabled === true;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* The one sentence every hub-gated skip says (#4236).
|
|
86
|
+
*
|
|
87
|
+
* The gate is a reasonable decision; reporting it as "Codex integration is OFF" was not. An
|
|
88
|
+
* operator whose `clientIntegrations` says nothing — or says `true` — was told their own switch
|
|
89
|
+
* was off, and `ocx restore back` went further and blamed a competing writer that did not
|
|
90
|
+
* exist. Name the gate and name the key that opens it.
|
|
91
|
+
*/
|
|
92
|
+
export const HUB_GATED_SKIP_MESSAGE =
|
|
93
|
+
"This machine is a hub; it does not rewrite its own Codex/Grok/Claude configs unless "
|
|
94
|
+
+ "unauthenticatedLoopbackListener is enabled.";
|
|
95
|
+
|
|
96
|
+
/** Why a local-client write was skipped. The gate outranks the toggle: it is the surprising one. */
|
|
97
|
+
export type LocalClientSkipReason = "desired_disabled" | "hub-gated";
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* "hub-gated" is claimed only when the toggle is ON and the gate is what stopped the write.
|
|
101
|
+
* With the toggle OFF the gate is moot: telling that operator to enable the loopback listener
|
|
102
|
+
* would send them to a key that cannot make the sync happen — the mirror image of the defect
|
|
103
|
+
* this reason exists to fix.
|
|
104
|
+
*/
|
|
105
|
+
export function localClientSkipReason(
|
|
106
|
+
config: LocalClientSyncConfig,
|
|
107
|
+
client: DurableIntentClientId = "codex",
|
|
108
|
+
): LocalClientSkipReason {
|
|
109
|
+
return integrationEnabled(config, client) && !localClientSyncAllowed(config)
|
|
110
|
+
? "hub-gated"
|
|
111
|
+
: "desired_disabled";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Pick the skip message for a snapshot: today's toggle text, or the hub-gate sentence.
|
|
116
|
+
*
|
|
117
|
+
* `hubSuffix` states what the hub-gated path still did (a catalog refresh, say) so the
|
|
118
|
+
* composed line stays as informative as the toggle one it replaces.
|
|
119
|
+
*/
|
|
120
|
+
export function localClientSkipMessage(
|
|
121
|
+
config: LocalClientSyncConfig,
|
|
122
|
+
integrationOffMessage: string,
|
|
123
|
+
hubSuffix?: string,
|
|
124
|
+
client: DurableIntentClientId = "codex",
|
|
125
|
+
): string {
|
|
126
|
+
if (localClientSkipReason(config, client) !== "hub-gated") return integrationOffMessage;
|
|
127
|
+
return hubSuffix ? `${HUB_GATED_SKIP_MESSAGE} ${hubSuffix}` : HUB_GATED_SKIP_MESSAGE;
|
|
128
|
+
}
|
|
129
|
+
|
|
84
130
|
export function shouldSyncCodexOnStart(config: LocalClientSyncConfig): boolean {
|
|
85
131
|
// A hub is a server for OTHER machines: it must not rewrite its own host's
|
|
86
132
|
// Codex/Claude/Grok client configs on startup (interview decision Q6, and the
|
|
@@ -8,7 +8,8 @@ import { createHash } from "node:crypto";
|
|
|
8
8
|
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
9
9
|
|
|
10
10
|
import { atomicWriteFile } from "../config";
|
|
11
|
-
import type { CodexWriteLockResult } from "./codex-write-lock";
|
|
11
|
+
import type { CodexWriteLockResult, CodexWriteLockSkipReason } from "./codex-write-lock";
|
|
12
|
+
import { HUB_GATED_SKIP_MESSAGE } from "./desired-state";
|
|
12
13
|
import { inspectCodexCoordinatorPath } from "./coordinator-doctor";
|
|
13
14
|
import { JOURNAL_PATH } from "./journal";
|
|
14
15
|
import { updateIntegrationRecord } from "./integration-record";
|
|
@@ -443,16 +444,20 @@ export function recomputeInjectWitness(options: {
|
|
|
443
444
|
export function codexInjectLockOutcome(
|
|
444
445
|
result: Exclude<CodexWriteLockResult<unknown>, { status: "acquired" }>,
|
|
445
446
|
): { success: false; message: string; retryable: boolean } | {
|
|
446
|
-
success: true; status: "skipped"; skippedReason:
|
|
447
|
+
success: true; status: "skipped"; skippedReason: CodexWriteLockSkipReason; message: string;
|
|
447
448
|
} {
|
|
448
449
|
if (result.status === "skipped") {
|
|
449
450
|
return {
|
|
450
451
|
success: true,
|
|
451
452
|
status: "skipped",
|
|
452
453
|
skippedReason: result.reason,
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
454
|
+
// Three distinct facts, three sentences. The hub gate in particular must not borrow the
|
|
455
|
+
// toggle's wording — that is the phantom "integration is OFF" report from #4236.
|
|
456
|
+
message: result.reason === "hub-gated"
|
|
457
|
+
? `${HUB_GATED_SKIP_MESSAGE} No Codex config, catalog, cache, or history was changed.`
|
|
458
|
+
: result.reason === "desired_disabled"
|
|
459
|
+
? "Codex integration is OFF; no Codex config, catalog, cache, or history was changed."
|
|
460
|
+
: "Codex integration was re-enabled; native restore was skipped.",
|
|
456
461
|
};
|
|
457
462
|
}
|
|
458
463
|
if (result.status === "busy") {
|
package/src/codex/inject.ts
CHANGED
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
withConfigMutationLockSync,
|
|
10
10
|
} from "../config";
|
|
11
11
|
import { CodexWriteLockSkipped, withCodexWriteLock } from "./codex-write-lock";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
localClientSkipMessage,
|
|
14
|
+
localClientSkipReason,
|
|
15
|
+
shouldSyncCodexOnStart,
|
|
16
|
+
} from "./desired-state";
|
|
13
17
|
import { resolveCodexHistoryTransition } from "./history-transition";
|
|
14
18
|
import {
|
|
15
19
|
buildInjectWitness,
|
|
@@ -79,9 +83,9 @@ import {
|
|
|
79
83
|
type ManagedSubagentDefaults,
|
|
80
84
|
} from "./subagent-defaults";
|
|
81
85
|
import type { OcxConfig } from "../types";
|
|
82
|
-
import { isLoopbackHostname, shouldInjectApiAuthHeader } from "./loopback-target";
|
|
86
|
+
import { effectiveLoopbackListenerPort, isLoopbackHostname, shouldInjectApiAuthHeader } from "./loopback-target";
|
|
83
87
|
|
|
84
|
-
export { isLoopbackHostname, shouldInjectApiAuthHeader } from "./loopback-target";
|
|
88
|
+
export { effectiveLoopbackListenerPort, isLoopbackHostname, shouldInjectApiAuthHeader } from "./loopback-target";
|
|
85
89
|
|
|
86
90
|
// Ownership predicates live in `./injected-marker` so `journal.ts` can reach them
|
|
87
91
|
// without importing this module back. Re-exported for existing external callers.
|
|
@@ -214,8 +218,11 @@ export function standaloneCodexRoutingTarget(
|
|
|
214
218
|
"hostname" | "unauthenticatedLoopbackListener" | "codexDesktopAuthless" | "codexClientCompaction"
|
|
215
219
|
>,
|
|
216
220
|
): CodexRoutingTarget {
|
|
221
|
+
// An enabled listener with no `port` is the companion form: it answers on `port` itself,
|
|
222
|
+
// bound to 127.0.0.1 (#4236). Resolving it through the shared helper is what makes the
|
|
223
|
+
// one-port hub work without every writer repeating `?? port`.
|
|
217
224
|
const loopback = config?.unauthenticatedLoopbackListener;
|
|
218
|
-
const effectivePort =
|
|
225
|
+
const effectivePort = effectiveLoopbackListenerPort(config, port) ?? port;
|
|
219
226
|
const hostname = loopback?.enabled ? undefined : config?.hostname;
|
|
220
227
|
const requiresAdmissionToken = loopback?.enabled ? false : shouldInjectApiAuthHeader(config);
|
|
221
228
|
return {
|
|
@@ -887,7 +894,8 @@ export interface CodexInjectResult {
|
|
|
887
894
|
success: boolean;
|
|
888
895
|
message: string;
|
|
889
896
|
status?: "skipped";
|
|
890
|
-
|
|
897
|
+
/** `hub-gated` is the hub-role gate (#4236), distinct from the user's own OFF switch. */
|
|
898
|
+
skippedReason?: "desired_disabled" | "desired_enabled" | "hub-gated";
|
|
891
899
|
nativeSubagentDefaultsWarning?: string;
|
|
892
900
|
}
|
|
893
901
|
|
|
@@ -1261,12 +1269,17 @@ export async function injectCodexConfig(
|
|
|
1261
1269
|
|
|
1262
1270
|
if (eligibility.kind === "legacy-uncoordinated") {
|
|
1263
1271
|
const applyLegacy = (): CodexInjectResult | undefined => {
|
|
1264
|
-
|
|
1272
|
+
const legacyGateSnapshot = loadConfig();
|
|
1273
|
+
if (!shouldSyncCodexOnStart(legacyGateSnapshot)) {
|
|
1265
1274
|
return {
|
|
1266
1275
|
success: true,
|
|
1267
1276
|
status: "skipped",
|
|
1268
|
-
skippedReason:
|
|
1269
|
-
message:
|
|
1277
|
+
skippedReason: localClientSkipReason(legacyGateSnapshot),
|
|
1278
|
+
message: localClientSkipMessage(
|
|
1279
|
+
legacyGateSnapshot,
|
|
1280
|
+
"Codex integration is OFF; no Codex config, catalog, cache, or history was changed.",
|
|
1281
|
+
"No Codex config, catalog, cache, or history was changed.",
|
|
1282
|
+
),
|
|
1270
1283
|
};
|
|
1271
1284
|
}
|
|
1272
1285
|
runClientWriteGuard(options.beforeClientWrite);
|
|
@@ -1295,8 +1308,11 @@ export async function injectCodexConfig(
|
|
|
1295
1308
|
}),
|
|
1296
1309
|
},
|
|
1297
1310
|
(ctx) => {
|
|
1298
|
-
|
|
1299
|
-
|
|
1311
|
+
const gateSnapshot = loadConfig();
|
|
1312
|
+
if (!shouldSyncCodexOnStart(gateSnapshot)) {
|
|
1313
|
+
// Carry WHY under the lock: "the hub does not write its own clients" and "the user
|
|
1314
|
+
// turned Codex off" produce the same no-write and must not produce the same sentence.
|
|
1315
|
+
throw new CodexWriteLockSkipped(localClientSkipReason(gateSnapshot));
|
|
1300
1316
|
}
|
|
1301
1317
|
// N and C are held here. Reject stale client work before publishing a
|
|
1302
1318
|
// transition or capturing preimages; rejection must not compensate over
|
|
@@ -36,6 +36,51 @@ export function isLoopbackHostname(hostname: string | undefined): boolean {
|
|
|
36
36
|
);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Bind scope again, from the other side: a wildcard listener already answers on 127.0.0.1.
|
|
41
|
+
*
|
|
42
|
+
* This matters only for the port-less "companion" loopback listener. On `0.0.0.0`/`::` the
|
|
43
|
+
* public socket owns loopback on that port, so a companion bound to the same port would
|
|
44
|
+
* collide; on a specific non-loopback address (a tailnet or LAN IP) 127.0.0.1 is free.
|
|
45
|
+
*/
|
|
46
|
+
export function isWildcardHostname(hostname: string | undefined): boolean {
|
|
47
|
+
const normalized = (hostname ?? "").trim().toLowerCase().replace(/\.$/, "").replace(/^\[(.*)\]$/, "$1");
|
|
48
|
+
if (normalized === "*" || normalized === "0") return true;
|
|
49
|
+
// Every spelling of the IPv4 unspecified address ("0.0.0.0", "00.0.0.000", …).
|
|
50
|
+
if (/^(0+\.){3}0+$/.test(normalized)) return true;
|
|
51
|
+
// Every spelling of the IPv6 unspecified address ("::", "::0", "0::", "0:0:0:0:0:0:0:0", …):
|
|
52
|
+
// nothing but zero groups and colons. A dual-stack `::` bind answers on 127.0.0.1 as well.
|
|
53
|
+
return normalized.includes(":") && /^[0:]+$/.test(normalized);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Is `{ enabled: true }` with no port legal for this bind address?
|
|
58
|
+
*
|
|
59
|
+
* The companion form binds `127.0.0.1:<proxy port>`, which is exactly the one-port hub shape:
|
|
60
|
+
* remote clients dial `hostname:port`, local processes dial `127.0.0.1:port`, and every
|
|
61
|
+
* hardcoded `http://127.0.0.1:<proxy port>` integration works with no rewriting. It is legal
|
|
62
|
+
* only when the public listener is NOT already holding that loopback address.
|
|
63
|
+
*/
|
|
64
|
+
export function loopbackCompanionAllowed(hostname: string | undefined): boolean {
|
|
65
|
+
return !isLoopbackHostname(hostname) && !isWildcardHostname(hostname);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The port local callers reach the unauthenticated listener on, or null when it is off.
|
|
70
|
+
*
|
|
71
|
+
* One resolver for every reader (#4236): an enabled listener with no `port` is the companion
|
|
72
|
+
* form and answers on the public port. Callers must not repeat `?? port` — the day the default
|
|
73
|
+
* changes, a forgotten site points a client config at a closed socket.
|
|
74
|
+
*/
|
|
75
|
+
export function effectiveLoopbackListenerPort(
|
|
76
|
+
config: Pick<OcxConfig, "unauthenticatedLoopbackListener"> | undefined,
|
|
77
|
+
publicPort: number,
|
|
78
|
+
): number | null {
|
|
79
|
+
const listener = config?.unauthenticatedLoopbackListener;
|
|
80
|
+
if (!listener?.enabled) return null;
|
|
81
|
+
return listener.port ?? publicPort;
|
|
82
|
+
}
|
|
83
|
+
|
|
39
84
|
export function shouldInjectApiAuthHeader(
|
|
40
85
|
config: Pick<OcxConfig, "hostname" | "unauthenticatedLoopbackListener"> | undefined,
|
|
41
86
|
): boolean {
|
package/src/codex/routing.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
selectPriorityTier,
|
|
20
20
|
} from "./pool-rotation";
|
|
21
21
|
import { CODEX_EXHAUSTED_USAGE_PERCENT, CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./quota";
|
|
22
|
-
import { isThirtyDayOnlyCodexPlan } from "./plan";
|
|
22
|
+
import { codexPlanKey, isThirtyDayOnlyCodexPlan } from "./plan";
|
|
23
23
|
import {
|
|
24
24
|
MAIN_CODEX_ACCOUNT_ID,
|
|
25
25
|
getMainAccountPlan,
|
|
@@ -1004,6 +1004,50 @@ export function isCodexAccountSoftAvoided(accountId: string, now = Date.now()):
|
|
|
1004
1004
|
return getCodexAccountSoftAvoidUntil(accountId, now) !== null;
|
|
1005
1005
|
}
|
|
1006
1006
|
|
|
1007
|
+
/**
|
|
1008
|
+
* Plan keys the operator excluded from automatic rotation. Absent or empty means no policy, so an
|
|
1009
|
+
* existing install rotates exactly as before. Compared with `codexPlanKey` because the stored plan
|
|
1010
|
+
* is an unrestricted provider string whose casing this repository does not control.
|
|
1011
|
+
*/
|
|
1012
|
+
function excludedCodexPoolPlanKeys(config: OcxConfig): ReadonlySet<string> | undefined {
|
|
1013
|
+
const configured = config.codexPool?.excludedPlans;
|
|
1014
|
+
if (!configured?.length) return undefined;
|
|
1015
|
+
const keys = configured
|
|
1016
|
+
.map(plan => codexPlanKey(plan))
|
|
1017
|
+
.filter((key): key is string => key !== undefined);
|
|
1018
|
+
return keys.length > 0 ? new Set(keys) : undefined;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Whether the operator's plan policy removes this account from automatic selection.
|
|
1023
|
+
*
|
|
1024
|
+
* Modelled on pause rather than usability: an excluded account keeps its credential, quota history,
|
|
1025
|
+
* and affinity, stays visible on the account surface, and is still reachable by explicit account
|
|
1026
|
+
* selection. Only automatic rotation skips it, which is the distinction #4211 asked for.
|
|
1027
|
+
*
|
|
1028
|
+
* It is checked in the same two places pause is checked, and that is not redundancy. The eligible
|
|
1029
|
+
* list is consulted only when routing picks a NEW account; an already-active or already-affined
|
|
1030
|
+
* account is served straight from {@link isCodexAccountSelectable}. A lapsed subscription leaves
|
|
1031
|
+
* behind exactly that account, so a policy that filtered only the eligible list would miss the case
|
|
1032
|
+
* it exists for.
|
|
1033
|
+
*
|
|
1034
|
+
* `__main__` is exempt. {@link getPoolAccountPlanForSelection} withholds the main plan during a
|
|
1035
|
+
* selection-only drain so routing never reads the fenced native credential for it, so a rule that
|
|
1036
|
+
* covered main would disagree with itself between drain and ordinary routing.
|
|
1037
|
+
*/
|
|
1038
|
+
function isCodexAccountPlanExcluded(
|
|
1039
|
+
config: OcxConfig,
|
|
1040
|
+
accountId: string,
|
|
1041
|
+
precomputed?: ReadonlySet<string>,
|
|
1042
|
+
): boolean {
|
|
1043
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) return false;
|
|
1044
|
+
// Callers that test a whole list pass the set once rather than rebuilding it per row.
|
|
1045
|
+
const excluded = precomputed ?? excludedCodexPoolPlanKeys(config);
|
|
1046
|
+
if (!excluded) return false;
|
|
1047
|
+
const plan = codexPlanKey(getPoolAccountPlan(config, accountId));
|
|
1048
|
+
return plan !== undefined && excluded.has(plan);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1007
1051
|
function isCodexAccountSelectable(
|
|
1008
1052
|
config: OcxConfig,
|
|
1009
1053
|
accountId: string,
|
|
@@ -1012,6 +1056,7 @@ function isCodexAccountSelectable(
|
|
|
1012
1056
|
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1013
1057
|
): boolean {
|
|
1014
1058
|
return !isCodexAccountPaused(config, accountId)
|
|
1059
|
+
&& !isCodexAccountPlanExcluded(config, accountId)
|
|
1015
1060
|
&& getCodexQuotaHealthSnapshot(accountId, quotaScope, now) === null
|
|
1016
1061
|
&& !isCodexAccountSoftAvoided(accountId, now)
|
|
1017
1062
|
&& isCodexAccountUsable(config, accountId, selectionOptions);
|
|
@@ -1242,10 +1287,12 @@ function getEligiblePoolAccounts(
|
|
|
1242
1287
|
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1243
1288
|
skipFailoverReadyCandidates = false,
|
|
1244
1289
|
): readonly string[] {
|
|
1290
|
+
const excludedPlans = excludedCodexPoolPlanKeys(config);
|
|
1245
1291
|
const ids = (config.codexAccounts ?? [])
|
|
1246
1292
|
.filter(account => isSelectableCodexPoolAccount(account)
|
|
1247
1293
|
&& account.id !== excludeId
|
|
1248
1294
|
&& !isCodexAccountPaused(config, account.id)
|
|
1295
|
+
&& !isCodexAccountPlanExcluded(config, account.id, excludedPlans)
|
|
1249
1296
|
&& !isAccountNeedsReauth(account.id)
|
|
1250
1297
|
&& (!skipFailoverReadyCandidates || !shouldFailover(config, account.id, now)))
|
|
1251
1298
|
.filter(account => getCodexQuotaHealthSnapshot(account.id, quotaScope, now) === null)
|
package/src/codex/runtime.ts
CHANGED
|
@@ -427,13 +427,47 @@ function sameRuntimeCommand(a: string, b: string): boolean {
|
|
|
427
427
|
return a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
-
/**
|
|
430
|
+
/**
|
|
431
|
+
* Rungs OpenCodex no longer lets the observed-runtime intersection remove, so a persisted
|
|
432
|
+
* diagnostic naming only these describes a policy that is gone rather than a live restriction.
|
|
433
|
+
* This is the single copy of the exemption: `catalog/effort.ts` imports it for the clamp
|
|
434
|
+
* predicate, and a leftover file written before the exemption must not keep warning about
|
|
435
|
+
* rungs the next sync will stop removing.
|
|
436
|
+
*/
|
|
437
|
+
export const UNCLAMPABLE_REASONING_EFFORTS: ReadonlySet<string> = new Set(["max", "ultra"]);
|
|
438
|
+
|
|
439
|
+
/** Removals that still describe a real restriction, ignoring rungs nothing clamps any more. */
|
|
440
|
+
export function liveRemovedEfforts(
|
|
441
|
+
diagnostic: EffortClampDiagnostic | null | undefined,
|
|
442
|
+
): readonly string[] {
|
|
443
|
+
if (!diagnostic) return [];
|
|
444
|
+
return diagnostic.removedEfforts.filter(effort => !UNCLAMPABLE_REASONING_EFFORTS.has(effort));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* True when a persisted clamp diagnostic still applies to the currently selected runtime.
|
|
449
|
+
*
|
|
450
|
+
* Two ways a stored diagnostic stops describing reality:
|
|
451
|
+
*
|
|
452
|
+
* 1. Every rung it names is one nothing clamps any more, so the file is inert until the next
|
|
453
|
+
* sync unlinks it.
|
|
454
|
+
* 2. The binary at that path was upgraded in place. Windows updates Codex without moving the
|
|
455
|
+
* executable, so path equality alone kept a 0.135.0 observation "current" for a 0.154.0
|
|
456
|
+
* runtime whose own bundled catalog carried the rungs the diagnostic claimed were missing.
|
|
457
|
+
* A known version mismatch therefore wins over a path match; an unknown version on either
|
|
458
|
+
* side stays conservative, because absence of a version is not evidence of an upgrade.
|
|
459
|
+
*/
|
|
431
460
|
export function effortClampAppliesToRuntime(
|
|
432
461
|
diagnostic: EffortClampDiagnostic | null | undefined,
|
|
433
462
|
runtime: Pick<ResolvedCodexRuntime, "command" | "version">,
|
|
434
463
|
): boolean {
|
|
435
|
-
if (!diagnostic || diagnostic.
|
|
436
|
-
if (sameRuntimeCommand(diagnostic.runtimePath, runtime.command))
|
|
464
|
+
if (!diagnostic || liveRemovedEfforts(diagnostic).length === 0) return false;
|
|
465
|
+
if (sameRuntimeCommand(diagnostic.runtimePath, runtime.command)) {
|
|
466
|
+
if (diagnostic.runtimeVersion && runtime.version) {
|
|
467
|
+
return diagnostic.runtimeVersion === runtime.version;
|
|
468
|
+
}
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
437
471
|
return Boolean(
|
|
438
472
|
diagnostic.runtimeVersion
|
|
439
473
|
&& runtime.version
|
package/src/codex/sync.ts
CHANGED
|
@@ -5,7 +5,12 @@ import { applyProxyEnv, loadConfig } from "../config";
|
|
|
5
5
|
import type { OcxConfig } from "../types";
|
|
6
6
|
import { collectOrcaCodexHomeDiagnostic } from "./home";
|
|
7
7
|
import { summarizeComboCatalogOmissions, type ComboCatalogOmission } from "./catalog/aggregation";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
localClientSkipMessage,
|
|
10
|
+
localClientSkipReason,
|
|
11
|
+
shouldSyncCodexOnStart,
|
|
12
|
+
type LocalClientSkipReason,
|
|
13
|
+
} from "./desired-state";
|
|
9
14
|
import { admitCodexWrite, type CodexAdmission } from "./admission";
|
|
10
15
|
import type { CodexCatalogSyncOptions } from "./catalog/sync";
|
|
11
16
|
import { resetCodexAppServerCatalogStateCache } from "./app-server-processes";
|
|
@@ -18,7 +23,8 @@ export interface CodexSyncResult {
|
|
|
18
23
|
*/
|
|
19
24
|
status: "applied" | "skipped" | "catalog-only" | "refused";
|
|
20
25
|
ok: boolean;
|
|
21
|
-
|
|
26
|
+
/** `hub-gated` is the hub-role gate, not the user's toggle — the two read very differently. */
|
|
27
|
+
skippedReason?: LocalClientSkipReason;
|
|
22
28
|
/** Present when unattended convergence refused another service's native home. */
|
|
23
29
|
authority?: "service-home";
|
|
24
30
|
added: number;
|
|
@@ -85,19 +91,24 @@ export async function syncModelsToCodex(
|
|
|
85
91
|
// durable user switch and must be read again at this production boundary: a
|
|
86
92
|
// PUT OFF while provider discovery is in flight cannot be allowed to commit
|
|
87
93
|
// through an older captured object.
|
|
88
|
-
const
|
|
94
|
+
const gateSnapshot = loadConfig();
|
|
95
|
+
const desiredDisabled = !shouldSyncCodexOnStart(gateSnapshot);
|
|
89
96
|
const catalogEvenWhenNotInjected = options.catalogEvenWhenNotInjected === true;
|
|
90
97
|
if (desiredDisabled && !catalogEvenWhenNotInjected) {
|
|
91
98
|
return {
|
|
92
99
|
status: "skipped",
|
|
93
|
-
skippedReason:
|
|
100
|
+
skippedReason: localClientSkipReason(gateSnapshot),
|
|
94
101
|
ok: true,
|
|
95
102
|
added: 0,
|
|
96
103
|
catalogPath: null,
|
|
97
104
|
catalogExists: false,
|
|
98
105
|
catalogWritten: false,
|
|
99
106
|
cacheSynced: false,
|
|
100
|
-
message:
|
|
107
|
+
message: localClientSkipMessage(
|
|
108
|
+
gateSnapshot,
|
|
109
|
+
"Codex integration is OFF; no Codex config, catalog, cache, or history was changed.",
|
|
110
|
+
"No Codex config, catalog, cache, or history was changed.",
|
|
111
|
+
),
|
|
101
112
|
};
|
|
102
113
|
}
|
|
103
114
|
// Catalog gathering precedes injection and can itself write the native
|
|
@@ -131,8 +142,16 @@ export async function syncModelsToCodex(
|
|
|
131
142
|
applyProxyEnv(config);
|
|
132
143
|
const refreshed = await refreshCatalogForSync(config, deps, { allowWhenDesiredDisabled: true }, log);
|
|
133
144
|
const message = refreshed.catalogWritten || refreshed.cacheSynced
|
|
134
|
-
?
|
|
135
|
-
|
|
145
|
+
? localClientSkipMessage(
|
|
146
|
+
gateSnapshot,
|
|
147
|
+
"Codex integration is OFF; catalog and models cache refreshed, Codex config untouched.",
|
|
148
|
+
"Catalog and models cache refreshed, Codex config untouched.",
|
|
149
|
+
)
|
|
150
|
+
: localClientSkipMessage(
|
|
151
|
+
gateSnapshot,
|
|
152
|
+
"Codex integration is OFF; catalog refresh skipped, Codex config untouched.",
|
|
153
|
+
"Catalog refresh skipped, Codex config untouched.",
|
|
154
|
+
);
|
|
136
155
|
return {
|
|
137
156
|
status: "catalog-only",
|
|
138
157
|
ok: true,
|
|
@@ -242,8 +261,9 @@ export async function syncModelsToCodex(
|
|
|
242
261
|
if (result.status === "skipped") {
|
|
243
262
|
return {
|
|
244
263
|
status: "skipped",
|
|
245
|
-
// The apply direction's only under-lock policy
|
|
246
|
-
|
|
264
|
+
// The apply direction's only under-lock policy skips are desired OFF and the hub gate;
|
|
265
|
+
// carry whichever the injector reported so the caller can say the honest thing.
|
|
266
|
+
skippedReason: result.skippedReason === "hub-gated" ? "hub-gated" : "desired_disabled",
|
|
247
267
|
ok: true,
|
|
248
268
|
added: 0,
|
|
249
269
|
catalogPath: null,
|
package/src/codex/warmup.ts
CHANGED
|
@@ -28,7 +28,8 @@ export interface CodexWarmupOptions {
|
|
|
28
28
|
|
|
29
29
|
const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
30
30
|
const DEFAULT_MODEL = "gpt-5.4-mini";
|
|
31
|
-
const FALLBACK_MODELS = ["gpt-5.5"];
|
|
31
|
+
const FALLBACK_MODELS = ["gpt-5.5", "gpt-5.6-luna"];
|
|
32
|
+
const isRetryableWarmupStatus = (status?: number): boolean => status === 400 || status === 404;
|
|
32
33
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
33
34
|
const MAX_TIMEOUT_MS = 0x7fff_ffff;
|
|
34
35
|
const MAX_ERROR_BODY_BYTES = 2048;
|
|
@@ -63,6 +64,17 @@ export function codexWarmupFailureReason(err: unknown): string {
|
|
|
63
64
|
return safeWarmupReason(err);
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
/**
|
|
68
|
+
* 400 and 404 mean the model or the account is not provisioned for this request, not that the
|
|
69
|
+
* credential is bad. Sharing the retry predicate keeps the operator-facing diagnostic from
|
|
70
|
+
* disagreeing with the fallback policy that produced the final error.
|
|
71
|
+
*/
|
|
72
|
+
export function isCodexWarmupProvisioningFailure(err: unknown): boolean {
|
|
73
|
+
return err instanceof CodexWarmupError
|
|
74
|
+
&& err.code === "http_status"
|
|
75
|
+
&& isRetryableWarmupStatus(err.status);
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
function eventTypeFromData(data: unknown): string | undefined {
|
|
67
79
|
if (!data || typeof data !== "object") return undefined;
|
|
68
80
|
const record = data as Record<string, unknown>;
|
|
@@ -286,8 +298,8 @@ export async function warmCodexAccount(options: CodexWarmupOptions): Promise<voi
|
|
|
286
298
|
await tryWarmup(options, primaryModel);
|
|
287
299
|
return;
|
|
288
300
|
} catch (err) {
|
|
289
|
-
// Retry with fallback models on 400 (model may not be available for this account).
|
|
290
|
-
if (!(err instanceof CodexWarmupError) || err.status
|
|
301
|
+
// Retry with fallback models on 400 or 404 (model may not be provisioned or available for this account).
|
|
302
|
+
if (!(err instanceof CodexWarmupError) || !isRetryableWarmupStatus(err.status)) throw err;
|
|
291
303
|
let lastErr = err;
|
|
292
304
|
for (const fallback of FALLBACK_MODELS) {
|
|
293
305
|
if (fallback === primaryModel) continue;
|
|
@@ -295,7 +307,12 @@ export async function warmCodexAccount(options: CodexWarmupOptions): Promise<voi
|
|
|
295
307
|
await tryWarmup(options, fallback);
|
|
296
308
|
return;
|
|
297
309
|
} catch (retryErr) {
|
|
298
|
-
if (retryErr instanceof CodexWarmupError)
|
|
310
|
+
if (retryErr instanceof CodexWarmupError) {
|
|
311
|
+
lastErr = retryErr;
|
|
312
|
+
if (!isRetryableWarmupStatus(retryErr.status)) throw retryErr;
|
|
313
|
+
} else {
|
|
314
|
+
throw retryErr;
|
|
315
|
+
}
|
|
299
316
|
}
|
|
300
317
|
}
|
|
301
318
|
throw lastErr;
|
|
@@ -175,7 +175,7 @@ export function listPendingTeardowns(): OutstandingTeardown[] {
|
|
|
175
175
|
}
|
|
176
176
|
const out: OutstandingTeardown[] = [];
|
|
177
177
|
for (const name of names) {
|
|
178
|
-
// One naming rule, shared with the
|
|
178
|
+
// One naming rule, shared with the package launcher: the two lanes drifting apart is
|
|
179
179
|
// exactly how the Node updater stopped seeing receipts at all.
|
|
180
180
|
if (!isPendingTeardownFileName(name)) continue;
|
|
181
181
|
const nonce = pendingTeardownNonceFromFileName(name)!;
|