@bitkyc08/opencodex 2.82.0 → 2.84.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 (266) hide show
  1. package/gui/dist/assets/App-CZtqiGcw.js +56 -0
  2. package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
  3. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  4. package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
  5. package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-BbEeRfPY.js} +1 -1
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +2 -2
  8. package/src/adapters/coding-agent/protocol.ts +15 -8
  9. package/src/adapters/cursor/catalog.ts +2 -2
  10. package/src/adapters/cursor/effort-map.ts +1 -1
  11. package/src/adapters/devin/live-models.ts +2 -2
  12. package/src/adapters/google-truncation.ts +22 -0
  13. package/src/adapters/google.ts +7 -3
  14. package/src/adapters/ollama-native.ts +47 -15
  15. package/src/adapters/openai-chat/messages.ts +8 -4
  16. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  17. package/src/adapters/openai-responses/passthrough.ts +5 -0
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/request-strips.ts +10 -2
  21. package/src/adapters/qoder/adapter.ts +16 -14
  22. package/src/bridge/errors.ts +26 -1
  23. package/src/bridge.ts +1 -1
  24. package/src/claude/context-windows.ts +57 -4
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/intercept/connect-proxy.ts +10 -3
  27. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  28. package/src/claude/intercept/picker-listener.ts +49 -1
  29. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  30. package/src/claude/intercept/settings.ts +101 -9
  31. package/src/cli/account-api.ts +65 -0
  32. package/src/cli/account-auth.ts +44 -12
  33. package/src/cli/account.ts +41 -5
  34. package/src/cli/capabilities-access-remote.ts +8 -0
  35. package/src/cli/capabilities-base.ts +13 -1
  36. package/src/cli/catalog-command-result.ts +2 -1
  37. package/src/cli/claude.ts +15 -6
  38. package/src/cli/config-command.ts +8 -2
  39. package/src/cli/debug.ts +8 -0
  40. package/src/cli/dispatch.ts +16 -3
  41. package/src/cli/export-command.ts +14 -1
  42. package/src/cli/hub-disable.ts +99 -0
  43. package/src/cli/hub.ts +4 -2
  44. package/src/cli/index.ts +13 -4
  45. package/src/cli/link-child.ts +104 -0
  46. package/src/cli/link-enrollment.ts +96 -0
  47. package/src/cli/link.ts +39 -24
  48. package/src/cli/observe-stream.ts +6 -1
  49. package/src/cli/observe.ts +10 -2
  50. package/src/cli/provider.ts +12 -0
  51. package/src/cli/registry.ts +5 -4
  52. package/src/cli/resolve.ts +1 -1
  53. package/src/cli/runtime-api.ts +27 -2
  54. package/src/cli/status.ts +104 -20
  55. package/src/client/connect.ts +39 -4
  56. package/src/client/hub-client.ts +14 -7
  57. package/src/client/hub-state.ts +25 -14
  58. package/src/client/link-home.ts +248 -0
  59. package/src/client/link-management-relay.ts +61 -0
  60. package/src/client/link-relay.ts +6 -1
  61. package/src/client/link-status.ts +14 -0
  62. package/src/client/machine-api.ts +14 -7
  63. package/src/client/machine-listener.ts +29 -4
  64. package/src/clients/config-export/model-metadata.ts +16 -0
  65. package/src/clients/config-export/zcode-store.ts +46 -11
  66. package/src/clients/config-export/zcode.ts +3 -5
  67. package/src/clients/config-export.ts +47 -17
  68. package/src/clients/omo-role-models.ts +6 -3
  69. package/src/codex/agent-role-models.ts +26 -8
  70. package/src/codex/auth-api/account-list.ts +17 -2
  71. package/src/codex/auth-api/account-selection.ts +17 -7
  72. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  73. package/src/codex/auth-api/routes.ts +5 -1
  74. package/src/codex/auth-context.ts +8 -3
  75. package/src/codex/autostart-health.ts +3 -1
  76. package/src/codex/catalog/bundled.ts +5 -1
  77. package/src/codex/catalog/effort.ts +2 -2
  78. package/src/codex/catalog/metadata.ts +4 -2
  79. package/src/codex/catalog/native-models.ts +17 -3
  80. package/src/codex/catalog/provider-models.ts +25 -0
  81. package/src/codex/desktop-app/darwin.ts +1 -1
  82. package/src/codex/desktop-app/handoff.ts +1 -1
  83. package/src/codex/desktop-app/linux.ts +1 -1
  84. package/src/codex/desktop-app/lock.ts +1 -1
  85. package/src/codex/desktop-app/types.ts +1 -1
  86. package/src/codex/desktop-app/windows.ts +1 -1
  87. package/src/codex/desktop-app-restart.ts +1 -1
  88. package/src/codex/main-account.ts +6 -7
  89. package/src/codex/native-profile-startup.ts +4 -1
  90. package/src/codex/prompt-journal.ts +20 -2
  91. package/src/codex/prompt-layers/encoding.ts +49 -0
  92. package/src/codex/prompt-layers/import-source.ts +30 -0
  93. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  94. package/src/codex/prompt-layers/toml-read.ts +52 -4
  95. package/src/codex/prompt-layers.ts +383 -20
  96. package/src/codex/prompt-lock-claim.ts +13 -3
  97. package/src/codex/prompt-lock-evidence.ts +3 -3
  98. package/src/codex/prompt-lock-owner.ts +14 -6
  99. package/src/codex/prompt-text-probe.ts +34 -5
  100. package/src/codex/quota.ts +13 -6
  101. package/src/codex/role-auto-assign.ts +2 -5
  102. package/src/codex/routing/active-account.ts +21 -0
  103. package/src/codex/routing.ts +1 -1
  104. package/src/codex/runtime.ts +196 -8
  105. package/src/codex/shim-overlay.ts +8 -4
  106. package/src/codex/shim.ts +1 -1
  107. package/src/combos/failover.ts +3 -0
  108. package/src/config/api-key-reconcile.ts +34 -0
  109. package/src/config/diagnostics.ts +16 -0
  110. package/src/config/feature-flags.ts +5 -0
  111. package/src/config/live-reconcile.ts +43 -29
  112. package/src/config/live-topology.ts +29 -0
  113. package/src/config/load-degrade.ts +2 -1
  114. package/src/config/paths.ts +3 -0
  115. package/src/config/salvage.ts +3 -1
  116. package/src/config/schema/config-schema.ts +2 -0
  117. package/src/generated/compatibility-version.json +373 -217
  118. package/src/grok/grpc-web.ts +23 -1
  119. package/src/grok/inject.ts +245 -221
  120. package/src/grok/reset-coupon-ledger.ts +293 -57
  121. package/src/grok/reset-coupons.ts +15 -6
  122. package/src/integrations/cursor-detect.ts +1 -1
  123. package/src/integrations/mutation-plan.ts +13 -7
  124. package/src/integrations/registry.ts +45 -11
  125. package/src/integrations/state.ts +19 -7
  126. package/src/integrations/target.ts +42 -5
  127. package/src/integrations/writer.ts +14 -6
  128. package/src/lib/app-owned-memory-stores.ts +4 -0
  129. package/src/lib/app-owned-memory.ts +11 -0
  130. package/src/lib/errors.ts +25 -0
  131. package/src/lib/json-byte-size.ts +31 -0
  132. package/src/lib/optional-shutdown-hooks.ts +35 -0
  133. package/src/lib/outbound-credential-registry.ts +210 -0
  134. package/src/lib/passive-read.ts +35 -0
  135. package/src/lib/redact.ts +2 -2
  136. package/src/lib/shadow-call.ts +44 -5
  137. package/src/link/codex-settings.ts +155 -0
  138. package/src/link/management-routes.ts +89 -0
  139. package/src/link/remote-probe.ts +38 -0
  140. package/src/link/ssh-argv.ts +26 -5
  141. package/src/link/ssh-runner.ts +58 -27
  142. package/src/link/status-projection.ts +22 -4
  143. package/src/link/supervisor.ts +144 -49
  144. package/src/link/tunnel-state.ts +11 -4
  145. package/src/oauth/anthropic-continuity.ts +4 -3
  146. package/src/oauth/anthropic-identity.ts +13 -2
  147. package/src/oauth/anthropic-model-quota.ts +13 -1
  148. package/src/oauth/anthropic-routing.ts +230 -23
  149. package/src/oauth/anthropic.ts +18 -4
  150. package/src/oauth/google-antigravity.ts +24 -7
  151. package/src/oauth/index.ts +25 -16
  152. package/src/oauth/kiro-account-identity.ts +106 -0
  153. package/src/oauth/kiro.ts +48 -5
  154. package/src/oauth/meta-muse-device.ts +1 -1
  155. package/src/oauth/plan.ts +29 -0
  156. package/src/oauth/store.ts +40 -3
  157. package/src/oauth/types.ts +3 -1
  158. package/src/plugins/loader.ts +11 -0
  159. package/src/providers/anthropic-billing-ownership.ts +74 -0
  160. package/src/providers/anthropic-billing-recovery.ts +31 -0
  161. package/src/providers/anthropic-billing-refusal.ts +55 -0
  162. package/src/providers/anthropic-fast.ts +1 -1
  163. package/src/providers/anthropic-reset-admission.ts +62 -0
  164. package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
  165. package/src/providers/api-key-resolve.ts +17 -2
  166. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  167. package/src/providers/devin-provider-merge-migration.ts +1 -1
  168. package/src/providers/github-copilot-auto.ts +169 -0
  169. package/src/providers/key-failover.ts +52 -5
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/model-rename-startup.ts +5 -3
  172. package/src/providers/muse-subscription-usage.ts +1 -1
  173. package/src/providers/quota/account-cache.ts +11 -0
  174. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  175. package/src/providers/quota/report-cache.ts +14 -0
  176. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  177. package/src/providers/quota.ts +4 -1
  178. package/src/providers/registry/entries-core.ts +20 -20
  179. package/src/providers/registry/entries-extended.ts +4 -1
  180. package/src/providers/registry/model-seeds.ts +20 -3
  181. package/src/providers/request-pacing.ts +9 -0
  182. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  183. package/src/providers/xai-fast-model.ts +1 -1
  184. package/src/responses/compaction.ts +14 -0
  185. package/src/responses/parser.ts +7 -1
  186. package/src/server/auth-cors.ts +42 -0
  187. package/src/server/configured-credentials.ts +194 -0
  188. package/src/server/inbound-body-admission.ts +24 -7
  189. package/src/server/index/link-listener.ts +30 -17
  190. package/src/server/index/link-management.ts +58 -0
  191. package/src/server/index/optional-listeners.ts +5 -1
  192. package/src/server/index/serve-options.ts +21 -2
  193. package/src/server/index.ts +1 -1
  194. package/src/server/lifecycle.ts +33 -5
  195. package/src/server/management/agent-settings-routes.ts +28 -10
  196. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  197. package/src/server/management/codex-agent-role-routes.ts +19 -6
  198. package/src/server/management/codex-prompt-routes.ts +136 -5
  199. package/src/server/management/codex-role-auto-assign.ts +1 -1
  200. package/src/server/management/grok-coupon-routes.ts +333 -57
  201. package/src/server/management/link-enrollment.ts +192 -0
  202. package/src/server/management/link-route-state.ts +185 -0
  203. package/src/server/management/link-routes.ts +92 -390
  204. package/src/server/management/model-rows.ts +3 -1
  205. package/src/server/management/provider-overwrite-carry.ts +10 -0
  206. package/src/server/management/provider-routes.ts +1 -0
  207. package/src/server/management/route-registry.ts +7 -5
  208. package/src/server/management/shared.ts +3 -0
  209. package/src/server/management-api.ts +3 -1
  210. package/src/server/messages-native.ts +68 -42
  211. package/src/server/relay-eager.ts +4 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-failure-attribution.ts +3 -0
  214. package/src/server/request-log-filter.ts +6 -1
  215. package/src/server/request-log-terminal-status.ts +243 -0
  216. package/src/server/request-log.ts +15 -118
  217. package/src/server/responses/adapter-continuation.ts +9 -1
  218. package/src/server/responses/adapter-dispatch.ts +293 -81
  219. package/src/server/responses/code-mode-collab-surface.ts +205 -0
  220. package/src/server/responses/codex-ws-exchange.ts +147 -6
  221. package/src/server/responses/codex-ws-wire.ts +203 -0
  222. package/src/server/responses/collaboration.ts +36 -7
  223. package/src/server/responses/compact.ts +129 -50
  224. package/src/server/responses/context-overflow.ts +13 -1
  225. package/src/server/responses/core-codex-account.ts +105 -1
  226. package/src/server/responses/core-combo-failure.ts +14 -0
  227. package/src/server/responses/core-combo.ts +7 -2
  228. package/src/server/responses/core-errors.ts +29 -1
  229. package/src/server/responses/core-opaque-recovery.ts +5 -5
  230. package/src/server/responses/core-options.ts +2 -0
  231. package/src/server/responses/core.ts +6 -6
  232. package/src/server/responses/deferred-collab-surface.ts +158 -0
  233. package/src/server/responses/deferred-send-reporter.ts +25 -0
  234. package/src/server/responses/empty-completion-guard.ts +18 -2
  235. package/src/server/responses/fetch-helpers.ts +12 -1
  236. package/src/server/responses/jev-model-invoke.ts +1 -1
  237. package/src/server/responses/passthrough-delivery.ts +4 -1
  238. package/src/server/responses/passthrough-dispatch.ts +130 -64
  239. package/src/server/responses/passthrough-execution.ts +3 -3
  240. package/src/server/responses/request-prepare.ts +6 -4
  241. package/src/server/responses/request-transport.ts +131 -13
  242. package/src/server/responses/run-turn-execution.ts +13 -5
  243. package/src/server/responses/sidecar-execution.ts +16 -2
  244. package/src/server/responses/spend-storage-error.ts +33 -0
  245. package/src/server/responses/ws-upstream.ts +9 -1
  246. package/src/server/startup-action-control.ts +22 -2
  247. package/src/server/startup-health-cache.ts +4 -0
  248. package/src/server/system-env-shell.ts +7 -2
  249. package/src/server/system-env.ts +11 -1
  250. package/src/service/cli.ts +5 -1
  251. package/src/service/desktop-startup.ts +147 -34
  252. package/src/service/desktop-supervision.d.mts +27 -1
  253. package/src/service/desktop-supervision.mjs +111 -5
  254. package/src/service/health-sentinel-launch.ts +176 -0
  255. package/src/service/health-sentinel-main.ts +447 -0
  256. package/src/service/health-sentinel-policy.ts +241 -0
  257. package/src/service/health-sentinel-probe.ts +54 -0
  258. package/src/service/health-sentinel-windows.ts +87 -0
  259. package/src/service/linux-linger.ts +20 -0
  260. package/src/types/config.ts +5 -0
  261. package/src/types/request.ts +5 -0
  262. package/src/usage/expected-prices.ts +5 -1
  263. package/src/usage/log.ts +19 -1
  264. package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
  265. package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
  266. package/gui/dist/assets/index-D3FBCNiP.js +0 -94
@@ -12,6 +12,7 @@ import { homedir } from "node:os";
12
12
  import { assertDroidSettingsUnambiguous } from "./droid-settings";
13
13
  import { readPath } from "./merge";
14
14
  import type { OwnershipRecord } from "./ownership";
15
+ import type { IntegrationIO } from "./config-io";
15
16
  import { dirname, join } from "node:path";
16
17
  import {
17
18
  ClientPathError,
@@ -49,6 +50,8 @@ import {
49
50
  raycastConfigPath,
50
51
  zcodeConfigPath,
51
52
  zcodeHomeDir,
53
+ zcodeLegacyHomeDir,
54
+ ZcodeSettingsPathError,
52
55
  zcodeProviderStorePath,
53
56
  buildZcodeStoreContribution,
54
57
  zcodeStoreSchemaEstablished,
@@ -75,12 +78,21 @@ import {
75
78
  */
76
79
  export type IntegrationClientId = ExportClientId;
77
80
 
81
+ export interface ResolvedIntegrationPaths {
82
+ configPath: string;
83
+ detectDir: string;
84
+ /** Snapshot of a mutable store resolution from the same bootstrap read. */
85
+ storePath?: string;
86
+ /** The default config path is only a recovery hint when the active root is unknown. */
87
+ pathError?: string;
88
+ }
89
+
78
90
  export interface IntegrationClientSpec {
79
91
  id: IntegrationClientId;
80
92
  /** The client's config file, honoring that client's own environment override. */
81
- configPath: (env?: NodeJS.ProcessEnv, home?: string) => string;
93
+ configPath: (env?: NodeJS.ProcessEnv, home?: string, io?: Pick<IntegrationIO, "readText">) => string;
82
94
  /** Directory whose existence is the cheap "is it installed?" signal. */
83
- detectDir: (env?: NodeJS.ProcessEnv, home?: string) => string;
95
+ detectDir: (env?: NodeJS.ProcessEnv, home?: string, io?: Pick<IntegrationIO, "readText">) => string;
84
96
  /**
85
97
  * The provider store this client reads INSTEAD of `configPath`.
86
98
  *
@@ -98,7 +110,7 @@ export interface IntegrationClientSpec {
98
110
  * client, never merged into on a guess.
99
111
  */
100
112
  currentStore?: {
101
- path: (env?: NodeJS.ProcessEnv, home?: string) => string;
113
+ path: (env?: NodeJS.ProcessEnv, home?: string, io?: Pick<IntegrationIO, "readText">) => string;
102
114
  format: ConfigFormat;
103
115
  establishes: (parsed: unknown) => boolean;
104
116
  buildContribution: BuildContribution;
@@ -139,7 +151,7 @@ export interface IntegrationClientSpec {
139
151
  * Droid uses the same seam to check competing settings against the export
140
152
  * context before status, preview, or mutation proceeds.
141
153
  */
142
- resolvePaths?: (env?: NodeJS.ProcessEnv, home?: string, exportContext?: ExportContext) => { configPath: string; detectDir: string };
154
+ resolvePaths?: (env?: NodeJS.ProcessEnv, home?: string, exportContext?: ExportContext, io?: Pick<IntegrationIO, "readText">) => ResolvedIntegrationPaths;
143
155
  /**
144
156
  * Where the client's config WOULD live, for a client whose real path cannot
145
157
  * be resolved yet.
@@ -183,10 +195,11 @@ export function resolveIntegrationPaths(
183
195
  env: NodeJS.ProcessEnv = process.env,
184
196
  home: string = homedir(),
185
197
  exportContext?: ExportContext,
186
- ): { configPath: string; detectDir: string } {
198
+ io?: Pick<IntegrationIO, "readText">,
199
+ ): ResolvedIntegrationPaths {
187
200
  const spec = INTEGRATION_CLIENTS[clientId];
188
- if (spec.resolvePaths) return spec.resolvePaths(env, home, exportContext);
189
- return { configPath: spec.configPath(env, home), detectDir: spec.detectDir(env, home) };
201
+ if (spec.resolvePaths) return spec.resolvePaths(env, home, exportContext, io);
202
+ return { configPath: spec.configPath(env, home, io), detectDir: spec.detectDir(env, home, io) };
190
203
  }
191
204
 
192
205
  export function assertDroidPathsUnambiguous(root: string, exportContext?: ExportContext): void {
@@ -357,15 +370,31 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
357
370
  },
358
371
  zcode: {
359
372
  id: "zcode",
360
- configPath: (env = process.env, home = homedir()) => zcodeConfigPath(env, home),
361
- detectDir: (env = process.env, home = homedir()) => zcodeHomeDir(env, home),
373
+ configPath: (env = process.env, home = homedir(), io) => zcodeConfigPath(env, home, io),
374
+ detectDir: (env = process.env, home = homedir(), io) => zcodeHomeDir(env, home, io),
375
+ resolvePaths: (env = process.env, home = homedir(), _context, io) => {
376
+ let root: string;
377
+ let pathError: string | undefined;
378
+ try { root = zcodeHomeDir(env, home, io); }
379
+ catch (error) {
380
+ if (!(error instanceof ZcodeSettingsPathError)) throw error;
381
+ root = zcodeLegacyHomeDir(env, home);
382
+ pathError = error.message;
383
+ }
384
+ // Passing the resolved root prevents a second bootstrap read. An explicit
385
+ // provider-file override outranks both the setting and ZCODE_DATA_DIR.
386
+ const storePath = zcodeProviderStorePath({ ...env, ZCODE_DATA_DIR: root }, home, io);
387
+ return { configPath: join(root, "v2", "config.json"), detectDir: root, storePath, ...(pathError ? { pathError } : {}) };
388
+ },
389
+ bindsDriftedRecord: (path, env = process.env, home = homedir()) =>
390
+ path === join(zcodeLegacyHomeDir(env, home), "v2", "config.json"),
362
391
  /*
363
392
  * ZCode 3.14 reads its providers from `v2/provider_config.json` and reaches
364
393
  * `v2/config.json` only through the import that seeded it. Where the new
365
394
  * file exists the import is spent, so our write is read by nobody (#5348).
366
395
  */
367
396
  currentStore: {
368
- path: (env = process.env, home = homedir()) => zcodeProviderStorePath(env, home),
397
+ path: (env = process.env, home = homedir(), io) => zcodeProviderStorePath(env, home, io),
369
398
  format: "json",
370
399
  establishes: zcodeStoreSchemaEstablished,
371
400
  buildContribution: buildZcodeStoreContribution,
@@ -496,7 +525,7 @@ export function boundIntegrationConfigPath(input: {
496
525
  if (
497
526
  record && record.clientId === input.clientId &&
498
527
  record.configPath !== input.resolvedPath &&
499
- input.statKind(record.configPath) === "file" &&
528
+ (input.clientId === "zcode" ? input.statKind(record.configPath) !== "missing" : input.statKind(record.configPath) === "file") &&
500
529
  INTEGRATION_CLIENTS[input.clientId].bindsDriftedRecord?.(record.configPath, input.env, input.home) === true
501
530
  ) {
502
531
  return record.configPath;
@@ -529,6 +558,11 @@ export function restoreOwnershipCollision(input: {
529
558
  }): string | null {
530
559
  const currentPath = input.currentPath;
531
560
  if (currentPath === null || currentPath === input.journaledPath) return null;
561
+ // A relocated ZCode store must not lose its single record to an older
562
+ // legacy undo, even when both journaled locations are legal for this home.
563
+ if (input.clientId === "zcode") {
564
+ return `that operation was recorded for ${input.journaledPath}, but ${currentPath} currently owns this integration`;
565
+ }
532
566
  const binds = INTEGRATION_CLIENTS[input.clientId].bindsDriftedRecord;
533
567
  if (!binds) return null;
534
568
  if (binds(input.journaledPath, input.env, input.home) !== true) return null;
@@ -39,6 +39,7 @@ import {
39
39
  assertDroidPathsUnambiguous,
40
40
  assertDroidRecordedSettingsUnambiguous,
41
41
  resolveIntegrationPaths,
42
+ type ResolvedIntegrationPaths,
42
43
  unresolvedPathHintFor,
43
44
  type IntegrationClientId,
44
45
  } from "./registry";
@@ -482,7 +483,7 @@ export interface IntegrationStateInput {
482
483
  store?: IntegrationStateStore;
483
484
  io?: IntegrationIO;
484
485
  /** Internal explicit profile target; never accepted as a caller-provided path. */
485
- resolvedPaths?: { configPath: string; detectDir: string };
486
+ resolvedPaths?: ResolvedIntegrationPaths;
486
487
  }
487
488
 
488
489
  export function exportContextOf(input: {
@@ -592,11 +593,11 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
592
593
  */
593
594
  let installed: boolean;
594
595
  let effective: IntegrationTarget;
595
- let record: OwnershipRecord | null;
596
+ let record: OwnershipRecord | null = null;
596
597
  try {
597
598
  // One resolution for both, so a client whose paths come from mutable state
598
599
  // cannot report one account's install beside another account's config path.
599
- const paths = resolveStatePaths(input);
600
+ const paths = resolveStatePaths(input, io);
600
601
  installed = io.statKind(paths.detectDir) === "dir";
601
602
  if (input.clientId === "cline") io = createClineIO(io, paths.configPath, store);
602
603
  /*
@@ -611,7 +612,7 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
611
612
  statKind: io.statKind, env: input.env, home: input.home,
612
613
  });
613
614
  effective = resolveIntegrationTarget({
614
- clientId: input.clientId, configPath: recordedPath, io, record, env: input.env, home: input.home,
615
+ clientId: input.clientId, configPath: recordedPath, io, record, env: input.env, home: input.home, resolvedPaths: paths,
615
616
  });
616
617
  } catch (error) {
617
618
  if (!(error instanceof ClientPathError)) throw error;
@@ -626,6 +627,10 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
626
627
  * yet. A client that can name where its config would go gets `installed:
627
628
  * false` and that location, which reads as "not installed" in the UI.
628
629
  */
630
+ if (input.clientId === "zcode" && record) return {
631
+ clientId: input.clientId, state: "unsafe", installed: false, configPath: record.configPath,
632
+ reason: "unresolvable-path", appliedAt: record.appliedAt, lastOpId: record.opId, ...retention,
633
+ };
629
634
  const hint = unresolvedPathHintFor(input.clientId, input.env, input.home);
630
635
  return {
631
636
  clientId: input.clientId,
@@ -638,6 +643,12 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
638
643
  }
639
644
 
640
645
  const configPath = effective.configPath;
646
+ if (effective.pathError) return {
647
+ clientId: input.clientId, state: "unsafe", installed, configPath,
648
+ reason: "unresolvable-path",
649
+ ...(record?.configPath === configPath ? { appliedAt: record.appliedAt, lastOpId: record.opId } : {}),
650
+ ...retention,
651
+ };
641
652
  if (input.clientId === "kilo") {
642
653
  const candidates = inspectKiloCandidates({ io, selectedPath: configPath, env: input.env, home: input.home });
643
654
  if (candidates.kind !== "ok") return {
@@ -715,9 +726,10 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
715
726
  };
716
727
  }
717
728
 
718
- function resolveStatePaths(input: IntegrationStateInput): { configPath: string; detectDir: string } {
729
+ function resolveStatePaths(input: IntegrationStateInput, io: IntegrationIO): ResolvedIntegrationPaths {
719
730
  const context = exportContextOf(input);
720
- const paths = input.resolvedPaths ?? resolveIntegrationPaths(input.clientId, input.env, input.home, context);
731
+ const paths = input.clientId === "zcode" || !input.resolvedPaths
732
+ ? resolveIntegrationPaths(input.clientId, input.env, input.home, context, io) : input.resolvedPaths;
721
733
  if (input.clientId === "droid" && input.resolvedPaths) assertDroidPathsUnambiguous(paths.detectDir, context);
722
734
  return paths;
723
735
  }
@@ -727,7 +739,7 @@ export function readOwnedDroidReasoningDefaults(input: IntegrationStateInput): R
727
739
  const store = input.store ?? createIntegrationStateStore();
728
740
  const io = input.io ?? store.io();
729
741
  try {
730
- const paths = resolveStatePaths(input);
742
+ const paths = resolveStatePaths(input, io);
731
743
  const record = store.readRecords().droid;
732
744
  if (!record || record.clientId !== "droid" || record.configPath !== paths.configPath) return {};
733
745
  const effective = resolveIntegrationTarget({
@@ -23,11 +23,11 @@
23
23
  * nesting into a file holding the user's other providers would trade a
24
24
  * silent no-op for a silent loss.
25
25
  */
26
- import { EXPORT_CLIENTS, ClientPathError, type BuildContribution, type ConfigFormat } from "../clients/config-export";
26
+ import { EXPORT_CLIENTS, ClientPathError, zcodeLegacyHomeDir, zcodeProviderStorePath, type BuildContribution, type ConfigFormat } from "../clients/config-export";
27
27
  import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
28
28
  import { AmbiguousSelectorError, InvalidSelectorError, readPath } from "./merge";
29
29
  import type { OwnershipRecord } from "./ownership";
30
- import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
30
+ import { INTEGRATION_CLIENTS, resolveIntegrationPaths, type ResolvedIntegrationPaths, type IntegrationClientId } from "./registry";
31
31
 
32
32
  export interface IntegrationTarget {
33
33
  /** The file this operation reads, writes, journals and records ownership for. */
@@ -51,6 +51,8 @@ export interface IntegrationTarget {
51
51
  * to is as effective as it ever was.
52
52
  */
53
53
  readonly ineffective: IneffectiveWrite | null;
54
+ /** Active root unknown; the named config is only a historical removal target. */
55
+ readonly pathError?: string;
54
56
  }
55
57
 
56
58
  export interface IneffectiveWrite {
@@ -153,12 +155,44 @@ export function resolveIntegrationTarget(args: {
153
155
  record: OwnershipRecord | null;
154
156
  env?: NodeJS.ProcessEnv;
155
157
  home?: string;
158
+ resolvedPaths?: ResolvedIntegrationPaths;
156
159
  }): IntegrationTarget {
157
160
  const { clientId, configPath, io, record } = args;
158
161
  const declared = INTEGRATION_CLIENTS[clientId].currentStore;
159
162
  if (!declared) return configFileTarget(clientId, configPath, null);
160
- const storePath = declared.path(args.env, args.home);
163
+ const paths = clientId === "zcode"
164
+ ? args.resolvedPaths ?? resolveIntegrationPaths(clientId, args.env, args.home, undefined, io)
165
+ : undefined;
166
+ const storePath = paths?.storePath ?? declared.path(args.env, args.home, io);
161
167
  const kind = io.statKind(storePath);
168
+ const owned = record !== null && record.clientId === clientId ? record : null;
169
+ const historicalStore = clientId === "zcode"
170
+ ? zcodeProviderStorePath({ ...(args.env ?? process.env), ZCODE_DATA_DIR: zcodeLegacyHomeDir(args.env, args.home) }, args.home, io)
171
+ : null;
172
+ if (clientId === "zcode" && owned && owned.configPath !== configPath && owned.configPath !== storePath
173
+ && owned.configPath !== historicalStore
174
+ && INTEGRATION_CLIENTS[clientId].bindsDriftedRecord?.(owned.configPath, args.env, args.home) !== true
175
+ && io.statKind(owned.configPath) !== "missing") {
176
+ throw new ClientPathError(`ZCode ownership remains at ${owned.configPath}. Its location cannot be bound to the current home or selectors; restore the original selectors and disable it before enabling a different data root. Ownership and recovery evidence are retained.`);
177
+ }
178
+ // An explicit provider-file selector can establish the active target even
179
+ // when the bootstrap is unreadable, but absence cannot justify a legacy write.
180
+ if (paths?.pathError && (!(args.env ?? process.env).ZCODE_PERSONAL_PROVIDER_CONFIG_FILE?.trim() || kind !== "file")) {
181
+ const removal = owned?.configPath === historicalStore
182
+ ? storeTarget(declared, historicalStore)
183
+ : configFileTarget(clientId, configPath, null);
184
+ return { ...removal, pathError: paths.pathError };
185
+ }
186
+ if (historicalStore && owned?.configPath === historicalStore && historicalStore !== storePath
187
+ && recordedBlockStillPresent(io, declared.format, historicalStore, owned)) {
188
+ return storeTarget(declared, historicalStore, { store: storePath, why: "owned-config-file" });
189
+ }
190
+ // Relocation must not orphan a block at the default historical path, even
191
+ // when the relocated store has not been created yet.
192
+ if (clientId === "zcode" && owned?.configPath === configPath && configPath !== paths?.configPath
193
+ && recordedBlockStillPresent(io, EXPORT_CLIENTS[clientId].format, configPath, owned)) {
194
+ return configFileTarget(clientId, configPath, { store: storePath, why: "owned-config-file" });
195
+ }
162
196
  if (kind === "missing") {
163
197
  const missing = declared.missingStore;
164
198
  return missing?.readsStore(storePath, path => io.statKind(path)) === true
@@ -182,7 +216,6 @@ export function resolveIntegrationTarget(args: {
182
216
  * file holds it. Disable has to remove what we wrote from where we wrote it,
183
217
  * and an apply must never leave a block in one file while writing another.
184
218
  */
185
- const owned = record !== null && record.clientId === clientId ? record : null;
186
219
  if (owned?.configPath === storePath && recordedBlockStillPresent(io, declared.format, storePath, owned)) {
187
220
  return storeTarget(declared, storePath, established ? null : unestablished);
188
221
  }
@@ -214,6 +247,7 @@ export function declaredIntegrationTarget(args: {
214
247
  resolvedConfigPath: string;
215
248
  env?: NodeJS.ProcessEnv;
216
249
  home?: string;
250
+ io?: Pick<IntegrationIO, "readText">;
217
251
  }): IntegrationTarget | null {
218
252
  const { clientId, configPath, resolvedConfigPath } = args;
219
253
  if (configPath === resolvedConfigPath) return configFileTarget(clientId, configPath, null);
@@ -222,8 +256,11 @@ export function declaredIntegrationTarget(args: {
222
256
  }
223
257
  const declared = INTEGRATION_CLIENTS[clientId].currentStore;
224
258
  if (!declared) return null;
259
+ if (clientId === "zcode" && configPath === zcodeProviderStorePath(
260
+ { ...(args.env ?? process.env), ZCODE_DATA_DIR: zcodeLegacyHomeDir(args.env, args.home) }, args.home, args.io,
261
+ )) return storeTarget(declared, configPath);
225
262
  try {
226
- if (declared.path(args.env, args.home) !== configPath) return null;
263
+ if (declared.path(args.env, args.home, args.io) !== configPath) return null;
227
264
  } catch (error) {
228
265
  // A store the operator relocated with a path we cannot resolve proves
229
266
  // nothing about the row, so the row is not a legal target.
@@ -39,6 +39,7 @@ import {
39
39
  assertDroidPathsUnambiguous,
40
40
  isLoopbackOnly,
41
41
  resolveIntegrationPaths,
42
+ type ResolvedIntegrationPaths,
42
43
  restoreOwnershipCollision,
43
44
  type IntegrationClientId,
44
45
  } from "./registry";
@@ -643,8 +644,15 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
643
644
  if (entry.clientId !== input.clientId) throw new Error("restore input names a different client than the operation");
644
645
 
645
646
  const clientId = entry.clientId;
646
- const resolvedPath = input.resolvedPaths?.configPath
647
- ?? INTEGRATION_CLIENTS[clientId].configPath(input.env, input.home);
647
+ let resolvedPath: string;
648
+ try {
649
+ resolvedPath = clientId === "zcode" || !input.resolvedPaths
650
+ ? resolveIntegrationPaths(clientId, input.env, input.home, undefined, io).configPath
651
+ : input.resolvedPaths.configPath;
652
+ } catch (error) {
653
+ if (!(error instanceof ClientPathError)) throw error;
654
+ return refuse(clientId, "unsafe", "unsafe", error.message);
655
+ }
648
656
  const configPath = entry.configPath;
649
657
  /*
650
658
  * Restore acts on the path the operation was journaled against. Resolving a
@@ -655,7 +663,7 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
655
663
  * shape those bytes are in, which is what the state below is measured against.
656
664
  */
657
665
  const rowTarget = declaredIntegrationTarget({
658
- clientId, configPath, resolvedConfigPath: resolvedPath, env: input.env, home: input.home,
666
+ clientId, configPath, resolvedConfigPath: resolvedPath, env: input.env, home: input.home, io,
659
667
  });
660
668
  if (rowTarget === null) {
661
669
  return refuse(clientId, "conflict", "conflict",
@@ -814,7 +822,7 @@ type FrozenIntegrationInput = IntegrationWriteInput & {
814
822
  io: IntegrationIO;
815
823
  env: NodeJS.ProcessEnv;
816
824
  home: string;
817
- resolvedPaths: { configPath: string; detectDir: string };
825
+ resolvedPaths: ResolvedIntegrationPaths;
818
826
  };
819
827
 
820
828
  /**
@@ -854,7 +862,7 @@ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegration
854
862
  */
855
863
  const resolvedPaths = input.resolvedPaths
856
864
  ? { ...input.resolvedPaths }
857
- : resolveIntegrationPaths(input.clientId, env, home, exportContextOf({ ...input, config, models: models.value }));
865
+ : resolveIntegrationPaths(input.clientId, env, home, exportContextOf({ ...input, config, models: models.value }), io);
858
866
  /*
859
867
  * The configuration and the roster are seams like the others, and they were the two still held
860
868
  * by reference. A coordinated write plans from this input, awaits the writer lock and a
@@ -899,7 +907,7 @@ function storeLockFile(frozen: FrozenIntegrationInput): string | null {
899
907
  if (!declared?.lockFile) return null;
900
908
  let storePath: string;
901
909
  try {
902
- storePath = declared.path(frozen.env, frozen.home);
910
+ storePath = declared.path(frozen.env, frozen.home, frozen.io);
903
911
  } catch (error) {
904
912
  if (error instanceof ClientPathError) return null;
905
913
  throw error;
@@ -169,12 +169,16 @@ export const APP_OWNED_RETAINED_STORE_REGISTRATIONS = [
169
169
  category: "caches",
170
170
  snapshot: usageSummaryRetainedStoreSnapshot,
171
171
  evictOldest: evictOldestUsageSummaryForBudget,
172
+ // Deletes one in-memory summary entry.
173
+ passiveEvictable: true,
172
174
  },
173
175
  {
174
176
  id: "usage_snapshot",
175
177
  category: "caches",
176
178
  snapshot: usageSnapshotRetainedStoreSnapshot,
177
179
  evictOldest: evictOldestUsageSnapshot,
180
+ // Drops the retained ledger snapshot or one unpinned aggregate, both in memory.
181
+ passiveEvictable: true,
178
182
  },
179
183
  {
180
184
  id: "cursor_blobs",
@@ -1,3 +1,5 @@
1
+ import { isPassiveRead } from "./passive-read";
2
+
1
3
  export type AppOwnedRetainedCategory = "logs" | "caches" | "blobs" | "continuation";
2
4
  export type AppOwnedObservedCategory = "translator" | "serialized_tails";
3
5
 
@@ -14,6 +16,11 @@ export interface RetainedStoreRegistration {
14
16
  category: AppOwnedRetainedCategory;
15
17
  snapshot(): RetainedStoreSnapshot;
16
18
  evictOldest(): number;
19
+ /**
20
+ * Eviction only releases memory: no spill, snapshot or other write. Only such stores are
21
+ * demoted inside a passive read (#6959); the rest wait for this process's own next enforcement.
22
+ */
23
+ passiveEvictable?: true;
17
24
  }
18
25
 
19
26
  export interface ObservedBufferRegistration {
@@ -168,11 +175,15 @@ function nextCandidate(
168
175
  snapshots: ReadonlyMap<string, RetainedStoreSnapshot>,
169
176
  ineligible: Set<string>,
170
177
  ): RetainedStoreRegistration | undefined {
178
+ // A passive read (a Remote Link Child's, #6959) demotes only stores whose eviction is memory-only:
179
+ // a continuation spills to disk and a replay cache rewrites its snapshot.
180
+ const passive = isPassiveRead();
171
181
  for (const category of CATEGORY_ORDER) {
172
182
  let candidate: RetainedStoreRegistration | undefined;
173
183
  let candidateAt = Number.POSITIVE_INFINITY;
174
184
  for (const registration of retainedStores.values()) {
175
185
  if (registration.category !== category || ineligible.has(registration.id)) continue;
186
+ if (passive && registration.passiveEvictable !== true) continue;
176
187
  const snapshot = snapshots.get(registration.id);
177
188
  if (!snapshot || snapshot.evictableBytes <= 0) continue;
178
189
  const oldestAt = snapshot.oldestAt;
package/src/lib/errors.ts CHANGED
@@ -17,6 +17,7 @@ export const ENCRYPTED_FUNCTION_OUTPUT_REJECTION =
17
17
  * string for a client to be able to tell this apart from a provider rate limit.
18
18
  */
19
19
  export const SEND_BUDGET_EXHAUSTED_CODE = "request_send_budget_exhausted";
20
+ export const SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE = "spend_ledger_storage_unavailable";
20
21
 
21
22
  /** First nonblank string across the canonical upstream error paths, in priority order. */
22
23
  export function upstreamErrorMessageFromPayload(payload: unknown): string | undefined {
@@ -357,6 +358,9 @@ export function isUpstreamResetReplayRefusedMessage(text: string): boolean {
357
358
 
358
359
  export function classifyError(status: number, type: string, message: string): OcxErrorPayload {
359
360
  const text = message.toLowerCase();
361
+ if (type === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE) {
362
+ return { message, type: "server_error", code: SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE };
363
+ }
360
364
  if (type === "previous_response_not_found") {
361
365
  return { message, type: "invalid_request_error", code: "previous_response_not_found" };
362
366
  }
@@ -639,6 +643,24 @@ export function adapterFailureFromMessage(message: string): { httpStatus: number
639
643
  return { httpStatus, error };
640
644
  }
641
645
 
646
+ /** Narrow structured classes carried through a synthesized bare-error terminal. */
647
+ export function recognizedUpstreamError(error: { type?: unknown; code?: unknown }):
648
+ { type: string; code: string; httpStatus: 429 | 503 } | undefined {
649
+ const code = typeof error.code === "string" ? error.code : undefined;
650
+ // An explicit unknown code must not gain a recognized class from its type or copy.
651
+ if (code !== undefined && !["rate_limit_exceeded", "rate_limit_error", "server_is_overloaded", "overloaded_error"].includes(code)) return undefined;
652
+ if (code === "rate_limit_exceeded" || code === "rate_limit_error"
653
+ || (code === undefined && error.type === "rate_limit_error")) {
654
+ return { type: "rate_limit_error", code: code ?? "rate_limit_exceeded", httpStatus: 429 };
655
+ }
656
+ if (code === "server_is_overloaded" || code === "overloaded_error"
657
+ || (code === undefined && error.type === "overloaded_error")) {
658
+ return { type: error.type === "overloaded_error" ? "overloaded_error" : "server_error",
659
+ code: code ?? "server_is_overloaded", httpStatus: 503 };
660
+ }
661
+ return undefined;
662
+ }
663
+
642
664
  /** Map a terminal Responses error object to the HTTP status we record in /api/logs. */
643
665
  export function httpStatusFromTerminalError(error: {
644
666
  type?: string;
@@ -646,10 +668,13 @@ export function httpStatusFromTerminalError(error: {
646
668
  message?: string;
647
669
  } | undefined): number {
648
670
  if (!error) return 502;
671
+ if (error.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE) return 503;
649
672
  if (error.code === "client_closed_request" || error.code === "client_cancelled") return 499;
650
673
  if (isCyberPolicyCode(error.code) || (error.message ? isCyberPolicyMessage(error.message) : false)) {
651
674
  return 400;
652
675
  }
676
+ const recognized = recognizedUpstreamError(error);
677
+ if (recognized) return recognized.httpStatus;
653
678
  if (error.type === "rate_limit_error" || error.code === "rate_limit_exceeded") return 429;
654
679
  if (error.type === "authentication_error" || error.code === "invalid_api_key") return 401;
655
680
  if (
@@ -1,5 +1,36 @@
1
1
  import { TRANSLATOR_MAX_TURN_BYTES, TranslatorBudgetExceededError } from "./translator-budget";
2
2
 
3
+ /** Count one JSON string formed from parts without joining, serializing or encoding it. */
4
+ export function jsonStringPartsUtf8Bytes(parts: readonly string[], limit = TRANSLATOR_MAX_TURN_BYTES): number {
5
+ let bytes = 0;
6
+ let highSurrogate = false;
7
+ const add = (count: number) => {
8
+ if (count > limit - bytes) throw new TranslatorBudgetExceededError("request_copies", limit);
9
+ bytes += count;
10
+ };
11
+ add(2); // The joined string has one pair of JSON quotes.
12
+ for (const text of parts) {
13
+ if (text.length > limit - bytes) throw new TranslatorBudgetExceededError("request_copies", limit);
14
+ for (let i = 0; i < text.length; i++) {
15
+ const code = text.charCodeAt(i);
16
+ if (highSurrogate) {
17
+ highSurrogate = false;
18
+ if (code >= 0xdc00 && code <= 0xdfff) { add(4); continue; }
19
+ add(6);
20
+ }
21
+ if (code >= 0xd800 && code <= 0xdbff) highSurrogate = true;
22
+ else if (code === 0x22 || code === 0x5c || code === 8 || code === 9 || code === 10 || code === 12 || code === 13) add(2);
23
+ else if (code < 0x20) add(6);
24
+ else if (code < 0x80) add(1);
25
+ else if (code < 0x800) add(2);
26
+ else if (code >= 0xdc00 && code <= 0xdfff) add(6);
27
+ else add(3);
28
+ }
29
+ }
30
+ if (highSurrogate) add(6);
31
+ return bytes;
32
+ }
33
+
3
34
  /** Measure plain JSON data without allocating its serialized string or UTF-8 copy. */
4
35
  export function jsonUtf8Bytes(value: unknown, limit = TRANSLATOR_MAX_TURN_BYTES): number {
5
36
  let bytes = 0;
@@ -70,5 +70,40 @@ export function runOptionalShutdownHooks(): void {
70
70
  /** Test-only reset so an isolated lifecycle test does not inherit registrations. */
71
71
  export function resetOptionalShutdownHooksForTests(): void {
72
72
  hooks.clear();
73
+ drainStartHooks.clear();
74
+ drainStartHooksRan = false;
73
75
  hooksRan = false;
74
76
  }
77
+
78
+ const drainStartHooks = new Map<string, ShutdownHook>();
79
+ let drainStartHooksRan = false;
80
+
81
+ /**
82
+ * Register work that must run the moment a drain begins, before in-flight turns are awaited.
83
+ * The service health sentinel (#6671) uses it: a deliberate stop whose drain stalls must stay a
84
+ * stop, so the sentinel is withdrawn before the drain can look unresponsive. Same contract as
85
+ * {@link registerOptionalShutdownHook}: keyed, synchronous, best-effort, detachable.
86
+ */
87
+ export function registerDrainStartHook(key: string, hook: ShutdownHook): () => void {
88
+ drainStartHooks.set(key, hook);
89
+ return () => {
90
+ if (drainStartHooks.get(key) === hook) drainStartHooks.delete(key);
91
+ };
92
+ }
93
+
94
+ /** Run every drain-start hook. Never throws. */
95
+ export function runDrainStartHooks(): void {
96
+ drainStartHooksRan = true;
97
+ for (const [key, hook] of [...drainStartHooks]) {
98
+ try {
99
+ hook();
100
+ } catch (err) {
101
+ console.warn(`[shutdown] drain-start hook "${key}" failed:`, err instanceof Error ? err.message : err);
102
+ }
103
+ }
104
+ }
105
+
106
+ /** Whether a drain has begun in this process; work that must not start mid-drain checks this. */
107
+ export function didRunDrainStartHooks(): boolean {
108
+ return drainStartHooksRan;
109
+ }