llm-relay 0.68.5 → 0.68.7

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 (141) hide show
  1. package/dist/accounting-state.d.ts +74 -0
  2. package/dist/accounting-state.js +281 -0
  3. package/dist/accounting-state.js.map +1 -0
  4. package/dist/accounting-store-io.d.ts +19 -11
  5. package/dist/accounting-store-io.js +52 -29
  6. package/dist/accounting-store-io.js.map +1 -1
  7. package/dist/accounting-store-schema.js +1 -1
  8. package/dist/accounting-store-schema.js.map +1 -1
  9. package/dist/accounting-store.d.ts +10 -0
  10. package/dist/accounting-store.js +8 -2
  11. package/dist/accounting-store.js.map +1 -1
  12. package/dist/accounting.js.map +1 -1
  13. package/dist/availability.js +4 -2
  14. package/dist/availability.js.map +1 -1
  15. package/dist/backend.d.ts +38 -7
  16. package/dist/backend.js +259 -212
  17. package/dist/backend.js.map +1 -1
  18. package/dist/benchmarks.d.ts +1 -1
  19. package/dist/breaker-persistence.js +6 -31
  20. package/dist/breaker-persistence.js.map +1 -1
  21. package/dist/candidate-runner.d.ts +370 -0
  22. package/dist/candidate-runner.js +1207 -0
  23. package/dist/candidate-runner.js.map +1 -0
  24. package/dist/candidates.d.ts +1 -1
  25. package/dist/candidates.js +243 -221
  26. package/dist/candidates.js.map +1 -1
  27. package/dist/catalog.js.map +1 -1
  28. package/dist/cli.js +104 -80
  29. package/dist/cli.js.map +1 -1
  30. package/dist/config-types.d.ts +358 -0
  31. package/dist/config-types.js +4 -0
  32. package/dist/config-types.js.map +1 -0
  33. package/dist/config.d.ts +2 -0
  34. package/dist/config.js +111 -97
  35. package/dist/config.js.map +1 -1
  36. package/dist/configured-limits.d.ts +5 -30
  37. package/dist/configured-limits.js +2 -1
  38. package/dist/configured-limits.js.map +1 -1
  39. package/dist/context-limits.js.map +1 -1
  40. package/dist/control-authorization.js.map +1 -1
  41. package/dist/credential-fleet.d.ts +10 -29
  42. package/dist/credential-fleet.js +9 -1
  43. package/dist/credential-fleet.js.map +1 -1
  44. package/dist/credential-select.d.ts +30 -0
  45. package/dist/credential-select.js +2 -2
  46. package/dist/credential-select.js.map +1 -1
  47. package/dist/dashboard-auth.js +15 -16
  48. package/dist/dashboard-auth.js.map +1 -1
  49. package/dist/dashboard-contract.d.ts +2 -0
  50. package/dist/dashboard-contract.js +2 -1
  51. package/dist/dashboard-contract.js.map +1 -1
  52. package/dist/dispatch-exhaustion-persistence.js +6 -31
  53. package/dist/dispatch-exhaustion-persistence.js.map +1 -1
  54. package/dist/dispatch.d.ts +1 -1
  55. package/dist/dispatch.js.map +1 -1
  56. package/dist/documents.d.ts +1 -0
  57. package/dist/documents.js +30 -1
  58. package/dist/documents.js.map +1 -1
  59. package/dist/dotenv.js +1 -1
  60. package/dist/dotenv.js.map +1 -1
  61. package/dist/dynamic-pools.d.ts +23 -1
  62. package/dist/dynamic-pools.js +53 -41
  63. package/dist/dynamic-pools.js.map +1 -1
  64. package/dist/emitSse.js +27 -18
  65. package/dist/emitSse.js.map +1 -1
  66. package/dist/kernel/contracts.d.ts +21 -0
  67. package/dist/kernel/contracts.js.map +1 -1
  68. package/dist/kernel/request-lifecycle.js +0 -2
  69. package/dist/kernel/request-lifecycle.js.map +1 -1
  70. package/dist/keys-cli.js.map +1 -1
  71. package/dist/keystore.js.map +1 -1
  72. package/dist/lane-cadence.js +3 -3
  73. package/dist/lane-cadence.js.map +1 -1
  74. package/dist/lane-manifest.js +8 -4
  75. package/dist/lane-manifest.js.map +1 -1
  76. package/dist/lane-probe.d.ts +1 -1
  77. package/dist/latency-demotion.d.ts +13 -1
  78. package/dist/latency-demotion.js.map +1 -1
  79. package/dist/mcp/lane-runner.d.ts +3 -0
  80. package/dist/mcp/lane-runner.js +27 -1
  81. package/dist/mcp/lane-runner.js.map +1 -1
  82. package/dist/mcp/server.d.ts +1 -1
  83. package/dist/mcp/server.js +1 -2
  84. package/dist/mcp/server.js.map +1 -1
  85. package/dist/metadata.js +8 -8
  86. package/dist/metadata.js.map +1 -1
  87. package/dist/onboarding.js.map +1 -1
  88. package/dist/openai-request.js +7 -3
  89. package/dist/openai-request.js.map +1 -1
  90. package/dist/ping/cadence.js +12 -8
  91. package/dist/ping/cadence.js.map +1 -1
  92. package/dist/ping/probe-cache.js +7 -29
  93. package/dist/ping/probe-cache.js.map +1 -1
  94. package/dist/ping/runtime-telemetry.js +9 -31
  95. package/dist/ping/runtime-telemetry.js.map +1 -1
  96. package/dist/quota-observation.js +2 -1
  97. package/dist/quota-observation.js.map +1 -1
  98. package/dist/rate-limits.d.ts +14 -13
  99. package/dist/rate-limits.js.map +1 -1
  100. package/dist/refusal-interpretation.js +55 -62
  101. package/dist/refusal-interpretation.js.map +1 -1
  102. package/dist/resolved-attempt.d.ts +3 -12
  103. package/dist/resolved-attempt.js.map +1 -1
  104. package/dist/responses-request.js +64 -54
  105. package/dist/responses-request.js.map +1 -1
  106. package/dist/routes/messages.d.ts +89 -0
  107. package/dist/routes/messages.js +804 -0
  108. package/dist/routes/messages.js.map +1 -0
  109. package/dist/routes/openai-front.d.ts +42 -0
  110. package/dist/routes/openai-front.js +446 -0
  111. package/dist/routes/openai-front.js.map +1 -0
  112. package/dist/secret-file-acl.js +5 -1
  113. package/dist/secret-file-acl.js.map +1 -1
  114. package/dist/self-update.js.map +1 -1
  115. package/dist/server.d.ts +46 -91
  116. package/dist/server.js +283 -3068
  117. package/dist/server.js.map +1 -1
  118. package/dist/storage/json-store.d.ts +60 -0
  119. package/dist/storage/json-store.js +99 -0
  120. package/dist/storage/json-store.js.map +1 -0
  121. package/dist/stream-commit.js +23 -23
  122. package/dist/stream-commit.js.map +1 -1
  123. package/dist/stream-pipeline.d.ts +54 -0
  124. package/dist/stream-pipeline.js +218 -0
  125. package/dist/stream-pipeline.js.map +1 -0
  126. package/dist/target-facts.js +26 -46
  127. package/dist/target-facts.js.map +1 -1
  128. package/dist/tool-dialects.js +7 -6
  129. package/dist/tool-dialects.js.map +1 -1
  130. package/dist/tool-use-ids.js.map +1 -1
  131. package/dist/usage-observer.js +38 -27
  132. package/dist/usage-observer.js.map +1 -1
  133. package/dist/validator.d.ts +1 -0
  134. package/dist/validator.js +65 -51
  135. package/dist/validator.js.map +1 -1
  136. package/package.json +1 -1
  137. package/scripts/install-skill.mjs +27 -14
  138. package/skills/llm-relay/SKILL.md +20 -616
  139. package/skills/llm-relay/references/direct-routing.md +142 -0
  140. package/skills/llm-relay/references/dispatch-lanes.md +292 -0
  141. package/skills/llm-relay/references/operations.md +204 -0
@@ -0,0 +1,74 @@
1
+ /**
2
+ * MODULE CHARTER: Per-Request Accounting Lifecycle & Price Port Bridge (accounting-state.ts)
3
+ *
4
+ * 1. Domain Boundary & Responsibilities:
5
+ * - Coordinates the per-request accounting lifecycle for all caller-visible proxy routes.
6
+ * - Accumulates token metrics across multiple candidate attempts, failover walks, and streaming responses.
7
+ * - Decouples pricing metadata and model catalog resolution from low-level storage mutation logic.
8
+ *
9
+ * 2. Request Accounting Lifecycle:
10
+ * - Initiation: `createRequestAccountingState()` establishes request identity, timing, and caller attribution.
11
+ * - Attempt Recording: Each backend egress records attempt start, headers arrival, streaming deltas, or premature error.
12
+ * - Completion: Upon stream finish or error, `commit()` resolves final token usage, prices attempts via the price port,
13
+ * and submits immutable `AccountingEvent` records to the registered `AccountingRecorder`.
14
+ *
15
+ * 3. Price Port Decoupling:
16
+ * - Pricing logic is encapsulated via `AccountingPricePort` functional interfaces.
17
+ * - Tier models and pricing rates are resolved from `tier-data.js` and `metadata.js` at runtime without locking the store.
18
+ * - Missing or unpriced models gracefully emit unpriced token facts without halting accounting pipelines.
19
+ *
20
+ * 4. Caller Attribution & Path Classification:
21
+ * - Routes are filtered via `isCallerVisibleAccountingPath()` to exclude health, models, and administrative endpoints.
22
+ * - Attributions cleanly distinguish relay-held infrastructure actions from caller-operated interactions.
23
+ */
24
+ import type { ServerResponse } from "node:http";
25
+ import type { Config } from "./config.js";
26
+ import type { ModelCatalog } from "./catalog.js";
27
+ import { type AccountingAttempt, type AccountingPricePort, type AccountingRecorder, type TokenFactsInput } from "./accounting.js";
28
+ import { type UsageAccumulator } from "./usage-observer.js";
29
+ import type { ResolvedAttempt } from "./resolved-attempt.js";
30
+ import type { Reshaper } from "./reshaper.js";
31
+ export type ModelCallRecorder = (providerKey: string, modelId: string, callResult: {
32
+ ok: boolean;
33
+ latencyMs: number;
34
+ completionTokens?: number;
35
+ }) => void;
36
+ export type ProxyAccountingAttribution = "relay_held" | "caller_operated" | "unknown";
37
+ export type ProxyAccountingFailureKind = "timeout" | "provider_error" | "auth_error" | "rate_limit" | "aborted" | "protocol" | "unknown";
38
+ export declare function isCallerVisibleAccountingPath(method: string | undefined, pathname: string): boolean;
39
+ export declare function recordEarlyTerminalAccounting(recorder: AccountingRecorder, startedAt: number, client: string): void;
40
+ export declare function serveAccountingAttribution(attempt: ResolvedAttempt): ProxyAccountingAttribution;
41
+ export declare function repairAccountingAttribution(state: ResolvedAttempt["credential"]["state"]): ProxyAccountingAttribution;
42
+ export declare function accountingTokens(usage: UsageAccumulator, estimatedInputTokens: number, includeEstimatedOutput: boolean): TokenFactsInput;
43
+ export declare function buildAccountingPricePort(catalog: ModelCatalog, cfg: Config): AccountingPricePort;
44
+ export declare class RequestAccountingState {
45
+ private readonly estimatedInputTokens;
46
+ private readonly pricePort?;
47
+ private readonly onServedLatency?;
48
+ private readonly request;
49
+ private readonly active;
50
+ private readonly committed;
51
+ private readonly attemptDeployments;
52
+ private responseTerminal;
53
+ private finalized;
54
+ private successfulServe;
55
+ private committedServeAttribution;
56
+ private lastFailure;
57
+ private lastAttribution;
58
+ constructor(recorder: AccountingRecorder, response: ServerResponse, startedAt: number, estimatedInputTokens: number, client: string, pricePort?: AccountingPricePort | undefined, onServedLatency?: ((provider: string, model: string, ms: number, tokens: number | undefined) => void) | undefined);
59
+ startServe(attempt: ResolvedAttempt, startedAt: number): AccountingAttempt | null;
60
+ startRepair(options: {
61
+ readonly resolvedAttempt: ResolvedAttempt | null;
62
+ readonly credentialState: ResolvedAttempt["credential"]["state"];
63
+ readonly provider: string | null;
64
+ readonly model: string | null;
65
+ readonly credentialId: string | null;
66
+ readonly startedAt: number;
67
+ }): AccountingAttempt | null;
68
+ markCommitted(attempt: AccountingAttempt | null, at: number): void;
69
+ complete(attempt: AccountingAttempt | null, outcome: "success" | "error" | "cancelled", failureKind: ProxyAccountingFailureKind | null, usage: UsageAccumulator, endedAt?: number, abandonedByRelay?: boolean): void;
70
+ private start;
71
+ isRequestClosed(): boolean;
72
+ private finalizeIfReady;
73
+ }
74
+ export declare function withRepairAccounting(reshaper: Reshaper, accounting: RequestAccountingState | null): Reshaper;
@@ -0,0 +1,281 @@
1
+ import { loadTierData, findTierModel } from "./tier-data.js";
2
+ import { resolveMetadata } from "./metadata.js";
3
+ import { createAccountingRequest, } from "./accounting.js";
4
+ import { createUsageAccumulator } from "./usage-observer.js";
5
+ export function isCallerVisibleAccountingPath(method, pathname) {
6
+ if (method !== "POST")
7
+ return false;
8
+ return pathname === "/v1/messages"
9
+ || pathname === "/v1/chat/completions"
10
+ || pathname === "/chat/completions"
11
+ || pathname === "/v1/responses"
12
+ || pathname === "/responses";
13
+ }
14
+ export function recordEarlyTerminalAccounting(recorder, startedAt, client) {
15
+ try {
16
+ createAccountingRequest({ recorder, startedAt, client }).complete({
17
+ outcome: "error",
18
+ failureKind: "unknown",
19
+ attribution: "unknown",
20
+ endedAt: Date.now(),
21
+ });
22
+ }
23
+ catch {
24
+ }
25
+ }
26
+ export function serveAccountingAttribution(attempt) {
27
+ if (attempt.credential.state === "declared-present")
28
+ return "relay_held";
29
+ if (attempt.credential.state === "not-declared" && attempt.target.credentialMode !== "contained") {
30
+ return "caller_operated";
31
+ }
32
+ return "unknown";
33
+ }
34
+ export function repairAccountingAttribution(state) {
35
+ return state === "declared-present" ? "relay_held" : "unknown";
36
+ }
37
+ export function accountingTokens(usage, estimatedInputTokens, includeEstimatedOutput) {
38
+ const estimatedOutputTokens = includeEstimatedOutput ? usage.estimatedOutputTokens : undefined;
39
+ const hasEstimatedOutput = typeof estimatedOutputTokens === "number"
40
+ && Number.isSafeInteger(estimatedOutputTokens)
41
+ && estimatedOutputTokens > 0;
42
+ const estimated = {
43
+ ...(estimatedInputTokens > 0
44
+ ? { inputTokens: estimatedInputTokens, inputMethod: "relay_estimate" }
45
+ : {}),
46
+ ...(hasEstimatedOutput
47
+ ? { outputTokens: estimatedOutputTokens, outputMethod: "relay_estimate" }
48
+ : {}),
49
+ };
50
+ return {
51
+ reported: {
52
+ ...(usage.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),
53
+ ...(usage.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),
54
+ ...(usage.cachedInputTokens !== undefined ? { cachedInputTokens: usage.cachedInputTokens } : {}),
55
+ ...(usage.cacheCreationInputTokens !== undefined
56
+ ? { cacheCreationInputTokens: usage.cacheCreationInputTokens }
57
+ : {}),
58
+ ...(usage.cacheReadInputTokens !== undefined ? { cacheReadInputTokens: usage.cacheReadInputTokens } : {}),
59
+ },
60
+ ...(estimatedInputTokens > 0 || hasEstimatedOutput
61
+ ? { estimated }
62
+ : {}),
63
+ };
64
+ }
65
+ export function buildAccountingPricePort(catalog, cfg) {
66
+ return (provider, model) => {
67
+ const p = cfg.providers[provider];
68
+ const providerLimits = p?.kind === "openai" && model
69
+ ? catalog.cachedLimits(provider, model)
70
+ : null;
71
+ const data = loadTierData();
72
+ const matched = data
73
+ ? findTierModel(model, data.byNorm, data.exactByNorm)
74
+ : null;
75
+ const tier = matched?.match === "exact" ? matched.rec : undefined;
76
+ const meta = resolveMetadata(model, {
77
+ providerLimits,
78
+ reference: tier
79
+ ? {
80
+ pricePromptPerToken: typeof tier.price_prompt === "number" ? tier.price_prompt : null,
81
+ priceCompletionPerToken: typeof tier.price_completion === "number" ? tier.price_completion : null,
82
+ from: `openrouter:${tier.norm}`,
83
+ }
84
+ : null,
85
+ });
86
+ if (meta.priceSource === null)
87
+ return null;
88
+ return {
89
+ pricePerMillionIn: meta.pricePerMTokIn,
90
+ pricePerMillionOut: meta.pricePerMTokOut,
91
+ priceSource: meta.priceSource,
92
+ };
93
+ };
94
+ }
95
+ export class RequestAccountingState {
96
+ estimatedInputTokens;
97
+ pricePort;
98
+ onServedLatency;
99
+ request;
100
+ active = new Set();
101
+ committed = new Set();
102
+ attemptDeployments = new Map();
103
+ responseTerminal = null;
104
+ finalized = false;
105
+ successfulServe = false;
106
+ committedServeAttribution = null;
107
+ lastFailure = "unknown";
108
+ lastAttribution = "unknown";
109
+ constructor(recorder, response, startedAt, estimatedInputTokens, client, pricePort, onServedLatency) {
110
+ this.estimatedInputTokens = estimatedInputTokens;
111
+ this.pricePort = pricePort;
112
+ this.onServedLatency = onServedLatency;
113
+ try {
114
+ this.request = createAccountingRequest({ recorder, startedAt, client, pricePort });
115
+ }
116
+ catch {
117
+ this.request = null;
118
+ }
119
+ response.once("finish", () => {
120
+ if (this.responseTerminal === null)
121
+ this.responseTerminal = "finished";
122
+ this.finalizeIfReady();
123
+ });
124
+ response.once("close", () => {
125
+ if (this.responseTerminal === null) {
126
+ this.responseTerminal = response.writableFinished ? "finished" : "cancelled";
127
+ }
128
+ this.finalizeIfReady();
129
+ });
130
+ }
131
+ startServe(attempt, startedAt) {
132
+ return this.start("serve", {
133
+ startedAt,
134
+ attribution: serveAccountingAttribution(attempt),
135
+ provider: attempt.target.provider,
136
+ model: attempt.target.model ?? null,
137
+ credentialId: attempt.credentialId,
138
+ });
139
+ }
140
+ startRepair(options) {
141
+ return this.start("repair", {
142
+ startedAt: options.startedAt,
143
+ attribution: repairAccountingAttribution(options.credentialState),
144
+ provider: options.resolvedAttempt?.target.provider ?? options.provider,
145
+ model: options.resolvedAttempt?.target.model ?? options.model,
146
+ credentialId: options.resolvedAttempt?.credentialId ?? options.credentialId,
147
+ });
148
+ }
149
+ markCommitted(attempt, at) {
150
+ if (attempt === null)
151
+ return;
152
+ try {
153
+ if (attempt.markCommitted({ at })) {
154
+ this.committed.add(attempt);
155
+ if (attempt.role === "serve")
156
+ this.committedServeAttribution = attempt.attribution;
157
+ }
158
+ }
159
+ catch {
160
+ }
161
+ }
162
+ complete(attempt, outcome, failureKind, usage, endedAt = Date.now(), abandonedByRelay = false) {
163
+ if (attempt === null)
164
+ return;
165
+ try {
166
+ attempt.complete({
167
+ outcome,
168
+ failureKind,
169
+ endedAt,
170
+ tokens: accountingTokens(usage, attempt.role === "serve" ? this.estimatedInputTokens : 0, attempt.role === "repair" || this.committed.has(attempt)),
171
+ abandonedByRelay,
172
+ });
173
+ }
174
+ catch {
175
+ }
176
+ const deployment = this.attemptDeployments.get(attempt);
177
+ this.attemptDeployments.delete(attempt);
178
+ if (this.onServedLatency &&
179
+ deployment &&
180
+ attempt.role === "serve" &&
181
+ outcome === "success" &&
182
+ typeof deployment.provider === "string" &&
183
+ typeof deployment.model === "string") {
184
+ const elapsed = endedAt - deployment.startedAt;
185
+ if (Number.isFinite(elapsed) && elapsed >= 0) {
186
+ try {
187
+ this.onServedLatency(deployment.provider, deployment.model, elapsed, usage.outputTokens);
188
+ }
189
+ catch {
190
+ }
191
+ }
192
+ }
193
+ this.active.delete(attempt);
194
+ this.committed.delete(attempt);
195
+ this.lastAttribution = attempt.attribution;
196
+ if (attempt.role === "serve" && outcome === "success")
197
+ this.successfulServe = true;
198
+ if (outcome !== "success" && failureKind !== null)
199
+ this.lastFailure = failureKind;
200
+ this.finalizeIfReady();
201
+ }
202
+ start(role, options) {
203
+ if (this.request === null || this.finalized || this.responseTerminal === "cancelled")
204
+ return null;
205
+ try {
206
+ const attempt = this.request.startAttempt({ role, ...options });
207
+ this.active.add(attempt);
208
+ this.attemptDeployments.set(attempt, {
209
+ provider: options.provider,
210
+ model: options.model,
211
+ startedAt: options.startedAt,
212
+ });
213
+ this.lastAttribution = options.attribution;
214
+ return attempt;
215
+ }
216
+ catch {
217
+ return null;
218
+ }
219
+ }
220
+ isRequestClosed() {
221
+ return this.responseTerminal !== null || this.finalized;
222
+ }
223
+ finalizeIfReady() {
224
+ if (this.finalized || this.request === null || this.responseTerminal === null)
225
+ return;
226
+ if (this.active.size > 0) {
227
+ if (this.responseTerminal !== "finished")
228
+ return;
229
+ for (const attempt of [...this.active]) {
230
+ this.complete(attempt, "error", "unknown", createUsageAccumulator());
231
+ }
232
+ }
233
+ this.finalized = true;
234
+ try {
235
+ if (this.responseTerminal === "cancelled") {
236
+ this.request.complete({
237
+ outcome: "cancelled",
238
+ failureKind: "aborted",
239
+ attribution: this.committedServeAttribution ?? this.lastAttribution,
240
+ });
241
+ }
242
+ else if (this.successfulServe) {
243
+ this.request.complete();
244
+ }
245
+ else {
246
+ this.request.complete({
247
+ outcome: "error",
248
+ failureKind: this.lastFailure,
249
+ attribution: this.committedServeAttribution ?? this.lastAttribution,
250
+ });
251
+ }
252
+ }
253
+ catch {
254
+ }
255
+ }
256
+ }
257
+ export function withRepairAccounting(reshaper, accounting) {
258
+ if (accounting === null)
259
+ return reshaper;
260
+ const hooks = {
261
+ startRepairAttempt(options) {
262
+ const attempt = accounting.startRepair(options);
263
+ if (attempt === null)
264
+ return null;
265
+ return {
266
+ complete(completion) {
267
+ accounting.complete(attempt, completion.outcome, completion.failureKind, completion.usage, completion.endedAt);
268
+ },
269
+ };
270
+ },
271
+ isRequestClosed() {
272
+ return accounting.isRequestClosed();
273
+ },
274
+ };
275
+ return {
276
+ reshape(request) {
277
+ return reshaper.reshape(request, hooks);
278
+ },
279
+ };
280
+ }
281
+ //# sourceMappingURL=accounting-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accounting-state.js","sourceRoot":"","sources":["../src/accounting-state.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAkB,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EACL,uBAAuB,GAMxB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,sBAAsB,EAAyB,MAAM,qBAAqB,CAAC;AAapF,MAAM,UAAU,6BAA6B,CAAC,MAA0B,EAAE,QAAgB;IACxF,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IACpC,OAAO,QAAQ,KAAK,cAAc;WAC7B,QAAQ,KAAK,sBAAsB;WACnC,QAAQ,KAAK,mBAAmB;WAChC,QAAQ,KAAK,eAAe;WAC5B,QAAQ,KAAK,YAAY,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,6BAA6B,CAC3C,QAA4B,EAC5B,SAAiB,EACjB,MAAc;IAEd,IAAI,CAAC;QACH,uBAAuB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC;YAChE,OAAO,EAAE,OAAO;YAChB,WAAW,EAAE,SAAS;YACtB,WAAW,EAAE,SAAS;YACtB,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE;SACpB,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;IAET,CAAC;AACH,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,OAAwB;IACjE,IAAI,OAAO,CAAC,UAAU,CAAC,KAAK,KAAK,kBAAkB;QAAE,OAAO,YAAY,CAAC;IACzE,IAAI,OAAO,CAAC,UAAU,CAAC,KAAK,KAAK,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,cAAc,KAAK,WAAW,EAAE,CAAC;QACjG,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,KAA6C;IACvF,OAAO,KAAK,KAAK,kBAAkB,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,KAAuB,EACvB,oBAA4B,EAC5B,sBAA+B;IAE/B,MAAM,qBAAqB,GAAG,sBAAsB,CAAC,CAAC,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/F,MAAM,kBAAkB,GAAG,OAAO,qBAAqB,KAAK,QAAQ;WAC/D,MAAM,CAAC,aAAa,CAAC,qBAAqB,CAAC;WAC3C,qBAAqB,GAAG,CAAC,CAAC;IAC/B,MAAM,SAAS,GAAG;QAChB,GAAG,CAAC,oBAAoB,GAAG,CAAC;YAC1B,CAAC,CAAC,EAAE,WAAW,EAAE,oBAAoB,EAAE,WAAW,EAAE,gBAAgB,EAAE;YACtE,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,kBAAkB;YACpB,CAAC,CAAC,EAAE,YAAY,EAAE,qBAAqB,EAAE,YAAY,EAAE,gBAAgB,EAAE;YACzE,CAAC,CAAC,EAAE,CAAC;KACR,CAAC;IACF,OAAO;QACL,QAAQ,EAAE;YACR,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9E,GAAG,CAAC,KAAK,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACjF,GAAG,CAAC,KAAK,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAChG,GAAG,CAAC,KAAK,CAAC,wBAAwB,KAAK,SAAS;gBAC9C,CAAC,CAAC,EAAE,wBAAwB,EAAE,KAAK,CAAC,wBAAwB,EAAE;gBAC9D,CAAC,CAAC,EAAE,CAAC;YACP,GAAG,CAAC,KAAK,CAAC,oBAAoB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,oBAAoB,EAAE,KAAK,CAAC,oBAAoB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC1G;QACD,GAAG,CAAC,oBAAoB,GAAG,CAAC,IAAI,kBAAkB;YAChD,CAAC,CAAC,EAAE,SAAS,EAAE;YACf,CAAC,CAAC,EAAE,CAAC;KACR,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,OAAqB,EAAE,GAAW;IACzE,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE;QACzB,MAAM,CAAC,GAAG,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAClC,MAAM,cAAc,GAAG,CAAC,EAAE,IAAI,KAAK,QAAQ,IAAI,KAAK;YAClD,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,QAAQ,EAAE,KAAK,CAAC;YACvC,CAAC,CAAC,IAAI,CAAC;QACT,MAAM,IAAI,GAAG,YAAY,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,IAAI;YAClB,CAAC,CAAC,aAAa,CAAY,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC;YAChE,CAAC,CAAC,IAAI,CAAC;QACT,MAAM,IAAI,GAAG,OAAO,EAAE,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;QAClE,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,EAAE;YAClC,cAAc;YACd,SAAS,EAAE,IAAI;gBACb,CAAC,CAAC;oBACA,mBAAmB,EAAE,OAAO,IAAI,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI;oBACrF,uBAAuB,EAAE,OAAO,IAAI,CAAC,gBAAgB,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI;oBACjG,IAAI,EAAE,cAAc,IAAI,CAAC,IAAI,EAAE;iBAChC;gBACD,CAAC,CAAC,IAAI;SACT,CAAC,CAAC;QACH,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAC3C,OAAO;YACL,iBAAiB,EAAE,IAAI,CAAC,cAAc;YACtC,kBAAkB,EAAE,IAAI,CAAC,eAAe;YACxC,WAAW,EAAE,IAAI,CAAC,WAAW;SAC9B,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,sBAAsB;IAmBd;IAEA;IACA;IArBF,OAAO,CAA2B;IAClC,MAAM,GAAG,IAAI,GAAG,EAAqB,CAAC;IACtC,SAAS,GAAG,IAAI,GAAG,EAAqB,CAAC;IACzC,kBAAkB,GAAG,IAAI,GAAG,EAG1C,CAAC;IACI,gBAAgB,GAAoC,IAAI,CAAC;IACzD,SAAS,GAAG,KAAK,CAAC;IAClB,eAAe,GAAG,KAAK,CAAC;IACxB,yBAAyB,GAAsC,IAAI,CAAC;IACpE,WAAW,GAA+B,SAAS,CAAC;IACpD,eAAe,GAA+B,SAAS,CAAC;IAEhE,YACE,QAA4B,EAC5B,QAAwB,EACxB,SAAiB,EACA,oBAA4B,EAC7C,MAAc,EACG,SAA+B,EAC/B,eAKR;QARQ,yBAAoB,GAApB,oBAAoB,CAAQ;QAE5B,cAAS,GAAT,SAAS,CAAsB;QAC/B,oBAAe,GAAf,eAAe,CAKvB;QAET,IAAI,CAAC;YACH,IAAI,CAAC,OAAO,GAAG,uBAAuB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QACrF,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACtB,CAAC;QAED,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE;YAC3B,IAAI,IAAI,CAAC,gBAAgB,KAAK,IAAI;gBAAE,IAAI,CAAC,gBAAgB,GAAG,UAAU,CAAC;YACvE,IAAI,CAAC,eAAe,EAAE,CAAC;QACzB,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE;YAC1B,IAAI,IAAI,CAAC,gBAAgB,KAAK,IAAI,EAAE,CAAC;gBACnC,IAAI,CAAC,gBAAgB,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC;YAC/E,CAAC;YACD,IAAI,CAAC,eAAe,EAAE,CAAC;QACzB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,UAAU,CAAC,OAAwB,EAAE,SAAiB;QACpD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;YACzB,SAAS;YACT,WAAW,EAAE,0BAA0B,CAAC,OAAO,CAAC;YAChD,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ;YACjC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,IAAI;YACnC,YAAY,EAAE,OAAO,CAAC,YAAY;SACnC,CAAC,CAAC;IACL,CAAC;IAED,WAAW,CAAC,OAOX;QACC,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE;YAC1B,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,WAAW,EAAE,2BAA2B,CAAC,OAAO,CAAC,eAAe,CAAC;YACjE,QAAQ,EAAE,OAAO,CAAC,eAAe,EAAE,MAAM,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ;YACtE,KAAK,EAAE,OAAO,CAAC,eAAe,EAAE,MAAM,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK;YAC7D,YAAY,EAAE,OAAO,CAAC,eAAe,EAAE,YAAY,IAAI,OAAO,CAAC,YAAY;SAC5E,CAAC,CAAC;IACL,CAAC;IAED,aAAa,CAAC,OAAiC,EAAE,EAAU;QACzD,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO;QAC7B,IAAI,CAAC;YACH,IAAI,OAAO,CAAC,aAAa,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;gBAClC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC5B,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO;oBAAE,IAAI,CAAC,yBAAyB,GAAG,OAAO,CAAC,WAAW,CAAC;YACrF,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;QAET,CAAC;IACH,CAAC;IAED,QAAQ,CACN,OAAiC,EACjC,OAA0C,EAC1C,WAA8C,EAC9C,KAAuB,EACvB,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,EACpB,gBAAgB,GAAG,KAAK;QAExB,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO;QAC7B,IAAI,CAAC;YACH,OAAO,CAAC,QAAQ,CAAC;gBACf,OAAO;gBACP,WAAW;gBACX,OAAO;gBACP,MAAM,EAAE,gBAAgB,CACtB,KAAK,EACL,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,EACxD,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CACzD;gBACD,gBAAgB;aACjB,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;QAET,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACxD,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACxC,IACE,IAAI,CAAC,eAAe;YACpB,UAAU;YACV,OAAO,CAAC,IAAI,KAAK,OAAO;YACxB,OAAO,KAAK,SAAS;YACrB,OAAO,UAAU,CAAC,QAAQ,KAAK,QAAQ;YACvC,OAAO,UAAU,CAAC,KAAK,KAAK,QAAQ,EACpC,CAAC;YACD,MAAM,OAAO,GAAG,OAAO,GAAG,UAAU,CAAC,SAAS,CAAC;YAC/C,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC,EAAE,CAAC;gBAC7C,IAAI,CAAC;oBACH,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;gBAC3F,CAAC;gBAAC,MAAM,CAAC;gBAET,CAAC;YACH,CAAC;QACH,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC5B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC;QAC3C,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,IAAI,OAAO,KAAK,SAAS;YAAE,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QACnF,IAAI,OAAO,KAAK,SAAS,IAAI,WAAW,KAAK,IAAI;YAAE,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAClF,IAAI,CAAC,eAAe,EAAE,CAAC;IACzB,CAAC;IAEO,KAAK,CACX,IAAwB,EACxB,OAMC;QAED,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,gBAAgB,KAAK,WAAW;YAAE,OAAO,IAAI,CAAC;QAClG,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;YAChE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACzB,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,OAAO,EAAE;gBACnC,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,SAAS,EAAE,OAAO,CAAC,SAAS;aAC7B,CAAC,CAAC;YACH,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC;YAC3C,OAAO,OAAO,CAAC;QACjB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,eAAe;QACb,OAAO,IAAI,CAAC,gBAAgB,KAAK,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC;IAC1D,CAAC;IAEO,eAAe;QACrB,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,IAAI,IAAI,CAAC,gBAAgB,KAAK,IAAI;YAAE,OAAO;QACtF,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,gBAAgB,KAAK,UAAU;gBAAE,OAAO;YACjD,KAAK,MAAM,OAAO,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBACvC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC,CAAC;YACvE,CAAC;QACH,CAAC;QACD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC;YACH,IAAI,IAAI,CAAC,gBAAgB,KAAK,WAAW,EAAE,CAAC;gBAC1C,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;oBACpB,OAAO,EAAE,WAAW;oBACpB,WAAW,EAAE,SAAS;oBACtB,WAAW,EAAE,IAAI,CAAC,yBAAyB,IAAI,IAAI,CAAC,eAAe;iBACpE,CAAC,CAAC;YACL,CAAC;iBAAM,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;gBAChC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YAC1B,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;oBACpB,OAAO,EAAE,OAAO;oBAChB,WAAW,EAAE,IAAI,CAAC,WAAW;oBAC7B,WAAW,EAAE,IAAI,CAAC,yBAAyB,IAAI,IAAI,CAAC,eAAe;iBACpE,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;QAET,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,oBAAoB,CAAC,QAAkB,EAAE,UAAyC;IAChG,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IACzC,MAAM,KAAK,GAA4B;QACrC,kBAAkB,CAAC,OAAO;YACxB,MAAM,OAAO,GAAG,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;YAChD,IAAI,OAAO,KAAK,IAAI;gBAAE,OAAO,IAAI,CAAC;YAClC,OAAO;gBACL,QAAQ,CAAC,UAAU;oBACjB,UAAU,CAAC,QAAQ,CACjB,OAAO,EACP,UAAU,CAAC,OAAO,EAClB,UAAU,CAAC,WAAW,EACtB,UAAU,CAAC,KAAK,EAChB,UAAU,CAAC,OAAO,CACnB,CAAC;gBACJ,CAAC;aACF,CAAC;QACJ,CAAC;QACD,eAAe;YACb,OAAO,UAAU,CAAC,eAAe,EAAE,CAAC;QACtC,CAAC;KACF,CAAC;IACF,OAAO;QACL,OAAO,CAAC,OAAO;YACb,OAAO,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC1C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -1,17 +1,25 @@
1
1
  /**
2
- * A deliberately small durability primitive for accounting's multi-file read model.
2
+ * MODULE CHARTER: Durable Snapshot Journal & File IO Subsystem (accounting-store-io.ts)
3
3
  *
4
- * It writes a full snapshot journal before replacing any target. A later process can
5
- * therefore replay the journal after any target-write prefix without applying a delta
6
- * twice. This module knows nothing about accounting's schemas; callers supply already
7
- * materialised snapshots and an explicit target allow-list. Only flat, portable direct
8
- * children of the root are supported for targets and the journal.
4
+ * 1. Domain Boundary & Responsibilities:
5
+ * - Durability and transaction primitive for multi-file snapshot partition writes (day shards, lifetime rollups).
6
+ * - Decoupled from higher-level accounting domain schemas: accepts opaque materialised snapshots and target allow-lists.
7
+ * - Enforces directory containment for all target files and journal storage under a flat root directory.
9
8
  *
10
- * Threat model: the root and its parent must be trusted and non-adversarial against
11
- * concurrent same-user replacement. Race-proof filesystem containment against an actor
12
- * able to rename or replace the root is out of scope; root and final reparse points are
13
- * still checked before filesystem operations. Writer ownership is enforced only among
14
- * instances in this process; cross-process concurrent writers are unsupported.
9
+ * 2. Atomic Rename & Durability Invariants:
10
+ * - All mutations follow a write-journal-first protocol before modifying any target snapshot file.
11
+ * - Files are written to adjacent temporary files (`.tmp.<pid>.<nonce>`), fsynced to disk, and atomically renamed.
12
+ * - Complete journal replay guarantees idempotency: target shards are replaced in full rather than updated via deltas.
13
+ *
14
+ * 3. Quarantine & Recovery Semantics:
15
+ * - Uncommitted or interrupted transactions are detected and replayed upon startup via `recoverPendingJournal()`.
16
+ * - Corrupted journals or files exceeding hard size limits are safely quarantined rather than panicking or crashing.
17
+ * - Bounded data loss is tracked and reported via `lowerBoundLoss` metadata when recovery cannot reconcile stale state.
18
+ *
19
+ * 4. File Descriptor & Concurrency Guarantees:
20
+ * - Single-writer mutex enforced in-process across store instances; cross-process concurrent writes are unsupported.
21
+ * - All open file handles are immediately closed in `finally` blocks, preventing file descriptor leaks on Windows/POSIX.
22
+ * - Hard byte ceilings (`SNAPSHOT_IO_HARD_MAX_FILE_BYTES`, `SNAPSHOT_IO_HARD_MAX_JOURNAL_BYTES`) guard against OOMs.
15
23
  */
16
24
  export declare const SNAPSHOT_JOURNAL_SCHEMA = "llm-relay.snapshot-journal.v1";
17
25
  export declare const SNAPSHOT_JOURNAL_VERSION = 1;
@@ -312,39 +312,62 @@ class SnapshotJournalIoImpl {
312
312
  return this.withPendingRecoveryLoss(mutationResult("failed", { error: safeError(error), retryable: true }));
313
313
  }
314
314
  }
315
+ validateSnapshotsPreWrite(snapshots) {
316
+ const config = this.config;
317
+ if (!config)
318
+ return { ok: false, failure: mutationResult("invalid", { error: this.configError ?? "config", retryable: false }) };
319
+ const recovery = this.recoverUnlocked();
320
+ if (recovery.status === "failed" || recovery.status === "invalid")
321
+ return { ok: false, failure: recovery };
322
+ const lowerBoundLoss = recovery.lowerBoundLoss;
323
+ const encoded = this.encodeSnapshots(snapshots);
324
+ if (encoded === null) {
325
+ return { ok: false, failure: mutationResult("invalid", { error: "snapshots", lowerBoundLoss, retryable: false }) };
326
+ }
327
+ return { ok: true, encoded, lowerBoundLoss };
328
+ }
329
+ serializeAndCommitJournal(config, encoded, lowerBoundLoss) {
330
+ const transactionId = this.transactionId();
331
+ const payload = {
332
+ schema: SNAPSHOT_JOURNAL_SCHEMA,
333
+ version: SNAPSHOT_JOURNAL_VERSION,
334
+ transactionId,
335
+ createdAtMs: this.now(),
336
+ targets: encoded,
337
+ };
338
+ const text = JSON.stringify(payload);
339
+ if (utf8Bytes(text) > config.maxJournalBytes) {
340
+ return { ok: false, failure: mutationResult("invalid", { error: "journal-size", lowerBoundLoss, retryable: false }) };
341
+ }
342
+ const written = this.atomicReplace(config.journalPath, Buffer.from(text, "utf8"), "journal", null);
343
+ if (!written.ok) {
344
+ return {
345
+ ok: false,
346
+ failure: this.withPendingRecoveryLoss(mutationResult("failed", { transactionId, lowerBoundLoss, error: written.error, retryable: true })),
347
+ };
348
+ }
349
+ const journal = this.decodeJournal(Buffer.from(text, "utf8"));
350
+ if (journal === null) {
351
+ return {
352
+ ok: false,
353
+ failure: this.withPendingRecoveryLoss(mutationResult("failed", { transactionId, lowerBoundLoss, error: "journal-encode", retryable: true })),
354
+ };
355
+ }
356
+ return { ok: true, journal, transactionId };
357
+ }
315
358
  commitUnlocked(snapshots) {
316
359
  try {
317
360
  const config = this.config;
318
361
  if (!config)
319
362
  return mutationResult("invalid", { error: this.configError ?? "config", retryable: false });
320
- const recovery = this.recoverUnlocked();
321
- if (recovery.status === "failed" || recovery.status === "invalid")
322
- return recovery;
323
- const lowerBoundLoss = recovery.lowerBoundLoss;
324
- const encoded = this.encodeSnapshots(snapshots);
325
- if (encoded === null)
326
- return mutationResult("invalid", { error: "snapshots", lowerBoundLoss, retryable: false });
327
- const transactionId = this.transactionId();
328
- const payload = {
329
- schema: SNAPSHOT_JOURNAL_SCHEMA,
330
- version: SNAPSHOT_JOURNAL_VERSION,
331
- transactionId,
332
- createdAtMs: this.now(),
333
- targets: encoded,
334
- };
335
- const text = JSON.stringify(payload);
336
- if (utf8Bytes(text) > config.maxJournalBytes) {
337
- return mutationResult("invalid", { error: "journal-size", lowerBoundLoss, retryable: false });
338
- }
339
- const written = this.atomicReplace(config.journalPath, Buffer.from(text, "utf8"), "journal", null);
340
- if (!written.ok)
341
- return this.withPendingRecoveryLoss(mutationResult("failed", { transactionId, lowerBoundLoss, error: written.error, retryable: true }));
342
- const journal = this.decodeJournal(Buffer.from(text, "utf8"));
343
- if (journal === null) {
344
- return this.withPendingRecoveryLoss(mutationResult("failed", { transactionId, lowerBoundLoss, error: "journal-encode", retryable: true }));
345
- }
346
- const result = this.applyJournal(journal, "committed");
347
- const merged = result.lowerBoundLoss === lowerBoundLoss ? result : { ...result, lowerBoundLoss };
363
+ const preWrite = this.validateSnapshotsPreWrite(snapshots);
364
+ if (!preWrite.ok)
365
+ return preWrite.failure;
366
+ const journalCommit = this.serializeAndCommitJournal(config, preWrite.encoded, preWrite.lowerBoundLoss);
367
+ if (!journalCommit.ok)
368
+ return journalCommit.failure;
369
+ const result = this.applyJournal(journalCommit.journal, "committed");
370
+ const merged = result.lowerBoundLoss === preWrite.lowerBoundLoss ? result : { ...result, lowerBoundLoss: preWrite.lowerBoundLoss };
348
371
  return this.withPendingRecoveryLoss(merged, merged.status === "committed");
349
372
  }
350
373
  catch (error) {
@@ -579,7 +602,7 @@ class SnapshotJournalIoImpl {
579
602
  throw error;
580
603
  const inspected = this.inspectExistingPath(path, false);
581
604
  if (inspected.status !== "ok")
582
- throw new Error(inspected.error ?? "path");
605
+ throw new Error(inspected.error ?? "path", { cause: error });
583
606
  return openSync(path, "r");
584
607
  }
585
608
  }