@juspay/neurolink 10.8.10 → 10.8.12

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.
@@ -11,8 +11,27 @@
11
11
  */
12
12
  import type { CommandModule } from "yargs";
13
13
  import type { Hono } from "hono";
14
- import type { AccountAllowlist, LoadedProxyConfig, ModelRouterInterface, ProxyGuardArgs, ProxyNeurolinkRuntime, ProxyStartArgs, ProxyStartStrategy, ProxyStatusArgs, ProxyTelemetryArgs, ProxyReadinessState } from "../../lib/types/index.js";
14
+ import type { AccountAllowlist, LoadedProxyConfig, ModelRouterInterface, ProxyGuardArgs, ProxyNeurolinkRuntime, ProxyStartArgs, ProxyStartStrategy, ProxySupervisorState, ProxyStatusArgs, ProxyTelemetryArgs, ProxyReadinessState } from "../../lib/types/index.js";
15
15
  import { ProxyRuntimeConfigStore } from "../../lib/proxy/runtimeConfig.js";
16
+ /**
17
+ * Drop a supervisor `version` that is not a string.
18
+ *
19
+ * `StateFileManager.load()` is a bare `JSON.parse(content) as T` — it validates
20
+ * nothing. A state file written by a different build, or half-written during a
21
+ * crash, can carry any JSON type here, and every status renderer interpolates
22
+ * the field straight into `v${...}`. Coercing at the single load boundary keeps
23
+ * a non-string from reaching the output as "v[object Object]".
24
+ */
25
+ export declare function normalizeSupervisorState(state: ProxySupervisorState | null): ProxySupervisorState | null;
26
+ /**
27
+ * Whether a rolling handoff can actually occur.
28
+ *
29
+ * A live supervisor PID alone is not enough: a supervisor from a build
30
+ * predating rolling state leaves `rolling` absent, and calling that a handoff
31
+ * makes both `/status` clients and the CLI wait for an activation that will
32
+ * never come. Gate on the capability, not on the process.
33
+ */
34
+ export declare function isRollingHandoffCapable(state: ProxySupervisorState | null, isRunning?: (pid: number) => boolean): boolean;
16
35
  /**
17
36
  * Best-effort check that a pid actually belongs to a neurolink proxy process,
18
37
  * so a stale/recycled supervisor pid is never mistaken for a live supervisor
@@ -81,8 +81,42 @@ function clearProxyState() {
81
81
  function saveProxySupervisorState(state) {
82
82
  proxySupervisorStateManager.save(state);
83
83
  }
84
+ /**
85
+ * Drop a supervisor `version` that is not a string.
86
+ *
87
+ * `StateFileManager.load()` is a bare `JSON.parse(content) as T` — it validates
88
+ * nothing. A state file written by a different build, or half-written during a
89
+ * crash, can carry any JSON type here, and every status renderer interpolates
90
+ * the field straight into `v${...}`. Coercing at the single load boundary keeps
91
+ * a non-string from reaching the output as "v[object Object]".
92
+ */
93
+ export function normalizeSupervisorState(state) {
94
+ if (!state) {
95
+ return null;
96
+ }
97
+ return typeof state.version === "string" || state.version === undefined
98
+ ? state
99
+ : { ...state, version: undefined };
100
+ }
101
+ /**
102
+ * Whether a rolling handoff can actually occur.
103
+ *
104
+ * A live supervisor PID alone is not enough: a supervisor from a build
105
+ * predating rolling state leaves `rolling` absent, and calling that a handoff
106
+ * makes both `/status` clients and the CLI wait for an activation that will
107
+ * never come. Gate on the capability, not on the process.
108
+ */
109
+ export function isRollingHandoffCapable(state, isRunning = isProcessRunning) {
110
+ if (!state) {
111
+ return false;
112
+ }
113
+ // Structural, not just non-null: the same unvalidated `as T` load that lets
114
+ // `version` be an object lets `rolling` be a string.
115
+ const hasRollingState = typeof state.rolling === "object" && state.rolling !== null;
116
+ return isRunning(state.pid) && hasRollingState;
117
+ }
84
118
  function loadProxySupervisorState() {
85
- return proxySupervisorStateManager.load();
119
+ return normalizeSupervisorState(proxySupervisorStateManager.load());
86
120
  }
87
121
  function clearProxySupervisorState() {
88
122
  proxySupervisorStateManager.clear();
@@ -1556,6 +1590,7 @@ export async function createProxyStartApp(params) {
1556
1590
  : undefined;
1557
1591
  const runtimeState = loadProxyState();
1558
1592
  const supervisorState = loadProxySupervisorState();
1593
+ const rollingSupervisorRunning = isRollingHandoffCapable(supervisorState);
1559
1594
  const updateState = loadUpdateState();
1560
1595
  const cooldowns = await loadAccountCooldowns();
1561
1596
  const storedAccountKeys = new Set();
@@ -1779,6 +1814,7 @@ export async function createProxyStartApp(params) {
1779
1814
  autoUpdate: {
1780
1815
  enabled: isProxyAutoUpdateEnabled(),
1781
1816
  supervisorPid: supervisorState?.pid ?? null,
1817
+ supervisorVersion: supervisorState?.version ?? null,
1782
1818
  rolling: supervisorState?.rolling ?? null,
1783
1819
  updaterPid: activeUpdaterPid ?? null,
1784
1820
  updaterRunning: activeUpdaterPid
@@ -1786,7 +1822,16 @@ export async function createProxyStartApp(params) {
1786
1822
  : false,
1787
1823
  liveVersion: PROXY_VERSION,
1788
1824
  latestVersion: updateState?.lastCheckVersion || null,
1825
+ lastDetectedVersion: updateState?.lastCheckVersion || null,
1826
+ installedVersion: updateState?.installedVersion ??
1827
+ updateState?.lastUpdateVersion ??
1828
+ null,
1829
+ activatedVersion: PROXY_VERSION,
1830
+ pendingActivationVersion: updateState?.pendingRestartVersion ?? null,
1789
1831
  pendingRestartVersion: updateState?.pendingRestartVersion ?? null,
1832
+ activationMode: rollingSupervisorRunning
1833
+ ? "rolling-handoff"
1834
+ : "restart",
1790
1835
  deferredUpdate: updateState?.deferredUpdate ?? null,
1791
1836
  lastCheckAt: updateState?.lastCheckAt ?? null,
1792
1837
  lastUpdateAt: updateState?.lastUpdateAt ?? null,
@@ -2426,6 +2471,7 @@ async function runLaunchdProxySupervisor(argv, spinner) {
2426
2471
  host,
2427
2472
  port,
2428
2473
  startTime: supervisorStartedAt,
2474
+ version: PROXY_VERSION,
2429
2475
  updaterPid: currentUpdaterPid,
2430
2476
  rolling: snapshot,
2431
2477
  });
@@ -2850,11 +2896,18 @@ export const proxyStatusCommand = {
2850
2896
  autoUpdateEnabled: isProxyAutoUpdateEnabled(),
2851
2897
  workerVersion: null,
2852
2898
  supervisorPid: null,
2899
+ supervisorVersion: supervisorState?.version ?? null,
2853
2900
  supervisorRunning: false,
2854
2901
  rolling: null,
2855
2902
  updaterPid: null,
2856
2903
  updaterRunning: false,
2857
2904
  latestVersion: updateState?.lastCheckVersion || null,
2905
+ lastDetectedVersion: updateState?.lastCheckVersion || null,
2906
+ installedVersion: updateState?.installedVersion ??
2907
+ updateState?.lastUpdateVersion ??
2908
+ null,
2909
+ activatedVersion: supervisorState?.rolling.active?.version ?? null,
2910
+ pendingActivationVersion: updateState?.pendingRestartVersion ?? null,
2858
2911
  pendingRestartVersion: updateState?.pendingRestartVersion ?? null,
2859
2912
  deferredUpdate: updateState?.deferredUpdate ?? null,
2860
2913
  lastUpdateFailure: updateState?.lastFailure ?? null,
@@ -2904,6 +2957,7 @@ export const proxyStatusCommand = {
2904
2957
  servingState?.lastConfigReloadError ?? null;
2905
2958
  status.supervisorPid = supervisorPid ?? null;
2906
2959
  status.supervisorRunning = supervisorRunning;
2960
+ status.supervisorVersion = supervisorState?.version ?? null;
2907
2961
  status.rolling = supervisorState?.rolling ?? null;
2908
2962
  status.updaterPid =
2909
2963
  supervisorState?.updaterPid ?? servingState?.updaterPid ?? null;
@@ -2927,6 +2981,7 @@ export const proxyStatusCommand = {
2927
2981
  typeof statusData.version === "string"
2928
2982
  ? statusData.version
2929
2983
  : null;
2984
+ status.activatedVersion = status.workerVersion;
2930
2985
  if (typeof liveConfig?.generation === "number") {
2931
2986
  status.configGeneration = liveConfig.generation;
2932
2987
  }
@@ -2959,6 +3014,9 @@ export const proxyStatusCommand = {
2959
3014
  if (status.supervisorPid) {
2960
3015
  logger.always(` ${chalk.bold("Supervisor:")} ${status.supervisorRunning ? chalk.cyan(status.supervisorPid) : chalk.red(`${status.supervisorPid} (not running)`)}`);
2961
3016
  }
3017
+ if (status.supervisorVersion) {
3018
+ logger.always(` ${chalk.bold("Supervisor version:")} ${chalk.cyan(`v${status.supervisorVersion}`)}`);
3019
+ }
2962
3020
  if (status.workerVersion) {
2963
3021
  logger.always(` ${chalk.bold("Version:")} ${chalk.cyan(`v${status.workerVersion}`)}`);
2964
3022
  }
@@ -2977,8 +3035,14 @@ export const proxyStatusCommand = {
2977
3035
  logger.always(` ${chalk.bold("Started:")} ${chalk.cyan(status.startTime)}`);
2978
3036
  logger.always(` ${chalk.bold("Uptime:")} ${chalk.cyan(formatUptime(status.uptime ?? 0))}`);
2979
3037
  logger.always(` ${chalk.bold("Auto-update:")} ${status.autoUpdateEnabled ? chalk.green(status.updaterRunning ? `enabled (PID ${status.updaterPid})` : "enabled (worker unavailable)") : chalk.yellow("disabled")}`);
2980
- if (status.pendingRestartVersion) {
2981
- logger.always(` ${chalk.bold("Pending:")} ${chalk.yellow(`v${status.pendingRestartVersion} installed; restart pending`)}`);
3038
+ if (status.pendingActivationVersion) {
3039
+ // A live supervisor PID alone does NOT mean rolling handoff is
3040
+ // available: a supervisor from a build predating rolling state leaves
3041
+ // `rolling` absent, and calling that a handoff tells the operator to
3042
+ // wait for an activation that will never come. Gate on the capability,
3043
+ // not on the process.
3044
+ const rollingCapable = status.supervisorRunning && status.rolling !== null;
3045
+ logger.always(` ${chalk.bold(rollingCapable ? "Pending handoff:" : "Pending restart:")} ${chalk.yellow(`v${status.pendingActivationVersion} installed; ${rollingCapable ? "rolling activation pending" : "restart pending"}`)}`);
2982
3046
  }
2983
3047
  if (status.rolling?.candidate) {
2984
3048
  logger.always(` ${chalk.bold("Handoff:")} ${chalk.yellow(`preparing v${status.rolling.candidate.expectedVersion} (PID ${status.rolling.candidate.pid})`)}`);
@@ -2995,6 +3059,12 @@ export const proxyStatusCommand = {
2995
3059
  if (status.latestVersion) {
2996
3060
  logger.always(` ${chalk.bold("Latest:")} ${chalk.cyan(`v${status.latestVersion}`)}`);
2997
3061
  }
3062
+ if (status.installedVersion) {
3063
+ logger.always(` ${chalk.bold("Installed:")} ${chalk.cyan(`v${status.installedVersion}`)}`);
3064
+ }
3065
+ if (status.activatedVersion) {
3066
+ logger.always(` ${chalk.bold("Activated:")} ${chalk.cyan(`v${status.activatedVersion}`)}`);
3067
+ }
2998
3068
  if (status.lastUpdateFailure) {
2999
3069
  logger.always(` ${chalk.bold("Update error:")} ${chalk.red(`${status.lastUpdateFailure.stage}: ${status.lastUpdateFailure.message}`)}`);
3000
3070
  }
@@ -48,7 +48,7 @@ export declare function suppressVersion(version: string, reason: string, stateFi
48
48
  * @param stateFilePath - Override path for testing
49
49
  */
50
50
  export declare function recordSuccessfulUpdate(version: string, stateFilePath?: string): void;
51
- /** Record that package installation completed but the live restart is pending. */
51
+ /** Record that the package was validated but live activation is still pending. */
52
52
  export declare function recordUpdateInstalled(version: string, stateFilePath?: string): void;
53
53
  /** Abandon a matching installed version so the next cycle may reinstall it. */
54
54
  export declare function abandonPendingUpdate(version: string, stateFilePath?: string): boolean;
@@ -87,6 +87,7 @@ export function getDefaultUpdateState() {
87
87
  lastCheckAt: new Date(0).toISOString(),
88
88
  lastCheckVersion: "",
89
89
  suppressedVersions: {},
90
+ installedVersion: null,
90
91
  lastUpdateAt: null,
91
92
  lastUpdateVersion: null,
92
93
  pendingRestartVersion: null,
@@ -124,6 +125,20 @@ export function loadUpdateState(stateFilePath) {
124
125
  ...getDefaultUpdateState(),
125
126
  ...candidate,
126
127
  suppressedVersions: candidate.suppressedVersions ?? {},
128
+ // Backfill order matters for state files written before `installedVersion`
129
+ // existed. Back then `recordUpdateInstalled()` set ONLY
130
+ // `pendingRestartVersion`, leaving `lastUpdateVersion` on the previously
131
+ // activated build — so a validated-but-not-yet-running update lives in
132
+ // `pendingRestartVersion` and is the newer of the two. Reading
133
+ // `lastUpdateVersion` first would report the superseded version as
134
+ // installed and re-offer an update that is already on disk.
135
+ installedVersion: typeof candidate.installedVersion === "string"
136
+ ? candidate.installedVersion
137
+ : typeof candidate.pendingRestartVersion === "string"
138
+ ? candidate.pendingRestartVersion
139
+ : typeof candidate.lastUpdateVersion === "string"
140
+ ? candidate.lastUpdateVersion
141
+ : null,
127
142
  pendingRestartVersion: typeof candidate.pendingRestartVersion === "string"
128
143
  ? candidate.pendingRestartVersion
129
144
  : null,
@@ -208,15 +223,17 @@ export function recordSuccessfulUpdate(version, stateFilePath) {
208
223
  const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
209
224
  state.lastUpdateAt = new Date().toISOString();
210
225
  state.lastUpdateVersion = version;
226
+ state.installedVersion = version;
211
227
  state.pendingRestartVersion = null;
212
228
  state.deferredUpdate = null;
213
229
  state.lastFailure = null;
214
230
  delete state.suppressedVersions[version];
215
231
  saveUpdateState(state, stateFilePath);
216
232
  }
217
- /** Record that package installation completed but the live restart is pending. */
233
+ /** Record that the package was validated but live activation is still pending. */
218
234
  export function recordUpdateInstalled(version, stateFilePath) {
219
235
  const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
236
+ state.installedVersion = version;
220
237
  state.pendingRestartVersion = version;
221
238
  state.lastFailure = null;
222
239
  saveUpdateState(state, stateFilePath);
@@ -297,6 +297,12 @@ export declare function createClaudeProxyRoutes(modelRouter?: ModelRouterInterfa
297
297
  declare function reconcileEligibleAccountRuntimeState(account: ProxyPassthroughAccount): void;
298
298
  export declare function getTransientSameAccountRetryDelayMs(retryNumber: number): number;
299
299
  declare function describeTransportError(error: unknown): string;
300
+ /**
301
+ * Determine whether a POST can be retried without risking duplicate provider
302
+ * work. Only failures that prove connection establishment did not complete are
303
+ * safe; a reset, socket error, or response timeout can happen after dispatch.
304
+ */
305
+ declare function isRetryableNetworkError(error: unknown): boolean;
300
306
  /**
301
307
  * Parse a Claude error payload when available.
302
308
  */
@@ -338,6 +344,7 @@ export declare const __testHooks: {
338
344
  maybeResetPrimaryToHome: typeof maybeResetPrimaryToHome;
339
345
  planCooldownFor429: typeof planCooldownFor429;
340
346
  reconcileCooldownFromQuota: typeof reconcileCooldownFromQuota;
347
+ isRetryableNetworkError: typeof isRetryableNetworkError;
341
348
  isPermanentRefreshFailure: typeof isPermanentRefreshFailure;
342
349
  getStreamFailureDetails: typeof getStreamFailureDetails;
343
350
  trackUpstreamReadableStream: typeof trackUpstreamReadableStream;
@@ -12,6 +12,7 @@
12
12
  import { access, readFile } from "node:fs/promises";
13
13
  import { homedir } from "node:os";
14
14
  import { join } from "node:path";
15
+ import { Agent } from "undici";
15
16
  import { buildStableClaudeCodeBillingHeader, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_OAUTH_BETAS, getOrCreateClaudeCodeIdentity, parseClaudeCodeUserId, } from "../../auth/anthropicOAuth.js";
16
17
  import { clearAccountCooldown, loadAccountCooldowns, saveAccountCooldown, } from "../../proxy/accountCooldown.js";
17
18
  import { anthropicAccountKeysEqual, ENV_ANTHROPIC_ACCOUNT_KEY, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, shouldLoadFallbackCredential, } from "../../proxy/accountSelection.js";
@@ -91,6 +92,21 @@ const AUTH_REFRESH_MAX_COOLDOWN_MS = 5 * 60 * 1000;
91
92
  * to cover the full lifecycle of streaming responses, including extended
92
93
  * thinking from Opus models (which can exceed 5 minutes for large contexts). */
93
94
  const UPSTREAM_FETCH_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
95
+ let anthropicUpstreamDispatcher;
96
+ function fetchAnthropicUpstream(url, init) {
97
+ // Node's global fetch applies Undici's 300s default headers timeout before
98
+ // the route's 15-minute abort signal. Keep both transport deadlines aligned
99
+ // with the proxy contract and instantiate lazily so importing routes has no
100
+ // open transport handles.
101
+ anthropicUpstreamDispatcher ??= new Agent({
102
+ headersTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
103
+ bodyTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
104
+ });
105
+ return fetch(url, {
106
+ ...init,
107
+ dispatcher: anthropicUpstreamDispatcher,
108
+ });
109
+ }
94
110
  const accountRuntimeState = new Map();
95
111
  /** Shared across requests so a concurrent burst gets at most two retries for
96
112
  * the account/window, rather than every request starting its own retry chain. */
@@ -1406,7 +1422,7 @@ async function handleClaudePassthroughRequest(args) {
1406
1422
  recordAttempt("passthrough", "passthrough");
1407
1423
  let response;
1408
1424
  try {
1409
- response = await fetch("https://api.anthropic.com/v1/messages?beta=true", {
1425
+ response = await fetchAnthropicUpstream("https://api.anthropic.com/v1/messages?beta=true", {
1410
1426
  method: "POST",
1411
1427
  headers: upstreamHeaders,
1412
1428
  body: bodyStr,
@@ -2510,10 +2526,11 @@ async function handleAnthropicStreamingSuccessResponse(args) {
2510
2526
  if (preflight.kind === "transport_error") {
2511
2527
  const message = describeTransportError(preflight.error);
2512
2528
  const partialBody = Buffer.concat(preflight.chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
2513
- logger.always(`[proxy] stream failed before first chunk account=${account.label}: ${message}; trying next account`);
2529
+ // The POST has already returned a response. The upstream may have started
2530
+ // processing it, so replaying it on another account could duplicate work.
2531
+ logger.always(`[proxy] stream failed before first chunk account=${account.label}: ${message}; returning terminal error to avoid replaying an ambiguous request`);
2514
2532
  recordAttemptError(account.label, account.type, 502);
2515
- logAttempt(502, "stream_error", message, { retryable: true });
2516
- tracer?.recordRetry(account.label, "stream_before_first_chunk");
2533
+ logAttempt(502, "stream_error", message, { retryable: false });
2517
2534
  upstreamSpan?.end();
2518
2535
  logProxyBody({
2519
2536
  phase: "upstream_response",
@@ -2529,8 +2546,16 @@ async function handleAnthropicStreamingSuccessResponse(args) {
2529
2546
  metadata: { logicalStatus: 502, transportError: message },
2530
2547
  });
2531
2548
  return {
2532
- retryNextAccount: true,
2533
- failure: { message, rateLimit: false },
2549
+ response: finalizeAnthropicTerminalTransportError({
2550
+ account,
2551
+ tracer,
2552
+ requestStartTime,
2553
+ attemptNumber,
2554
+ logProxyBody,
2555
+ logFinalRequest,
2556
+ errorType: "stream_error",
2557
+ message,
2558
+ }),
2534
2559
  };
2535
2560
  }
2536
2561
  if (preflight.kind === "empty") {
@@ -3176,7 +3201,7 @@ async function handleAnthropicAuthRetry(args) {
3176
3201
  metadata: { upstreamMethod: "POST", upstreamUrl: url },
3177
3202
  });
3178
3203
  try {
3179
- const retryResp = await fetch(url, {
3204
+ const retryResp = await fetchAnthropicUpstream(url, {
3180
3205
  method: "POST",
3181
3206
  headers,
3182
3207
  body: retryBodyStr,
@@ -3387,11 +3412,37 @@ async function handleAnthropicAuthRetry(args) {
3387
3412
  : String(retryFetchErr);
3388
3413
  authRetryError = `network error on retry ${authRetry + 1}: ${message}`;
3389
3414
  currentLastError = authRetryError;
3415
+ const retryable = isRetryableNetworkError(retryFetchErr);
3390
3416
  retryLogAttempt(502, "network_error", message, {
3391
- retryable: isRetryableNetworkError(retryFetchErr),
3417
+ retryable,
3392
3418
  errorCode: getErrorCode(retryFetchErr) ?? "unknown",
3393
3419
  });
3394
3420
  logger.debug(`[proxy] ${authRetryError}`);
3421
+ if (!retryable) {
3422
+ // Once a POST has left this process, a reset/timeout or unknown fetch
3423
+ // failure is ambiguous: retrying it on another account can duplicate
3424
+ // the request. Only connection-establishment failures are replay-safe.
3425
+ currentUpstreamSpan?.end();
3426
+ return {
3427
+ response: finalizeAnthropicTerminalTransportError({
3428
+ account,
3429
+ tracer,
3430
+ requestStartTime,
3431
+ attemptNumber: retryAttemptNumber,
3432
+ logProxyBody,
3433
+ logFinalRequest,
3434
+ errorType: "network_error",
3435
+ message,
3436
+ }),
3437
+ continueLoop: false,
3438
+ lastError: currentLastError,
3439
+ authFailureMessage: currentAuthFailureMessage,
3440
+ sawRateLimit: currentSawRateLimit,
3441
+ sawTransientFailure: currentSawTransientFailure,
3442
+ sawNetworkError: currentSawNetworkError,
3443
+ upstreamSpan: undefined,
3444
+ };
3445
+ }
3395
3446
  break;
3396
3447
  }
3397
3448
  }
@@ -3470,6 +3521,27 @@ function finalizeAnthropicTerminalFetchError(args) {
3470
3521
  errorType: terminalError.errorType,
3471
3522
  });
3472
3523
  }
3524
+ function finalizeAnthropicTerminalTransportError(args) {
3525
+ const { account, tracer, requestStartTime, attemptNumber, logProxyBody, logFinalRequest, errorType, message, } = args;
3526
+ tracer?.setError(errorType, message);
3527
+ tracer?.end(502, Date.now() - requestStartTime);
3528
+ logFinalRequest(502, account.label, account.type, errorType, message);
3529
+ const clientError = buildClaudeError(502, message);
3530
+ const clientErrorBody = JSON.stringify(clientError);
3531
+ logProxyBody({
3532
+ phase: "client_response",
3533
+ headers: { "content-type": "application/json" },
3534
+ body: clientErrorBody,
3535
+ bodySize: Buffer.byteLength(clientErrorBody, "utf8"),
3536
+ contentType: "application/json",
3537
+ account: account.label,
3538
+ accountType: account.type,
3539
+ attempt: attemptNumber,
3540
+ responseStatus: 502,
3541
+ durationMs: Date.now() - requestStartTime,
3542
+ });
3543
+ return clientError;
3544
+ }
3473
3545
  async function handleAnthropicNonOkResponse(args) {
3474
3546
  const { response, account, accountState, enabledAccounts, orderedAccounts, tracer, requestStartTime, fetchStartMs, attemptNumber, logAttempt, logProxyBody, logFinalRequest, lastError, authFailureMessage, sawTransientFailure, invalidRequestFailure, } = args;
3475
3547
  let currentLastError = lastError;
@@ -4043,7 +4115,7 @@ async function fetchAnthropicAccountResponse(args) {
4043
4115
  const currentUpstreamSpan = upstreamSpan;
4044
4116
  let response;
4045
4117
  try {
4046
- response = await fetch(url, {
4118
+ response = await fetchAnthropicUpstream(url, {
4047
4119
  method: "POST",
4048
4120
  headers,
4049
4121
  body: finalBodyStr,
@@ -4998,37 +5070,23 @@ function describeTransportError(error) {
4998
5070
  return detail ? `${message} (${detail})` : message;
4999
5071
  }
5000
5072
  /**
5001
- * Determine whether a thrown fetch error is a transient connectivity issue.
5073
+ * Determine whether a POST can be retried without risking duplicate provider
5074
+ * work. Only failures that prove connection establishment did not complete are
5075
+ * safe; a reset, socket error, or response timeout can happen after dispatch.
5002
5076
  */
5003
5077
  function isRetryableNetworkError(error) {
5004
5078
  const code = getErrorCode(error);
5005
- if (code &&
5079
+ return (code !== undefined &&
5006
5080
  [
5007
5081
  "ECONNREFUSED",
5008
- "ECONNRESET",
5082
+ "EADDRNOTAVAIL",
5009
5083
  // The Anthropic host is fixed, so ENOTFOUND can be a transient resolver
5010
5084
  // outage. Keep it inside the existing bounded same-account retry budget.
5011
5085
  "ENOTFOUND",
5012
- "ETIMEDOUT",
5013
5086
  "EHOSTUNREACH",
5014
5087
  "UND_ERR_CONNECT_TIMEOUT",
5015
5088
  "UND_ERR_CONNECT",
5016
- "UND_ERR_SOCKET",
5017
- "UND_ERR_HEADERS_TIMEOUT",
5018
- ].includes(code)) {
5019
- return true;
5020
- }
5021
- const message = error instanceof Error ? error.message : String(error);
5022
- const normalized = message.toLowerCase();
5023
- return (normalized.includes("econnrefused") ||
5024
- normalized.includes("econnreset") ||
5025
- normalized.includes("enotfound") ||
5026
- normalized.includes("etimedout") ||
5027
- normalized.includes("timed out") ||
5028
- normalized.includes("connection error") ||
5029
- normalized.includes("connect error") ||
5030
- normalized.includes("fetch failed") ||
5031
- normalized.includes("socket hang up"));
5089
+ ].includes(code));
5032
5090
  }
5033
5091
  const TRANSIENT_HTTP_STATUSES = new Set([
5034
5092
  408, 500, 502, 503, 504, 520, 521, 522, 523, 524, 525, 526, 529,
@@ -5158,6 +5216,7 @@ export const __testHooks = {
5158
5216
  maybeResetPrimaryToHome,
5159
5217
  planCooldownFor429,
5160
5218
  reconcileCooldownFromQuota,
5219
+ isRetryableNetworkError,
5161
5220
  isPermanentRefreshFailure,
5162
5221
  getStreamFailureDetails,
5163
5222
  trackUpstreamReadableStream,
@@ -870,6 +870,8 @@ export type ProxySupervisorState = {
870
870
  host: string;
871
871
  port: number;
872
872
  startTime: string;
873
+ /** Version loaded by the long-lived supervisor process. */
874
+ version?: string;
873
875
  updaterPid?: number;
874
876
  rolling: ProxyRollingState;
875
877
  };
@@ -1724,6 +1724,15 @@ export type UpdateState = {
1724
1724
  lastCheckAt: string;
1725
1725
  lastCheckVersion: string;
1726
1726
  suppressedVersions: Record<string, SuppressedVersion>;
1727
+ /**
1728
+ * Last package version whose stable trampoline was successfully validated.
1729
+ *
1730
+ * Optional because `UpdateState` is part of the published type surface and a
1731
+ * required addition would break every downstream object literal — and because
1732
+ * state files written before this field existed legitimately omit it.
1733
+ * `loadUpdateState()` always materializes it, so runtime readers see a value.
1734
+ */
1735
+ installedVersion?: string | null;
1727
1736
  lastUpdateAt: string | null;
1728
1737
  lastUpdateVersion: string | null;
1729
1738
  /** Installed by the updater but not yet confirmed as the running version. */
@@ -48,7 +48,7 @@ export declare function suppressVersion(version: string, reason: string, stateFi
48
48
  * @param stateFilePath - Override path for testing
49
49
  */
50
50
  export declare function recordSuccessfulUpdate(version: string, stateFilePath?: string): void;
51
- /** Record that package installation completed but the live restart is pending. */
51
+ /** Record that the package was validated but live activation is still pending. */
52
52
  export declare function recordUpdateInstalled(version: string, stateFilePath?: string): void;
53
53
  /** Abandon a matching installed version so the next cycle may reinstall it. */
54
54
  export declare function abandonPendingUpdate(version: string, stateFilePath?: string): boolean;
@@ -87,6 +87,7 @@ export function getDefaultUpdateState() {
87
87
  lastCheckAt: new Date(0).toISOString(),
88
88
  lastCheckVersion: "",
89
89
  suppressedVersions: {},
90
+ installedVersion: null,
90
91
  lastUpdateAt: null,
91
92
  lastUpdateVersion: null,
92
93
  pendingRestartVersion: null,
@@ -124,6 +125,20 @@ export function loadUpdateState(stateFilePath) {
124
125
  ...getDefaultUpdateState(),
125
126
  ...candidate,
126
127
  suppressedVersions: candidate.suppressedVersions ?? {},
128
+ // Backfill order matters for state files written before `installedVersion`
129
+ // existed. Back then `recordUpdateInstalled()` set ONLY
130
+ // `pendingRestartVersion`, leaving `lastUpdateVersion` on the previously
131
+ // activated build — so a validated-but-not-yet-running update lives in
132
+ // `pendingRestartVersion` and is the newer of the two. Reading
133
+ // `lastUpdateVersion` first would report the superseded version as
134
+ // installed and re-offer an update that is already on disk.
135
+ installedVersion: typeof candidate.installedVersion === "string"
136
+ ? candidate.installedVersion
137
+ : typeof candidate.pendingRestartVersion === "string"
138
+ ? candidate.pendingRestartVersion
139
+ : typeof candidate.lastUpdateVersion === "string"
140
+ ? candidate.lastUpdateVersion
141
+ : null,
127
142
  pendingRestartVersion: typeof candidate.pendingRestartVersion === "string"
128
143
  ? candidate.pendingRestartVersion
129
144
  : null,
@@ -208,15 +223,17 @@ export function recordSuccessfulUpdate(version, stateFilePath) {
208
223
  const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
209
224
  state.lastUpdateAt = new Date().toISOString();
210
225
  state.lastUpdateVersion = version;
226
+ state.installedVersion = version;
211
227
  state.pendingRestartVersion = null;
212
228
  state.deferredUpdate = null;
213
229
  state.lastFailure = null;
214
230
  delete state.suppressedVersions[version];
215
231
  saveUpdateState(state, stateFilePath);
216
232
  }
217
- /** Record that package installation completed but the live restart is pending. */
233
+ /** Record that the package was validated but live activation is still pending. */
218
234
  export function recordUpdateInstalled(version, stateFilePath) {
219
235
  const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
236
+ state.installedVersion = version;
220
237
  state.pendingRestartVersion = version;
221
238
  state.lastFailure = null;
222
239
  saveUpdateState(state, stateFilePath);
@@ -297,6 +297,12 @@ export declare function createClaudeProxyRoutes(modelRouter?: ModelRouterInterfa
297
297
  declare function reconcileEligibleAccountRuntimeState(account: ProxyPassthroughAccount): void;
298
298
  export declare function getTransientSameAccountRetryDelayMs(retryNumber: number): number;
299
299
  declare function describeTransportError(error: unknown): string;
300
+ /**
301
+ * Determine whether a POST can be retried without risking duplicate provider
302
+ * work. Only failures that prove connection establishment did not complete are
303
+ * safe; a reset, socket error, or response timeout can happen after dispatch.
304
+ */
305
+ declare function isRetryableNetworkError(error: unknown): boolean;
300
306
  /**
301
307
  * Parse a Claude error payload when available.
302
308
  */
@@ -338,6 +344,7 @@ export declare const __testHooks: {
338
344
  maybeResetPrimaryToHome: typeof maybeResetPrimaryToHome;
339
345
  planCooldownFor429: typeof planCooldownFor429;
340
346
  reconcileCooldownFromQuota: typeof reconcileCooldownFromQuota;
347
+ isRetryableNetworkError: typeof isRetryableNetworkError;
341
348
  isPermanentRefreshFailure: typeof isPermanentRefreshFailure;
342
349
  getStreamFailureDetails: typeof getStreamFailureDetails;
343
350
  trackUpstreamReadableStream: typeof trackUpstreamReadableStream;