@gajae-code/coding-agent 0.15.6 → 0.16.1

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 (270) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/args.d.ts +2 -0
  5. package/dist/types/cli/root-flags.d.ts +14 -0
  6. package/dist/types/cli/update-cli.d.ts +14 -0
  7. package/dist/types/cli.d.ts +7 -0
  8. package/dist/types/commands/launch.d.ts +10 -0
  9. package/dist/types/commands/sdk.d.ts +2 -0
  10. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  11. package/dist/types/config/billing-path.d.ts +25 -0
  12. package/dist/types/config/config-schema-version.d.ts +13 -1
  13. package/dist/types/config/file-lock.d.ts +5 -0
  14. package/dist/types/config/model-registry.d.ts +22 -0
  15. package/dist/types/config/settings-schema.d.ts +27 -8
  16. package/dist/types/config/settings.d.ts +2 -0
  17. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  18. package/dist/types/crash/record-loader.d.ts +15 -0
  19. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  20. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  21. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  22. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  23. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  24. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  26. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  27. package/dist/types/main.d.ts +1 -0
  28. package/dist/types/master-mode/context.d.ts +21 -0
  29. package/dist/types/master-mode/first-request.d.ts +34 -0
  30. package/dist/types/master-mode/snapshot.d.ts +32 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  32. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  33. package/dist/types/modes/components/model-selector.d.ts +4 -0
  34. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  35. package/dist/types/modes/components/session-selector.d.ts +1 -1
  36. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  37. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  38. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  39. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  40. package/dist/types/modes/interactive-mode.d.ts +16 -2
  41. package/dist/types/modes/print-mode.d.ts +17 -0
  42. package/dist/types/modes/theme/theme.d.ts +2 -1
  43. package/dist/types/modes/types.d.ts +2 -0
  44. package/dist/types/modes/ui-language.d.ts +7 -1
  45. package/dist/types/sdk/broker/broker.d.ts +79 -2
  46. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  47. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  48. package/dist/types/sdk/broker/index.d.ts +3 -0
  49. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  50. package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
  51. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  52. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  53. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  54. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  55. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  56. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  57. package/dist/types/sdk/broker/state-version.d.ts +21 -1
  58. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  59. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  60. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  61. package/dist/types/sdk/bus/index.d.ts +9 -1
  62. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  63. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  64. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  65. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  67. package/dist/types/sdk/cli/index.d.ts +1 -0
  68. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  69. package/dist/types/sdk/cli/rows.d.ts +12 -5
  70. package/dist/types/sdk/cli/session-cli.d.ts +20 -3
  71. package/dist/types/sdk/client/client.d.ts +3 -2
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/events.d.ts +1 -0
  77. package/dist/types/sdk/host/host.d.ts +10 -0
  78. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  79. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  80. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +70 -17
  82. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  83. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  84. package/dist/types/sdk/lifecycle-session.d.ts +3 -1
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  86. package/dist/types/sdk/receipt-state.d.ts +2 -0
  87. package/dist/types/sdk/router/session-router.d.ts +6 -0
  88. package/dist/types/sdk/session.d.ts +16 -0
  89. package/dist/types/sdk/startup-capability.d.ts +2 -1
  90. package/dist/types/sdk/turn-result.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +52 -7
  92. package/dist/types/session/credential-store-errors.d.ts +1 -0
  93. package/dist/types/session/fold-coordinator.d.ts +29 -2
  94. package/dist/types/session/session-manager.d.ts +79 -2
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/tools/ask.d.ts +1 -0
  98. package/dist/types/tools/bash.d.ts +16 -0
  99. package/dist/types/tools/browser/launch.d.ts +10 -0
  100. package/dist/types/tools/browser/registry.d.ts +8 -0
  101. package/dist/types/tools/gh.d.ts +8 -0
  102. package/dist/types/tools/index.d.ts +10 -10
  103. package/dist/types/tools/job.d.ts +3 -1
  104. package/dist/types/utils/external-editor.d.ts +7 -0
  105. package/examples/extensions/README.md +1 -1
  106. package/examples/extensions/pirate.ts +7 -4
  107. package/package.json +7 -7
  108. package/scripts/generate-sdk-operation-inventory.ts +13 -1
  109. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  110. package/src/async/job-manager.ts +107 -9
  111. package/src/browser-backend.ts +29 -0
  112. package/src/cli/accounts-cli.ts +9 -5
  113. package/src/cli/args.ts +17 -0
  114. package/src/cli/fast-help.ts +1 -0
  115. package/src/cli/plugin-cli.ts +29 -0
  116. package/src/cli/root-flags.ts +5 -0
  117. package/src/cli/update-cli.ts +140 -23
  118. package/src/commands/launch.ts +86 -18
  119. package/src/commands/sdk.ts +135 -28
  120. package/src/config/autorouting-tier-map.ts +12 -0
  121. package/src/config/billing-path.ts +42 -0
  122. package/src/config/config-schema-version.ts +18 -1
  123. package/src/config/file-lock.ts +158 -83
  124. package/src/config/model-preset-registry.ts +178 -23
  125. package/src/config/model-profiles.ts +5 -5
  126. package/src/config/model-registry.ts +291 -42
  127. package/src/config/model-resolver.ts +5 -0
  128. package/src/config/settings-schema.ts +27 -8
  129. package/src/config/settings.ts +51 -15
  130. package/src/coordinator-mcp/projection-scan.ts +27 -6
  131. package/src/coordinator-mcp/question-state.ts +9 -11
  132. package/src/coordinator-mcp/server.ts +115 -21
  133. package/src/crash/index-store.ts +2 -0
  134. package/src/crash/record-loader.ts +10 -0
  135. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  136. package/src/extensibility/extensions/runner.ts +27 -0
  137. package/src/extensibility/extensions/types.ts +8 -3
  138. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  139. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  140. package/src/extensibility/skills.ts +22 -12
  141. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  142. package/src/gjc-runtime/launch-worktree.ts +285 -4
  143. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  144. package/src/gjc-runtime/session-state-lock.ts +213 -14
  145. package/src/gjc-runtime/session-state-sidecar.ts +938 -39
  146. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  147. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
  148. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  149. package/src/hooks/skill-state.ts +6 -0
  150. package/src/internal-urls/docs-index.generated.ts +15 -15
  151. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  152. package/src/main.ts +61 -3
  153. package/src/master-mode/context.ts +50 -0
  154. package/src/master-mode/first-request.ts +65 -0
  155. package/src/master-mode/snapshot.ts +205 -0
  156. package/src/modes/acp/acp-agent.ts +610 -161
  157. package/src/modes/components/footer.ts +4 -2
  158. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  159. package/src/modes/components/model-selector.ts +76 -8
  160. package/src/modes/components/oauth-selector.ts +6 -1
  161. package/src/modes/components/session-selector.ts +23 -15
  162. package/src/modes/components/sessions-dashboard.ts +8 -5
  163. package/src/modes/components/settings-selector.ts +1 -0
  164. package/src/modes/components/status-line/model-name.ts +53 -0
  165. package/src/modes/components/status-line/priority-row.ts +107 -0
  166. package/src/modes/components/status-line/segments.ts +23 -9
  167. package/src/modes/components/tool-status-header.ts +151 -15
  168. package/src/modes/controllers/command-controller.ts +1 -1
  169. package/src/modes/controllers/event-controller.ts +30 -4
  170. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  171. package/src/modes/controllers/input-controller.ts +30 -2
  172. package/src/modes/controllers/selector-controller.ts +200 -98
  173. package/src/modes/interactive-mode.ts +36 -12
  174. package/src/modes/print-mode.ts +64 -0
  175. package/src/modes/prompt-action-autocomplete.ts +30 -4
  176. package/src/modes/runtime-init.ts +12 -9
  177. package/src/modes/theme/theme.ts +5 -0
  178. package/src/modes/types.ts +2 -0
  179. package/src/modes/ui-language.ts +49 -2
  180. package/src/modes/utils/ui-helpers.ts +22 -3
  181. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  182. package/src/prompts/system/master-mode.md +28 -0
  183. package/src/prompts/tools/ask.md +1 -1
  184. package/src/prompts/tools/bash.md +1 -0
  185. package/src/prompts/tools/job.md +1 -1
  186. package/src/sdk/acp/adapter.ts +5 -0
  187. package/src/sdk/broker/broker.ts +1646 -27
  188. package/src/sdk/broker/endpoint-authority.ts +86 -0
  189. package/src/sdk/broker/ensure.ts +81 -3
  190. package/src/sdk/broker/index.ts +3 -0
  191. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  192. package/src/sdk/broker/lifecycle.ts +440 -75
  193. package/src/sdk/broker/master-capability.ts +232 -0
  194. package/src/sdk/broker/session-index.ts +121 -17
  195. package/src/sdk/broker/session-scope.ts +272 -0
  196. package/src/sdk/broker/spawn-authority.ts +1031 -0
  197. package/src/sdk/broker/spawn-substrate.ts +591 -0
  198. package/src/sdk/broker/startup-budget.ts +144 -2
  199. package/src/sdk/broker/state-version.ts +43 -14
  200. package/src/sdk/broker/transport.ts +1 -0
  201. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  202. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  203. package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
  204. package/src/sdk/bus/discord-daemon.ts +50 -10
  205. package/src/sdk/bus/index.ts +84 -7
  206. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  207. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  208. package/src/sdk/bus/notification-service.ts +22 -5
  209. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  210. package/src/sdk/bus/reconciliation-store.ts +55 -31
  211. package/src/sdk/bus/slack-conversation.ts +1 -1
  212. package/src/sdk/bus/slack-daemon.ts +72 -38
  213. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  214. package/src/sdk/cli/index.ts +1 -0
  215. package/src/sdk/cli/master-cli.ts +259 -0
  216. package/src/sdk/cli/rows.ts +41 -15
  217. package/src/sdk/cli/session-cli.ts +297 -26
  218. package/src/sdk/client/client.ts +14 -8
  219. package/src/sdk/host/control/dispatch.ts +101 -40
  220. package/src/sdk/host/control/index.ts +2 -0
  221. package/src/sdk/host/control/operations.ts +2 -0
  222. package/src/sdk/host/control/runtime-gate.ts +203 -11
  223. package/src/sdk/host/events.ts +1 -0
  224. package/src/sdk/host/host.ts +29 -2
  225. package/src/sdk/host/query/cursor.ts +52 -7
  226. package/src/sdk/host/query/handlers.ts +33 -12
  227. package/src/sdk/host/query/revision-store.ts +12 -0
  228. package/src/sdk/host/session-runtime.test.ts +673 -14
  229. package/src/sdk/host/session-runtime.ts +674 -103
  230. package/src/sdk/host/surface-policy.ts +3 -3
  231. package/src/sdk/lifecycle/broker-client.ts +28 -0
  232. package/src/sdk/lifecycle/service.ts +279 -20
  233. package/src/sdk/lifecycle-session.ts +17 -4
  234. package/src/sdk/prompt-deadline-manager.ts +2 -1
  235. package/src/sdk/protocol/operation-inventory.generated.json +105 -3
  236. package/src/sdk/protocol/operation-registry.ts +14 -4
  237. package/src/sdk/receipt-state.ts +10 -0
  238. package/src/sdk/router/session-router.ts +219 -118
  239. package/src/sdk/session.ts +416 -195
  240. package/src/sdk/startup-capability.ts +1 -0
  241. package/src/sdk/turn-result.ts +22 -0
  242. package/src/session/agent-session.ts +1628 -318
  243. package/src/session/blob-store.ts +15 -5
  244. package/src/session/credential-store-errors.ts +2 -0
  245. package/src/session/fold-coordinator.ts +40 -3
  246. package/src/session/internal/managed-session-storage.ts +153 -78
  247. package/src/session/session-manager.ts +850 -97
  248. package/src/session/yield-queue.ts +24 -10
  249. package/src/setup/frictionless-onboarding.ts +2 -0
  250. package/src/slash-commands/builtin-registry.ts +41 -0
  251. package/src/task/executor.ts +9 -4
  252. package/src/telemetry/events.ts +1138 -12
  253. package/src/tools/ask-contract.test.ts +64 -0
  254. package/src/tools/ask-contract.ts +36 -1
  255. package/src/tools/ask.ts +10 -8
  256. package/src/tools/bash-interactive.ts +11 -2
  257. package/src/tools/bash.ts +412 -75
  258. package/src/tools/browser/launch.ts +37 -0
  259. package/src/tools/browser/registry.ts +99 -9
  260. package/src/tools/cron.ts +6 -4
  261. package/src/tools/descriptors.test.ts +15 -0
  262. package/src/tools/descriptors.ts +5 -1
  263. package/src/tools/gh.ts +104 -48
  264. package/src/tools/github-cache.ts +2 -2
  265. package/src/tools/index.ts +10 -6
  266. package/src/tools/job.ts +4 -1
  267. package/src/tools/monitor.ts +12 -9
  268. package/src/tools/resolve.ts +30 -10
  269. package/src/tools/tool-catalog.generated.ts +2 -2
  270. package/src/utils/external-editor.ts +72 -1
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Billing path for a resolved binding's credential source — audit-reversible.
3
+ * Derived from (model.api, credential-provenance) where the binding actually
4
+ * resolves credentials, not from a provider-name allowlist.
5
+ */
6
+ import type { Api } from "@gajae-code/ai/core";
7
+ import type { EffectiveProviderAuth } from "./provider-selection-policy";
8
+
9
+ /**
10
+ * Label shown in the selector row + active-model line. Only the category is
11
+ * disclosed (never the key material or project id).
12
+ */
13
+ export type BillingPathKind = "metered-api" | "cloud-project" | "bundled";
14
+
15
+ export interface BillingPath {
16
+ kind: BillingPathKind;
17
+ /** Stable per-credential-source key for one-time notice dismissal. */
18
+ dismissKey: string;
19
+ label: string;
20
+ }
21
+
22
+ /**
23
+ * Derive from the concrete binding (api + credential provenance).
24
+ * - oauth/keyless/unknown => bundled (plan/proxied/kNoAuth-like)
25
+ * - key + cloud-project api (google-vertex / bedrock) => cloud-project
26
+ * - key + any other api (including custom OpenAI-compatible) => metered-api
27
+ */
28
+ export function deriveBillingPath(
29
+ provider: string,
30
+ api: Api | string | undefined,
31
+ effectiveAuth: EffectiveProviderAuth,
32
+ ): BillingPath | undefined {
33
+ if (effectiveAuth === "oauth" || effectiveAuth === "keyless" || effectiveAuth === "unknown") return undefined;
34
+ if (effectiveAuth !== "key") return undefined;
35
+ const apiKind = typeof api === "string" ? api : undefined;
36
+ const cloudApis = new Set(["google-vertex", "bedrock-converse-stream"]);
37
+ const isCloud = apiKind ? cloudApis.has(apiKind) : false;
38
+ const kind: BillingPathKind = isCloud ? "cloud-project" : "metered-api";
39
+ const label = kind === "cloud-project" ? "cloud-project billing" : "metered API";
40
+ const dismissKey = `${kind}:${provider.trim().toLowerCase()}`;
41
+ return { kind, dismissKey, label };
42
+ }
@@ -7,4 +7,21 @@
7
7
  * this dependency-light module so the resolver can import it without pulling in
8
8
  * the TUI/AI-heavy settings schema.
9
9
  */
10
- export const CONFIG_SCHEMA_VERSION = 1;
10
+ export const CONFIG_SCHEMA_VERSION = 2;
11
+
12
+ /**
13
+ * Normalize a durable `configSchemaVersion` marker into a comparable version.
14
+ *
15
+ * Every version decision (future-schema detection, migration-pending state, and
16
+ * the ordered migration registry) must read the SAME normalized value: a
17
+ * malformed marker read as a future schema by one guard and as unmigrated by
18
+ * the next would skip legacy migrations while still re-running v0 transforms.
19
+ * A finite malformed value floors to the nearest completed version; a
20
+ * non-finite or non-numeric one carries no version information and normalizes
21
+ * to 0 (fully unmigrated). `undefined` stays `undefined` (no marker written).
22
+ */
23
+ export function normalizeConfigSchemaVersion(value: unknown): number | undefined {
24
+ if (value === undefined) return undefined;
25
+ if (typeof value !== "number" || !Number.isFinite(value)) return 0;
26
+ return Math.max(0, Math.floor(value));
27
+ }
@@ -1,9 +1,9 @@
1
1
  import * as crypto from "node:crypto";
2
- import type { BigIntStats, Stats } from "node:fs";
2
+ import type { BigIntStats } from "node:fs";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path from "node:path";
5
- import type { NativeDirectoryTreeResult, NativeExactUnlinkResult } from "@gajae-code/natives";
6
- import { renameNoReplacePathAsync } from "@gajae-code/natives";
5
+ import type { NativeDirectoryTreeResult, NativeExactUnlinkResult, NativeNoReplaceResult } from "@gajae-code/natives";
6
+ import { renameDirectoryNoReplacePathAsync, renameNoReplacePathAsync } from "@gajae-code/natives";
7
7
  import { isEnoent } from "@gajae-code/utils/fs-error";
8
8
  import { nativeProcessBindings } from "@gajae-code/utils/native-process";
9
9
 
@@ -49,6 +49,10 @@ type LockInfo = FileLockOwnerToken;
49
49
 
50
50
  export const FileLockTestHooks: {
51
51
  afterParentMkdir?: (lockPath: string) => void | Promise<void>;
52
+ nativePublicationBindings?: () => {
53
+ renameNoReplacePathAsync: typeof renameNoReplacePathAsync;
54
+ renameDirectoryNoReplacePathAsync: typeof renameDirectoryNoReplacePathAsync;
55
+ };
52
56
  nativeQuarantineBindings?: () => NativeFileLockBindings;
53
57
  } = {};
54
58
 
@@ -423,6 +427,118 @@ async function ensureLockParent(directory: string): Promise<void> {
423
427
  }
424
428
  }
425
429
 
430
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
431
+ return (
432
+ typeof value === "object" &&
433
+ value !== null &&
434
+ !Array.isArray(value) &&
435
+ Object.getPrototypeOf(value) === Object.prototype
436
+ );
437
+ }
438
+
439
+ function isValidNativeNoReplaceResult(value: unknown): value is NativeNoReplaceResult {
440
+ if (!isPlainRecord(value)) return false;
441
+ const expectedKeys = [
442
+ "ok",
443
+ "code",
444
+ "mutationState",
445
+ "durabilityState",
446
+ "reason",
447
+ "primitive",
448
+ "phase",
449
+ "diagnostic",
450
+ ];
451
+ if (Object.keys(value).some(key => !expectedKeys.includes(key))) return false;
452
+ if (
453
+ typeof value.ok !== "boolean" ||
454
+ (value.code !== undefined && (typeof value.code !== "string" || !/^[a-z0-9_]{1,64}$/.test(value.code))) ||
455
+ !(["not_committed", "committed", "unknown"] as const).includes(value.mutationState as never) ||
456
+ !(["not_attempted", "proven", "not_provable"] as const).includes(value.durabilityState as never) ||
457
+ !(
458
+ [
459
+ "none",
460
+ "destination_exists",
461
+ "atomic_unavailable",
462
+ "cross_device",
463
+ "permission_denied",
464
+ "io_failure",
465
+ "invalid_request",
466
+ "interrupted",
467
+ "identity_violation",
468
+ "durability_not_provable",
469
+ "unknown",
470
+ ] as const
471
+ ).includes(value.reason as never) ||
472
+ !(
473
+ [
474
+ "renameat2_noreplace",
475
+ "linkat_noreplace",
476
+ "mkdirat_renameat_noreplace",
477
+ "renameatx_np_excl",
478
+ "windows_rename_noreplace",
479
+ "unsupported",
480
+ "unknown",
481
+ ] as const
482
+ ).includes(value.primitive as never) ||
483
+ !(
484
+ [
485
+ "preflight",
486
+ "file_sync",
487
+ "rename",
488
+ "source_unlink",
489
+ "source_parent_sync",
490
+ "destination_parent_sync",
491
+ "terminal_identity",
492
+ "complete",
493
+ "unknown",
494
+ ] as const
495
+ ).includes(value.phase as never)
496
+ )
497
+ return false;
498
+ if (!isPlainRecord(value.diagnostic)) return false;
499
+ if (
500
+ Object.keys(value.diagnostic).some(
501
+ key => !["schemaVersion", "collectionState", "osCode", "syncFailures"].includes(key),
502
+ ) ||
503
+ value.diagnostic.schemaVersion !== 1 ||
504
+ !(["complete", "partial", "unavailable"] as const).includes(value.diagnostic.collectionState as never) ||
505
+ (value.diagnostic.osCode !== undefined &&
506
+ (typeof value.diagnostic.osCode !== "number" || !Number.isInteger(value.diagnostic.osCode))) ||
507
+ (value.diagnostic.syncFailures !== undefined && !Array.isArray(value.diagnostic.syncFailures))
508
+ )
509
+ return false;
510
+ if (value.ok)
511
+ return (
512
+ value.mutationState === "committed" &&
513
+ value.reason === "none" &&
514
+ value.phase === "complete" &&
515
+ (value.durabilityState === "not_attempted" || value.durabilityState === "proven")
516
+ );
517
+ if (value.mutationState === "not_committed") return value.durabilityState === "not_attempted";
518
+ return value.mutationState === "unknown" && value.durabilityState === "not_provable";
519
+ }
520
+
521
+ /**
522
+ * Only a complete native envelope proving that no namespace mutation happened
523
+ * may authorize the directory fallback. A legacy or malformed result is
524
+ * treated as an unknown publication outcome and never followed by another
525
+ * mutating primitive.
526
+ */
527
+ function isPreMutationUnsupportedRenameResult(value: unknown): value is NativeNoReplaceResult {
528
+ if (!isValidNativeNoReplaceResult(value)) return false;
529
+ if (
530
+ value.ok !== false ||
531
+ (value.code !== "invalid_request" && value.code !== "atomic_unavailable") ||
532
+ value.mutationState !== "not_committed" ||
533
+ value.durabilityState !== "not_attempted" ||
534
+ value.reason !== value.code ||
535
+ (value.primitive !== "renameat2_noreplace" && value.primitive !== "renameatx_np_excl") ||
536
+ (value.phase !== "preflight" && value.phase !== "rename")
537
+ )
538
+ return false;
539
+ return true;
540
+ }
541
+
426
542
  async function localLockKey(lockPath: string): Promise<string> {
427
543
  try {
428
544
  return normalizeLockKey(await fs.realpath(lockPath));
@@ -452,17 +568,9 @@ function ownerIncarnationChanged(owner: FileLockOwnerToken, startTimeCache?: Map
452
568
  /** Outcome of a guarded lock-dir removal attempt (`removeFileLockDirForGc`). */
453
569
  export type FileLockGcRemoval = "removed" | "owner_changed" | "missing" | "cleanup_failed";
454
570
 
455
- interface LockDirStatToken {
456
- dev: number;
457
- ino: number;
458
- mtimeMs: number;
459
- ctimeMs: number;
460
- }
461
-
462
571
  type LockStaleSnapshot =
463
572
  | { stale: false }
464
- | { stale: true; owner: FileLockOwnerToken; identity: GenericFileLockDirIdentity }
465
- | { stale: true; owner: null; stat: LockDirStatToken };
573
+ | { stale: true; owner: FileLockOwnerToken; identity: GenericFileLockDirIdentity };
466
574
 
467
575
  /** Identity evidence carried by the generic stale verdict into a later removal. */
468
576
  export interface GenericFileLockDirIdentity {
@@ -634,22 +742,9 @@ function ownerLiveness(pid: number): OwnerLiveness {
634
742
  }
635
743
  }
636
744
 
637
- function statToken(stats: Stats): LockDirStatToken {
638
- return {
639
- dev: stats.dev,
640
- ino: stats.ino,
641
- mtimeMs: stats.mtimeMs,
642
- ctimeMs: stats.ctimeMs,
643
- };
644
- }
645
-
646
- function sameStatToken(a: LockDirStatToken, b: LockDirStatToken): boolean {
647
- return a.dev === b.dev && a.ino === b.ino && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
648
- }
649
-
650
745
  async function staleLockSnapshot(
651
746
  lockPath: string,
652
- staleMs: number,
747
+ _staleMs: number,
653
748
  ownerHostId?: string,
654
749
  previousOwnerHostIds: readonly string[] = [],
655
750
  startTimeCache?: Map<string, string | null>,
@@ -673,26 +768,12 @@ async function staleLockSnapshot(
673
768
  if (isTransientReleaseError(error)) return { stale: false };
674
769
  throw error;
675
770
  }
676
- if (!info && ownerHostId !== undefined) return { stale: false };
677
771
  if (!info) {
678
- // A present but malformed owner record is not equivalent to a lock directory
679
- // caught between mkdir and metadata publication. Refuse stale fallback when the
680
- // metadata path exists: an unreadable live holder must never be reclaimed by
681
- // elapsed mtime alone.
682
- try {
683
- await fs.lstat(path.join(lockPath, "info"));
684
- return { stale: false };
685
- } catch (error) {
686
- if (!isEnoent(error)) throw error;
687
- }
688
- try {
689
- const stats = await fs.stat(lockPath);
690
- if (Date.now() - stats.mtimeMs <= staleMs) return { stale: false };
691
- return { stale: true, owner: null, stat: statToken(stats) };
692
- } catch (err) {
693
- if (isEnoent(err)) return { stale: false };
694
- throw err;
695
- }
772
+ // A directory without a valid owner record is either a contender between
773
+ // native mkdirat ownership and metadata publication, or malformed/foreign
774
+ // state. Neither case carries enough identity evidence for stale removal;
775
+ // elapsed mtime must never make this empty namespace reclaimable.
776
+ return { stale: false };
696
777
  }
697
778
 
698
779
  // A host-qualified lock may only be reclaimed after proving that its owner is
@@ -741,38 +822,7 @@ async function staleLockSnapshot(
741
822
 
742
823
  async function removeStaleLockForAcquire(lockPath: string, snapshot: LockStaleSnapshot): Promise<boolean> {
743
824
  if (!snapshot.stale) return false;
744
- if (snapshot.owner) {
745
- return (await removeFileLockDirForGc(lockPath, snapshot.owner, snapshot.identity)) === "removed";
746
- }
747
-
748
- let currentInfo: LockInfo | null;
749
- try {
750
- currentInfo = await readLockInfo(lockPath);
751
- } catch (error) {
752
- if (isTransientReleaseError(error)) return false;
753
- throw error;
754
- }
755
- if (currentInfo) return false;
756
- try {
757
- const currentStats = await fs.lstat(lockPath);
758
- if (currentStats.isSymbolicLink() || !currentStats.isDirectory()) return false;
759
- if (!sameStatToken(statToken(currentStats), snapshot.stat)) return false;
760
- const nativeCapturePath = await canonicalLockPathPreservingFinal(lockPath);
761
- const captured = nativeFileLockBindings().snapshotDirectoryTree(nativeCapturePath);
762
- if (captured.code === "sharing_violation") throwTransientNativeResult(captured.code);
763
- if (!captured.ok || !captured.snapshot) return false;
764
- if (
765
- captured.snapshot.rootDev !== String(currentStats.dev) ||
766
- captured.snapshot.rootIno !== String(currentStats.ino)
767
- )
768
- return false;
769
- const removed = nativeFileLockBindings().exactRemoveDirectoryTree(nativeCapturePath, captured.snapshot);
770
- return removed.ok || removed.code === "not_found";
771
- } catch (err) {
772
- if (isEnoent(err)) return false;
773
- if (isTransientReleaseError(err)) return false;
774
- throw err;
775
- }
825
+ return (await removeFileLockDirForGc(lockPath, snapshot.owner, snapshot.identity)) === "removed";
776
826
  }
777
827
 
778
828
  /**
@@ -812,7 +862,7 @@ export async function genericFileLockDirStaleVerdict(
812
862
  ownerHostId?: string,
813
863
  ): Promise<GenericFileLockDirStaleVerdict> {
814
864
  const verdict = await staleLockSnapshot(lockDir, staleMs, ownerHostId);
815
- if (!verdict.stale || verdict.owner === null) return { stale: false };
865
+ if (!verdict.stale) return { stale: false };
816
866
  return {
817
867
  stale: true,
818
868
  identity: verdict.identity,
@@ -841,11 +891,32 @@ async function tryAcquireLock(
841
891
  // already-created staging entry and its parent before publication so aliases
842
892
  // retain the same lock identity as the ordinary path.
843
893
  const canonicalParent = await fs.realpath(path.dirname(lockPath));
844
- const published = await renameNoReplacePathAsync(
845
- await fs.realpath(pendingPath),
846
- path.join(canonicalParent, path.basename(lockPath)),
847
- );
848
- if (!published.ok) {
894
+ const destinationPath = path.join(canonicalParent, path.basename(lockPath));
895
+ // Resolve only the stable parent: resolving the mutable staging final
896
+ // component would follow an attacker-replaced symlink before native no-follow
897
+ // validation gets a chance to reject it.
898
+ const canonicalPendingPath = path.join(canonicalParent, path.basename(pendingPath));
899
+ const publication = FileLockTestHooks.nativePublicationBindings?.() ?? {
900
+ renameNoReplacePathAsync,
901
+ renameDirectoryNoReplacePathAsync,
902
+ };
903
+ const published = await publication.renameNoReplacePathAsync(canonicalPendingPath, destinationPath);
904
+ let publishedSuccessfully = published.ok;
905
+ if (!published.ok && isPreMutationUnsupportedRenameResult(published)) {
906
+ const fallback = await publication.renameDirectoryNoReplacePathAsync(canonicalPendingPath, destinationPath);
907
+ if (fallback.ok) {
908
+ publishedSuccessfully = true;
909
+ } else if (fallback.reason === "destination_exists") {
910
+ return null;
911
+ } else {
912
+ const failure = new Error(
913
+ `Failed to publish file lock: ${fallback.code ?? fallback.reason ?? "unknown"}.`,
914
+ ) as NodeJS.ErrnoException;
915
+ if (fallback.code) failure.code = fallback.code;
916
+ throw failure;
917
+ }
918
+ }
919
+ if (!publishedSuccessfully) {
849
920
  if (published.reason === "destination_exists") return null;
850
921
  const failure = new Error(
851
922
  `Failed to publish file lock: ${published.code ?? published.reason ?? "unknown"}.`,
@@ -1103,6 +1174,10 @@ async function lockHolderDescription(lockPath: string): Promise<string> {
1103
1174
  }
1104
1175
 
1105
1176
  async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<() => Promise<void>> {
1177
+ const requestedFilePath: unknown = filePath;
1178
+ if (typeof requestedFilePath !== "string" || requestedFilePath.length === 0 || !path.isAbsolute(requestedFilePath))
1179
+ throw new TypeError("filePath must be a non-empty absolute path");
1180
+ if (requestedFilePath.includes("\0")) throw new TypeError("filePath must not contain NUL bytes");
1106
1181
  if (options.ownerHostId !== undefined && !options.ownerHostId) throw new Error("ownerHostId must be non-empty");
1107
1182
  if (options.previousOwnerHostIds?.some(hostId => !hostId))
1108
1183
  throw new Error("previousOwnerHostIds must contain only non-empty identities");
@@ -47,6 +47,7 @@ const MODEL_PRESET_REGISTRY_MAX_STATE_BYTES = 32 * 1024 * 1024;
47
47
  const MODEL_PRESET_REGISTRY_MAX_HISTORY = 4;
48
48
  const MODEL_PRESET_REGISTRY_MAX_RETENTION_ANCESTRY = 64;
49
49
  const MODEL_PRESET_REGISTRY_MAX_ERROR_BYTES = 1024;
50
+ const MODEL_PRESET_REGISTRY_IDENTITY_SKELETON_CACHE_MAX = 200_000;
50
51
  const MODEL_PRESET_REGISTRY_FETCH_TIMEOUT_MS = 8_000;
51
52
  const REVISION_PATTERN = /^[0-9]{8}$/;
52
53
  const SHA256_PATTERN = /^[a-f0-9]{64}$/;
@@ -592,10 +593,23 @@ function assertSafeRegistryDocument(value: unknown, location = "$"): void {
592
593
  }
593
594
  }
594
595
 
596
+ /**
597
+ * Confusable folding is a pure string function evaluated once per identity per
598
+ * document, and startup validates the same catalog identities repeatedly. The
599
+ * bounded memo keeps the fold cost proportional to the distinct identity count
600
+ * instead of the number of validation passes.
601
+ */
602
+ const identitySkeletonCache = new Map<string, string>();
603
+
595
604
  function identitySkeleton(value: string): string {
596
- return [...value.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase()]
605
+ const cached = identitySkeletonCache.get(value);
606
+ if (cached !== undefined) return cached;
607
+ const skeleton = [...value.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase()]
597
608
  .map(character => IDENTITY_HOMOGLYPHS.get(character) ?? character)
598
609
  .join("");
610
+ if (identitySkeletonCache.size >= MODEL_PRESET_REGISTRY_IDENTITY_SKELETON_CACHE_MAX) identitySkeletonCache.clear();
611
+ identitySkeletonCache.set(value, skeleton);
612
+ return skeleton;
599
613
  }
600
614
 
601
615
  function assertUniqueRegistryNames(values: readonly string[], label: string): void {
@@ -824,23 +838,21 @@ function assertProfilePresetReferences(
824
838
  }
825
839
  }
826
840
 
827
- function validateGeneration(
841
+ function validateGenerationUncached(
828
842
  generation: AcceptedGeneration,
829
843
  trustedKeys: ReadonlyMap<string, ModelPresetRegistryTrustedKey>,
830
- allowRevoked = false,
831
- ): AcceptedGeneration {
832
- const manifest = ModelPresetRegistryManifestSchema.parse(generation.manifest);
844
+ allowRevoked: boolean,
845
+ ): void {
846
+ const { manifest, snapshot, profiles, presets } = generation;
833
847
  assertSafeRegistryDocument(manifest);
834
848
  verifyManifest(manifest, trustedKeys, allowRevoked);
835
849
  assertManifestBindings(manifest);
836
- const snapshotBytes = new TextEncoder().encode(canonicalModelPresetRegistryJson(generation.snapshot));
850
+ const snapshotBytes = new TextEncoder().encode(canonicalModelPresetRegistryJson(snapshot));
837
851
  assertContentDescriptor(snapshotBytes, manifest.signed.snapshot, "Cached registry snapshot");
838
- const snapshot = ModelPresetRegistrySnapshotSchema.parse(generation.snapshot);
839
852
  assertSafeRegistryDocument(snapshot);
840
853
  assertSnapshotBindings(manifest, snapshot);
841
- const profileBytes = new TextEncoder().encode(canonicalModelPresetRegistryJson(generation.profiles));
854
+ const profileBytes = new TextEncoder().encode(canonicalModelPresetRegistryJson(profiles));
842
855
  assertContentDescriptor(profileBytes, manifest.signed.contents.profiles, "Cached registry profiles");
843
- const profiles = ModelPresetRegistryProfilesSchema.parse(generation.profiles);
844
856
  assertSafeRegistryDocument(profiles);
845
857
  assertRegistryIdentityPolicy(profiles);
846
858
  if (
@@ -848,9 +860,8 @@ function validateGeneration(
848
860
  profiles.profiles.length !== manifest.signed.contents.profiles.count
849
861
  )
850
862
  throw new Error("Cached registry profile identity is invalid.");
851
- const presetBytes = new TextEncoder().encode(canonicalModelPresetRegistryJson(generation.presets));
863
+ const presetBytes = new TextEncoder().encode(canonicalModelPresetRegistryJson(presets));
852
864
  assertContentDescriptor(presetBytes, manifest.signed.contents.presets, "Cached registry presets");
853
- const presets = ModelPresetRegistryPresetsSchema.parse(generation.presets);
854
865
  assertSafeRegistryDocument(presets);
855
866
  assertRegistryIdentityPolicy(presets);
856
867
  if (
@@ -859,24 +870,73 @@ function validateGeneration(
859
870
  )
860
871
  throw new Error("Cached registry preset identity is invalid.");
861
872
  assertProfilePresetReferences(profiles, presets);
862
- const manifestSha256 = sha256(canonicalModelPresetRegistryJson(manifest));
863
- if (manifestSha256 !== generation.manifestSha256) throw new Error("Cached registry manifest digest is invalid.");
873
+ if (sha256(canonicalModelPresetRegistryJson(manifest)) !== generation.manifestSha256)
874
+ throw new Error("Cached registry manifest digest is invalid.");
864
875
  assertSafeRegistryDocument(generation.retainedProfiles);
865
876
  assertSafeRegistryDocument(generation.retainedPresets);
866
877
  assertSafeRegistryDocument(generation.retainedDynamicProviders);
867
- const effectiveProfiles = ModelPresetRegistryProfilesSchema.parse({
878
+ const effectiveProfiles: ModelPresetRegistryProfiles = {
868
879
  ...profiles,
869
880
  dynamicProviders: [...generation.retainedDynamicProviders, ...profiles.dynamicProviders],
870
881
  profiles: [...generation.retainedProfiles, ...profiles.profiles],
871
- });
872
- const effectivePresets = ModelPresetRegistryPresetsSchema.parse({
882
+ };
883
+ const effectivePresets: ModelPresetRegistryPresets = {
873
884
  ...presets,
874
885
  presets: [...generation.retainedPresets, ...presets.presets],
875
- });
886
+ };
876
887
  assertRegistryIdentityPolicy(effectiveProfiles);
877
888
  assertRegistryIdentityPolicy(effectivePresets);
878
889
  assertProfilePresetReferences(effectiveProfiles, effectivePresets);
879
- return { ...generation, manifest, snapshot, profiles, presets, manifestSha256 };
890
+ }
891
+
892
+ /**
893
+ * Signature and binding verification is a pure function of the generation, the
894
+ * trusted key set, and the revocation allowance, and one accepted-registry read
895
+ * verifies the same generation objects from several recovery angles. The memo
896
+ * records both outcomes per (generation, keys, allowance) so a startup read
897
+ * verifies each cached generation exactly once instead of once per angle.
898
+ */
899
+ type GenerationValidationOutcome = { ok: true } | { ok: false; error: unknown };
900
+
901
+ const generationValidationCache = new WeakMap<
902
+ AcceptedGeneration,
903
+ WeakMap<ReadonlyMap<string, ModelPresetRegistryTrustedKey>, Map<boolean, GenerationValidationOutcome>>
904
+ >();
905
+
906
+ /**
907
+ * Every caller supplies a generation whose documents were already schema-parsed
908
+ * (cache reads through `parseState`, refresh through `parseCanonicalDocument`),
909
+ * so this asserts the cryptographic and structural bindings and returns the
910
+ * input rather than re-deriving an identical parse.
911
+ */
912
+ function validateGeneration(
913
+ generation: AcceptedGeneration,
914
+ trustedKeys: ReadonlyMap<string, ModelPresetRegistryTrustedKey>,
915
+ allowRevoked = false,
916
+ ): AcceptedGeneration {
917
+ let byKeys = generationValidationCache.get(generation);
918
+ if (!byKeys) {
919
+ byKeys = new WeakMap();
920
+ generationValidationCache.set(generation, byKeys);
921
+ }
922
+ let byAllowance = byKeys.get(trustedKeys);
923
+ if (!byAllowance) {
924
+ byAllowance = new Map();
925
+ byKeys.set(trustedKeys, byAllowance);
926
+ }
927
+ const memoized = byAllowance.get(allowRevoked);
928
+ if (memoized) {
929
+ if (!memoized.ok) throw memoized.error;
930
+ return generation;
931
+ }
932
+ try {
933
+ validateGenerationUncached(generation, trustedKeys, allowRevoked);
934
+ } catch (error) {
935
+ byAllowance.set(allowRevoked, { ok: false, error });
936
+ throw error;
937
+ }
938
+ byAllowance.set(allowRevoked, { ok: true });
939
+ return generation;
880
940
  }
881
941
 
882
942
  function readJsonSync(file: string): unknown | undefined {
@@ -1167,11 +1227,7 @@ function recoverStateCopiesSync(
1167
1227
  (!primaryEvidence.allGenerationsVerified || primaryEvidence.duplicateRevision)
1168
1228
  )
1169
1229
  if (!options.allowInvalidPrimaryFallback || !backupEvidence?.fullyVerified) {
1170
- try {
1171
- validateStateGenerations(primaryEvidence.state, trustedKeys);
1172
- } catch (error) {
1173
- throw error;
1174
- }
1230
+ validateStateGenerations(primaryEvidence.state, trustedKeys);
1175
1231
  }
1176
1232
  if (
1177
1233
  backupEvidence &&
@@ -1492,9 +1548,92 @@ function acceptedRegistryFromState(
1492
1548
  };
1493
1549
  }
1494
1550
 
1551
+ /**
1552
+ * Accepted-registry reads are pure over the on-disk state, backup, and control
1553
+ * files, and a single launch resolves the registry from several subsystems.
1554
+ * The memo keys on the content digest of those inputs, so any byte change from
1555
+ * any writer (refresh, rollback, pin, tampering) is re-verified from scratch
1556
+ * while an unchanged launch pays the verification cost once. Digesting the
1557
+ * inputs is bounded by their size cap and stays far below the signature,
1558
+ * schema, and canonical-form work it replaces.
1559
+ */
1560
+ const acceptedRegistryReadCache = new Map<string, { fingerprint: string; value: AcceptedModelPresetRegistry }>();
1561
+
1562
+ function cloneAcceptedModelPresetRegistry(value: AcceptedModelPresetRegistry): AcceptedModelPresetRegistry {
1563
+ return {
1564
+ ...value,
1565
+ profiles: new Map(
1566
+ [...value.profiles].map(([id, profile]) => [id, structuredClone(profile)] as [string, ModelProfileDefinition]),
1567
+ ),
1568
+ presets: structuredClone(value.presets),
1569
+ dynamicProviders: [...value.dynamicProviders],
1570
+ retainedProfiles: [...value.retainedProfiles],
1571
+ retainedPresets: [...value.retainedPresets],
1572
+ };
1573
+ }
1574
+
1575
+ function registryFileFingerprint(file: string): string {
1576
+ try {
1577
+ return sha256(fsSync.readFileSync(file));
1578
+ } catch {
1579
+ return "absent";
1580
+ }
1581
+ }
1582
+
1583
+ function acceptedRegistryFingerprint(
1584
+ agentDir: string,
1585
+ dependencies: Omit<ModelPresetRegistryDependencies, "agentDir">,
1586
+ ): string {
1587
+ const paths = registryPaths(agentDir);
1588
+ return [
1589
+ refreshDependencyId(effectiveTrustedKeys(dependencies, agentDir) as object),
1590
+ ...[paths.state, paths.backup, paths.control].map(registryFileFingerprint),
1591
+ ].join("|");
1592
+ }
1593
+
1594
+ async function registryFileFingerprintAsync(file: string): Promise<string> {
1595
+ try {
1596
+ return sha256(new Uint8Array(await Bun.file(file).arrayBuffer()));
1597
+ } catch {
1598
+ return "absent";
1599
+ }
1600
+ }
1601
+
1602
+ async function acceptedRegistryFingerprintAsync(
1603
+ agentDir: string,
1604
+ dependencies: Omit<ModelPresetRegistryDependencies, "agentDir">,
1605
+ ): Promise<string> {
1606
+ const paths = registryPaths(agentDir);
1607
+ const files = await Promise.all(
1608
+ [paths.state, paths.backup, paths.control].map(file => registryFileFingerprintAsync(file)),
1609
+ );
1610
+ return [refreshDependencyId(effectiveTrustedKeys(dependencies, agentDir) as object), ...files].join("|");
1611
+ }
1612
+
1613
+ /** Only the dependency-free startup read is memoized; injected dependencies always recompute. */
1614
+ function acceptedRegistryReadIsCacheable(dependencies: Omit<ModelPresetRegistryDependencies, "agentDir">): boolean {
1615
+ return Object.keys(dependencies).length === 0;
1616
+ }
1617
+
1495
1618
  export function loadAcceptedModelPresetRegistry(
1496
1619
  agentDir = getAgentDir(),
1497
1620
  dependencies: Omit<ModelPresetRegistryDependencies, "agentDir"> = {},
1621
+ ): AcceptedModelPresetRegistry {
1622
+ if (!acceptedRegistryReadIsCacheable(dependencies) || environmentDisabled())
1623
+ return loadAcceptedModelPresetRegistryUncached(agentDir, dependencies);
1624
+ const fingerprint = acceptedRegistryFingerprint(agentDir, dependencies);
1625
+ const cached = acceptedRegistryReadCache.get(agentDir);
1626
+ if (cached && cached.fingerprint === fingerprint) return cloneAcceptedModelPresetRegistry(cached.value);
1627
+ const value = loadAcceptedModelPresetRegistryUncached(agentDir, dependencies);
1628
+ const verifiedFingerprint = acceptedRegistryFingerprint(agentDir, dependencies);
1629
+ if (verifiedFingerprint !== fingerprint) return loadAcceptedModelPresetRegistryUncached(agentDir, dependencies);
1630
+ acceptedRegistryReadCache.set(agentDir, { fingerprint, value });
1631
+ return cloneAcceptedModelPresetRegistry(value);
1632
+ }
1633
+
1634
+ function loadAcceptedModelPresetRegistryUncached(
1635
+ agentDir: string,
1636
+ dependencies: Omit<ModelPresetRegistryDependencies, "agentDir">,
1498
1637
  ): AcceptedModelPresetRegistry {
1499
1638
  if (environmentDisabled())
1500
1639
  return {
@@ -1555,6 +1694,22 @@ export function loadAcceptedModelPresetRegistry(
1555
1694
  export async function loadAcceptedModelPresetRegistryAsync(
1556
1695
  agentDir = getAgentDir(),
1557
1696
  dependencies: Omit<ModelPresetRegistryDependencies, "agentDir"> = {},
1697
+ ): Promise<AcceptedModelPresetRegistry> {
1698
+ if (!acceptedRegistryReadIsCacheable(dependencies) || environmentDisabled())
1699
+ return loadAcceptedModelPresetRegistryAsyncUncached(agentDir, dependencies);
1700
+ const fingerprint = await acceptedRegistryFingerprintAsync(agentDir, dependencies);
1701
+ const cached = acceptedRegistryReadCache.get(agentDir);
1702
+ if (cached && cached.fingerprint === fingerprint) return cloneAcceptedModelPresetRegistry(cached.value);
1703
+ const value = await loadAcceptedModelPresetRegistryAsyncUncached(agentDir, dependencies);
1704
+ const verifiedFingerprint = await acceptedRegistryFingerprintAsync(agentDir, dependencies);
1705
+ if (verifiedFingerprint !== fingerprint) return loadAcceptedModelPresetRegistryAsyncUncached(agentDir, dependencies);
1706
+ acceptedRegistryReadCache.set(agentDir, { fingerprint, value });
1707
+ return cloneAcceptedModelPresetRegistry(value);
1708
+ }
1709
+
1710
+ async function loadAcceptedModelPresetRegistryAsyncUncached(
1711
+ agentDir: string,
1712
+ dependencies: Omit<ModelPresetRegistryDependencies, "agentDir">,
1558
1713
  ): Promise<AcceptedModelPresetRegistry> {
1559
1714
  if (environmentDisabled())
1560
1715
  return {