@bitkyc08/opencodex 2.77.0 → 2.78.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 (222) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-BtfG5id-.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
  5. package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  15. package/src/adapters/devin/trajectory.ts +49 -0
  16. package/src/adapters/devin.ts +13 -0
  17. package/src/adapters/ollama-native.ts +35 -19
  18. package/src/claude/intercept/client-hello.ts +103 -0
  19. package/src/claude/intercept/model-bindings.ts +3 -2
  20. package/src/claude/intercept/picker-listener.ts +279 -66
  21. package/src/cli/access-audio-input.ts +84 -0
  22. package/src/cli/access-audio-live.ts +99 -0
  23. package/src/cli/access-audio.ts +123 -0
  24. package/src/cli/access-data-client.ts +127 -0
  25. package/src/cli/access-data-plane.ts +104 -0
  26. package/src/cli/access-data-response.ts +76 -0
  27. package/src/cli/access.ts +65 -2
  28. package/src/cli/account-api.ts +30 -6
  29. package/src/cli/account-auth.ts +111 -42
  30. package/src/cli/account-key-quota.ts +118 -0
  31. package/src/cli/account-main.ts +11 -4
  32. package/src/cli/account-next-actions.ts +63 -0
  33. package/src/cli/account-policy-dto.ts +68 -0
  34. package/src/cli/account-policy.ts +167 -0
  35. package/src/cli/account-target.ts +8 -0
  36. package/src/cli/account.ts +34 -8
  37. package/src/cli/agent-runtime-settings.ts +130 -0
  38. package/src/cli/agent-settings.ts +101 -0
  39. package/src/cli/agent.ts +19 -2
  40. package/src/cli/alias.ts +7 -2
  41. package/src/cli/aside-profiles.ts +2 -2
  42. package/src/cli/capabilities-access-remote.ts +147 -0
  43. package/src/cli/capabilities-accounts.ts +195 -0
  44. package/src/cli/capabilities-agents-routing.ts +414 -0
  45. package/src/cli/capabilities-base.ts +783 -0
  46. package/src/cli/capabilities-command.ts +1 -0
  47. package/src/cli/capabilities-integrations.ts +264 -0
  48. package/src/cli/capabilities-lab.ts +214 -0
  49. package/src/cli/capabilities-observe-system.ts +662 -0
  50. package/src/cli/capabilities-provider-models.ts +302 -0
  51. package/src/cli/capabilities.ts +30 -1199
  52. package/src/cli/capability-types.ts +54 -0
  53. package/src/cli/catalog-command-result.ts +57 -0
  54. package/src/cli/chatgpt-command.ts +25 -2
  55. package/src/cli/claude-agent-startup-sync.ts +17 -0
  56. package/src/cli/claude-desktop-profile.ts +100 -0
  57. package/src/cli/claude-desktop.ts +39 -6
  58. package/src/cli/codex-shim-autorestore.ts +2 -1
  59. package/src/cli/codex-shim-readiness.ts +23 -3
  60. package/src/cli/combo-input.ts +88 -0
  61. package/src/cli/combo-result.ts +52 -0
  62. package/src/cli/combo-stats.ts +123 -0
  63. package/src/cli/combo.ts +37 -14
  64. package/src/cli/companion-timeline.ts +142 -0
  65. package/src/cli/companion-usage.ts +146 -0
  66. package/src/cli/companion.ts +12 -3
  67. package/src/cli/config-command.ts +24 -8
  68. package/src/cli/dispatch.ts +52 -79
  69. package/src/cli/doctor.ts +22 -4
  70. package/src/cli/help-catalog.ts +31 -14
  71. package/src/cli/help-recovery.ts +2 -2
  72. package/src/cli/help.ts +25 -11
  73. package/src/cli/index.ts +38 -27
  74. package/src/cli/injection-follow.ts +35 -0
  75. package/src/cli/inspect.ts +5 -0
  76. package/src/cli/integration-aside-sync.ts +69 -0
  77. package/src/cli/integration-cursor.ts +106 -0
  78. package/src/cli/integration-input.ts +60 -0
  79. package/src/cli/integration-journal.ts +49 -0
  80. package/src/cli/integration-plan-dto.ts +156 -0
  81. package/src/cli/integration-preview.ts +215 -0
  82. package/src/cli/integrations.ts +24 -18
  83. package/src/cli/json-input.ts +134 -0
  84. package/src/cli/link.ts +32 -4
  85. package/src/cli/local-sync-result.ts +41 -0
  86. package/src/cli/log-follow.ts +84 -0
  87. package/src/cli/log-view-filter.ts +146 -0
  88. package/src/cli/logout-command.ts +44 -0
  89. package/src/cli/model-picker-ordering.ts +137 -0
  90. package/src/cli/models-custom-input.ts +107 -0
  91. package/src/cli/models-custom-runtime.ts +112 -0
  92. package/src/cli/models-order.ts +194 -0
  93. package/src/cli/models-runtime-subcommands.ts +2 -0
  94. package/src/cli/models-runtime.ts +7 -0
  95. package/src/cli/models.ts +66 -81
  96. package/src/cli/observe-stream.ts +142 -0
  97. package/src/cli/observe.ts +75 -34
  98. package/src/cli/provider-batch.ts +77 -0
  99. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  100. package/src/cli/provider-result.ts +124 -0
  101. package/src/cli/provider-runtime.ts +15 -12
  102. package/src/cli/provider-settings.ts +143 -0
  103. package/src/cli/provider.ts +112 -34
  104. package/src/cli/registry.ts +62 -20
  105. package/src/cli/remote-workspace-hub.ts +83 -0
  106. package/src/cli/remote-workspace.ts +6 -1
  107. package/src/cli/restart-failure.ts +27 -0
  108. package/src/cli/root.ts +12 -0
  109. package/src/cli/route-policy-write.ts +137 -0
  110. package/src/cli/route-policy.ts +8 -0
  111. package/src/cli/runtime-api.ts +56 -79
  112. package/src/cli/secret-args.ts +149 -0
  113. package/src/cli/settings-result.ts +30 -0
  114. package/src/cli/storage.ts +77 -7
  115. package/src/cli/system-command.ts +60 -2
  116. package/src/cli/system-restart-client.ts +20 -3
  117. package/src/cli/system-settings-parity.ts +83 -0
  118. package/src/cli/update-restart-candidate.ts +21 -0
  119. package/src/cli/update-restart-child.ts +81 -0
  120. package/src/cli/update-restart-home.ts +38 -0
  121. package/src/cli/update-restart-transport.ts +193 -0
  122. package/src/cli/update-restart.ts +260 -0
  123. package/src/cli/usage-model-search.ts +58 -0
  124. package/src/cli/usage-report.ts +13 -5
  125. package/src/cli/v2-input.ts +66 -0
  126. package/src/cli/v2-local-output.ts +37 -0
  127. package/src/cli/v2-runtime.ts +90 -0
  128. package/src/cli/v2.ts +102 -144
  129. package/src/clients/config-export/dsh.ts +21 -0
  130. package/src/clients/config-export.ts +7 -1
  131. package/src/codex/catalog/build-entries.ts +1 -1
  132. package/src/codex/catalog/bundled.ts +9 -2
  133. package/src/codex/catalog/heal-observation.ts +66 -0
  134. package/src/codex/catalog/publication-observer.ts +19 -0
  135. package/src/codex/catalog/remote.ts +13 -15
  136. package/src/codex/catalog/restore.ts +19 -6
  137. package/src/codex/catalog/retained-sync.ts +58 -10
  138. package/src/codex/catalog/routed-removal.ts +116 -0
  139. package/src/codex/catalog/write-audit-contract.ts +21 -0
  140. package/src/codex/catalog/write-audit.ts +171 -0
  141. package/src/codex/catalog-self-heal.ts +284 -0
  142. package/src/codex/catalog-write-serialization.ts +71 -7
  143. package/src/codex/codex-home-owner.ts +124 -0
  144. package/src/codex/convergence.ts +55 -15
  145. package/src/codex/features.ts +8 -1
  146. package/src/codex/inject/restore.ts +46 -4
  147. package/src/codex/inject.ts +11 -1
  148. package/src/codex/internal/catalog-writer.ts +120 -13
  149. package/src/codex/journal.ts +49 -13
  150. package/src/codex/management-convergence.ts +3 -0
  151. package/src/codex/quota-types.ts +3 -0
  152. package/src/codex/quota.ts +13 -3
  153. package/src/codex/refresh.ts +20 -9
  154. package/src/codex/runtime.ts +7 -1
  155. package/src/codex/shim-diagnostics.ts +80 -0
  156. package/src/codex/shim-fingerprint.ts +7 -2
  157. package/src/codex/shim-migration.ts +153 -0
  158. package/src/codex/shim-overlay.ts +509 -0
  159. package/src/codex/shim-path-resolution.ts +14 -2
  160. package/src/codex/shim-probe.ts +4 -3
  161. package/src/codex/shim-state-file.ts +93 -7
  162. package/src/codex/shim.ts +15 -68
  163. package/src/codex/sync.ts +24 -4
  164. package/src/combos/failover.ts +19 -0
  165. package/src/combos/index.ts +1 -0
  166. package/src/combos/resolve.ts +12 -0
  167. package/src/config/atomic-write.ts +13 -8
  168. package/src/config/diagnostics.ts +19 -1
  169. package/src/config/load-degrade.ts +3 -0
  170. package/src/config/schema/chatgpt-desktop.ts +22 -0
  171. package/src/config/schema/config-schema.ts +32 -14
  172. package/src/config/schema/leaf-validators.ts +3 -2
  173. package/src/generated/compatibility-version.json +445 -129
  174. package/src/integrations/merge.ts +13 -5
  175. package/src/integrations/mutation-plan.ts +46 -3
  176. package/src/integrations/omp-yaml-source.ts +181 -1
  177. package/src/integrations/ownership-policy.ts +66 -0
  178. package/src/integrations/registry.ts +50 -1
  179. package/src/integrations/state.ts +46 -21
  180. package/src/integrations/target.ts +25 -2
  181. package/src/integrations/writer.ts +89 -31
  182. package/src/lib/package-tree-integrity.ts +42 -1
  183. package/src/lib/proxy-env.ts +3 -0
  184. package/src/lib/test-home-guard.ts +2 -1
  185. package/src/oauth/anthropic-account-refusal.ts +19 -8
  186. package/src/oauth/anthropic-routing.ts +17 -12
  187. package/src/oauth/anthropic.ts +27 -2
  188. package/src/oauth/index.ts +6 -8
  189. package/src/oauth/pool-settings-capability.ts +9 -2
  190. package/src/protocols/plan-snapshot.ts +1 -1
  191. package/src/protocols/settings.ts +28 -4
  192. package/src/server/caller-session-identity.ts +32 -0
  193. package/src/server/claude-messages.ts +11 -4
  194. package/src/server/index/serve-options.ts +12 -7
  195. package/src/server/index.ts +1 -1
  196. package/src/server/management/agent-settings-routes.ts +6 -5
  197. package/src/server/management/context.ts +8 -1
  198. package/src/server/management/cursor-integration-routes.ts +3 -3
  199. package/src/server/management/integration-routes.ts +5 -5
  200. package/src/server/management/model-routes.ts +3 -3
  201. package/src/server/management/native-integration-routes.ts +2 -2
  202. package/src/server/management/oauth-account-routes.ts +96 -30
  203. package/src/server/management/route-registry.ts +25 -24
  204. package/src/server/messages-native-eligibility.ts +3 -6
  205. package/src/server/messages-native-oauth.ts +82 -35
  206. package/src/server/messages-native.ts +78 -16
  207. package/src/server/responses/adapter-dispatch.ts +53 -28
  208. package/src/server/responses/core-auth.ts +10 -4
  209. package/src/server/responses/core-combo.ts +46 -6
  210. package/src/service/cli.ts +6 -1
  211. package/src/service/ownership-mutation-lease.d.mts +26 -0
  212. package/src/service/ownership-mutation-lease.mjs +76 -1
  213. package/src/types/config.ts +2 -0
  214. package/src/types/provider.ts +10 -10
  215. package/src/update/index.ts +2 -1
  216. package/src/update/update-failure-guidance.d.mts +6 -0
  217. package/src/update/update-failure-guidance.mjs +30 -5
  218. package/gui/dist/assets/App-COdEeqSj.css +0 -1
  219. package/gui/dist/assets/App-DEpCJEP_.js +0 -54
  220. package/gui/dist/assets/index-Cd7ittMv.css +0 -1
  221. package/gui/dist/assets/index-DwJKFJm5.js +0 -94
  222. package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
@@ -4,7 +4,7 @@
4
4
  * A Messages request whose settled route is a proxy-managed key on the `anthropic` adapter is
5
5
  * sent as Messages: the source body (after the ingress's managed-client steps) cut to a field
6
6
  * allowlist, the wire model, and the provider's own key. With `managedMessagesNativeOAuth` on
7
- * (PF-10), an unpooled Anthropic OAuth account is sent the same way with the access token the
7
+ * (PF-10), an Anthropic OAuth account is sent the same way with the access token the
8
8
  * existing OAuth selection resolves at dispatch (`messages-native-oauth.ts`). Modelled on the native Chat lane and
9
9
  * built on the same shared pieces — attempt row, finish-once final log, spend tracker, proactive
10
10
  * key selection, key failover and 429 replay, connection policy — so nothing the Responses
@@ -17,6 +17,13 @@
17
17
  *
18
18
  * Loaded lazily by `claude-messages.ts` only when the switch is on and a route is eligible.
19
19
  */
20
+ import { resolveAnthropicModelRoute } from "../oauth/anthropic-model-routes";
21
+ import { bindAnthropicRefusalCredential, rotateAnthropicAccountOnResponse } from "../oauth/anthropic-account-refusal";
22
+ import { claimAnthropicFamilyRevalidation } from "../oauth/anthropic-model-quota";
23
+ import { captureConfigGeneration } from "../lib/state-store-sweeper";
24
+ import { recordAnthropicAccountQuotaFromHeaders } from "../providers/quota";
25
+ import { credentialGeneration, getAccountCredentialWithStatus } from "../oauth/store";
26
+ import { ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST } from "../oauth/anthropic-routing";
20
27
  import { enforceAnthropicImageLimits } from "../adapters/anthropic-image-guard";
21
28
  import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
22
29
  import { formatAnthropicErrorBody } from "../adapters/anthropic";
@@ -84,6 +91,7 @@ import { nativeMessagesDeclineReason, type NativeMessagesSelector } from "./mess
84
91
  import {
85
92
  nativeOAuthBindingIsCurrent,
86
93
  NativeOAuthSelectionChangedError,
94
+ NativeOAuthModelRouteError,
87
95
  resolveNativeOAuthBinding,
88
96
  restoreOAuthToolNamesInMessage,
89
97
  restoreOAuthToolNamesInSse,
@@ -150,6 +158,7 @@ export interface HandleNativeMessagesOptions {
150
158
  */
151
159
  callerAnthropicBeta?: string | null;
152
160
  clientIdentity?: AnthropicClientIdentity;
161
+ sessionKey?: string | null;
153
162
  }
154
163
 
155
164
  type FinishLog = (status: number, message?: string, meta?: FinalRequestLogMeta) => void;
@@ -373,7 +382,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
373
382
  const oauthProvider = (binding: NativeOAuthBinding): OcxProviderConfig => ({ ...route.provider, apiKey: binding.snapshot.accessToken });
374
383
  if (route.provider.authMode === "oauth") {
375
384
  try {
376
- oauthBinding = await resolveNativeOAuthBinding(config);
385
+ oauthBinding = await resolveNativeOAuthBinding(config, { sessionKey: options.sessionKey, model: route.modelId });
377
386
  } catch (error) {
378
387
  cleanupAbort();
379
388
  upstream.abort();
@@ -381,6 +390,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
381
390
  const localRefusal = localOAuthFailure(error);
382
391
  if (localRefusal) return localRefusal;
383
392
  if (error instanceof NativeOAuthSelectionChangedError) return fail(409, error.message, "api_error");
393
+ if (error instanceof NativeOAuthModelRouteError) return fail(400, error.message, "invalid_request_error");
384
394
  return fail(401, publicOAuthAuthenticationErrorMessage(error), "authentication_error");
385
395
  }
386
396
  } else {
@@ -455,7 +465,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
455
465
  const transientSendAvailable = (): boolean => remainingTransientSends() > 0;
456
466
  const selector: NativeMessagesSelector = options.selector ?? {};
457
467
 
458
- const send = async (recovery?: "rate-limit-429" | "key-429" | "key-401"): Promise<Response> => {
468
+ const send = async (recovery?: "rate-limit-429" | "oauth-account-403" | "key-429" | "key-401"): Promise<Response> => {
459
469
  const remaining = remainingTransientSends();
460
470
  if (requestTransientPolicy && remaining <= 0) {
461
471
  throw new Error("native Messages transient send budget exhausted before recovery dispatch");
@@ -479,10 +489,10 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
479
489
  for (let attempt = 0; !nativeOAuthBindingIsCurrent(oauthBinding); attempt++) {
480
490
  if (attempt >= 3) throw new NativeOAuthSelectionChangedError();
481
491
  try {
482
- oauthBinding = await resolveNativeOAuthBinding(config);
492
+ oauthBinding = await resolveNativeOAuthBinding(config, { sessionKey: options.sessionKey, model: route.modelId });
483
493
  } catch (error) {
484
494
  if (error instanceof OAuthAccountPausedError || error instanceof OAuthLoginRequiredError
485
- || error instanceof AnthropicAccountCooldownError) throw error;
495
+ || error instanceof AnthropicAccountCooldownError || error instanceof NativeOAuthModelRouteError) throw error;
486
496
  throw new NativeOAuthSelectionChangedError();
487
497
  }
488
498
  rebuildFor(oauthProvider(oauthBinding));
@@ -500,15 +510,34 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
500
510
  const headers = new Headers(wire.headers);
501
511
  const encoding = new Headers(init.headers).get("accept-encoding");
502
512
  if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
503
- if (init.signal?.aborted) throw init.signal.reason;
504
- if (!spendTracker.charge()) throw new NativeMessagesSpendRefusal();
505
- noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
506
- const dispatched = await sendWithConnectionPolicy(
507
- (activeProvider as OcxProviderTransport).fetch ?? execute,
508
- wire.url,
509
- applyUpstreamRecoveryInit({ ...init, method: "POST", headers, body: wire.body }, transportRecovery),
510
- { providerName: route.providerName, provider: activeProvider },
511
- );
513
+ const snapshot = oauthBinding?.snapshot;
514
+ const providerAccountUuid = oauthBinding?.providerAccountUuid;
515
+ const writerGeneration = snapshot ? captureConfigGeneration() : 0;
516
+ const ownsBearer = snapshot && headers.get("authorization") === `Bearer ${snapshot.accessToken}` && !headers.has("x-api-key");
517
+ const releaseFamily = snapshot ? claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
518
+ if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
519
+ let dispatched: Response;
520
+ try {
521
+ if (init.signal?.aborted) throw init.signal.reason;
522
+ if (!spendTracker.charge()) throw new NativeMessagesSpendRefusal();
523
+ noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
524
+ dispatched = await sendWithConnectionPolicy(
525
+ (activeProvider as OcxProviderTransport).fetch ?? execute,
526
+ wire.url,
527
+ applyUpstreamRecoveryInit({ ...init, method: "POST", headers, body: wire.body }, transportRecovery),
528
+ { providerName: route.providerName, provider: activeProvider },
529
+ );
530
+ } finally { releaseFamily(); }
531
+ if (ownsBearer && snapshot) {
532
+ try {
533
+ const current = getAccountCredentialWithStatus("anthropic", snapshot.accountId);
534
+ if (current && !current.needsReauth && credentialGeneration(current.credential) === snapshot.generation
535
+ && current.credential.accountId === providerAccountUuid) {
536
+ bindAnthropicRefusalCredential(dispatched, snapshot, providerAccountUuid);
537
+ recordAnthropicAccountQuotaFromHeaders(snapshot.accountId, dispatched.headers, writerGeneration, dispatched.status, route.modelId);
538
+ }
539
+ } catch { /* Passive observation must not fail the response. */ }
540
+ }
512
541
  if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
513
542
  return dispatched;
514
543
  },
@@ -544,7 +573,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
544
573
  rebuildFor(rotated);
545
574
  response = await send("key-401");
546
575
  }
547
- const retryPolicy = rateLimitRetryPolicyFor(activeProvider);
576
+ const retryPolicy = oauthBinding ? undefined : rateLimitRetryPolicyFor(activeProvider);
548
577
  let retries = 0;
549
578
  // A refusal this proxy synthesized for a reset replay is not a provider rate limit.
550
579
  while (
@@ -577,6 +606,37 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
577
606
  rebuildFor(rotated);
578
607
  response = await send("key-429");
579
608
  }
609
+ const oauthRetryKey = {};
610
+ const triedAccountIds = new Set<string>();
611
+ let oauthFailovers = 0;
612
+ while (oauthBinding && (response.status === 429 || response.status === 403)) {
613
+ const sendingBinding = oauthBinding;
614
+ const expectedRecoverySelection = sendingBinding.selection;
615
+ triedAccountIds.add(sendingBinding.snapshot.accountId);
616
+ const nextAccountId = await rotateAnthropicAccountOnResponse(response, {
617
+ config, accountId: sendingBinding.snapshot.accountId, model: route.modelId,
618
+ sessionKey: sendingBinding.sessionKey, decision: sendingBinding.routeDecision,
619
+ excludedAccountIds: triedAccountIds,
620
+ currentDecision: () => {
621
+ const resolved = resolveAnthropicModelRoute(config, route.modelId);
622
+ if (resolved.error) throw new NativeOAuthModelRouteError(`Invalid Anthropic model routes: ${resolved.error}`);
623
+ return resolved.decision;
624
+ },
625
+ signal: upstream.signal, requestKey: oauthRetryKey,
626
+ canRetry: transientSendAvailable() && oauthFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
627
+ });
628
+ if (!nextAccountId) break;
629
+ const recovery = response.status === 403 ? "oauth-account-403" : "rate-limit-429";
630
+ discard(response);
631
+ oauthBinding = await resolveNativeOAuthBinding(config, {
632
+ sessionKey: options.sessionKey, model: route.modelId, candidateAccountId: nextAccountId,
633
+ expectedRecoverySelection, expectedRecoveryRouteDecision: sendingBinding.routeDecision,
634
+ });
635
+ oauthFailovers++;
636
+ rebuildFor(oauthProvider(oauthBinding));
637
+ logCtx.provider = formatAnthropicProviderForLog(route.providerName, oauthBinding.snapshot.accountId, config);
638
+ response = await send(recovery);
639
+ }
580
640
  } catch (error) {
581
641
  releaseRetainedRequest();
582
642
  cleanupAbort();
@@ -591,6 +651,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
591
651
  const localRefusal = localOAuthFailure(sendError);
592
652
  if (localRefusal) return localRefusal;
593
653
  if (sendError instanceof NativeOAuthSelectionChangedError) return fail(409, sendError.message, "api_error");
654
+ if (sendError instanceof NativeOAuthModelRouteError) return fail(400, sendError.message, "invalid_request_error");
594
655
  if (sendError instanceof NativeOpaqueStateRefusal) {
595
656
  logCtx.errorCode = "unsupported_feature";
596
657
  return fail(400, sendError.message, "invalid_request_error", "unsupported_feature");
@@ -791,6 +852,7 @@ export function nativeMessagesCountBody(
791
852
  cc: OcxConfig["claudeCode"],
792
853
  body: Rec,
793
854
  rows: Pick<NativeMessagesSelector, "effortRow" | "fastRow">,
855
+ clientIdentity?: AnthropicClientIdentity,
794
856
  ): Rec | undefined {
795
857
  if (typeof body.model !== "string") return undefined;
796
858
  try {
@@ -812,7 +874,7 @@ export function nativeMessagesCountBody(
812
874
  };
813
875
  if (nativeMessagesDeclineReason(route, body, config, selector) !== undefined) return undefined;
814
876
  // The body without a credential: counting never resolves or refreshes an OAuth account.
815
- return anthropicMessagesNativeWireBody(route.provider, route.modelId, body).wireBody;
877
+ return anthropicMessagesNativeWireBody(route.provider, route.modelId, body, { clientIdentity }).wireBody;
816
878
  } catch {
817
879
  return undefined;
818
880
  }
@@ -1,5 +1,8 @@
1
1
  import { classifyAnthropic429 } from "../../oauth/anthropic-rate-limit-policy";
2
2
  import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
3
+ import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
4
+ import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset-replay";
5
+ import { transientSendCapFor } from "./request-send-budget";
3
6
  import { isNonReplayableResponse } from "../../lib/upstream-retry";
4
7
  import { isLocalUpstream } from "../../lib/local-upstream";
5
8
  import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
@@ -25,6 +28,7 @@ import { withProviderRequestSlot } from "../../providers/request-pacing";
25
28
  import { providerFetch, fetchWithHeaderTimeout, safeHostLabel } from "./fetch-helpers";
26
29
  import {
27
30
  transientRetryPolicyFor,
31
+ resetReplayPolicyFor,
28
32
  rateLimitRetryPolicyFor,
29
33
  hasKeyPoolFailover,
30
34
  rotateProviderTransportOn401,
@@ -156,6 +160,8 @@ export async function prepareAdapterExchange(
156
160
  | "reserveCredentialHop"
157
161
  | "pendingHopPermit"
158
162
  | "workflowRootId"
163
+ | "claimAmbiguousResend"
164
+ | "sendsUsed"
159
165
  >,
160
166
  ) {
161
167
  const { options, config, logCtx, req } = requestContext;
@@ -189,9 +195,18 @@ export async function prepareAdapterExchange(
189
195
  recoveryClassFor,
190
196
  sendBudgetExhausted,
191
197
  reserveCredentialHop,
198
+ claimAmbiguousResend,
192
199
  } = sendBudgetState;
193
200
 
194
201
 
202
+ let selfContainedJudgment: boolean | undefined;
203
+ const requestIsSelfContained = (): boolean =>
204
+ selfContainedJudgment ??= selfContainedResponsesBody(parsed._rawBody);
205
+ const claimPreHeaderResend = (): boolean => authorizeResendForRecovery(
206
+ "pre-header", "connection-reset",
207
+ ambiguousResendAllowanceFor(route.provider, requestIsSelfContained, claimAmbiguousResend),
208
+ ).allowed;
209
+
195
210
  const upstream = new AbortController();
196
211
  const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
197
212
  const connectMs = config.connectTimeoutMs ?? 200_000;
@@ -343,6 +358,13 @@ export async function prepareAdapterExchange(
343
358
  const compactPrepaid = options.compactionRecoveryAttempted ? sendBudgetState.pendingHopPermit : undefined;
344
359
  if (compactPrepaid) sendBudgetState.pendingHopPermit = undefined;
345
360
  let compactPrepaidUsed = false;
361
+ // Combo and emergency compaction admission already book this target's first send.
362
+ // Its configured initial ceiling is target-local; the shared remainder below still
363
+ // accounts for earlier targets without deducting their sends from this target twice.
364
+ const initialSendCap = transientPolicy || resetReplayPolicyFor(route.provider)
365
+ ? transientSendCapFor(transientPolicy?.attempts,
366
+ (options.comboAttempt || compactPrepaid) ? 0 : sendBudgetState.sendsUsed)
367
+ : 1;
346
368
  const fetchWithRetryPolicy = (route.provider.adapter === "google" || transientPolicy)
347
369
  ? fetchWithTransientRetry
348
370
  : fetchWithResetRetry;
@@ -367,15 +389,12 @@ export async function prepareAdapterExchange(
367
389
  {
368
390
  abortSignal: upstream.signal,
369
391
  label: safeHostLabel(builtInitialRequest.url),
370
- ...(transientPolicy || compactPrepaid
371
- // Draws the remainder, not the raw policy. A combo child inherits the parent's
372
- // holder but used to take a fresh full allowance on its own first send, so the
373
- // shared counter was inherited without ever being read as a limit.
392
+ claimAmbiguousResend: claimPreHeaderResend,
393
+ ...(transientPolicy || resetReplayPolicyFor(route.provider) || compactPrepaid
374
394
  ? {
375
- // The first emergency send is already paid for. Only retries consume the
376
- // remaining allowance; treating the booking as unavailable blocks a cap of two.
377
- attempts: Math.min(transientPolicy?.attempts ?? 1,
378
- remainingTransientSendBudget(transientPolicy?.attempts ?? 1) + (compactPrepaid ? 1 : 0)),
395
+ // A pending compaction permit already booked this leg's first physical send.
396
+ attempts: Math.min(initialSendCap,
397
+ remainingTransientSendBudget(initialSendCap) + (compactPrepaid ? 1 : 0)),
379
398
  onSendsConsumed: noteTransientSends,
380
399
  }
381
400
  : {}),
@@ -545,14 +564,21 @@ export async function prepareAdapterExchange(
545
564
  const refetchWithPolicy = (route.provider.adapter === "google" || refetchTransientPolicy)
546
565
  ? fetchWithTransientRetry
547
566
  : fetchWithResetRetry;
548
- // Same rule as the passthrough rebuild: spend the base allowance first, then the one
549
- // shared final-recovery reserve, so a recovery that follows a spent streak still gets
550
- // its single send instead of dying at three.
551
- const refetchAllowance = refetchTransientPolicy
552
- ? recoverySendAllowance(
553
- refetchTransientPolicy.attempts,
554
- recoveryClassFor(recovery),
567
+ const helperCountsSends = refetchTransientPolicy !== null || resetReplayPolicyFor(route.provider) !== null;
568
+ const prepaid = sendBudgetState.pendingHopPermit;
569
+ const configuredTotal = refetchTransientPolicy?.attempts;
570
+ const refetchCap = transientSendCapFor(configuredTotal,
571
+ sendBudgetState.sendsUsed - (prepaid ? 1 : 0));
572
+ // An exact total includes a booked hop, even when it consumed the last base slot.
573
+ // Keep that funded send while forbidding the final reserve from widening the total.
574
+ const prepaidLastSlot = helperCountsSends && configuredTotal !== undefined && prepaid
575
+ && refetchCap > 0 && remainingTransientSendBudget(refetchCap) === 0;
576
+ if (prepaidLastSlot) sendBudgetState.pendingHopPermit = undefined;
577
+ const refetchAllowance = helperCountsSends
578
+ ? prepaidLastSlot ? { attempts: 1, permit: prepaid } : recoverySendAllowance(
579
+ refetchCap, recoveryClassFor(recovery),
555
580
  `${route.providerName}|${route.modelId}|${recovery}`,
581
+ { allowFinalRecoveryReserve: configuredTotal === undefined },
556
582
  )
557
583
  : undefined;
558
584
  try {
@@ -566,7 +592,7 @@ export async function prepareAdapterExchange(
566
592
  // can be refused above before it does. use() past the first attempt is a no-op.
567
593
  onDispatch?.();
568
594
  if (preserveFailureResponse) replacementAdmitted = true;
569
- if (!refetchTransientPolicy) chargeFastDowngradeWorkflowSend();
595
+ if (!refetchAllowance) chargeFastDowngradeWorkflowSend();
570
596
  return fetchWithHeaderTimeout(retryRequest.url,
571
597
  applyUpstreamRecoveryInit({
572
598
  method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
@@ -580,6 +606,7 @@ export async function prepareAdapterExchange(
580
606
  {
581
607
  abortSignal: upstream.signal,
582
608
  label: safeHostLabel(retryRequest.url),
609
+ claimAmbiguousResend: claimPreHeaderResend,
583
610
  ...(refetchAllowance
584
611
  ? {
585
612
  attempts: refetchAllowance.attempts,
@@ -638,7 +665,7 @@ export async function prepareAdapterExchange(
638
665
  const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
639
666
  const hop = reserveCredentialHop("auth-recovery",
640
667
  `${route.providerName}|${route.modelId}|antigravity-auth`,
641
- !adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null);
668
+ !adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null));
642
669
  if (!hop.allowed) return null;
643
670
  try {
644
671
  const snapshot = await failoverAccountSnapshot(route.providerName, nextId);
@@ -708,7 +735,7 @@ export async function prepareAdapterExchange(
708
735
  const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
709
736
  const hop = reserveCredentialHop("auth-recovery",
710
737
  `${route.providerName}|${route.modelId}|terminal-refresh-account`,
711
- !adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null);
738
+ !adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null));
712
739
  if (hop.allowed) {
713
740
  try {
714
741
  const admitted = await applyFailoverSnapshot(alternate);
@@ -861,7 +888,7 @@ export async function prepareAdapterExchange(
861
888
  const hop = reserveCredentialHop(
862
889
  "repair",
863
890
  `${route.providerName}|${route.modelId}|anthropic-fast-downgrade`,
864
- !adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
891
+ !adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
865
892
  );
866
893
  if (hop.allowed) {
867
894
  anthropicFastDowngradeGuard.attempted = true;
@@ -1059,10 +1086,9 @@ export async function prepareAdapterExchange(
1059
1086
  const hop = reserveCredentialHop(
1060
1087
  "auth-recovery",
1061
1088
  `${route.providerName}|${route.modelId}|adapter-recovery-oauth-429`,
1062
- // Only a helper-routed replay reports this send back. A reset-only refetch reports
1063
- // nothing and an adapter ladder settles the booking itself, so promising an external
1064
- // report on either would leave a booking pending until it swallowed a later charge.
1065
- !adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
1089
+ // Transient and opted-in reset helpers report physical sends; an adapter-owned
1090
+ // ladder settles its own booking.
1091
+ !adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
1066
1092
  );
1067
1093
  if (!hop.allowed) break;
1068
1094
  const nextAccountId = rotateGenericOAuthAccountOnRefusal(
@@ -1164,10 +1190,9 @@ export async function prepareAdapterExchange(
1164
1190
  const hop = reserveCredentialHop(
1165
1191
  "auth-recovery",
1166
1192
  `${route.providerName}|${route.modelId}|adapter-recovery-oauth-429`,
1167
- // Only a helper-routed replay reports this send back. A reset-only refetch reports
1168
- // nothing and an adapter ladder settles the booking itself, so promising an external
1169
- // report on either would leave a booking pending until it swallowed a later charge.
1170
- !adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
1193
+ // Transient and opted-in reset helpers report physical sends; an adapter-owned
1194
+ // ladder settles its own booking.
1195
+ !adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
1171
1196
  );
1172
1197
  if (!hop.allowed) break;
1173
1198
  const nextAccountId = rotateGenericOAuthAccountOn429(
@@ -1276,7 +1301,7 @@ export async function prepareAdapterExchange(
1276
1301
  const hop = reserveCredentialHop(
1277
1302
  "auth-recovery",
1278
1303
  `${route.providerName}|${route.modelId}|adapter-recovery-oauth-verify`,
1279
- !adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
1304
+ !adapterOwnsDispatch && (transientRetryPolicyFor(route.provider) !== null || resetReplayPolicyFor(route.provider) !== null),
1280
1305
  );
1281
1306
  if (!hop.allowed) break;
1282
1307
  // Auth-refusal rotation, not the rate-limit one: a verification refusal
@@ -41,13 +41,19 @@ import { markLocalRequestLogRefusal } from "../request-log";
41
41
  import { CODEX_POOL_REFRESH_INCOMPLETE_LOG_REASON } from "../../codex/pool-refresh-backoff";
42
42
  import { codexAuthContextLogLabel } from "../../codex/account-label";
43
43
  import { forceRefreshMainAccountToken } from "../../codex/main-account";
44
+ import { safeCallerSessionId } from "../caller-session-identity";
44
45
 
45
- /** Keep synthesized Claude identity out of request headers reused by policy/combo fallback. */
46
+ /** Normalize caller aliases only on native egress; preserve ingress and fallback identity. */
46
47
  export function withClaudeNativeSession(headers: Headers, provider: OcxProviderConfig, sessionId?: string): Headers {
47
- if (!sessionId || !isCanonicalOpenAiForwardProvider(provider)
48
- || headers.has("session_id") || headers.has("session-id") || headers.has("thread-id")) return headers;
48
+ // Empty values count as absent, matching materializeCodexUpstreamAuth, so first attempts and
49
+ // retries rebuilt from caller headers pick the same identity.
50
+ if (!isCanonicalOpenAiForwardProvider(provider) || headers.get("session_id")) return headers;
51
+ const alias = ["session-id", "thread-id"].find(name => headers.get(name));
52
+ const nativeSession = alias ? safeCallerSessionId(headers.get(alias)) : sessionId;
53
+ // A non-empty invalid alias suppresses weaker metadata identity.
54
+ if (!nativeSession) return headers;
49
55
  const forwarded = new Headers(headers);
50
- forwarded.set("session_id", sessionId);
56
+ forwarded.set("session_id", nativeSession);
51
57
  return forwarded;
52
58
  }
53
59
 
@@ -28,6 +28,7 @@ import {
28
28
  concreteComboRequestBody,
29
29
  comboDefaultEffort,
30
30
  isComboTargetInCooldown,
31
+ snapshotComboQuotaCooldowns,
31
32
  noteComboSuccess,
32
33
  comboFailureDecision,
33
34
  advanceComboAfterFailure,
@@ -72,6 +73,7 @@ import { isPlainObject } from "../../lib/plain-data";
72
73
  import {
73
74
  clientCancelledResponse,
74
75
  comboUnavailable,
76
+ comboUnavailableResponse,
75
77
  targetIncompatibleResponse,
76
78
  unreadableEncryptedAgentTaskResponse,
77
79
  } from "./core-errors";
@@ -412,8 +414,8 @@ export async function executeComboResponses(
412
414
  }
413
415
  };
414
416
  const adoptFailedChildLog = (childLog: RequestLogContext): void => {
415
- // Attempts remain the complete physical history; the logical row mirrors the most recent
416
- // failed target so an exhausted combo still has useful top-level reasoning diagnostics.
417
+ // Attempts remain the complete physical history; the logical row mirrors the failed
418
+ // target whose response is returned, including an earlier quota refusal on exhaustion.
417
419
  Object.assign(logCtx, childLog, {
418
420
  requestedModel,
419
421
  model: requestedModel,
@@ -676,6 +678,41 @@ export async function executeComboResponses(
676
678
  // is gone, so carry the loop's own classification decision instead of re-deriving a
677
679
  // weaker one from the status alone (#4149).
678
680
  let lastFailureClassifiesOverflow = false;
681
+ // The first quota/rate-limit refusal of the ladder. A fallback that then refuses its own
682
+ // credential or plan (401/403, or a 400 the ladder hopped past) says nothing about why the
683
+ // request failed: the primary target ran out. Returning that fallback refusal told clients
684
+ // "OpenAI account pool has no usable account credential" while every Claude account was spent.
685
+ let quotaFailure: Response | undefined;
686
+ let quotaFailedChildLog: RequestLogContext | undefined;
687
+ // Snapshotted before dispatch: a 401/403 inside this ladder cools its own provider, and that
688
+ // fresh cooldown is not a reason the request found no target.
689
+ const quotaCooldownSnapshot = snapshotComboQuotaCooldowns(config, comboId);
690
+ const exhaustedFailure = (): Response => {
691
+ const returnsQuota = lastFailure && [400, 401, 403].includes(lastFailure.status) && quotaFailure;
692
+ const failedChildLog = returnsQuota ? quotaFailedChildLog : lastFailedChildLog;
693
+ if (failedChildLog) adoptFailedChildLog(failedChildLog);
694
+ if (!lastFailure || ![400, 401, 403].includes(lastFailure.status)) return lastFailure!;
695
+ if (quotaFailure) return quotaFailure;
696
+ // The quota-refused target was cooled before this request picked, so the ladder never saw
697
+ // its refusal. The cooldown, with its Retry-After, is still the honest answer.
698
+ let quotaCooldownUntil: number | undefined;
699
+ const now = Date.now();
700
+ for (const { target, cooldownUntil } of quotaCooldownSnapshot) {
701
+ if (cooldownUntil <= now) continue;
702
+ try {
703
+ if (!targetEligible(target)) continue;
704
+ } catch {
705
+ // An eligibility check can exhaust the translator budget; it is not quota evidence.
706
+ continue;
707
+ }
708
+ quotaCooldownUntil = Math.min(quotaCooldownUntil ?? cooldownUntil, cooldownUntil);
709
+ }
710
+ const remainingQuotaCooldownMs = quotaCooldownUntil === undefined ? 0 : quotaCooldownUntil - Date.now();
711
+ return remainingQuotaCooldownMs > 0
712
+ ? comboUnavailableResponse(`No available targets for combo: ${comboId}`, {
713
+ retryAfter: String(Math.max(1, Math.ceil(remainingQuotaCooldownMs / 1000))),
714
+ }) : lastFailure;
715
+ };
679
716
  while (pick) {
680
717
  if (options.abortSignal?.aborted) return clientCancelledResponse();
681
718
  const firstComboTarget = comboTargetsDispatched === 0;
@@ -698,8 +735,7 @@ export async function executeComboResponses(
698
735
  // return the last real upstream answer with its status, headers and any quota body
699
736
  // intact rather than to mint a synthetic error, and a later target only exists because
700
737
  // an earlier one already recorded one.
701
- if (lastFailedChildLog) adoptFailedChildLog(lastFailedChildLog);
702
- return lastFailure!;
738
+ return exhaustedFailure();
703
739
  }
704
740
  const targetSendBudget = comboSendScope
705
741
  ? comboTargetSendBudget(comboSendScope, combo.targets.length - 1 - comboTargetsDispatched)
@@ -979,6 +1015,10 @@ export async function executeComboResponses(
979
1015
  );
980
1016
  attemptRetained = true;
981
1017
  lastFailure = failure.response;
1018
+ if (!quotaFailure && (lastFailure.status === 429 || lastFailure.status === 402)) {
1019
+ quotaFailure = lastFailure;
1020
+ quotaFailedChildLog = childLog;
1021
+ }
982
1022
  lastFailedChildLog = childLog;
983
1023
  // A replacement that answers 200 is unmarked, and its zero-output failure only exists once
984
1024
  // preflight has rebuilt the stream as a fresh Response. A spent grant never hops: a status the
@@ -1141,9 +1181,9 @@ export async function executeComboResponses(
1141
1181
  }
1142
1182
  // Waiting or recovery may have observed cancellation after the check above.
1143
1183
  if (options.abortSignal?.aborted) return clientCancelledResponse();
1144
- adoptFailedChildLog(childLog);
1145
1184
  }
1146
1185
  }
1186
+ const failure = exhaustedFailure();
1147
1187
  if (
1148
1188
  lastFailure?.status === 413
1149
1189
  && lastFailureClassifiesOverflow
@@ -1152,5 +1192,5 @@ export async function executeComboResponses(
1152
1192
  ? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
1153
1193
  : jsonContextOverflowResponse();
1154
1194
  }
1155
- return lastFailure!;
1195
+ return failure;
1156
1196
  }
@@ -16,7 +16,8 @@ import { resolveServiceListenPort, reportServiceServing } from "./health";
16
16
  import { platformOps, proxyStillLiveAfterStop, stopTrackedProxyForServiceCommand, installServiceSafely, installFreshWindowsSchedulerSafely, removeServiceInstallState, isServiceInstalled } from "./orchestration";
17
17
  import { repairService } from "./repair";
18
18
  import type { ServiceRepairVerb } from "./repair";
19
- import { TASK, plistPath, readServiceBackend, releaseServiceOwner, resolveServiceOwnership } from "./state";
19
+ import { TASK, plistPath, readServiceBackend, releaseServiceOwner, resolveServiceOwnership, serviceStatePaths } from "./state";
20
+ import { ownershipMutationLeaseStatusLine } from "./ownership-mutation-lease.mjs";
20
21
  import { foreignServiceOwnerRefusal, unknownServiceOwnerRefusal } from "./repair";
21
22
  import type { ServiceBackend } from "./state";
22
23
  import { unitPath } from "./systemd";
@@ -419,6 +420,10 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
419
420
  // subsumes the not-installed case and adds the serving / stale-plist split.
420
421
  console.log(await serviceStatusReport());
421
422
  }
423
+ // A busy lease is why a supervised start can keep failing while the line above only
424
+ // says "not running"; report its recorded PID with unverified identity and lease age here.
425
+ const lease = ownershipMutationLeaseStatusLine(serviceStatePaths());
426
+ if (lease) console.log(lease);
422
427
  console.log(`Diagnostics: ${serviceDiagnosticsSummary()}`);
423
428
  break;
424
429
  }
@@ -5,6 +5,22 @@ export interface OwnershipMutationLeaseOptions {
5
5
  readonly processAlive?: (pid: number) => boolean;
6
6
  readonly beforeRelease?: (lockPath: string) => void;
7
7
  readonly joinToken?: string;
8
+ /** Names the current executable at the recorded PID, identity unverified; defaults to `tasklist` or `ps`. */
9
+ readonly processImage?: (pid: number) => string | null;
10
+ }
11
+
12
+ /** The recorded lease owner, identity unverified, read for diagnostics; never used to reclaim it. */
13
+ export interface OwnershipMutationLeaseHolder {
14
+ readonly path: string;
15
+ /** The recorded PID; null when the directory holds no parseable owner. */
16
+ readonly pid: number | null;
17
+ /** Liveness of the current process at the recorded PID; identity is not verified. */
18
+ readonly alive: boolean | null;
19
+ /** The current executable at the recorded PID when alive and readable; identity is not verified. */
20
+ readonly image: string | null;
21
+ /** Milliseconds since the owner was written, on the clock stale recovery uses. */
22
+ readonly ageMs: number | null;
23
+ readonly record: "complete" | "incomplete" | "empty" | "unreadable";
8
24
  }
9
25
 
10
26
  export interface OwnershipMutationLease { readonly token: string; release(): void }
@@ -30,3 +46,13 @@ export declare function withOwnershipMutationLease<T>(
30
46
  run: () => T,
31
47
  options?: OwnershipMutationLeaseOptions,
32
48
  ): T;
49
+
50
+ export declare function inspectOwnershipMutationLease(
51
+ statePaths: readonly string[],
52
+ options?: Pick<OwnershipMutationLeaseOptions, "now" | "processAlive" | "processImage">,
53
+ ): OwnershipMutationLeaseHolder | null;
54
+
55
+ export declare function ownershipMutationLeaseStatusLine(
56
+ statePaths: readonly string[],
57
+ options?: Pick<OwnershipMutationLeaseOptions, "now" | "processAlive" | "processImage">,
58
+ ): string | null;