llm-relay 0.61.0 → 0.63.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.
- package/dist/accounting-store-io.js +0 -33
- package/dist/accounting-store-io.js.map +1 -1
- package/dist/accounting-store-schema.js +0 -65
- package/dist/accounting-store-schema.js.map +1 -1
- package/dist/accounting-store.js +0 -195
- package/dist/accounting-store.js.map +1 -1
- package/dist/accounting.js +0 -72
- package/dist/accounting.js.map +1 -1
- package/dist/anthropic.js +0 -18
- package/dist/anthropic.js.map +1 -1
- package/dist/authEnv.js +0 -78
- package/dist/authEnv.js.map +1 -1
- package/dist/availability-snapshot.js +0 -58
- package/dist/availability-snapshot.js.map +1 -1
- package/dist/availability.js +0 -162
- package/dist/availability.js.map +1 -1
- package/dist/backend.js +1 -416
- package/dist/backend.js.map +1 -1
- package/dist/benchmarks.js +1 -63
- package/dist/benchmarks.js.map +1 -1
- package/dist/breaker-persistence.js +0 -59
- package/dist/breaker-persistence.js.map +1 -1
- package/dist/candidates.js +0 -64
- package/dist/candidates.js.map +1 -1
- package/dist/catalog.js +1 -140
- package/dist/catalog.js.map +1 -1
- package/dist/circuit-breaker.js +1 -90
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/claude-hook.js +0 -81
- package/dist/claude-hook.js.map +1 -1
- package/dist/cli.d.ts +89 -1
- package/dist/cli.js +115 -702
- package/dist/cli.js.map +1 -1
- package/dist/config-edit.js +0 -11
- package/dist/config-edit.js.map +1 -1
- package/dist/config.d.ts +21 -0
- package/dist/config.js +22 -372
- package/dist/config.js.map +1 -1
- package/dist/configured-limits.js +0 -58
- package/dist/configured-limits.js.map +1 -1
- package/dist/context-limits.js +0 -97
- package/dist/context-limits.js.map +1 -1
- package/dist/control-authorization.js +0 -36
- package/dist/control-authorization.js.map +1 -1
- package/dist/cooldown-clear.js +0 -6
- package/dist/cooldown-clear.js.map +1 -1
- package/dist/credential-fleet.js +0 -5
- package/dist/credential-fleet.js.map +1 -1
- package/dist/credential-select.js +4 -30
- package/dist/credential-select.js.map +1 -1
- package/dist/dashboard-auth.js +0 -29
- package/dist/dashboard-auth.js.map +1 -1
- package/dist/dashboard-contract.js +0 -46
- package/dist/dashboard-contract.js.map +1 -1
- package/dist/dashboard-routes.js +0 -32
- package/dist/dashboard-routes.js.map +1 -1
- package/dist/dashboard-snapshot.js +0 -154
- package/dist/dashboard-snapshot.js.map +1 -1
- package/dist/dashboard-static.js +0 -22
- package/dist/dashboard-static.js.map +1 -1
- package/dist/delegate-gate/cast-necessity.js +1 -22
- package/dist/delegate-gate/cast-necessity.js.map +1 -1
- package/dist/delegate-gate/cli.js +0 -6
- package/dist/delegate-gate/cli.js.map +1 -1
- package/dist/delegate-gate/diff-parser.js +0 -32
- package/dist/delegate-gate/diff-parser.js.map +1 -1
- package/dist/delegate-gate/fix.js +0 -10
- package/dist/delegate-gate/fix.js.map +1 -1
- package/dist/delegate-gate/gate.js +0 -12
- package/dist/delegate-gate/gate.js.map +1 -1
- package/dist/delegate-gate/minimality.js +0 -38
- package/dist/delegate-gate/minimality.js.map +1 -1
- package/dist/delegate-gate/post-image.js +1 -13
- package/dist/delegate-gate/post-image.js.map +1 -1
- package/dist/delegate-gate/shared-state.js +0 -23
- package/dist/delegate-gate/shared-state.js.map +1 -1
- package/dist/delegate-gate/test-assertions.js +1 -54
- package/dist/delegate-gate/test-assertions.js.map +1 -1
- package/dist/delegate-gate/types.js +0 -11
- package/dist/delegate-gate/types.js.map +1 -1
- package/dist/dialect-stream.js +9 -37
- package/dist/dialect-stream.js.map +1 -1
- package/dist/dispatch-exhaustion-persistence.js +0 -35
- package/dist/dispatch-exhaustion-persistence.js.map +1 -1
- package/dist/dispatch.js +0 -269
- package/dist/dispatch.js.map +1 -1
- package/dist/documents.js +0 -92
- package/dist/documents.js.map +1 -1
- package/dist/dotenv.js +0 -22
- package/dist/dotenv.js.map +1 -1
- package/dist/dynamic-pools.js +0 -93
- package/dist/dynamic-pools.js.map +1 -1
- package/dist/emitSse.js +0 -64
- package/dist/emitSse.js.map +1 -1
- package/dist/executable-lookup.d.ts +28 -0
- package/dist/executable-lookup.js +42 -0
- package/dist/executable-lookup.js.map +1 -0
- package/dist/hard-cap.js +1 -32
- package/dist/hard-cap.js.map +1 -1
- package/dist/host-routing.js +0 -40
- package/dist/host-routing.js.map +1 -1
- package/dist/installed-hosts.d.ts +64 -0
- package/dist/installed-hosts.js +75 -0
- package/dist/installed-hosts.js.map +1 -0
- package/dist/json-shape.js +0 -19
- package/dist/json-shape.js.map +1 -1
- package/dist/kernel/contracts.js +0 -26
- package/dist/kernel/contracts.js.map +1 -1
- package/dist/kernel/request-lifecycle.js +0 -4
- package/dist/kernel/request-lifecycle.js.map +1 -1
- package/dist/key-checker.js +0 -115
- package/dist/key-checker.js.map +1 -1
- package/dist/key-import.js +0 -20
- package/dist/key-import.js.map +1 -1
- package/dist/keys-cli.js +0 -15
- package/dist/keys-cli.js.map +1 -1
- package/dist/keystore.js +2 -51
- package/dist/keystore.js.map +1 -1
- package/dist/lane-cadence.js +1 -51
- package/dist/lane-cadence.js.map +1 -1
- package/dist/lane-manifest.js +0 -54
- package/dist/lane-manifest.js.map +1 -1
- package/dist/lane-probe.js +0 -36
- package/dist/lane-probe.js.map +1 -1
- package/dist/lane-quota-probe.js +0 -61
- package/dist/lane-quota-probe.js.map +1 -1
- package/dist/log.js +0 -17
- package/dist/log.js.map +1 -1
- package/dist/mcp/lane-runner.d.ts +114 -0
- package/dist/mcp/lane-runner.js +186 -0
- package/dist/mcp/lane-runner.js.map +1 -0
- package/dist/mcp/protocol.d.ts +98 -0
- package/dist/mcp/protocol.js +67 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/server.d.ts +78 -0
- package/dist/mcp/server.js +360 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/metadata.js +0 -73
- package/dist/metadata.js.map +1 -1
- package/dist/network-block.js +0 -45
- package/dist/network-block.js.map +1 -1
- package/dist/offload.js +0 -21
- package/dist/offload.js.map +1 -1
- package/dist/onboarding.js +0 -36
- package/dist/onboarding.js.map +1 -1
- package/dist/openai-dialect.js +0 -32
- package/dist/openai-dialect.js.map +1 -1
- package/dist/openai-request.js +0 -236
- package/dist/openai-request.js.map +1 -1
- package/dist/os-keyring.d.ts +11 -2
- package/dist/os-keyring.js +3 -39
- package/dist/os-keyring.js.map +1 -1
- package/dist/ping/cadence.js +3 -90
- package/dist/ping/cadence.js.map +1 -1
- package/dist/ping/metrics.js +0 -91
- package/dist/ping/metrics.js.map +1 -1
- package/dist/ping/ping.js +0 -17
- package/dist/ping/ping.js.map +1 -1
- package/dist/ping/probe-cache.js +2 -53
- package/dist/ping/probe-cache.js.map +1 -1
- package/dist/ping/quota.js +0 -17
- package/dist/ping/quota.js.map +1 -1
- package/dist/ping/runtime-telemetry.js +1 -18
- package/dist/ping/runtime-telemetry.js.map +1 -1
- package/dist/pool-health.js +0 -49
- package/dist/pool-health.js.map +1 -1
- package/dist/presets.js +0 -4
- package/dist/presets.js.map +1 -1
- package/dist/process-safety-net.js +0 -36
- package/dist/process-safety-net.js.map +1 -1
- package/dist/quota-demotion.js +1 -35
- package/dist/quota-demotion.js.map +1 -1
- package/dist/quota-observation.js +0 -21
- package/dist/quota-observation.js.map +1 -1
- package/dist/rate-limits.js +1 -94
- package/dist/rate-limits.js.map +1 -1
- package/dist/refusal-interpretation.js +2 -327
- package/dist/refusal-interpretation.js.map +1 -1
- package/dist/registry.js +0 -11
- package/dist/registry.js.map +1 -1
- package/dist/repair.js +1 -119
- package/dist/repair.js.map +1 -1
- package/dist/request-log.js +0 -2
- package/dist/request-log.js.map +1 -1
- package/dist/reshaper.js +0 -85
- package/dist/reshaper.js.map +1 -1
- package/dist/resolved-attempt.js +0 -3
- package/dist/resolved-attempt.js.map +1 -1
- package/dist/responses-request.js +0 -187
- package/dist/responses-request.js.map +1 -1
- package/dist/routes/admin.js +0 -38
- package/dist/routes/admin.js.map +1 -1
- package/dist/secret-file-acl.js +0 -26
- package/dist/secret-file-acl.js.map +1 -1
- package/dist/self-update.js +0 -118
- package/dist/self-update.js.map +1 -1
- package/dist/server.js +15 -952
- package/dist/server.js.map +1 -1
- package/dist/session-pin.js +0 -9
- package/dist/session-pin.js.map +1 -1
- package/dist/setup-claude.js +0 -9
- package/dist/setup-claude.js.map +1 -1
- package/dist/spend-headroom.js +0 -20
- package/dist/spend-headroom.js.map +1 -1
- package/dist/sse-frames.js +0 -31
- package/dist/sse-frames.js.map +1 -1
- package/dist/sse.js +1 -19
- package/dist/sse.js.map +1 -1
- package/dist/state-paths.js +0 -16
- package/dist/state-paths.js.map +1 -1
- package/dist/stream-commit.js +1 -27
- package/dist/stream-commit.js.map +1 -1
- package/dist/target-facts.js +2 -130
- package/dist/target-facts.js.map +1 -1
- package/dist/telemetry.js +0 -8
- package/dist/telemetry.js.map +1 -1
- package/dist/think-tags.js +1 -38
- package/dist/think-tags.js.map +1 -1
- package/dist/tier-data.js +0 -29
- package/dist/tier-data.js.map +1 -1
- package/dist/tool-dialects.js +1 -103
- package/dist/tool-dialects.js.map +1 -1
- package/dist/tool-use-ids.js +0 -51
- package/dist/tool-use-ids.js.map +1 -1
- package/dist/usage-observer.js +0 -52
- package/dist/usage-observer.js.map +1 -1
- package/dist/validator.js +1 -25
- package/dist/validator.js.map +1 -1
- package/dist/winenv.js +0 -63
- package/dist/winenv.js.map +1 -1
- package/dist/write-behind.js +0 -12
- package/dist/write-behind.js.map +1 -1
- package/package.json +2 -2
- package/scripts/install-skill.mjs +62 -9
- package/skills/llm-relay/SKILL.md +26 -1
package/dist/accounting-store.js
CHANGED
|
@@ -8,7 +8,6 @@ import { emptyAccountingSpendCell, mergeAccountingSpendCells, ACCOUNTING_DAY_SCH
|
|
|
8
8
|
import { DASHBOARD_REQUEST_ID_PATTERN, isDashboardAttemptId, isDashboardFailureKind, isDashboardOutcome, isDashboardSafeId, isDashboardUtcTimestamp, } from "./dashboard-contract.js";
|
|
9
9
|
import { DEFAULT_FLUSH_DELAY_MS, MAX_FLUSH_DELAY_MS, WriteBehindTimer } from "./write-behind.js";
|
|
10
10
|
export { ACCOUNTING_DAY_SCHEMA, ACCOUNTING_DEDUP_SCHEMA, ACCOUNTING_LIFETIME_SCHEMA, ACCOUNTING_MAX_DAY_ROWS, ACCOUNTING_MAX_DEDUP_IDS, ACCOUNTING_MAX_DETAIL_ATTEMPTS, ACCOUNTING_MAX_FILE_BYTES, ACCOUNTING_MAX_LOSS_MARKERS, ACCOUNTING_MAX_MONTHS, ACCOUNTING_MAX_PACKET_ATTEMPTS, ACCOUNTING_MAX_RECENT_ROWS, ACCOUNTING_MAX_ROWS_PER_CELL, ACCOUNTING_MAX_SAMPLES, ACCOUNTING_MINUTE_SCHEMA, ACCOUNTING_RECENT_SCHEMA, ACCOUNTING_STORE_VERSION, } from "./accounting-store-schema.js";
|
|
11
|
-
/** Sized for a Claude Code session holding many parallel subagent streams in flight. */
|
|
12
11
|
export const ACCOUNTING_MAX_PENDING_REQUESTS = 256;
|
|
13
12
|
export const ACCOUNTING_MAX_PENDING_ATTEMPTS_PER_REQUEST = ACCOUNTING_MAX_PACKET_ATTEMPTS;
|
|
14
13
|
export const ACCOUNTING_MAX_READ_DAYS = 31;
|
|
@@ -103,8 +102,6 @@ function emptyMetric() { return { sumMs: null, known: 0, unknown: 0, lost: 0, ov
|
|
|
103
102
|
function emptyAggregate() {
|
|
104
103
|
return { requests: 0, attempts: 0, served: 0, errored: 0, cancelled: 0, tokens: emptyTokens(), requestTokens: emptyTokens(), latency: emptyMetric(), commit: emptyMetric(), spend: emptySpend(), unpricedRequests: 0 };
|
|
105
104
|
}
|
|
106
|
-
// Cell keys are owned by the schema module's SPEND_CELL_KEYS; the adders below
|
|
107
|
-
// reach them through emptyAccountingAggregateSpend() rather than a second list.
|
|
108
105
|
function emptySpend() {
|
|
109
106
|
return {
|
|
110
107
|
providerPublishedReported: emptyAccountingSpendCell(),
|
|
@@ -113,26 +110,18 @@ function emptySpend() {
|
|
|
113
110
|
referenceEstimated: emptyAccountingSpendCell(),
|
|
114
111
|
};
|
|
115
112
|
}
|
|
116
|
-
/** Route one priced spend to its aggregate cell by (priceSource, tokenBasis). */
|
|
117
113
|
function spendCell(spend, record) {
|
|
118
114
|
if (record.priceSource === "provider_published") {
|
|
119
115
|
return record.tokenBasis === "reported" ? spend.providerPublishedReported : spend.providerPublishedEstimated;
|
|
120
116
|
}
|
|
121
117
|
return record.tokenBasis === "reported" ? spend.referenceReported : spend.referenceEstimated;
|
|
122
118
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Fold one priced spend into an aggregate's ATTEMPT-side cells. First contributor
|
|
125
|
-
* seeds the cell; later ones merge through the schema's checked integer adder so a
|
|
126
|
-
* safe-integer overflow degrades the amount to null (announced by the caller's
|
|
127
|
-
* exact flag) rather than wrapping.
|
|
128
|
-
*/
|
|
129
119
|
function addAttemptSpend(target, record) {
|
|
130
120
|
if (record === null)
|
|
131
121
|
return true;
|
|
132
122
|
target.spend ??= emptySpend();
|
|
133
123
|
return addSpendIntoCells(target.spend, record);
|
|
134
124
|
}
|
|
135
|
-
/** Cell-level fold shared by both scopes. */
|
|
136
125
|
function addSpendIntoCells(cells, record) {
|
|
137
126
|
const cell = spendCell(cells, record);
|
|
138
127
|
const knownBefore = cell.known;
|
|
@@ -156,16 +145,7 @@ function emptyDay(date) {
|
|
|
156
145
|
function emptyLifetime() { return { schema: ACCOUNTING_LIFETIME_SCHEMA, version: ACCOUNTING_STORE_VERSION, firstRequestAt: null, lastRequestAt: null, aggregate: emptyAggregate(), months: {}, coverage: emptyCoverage() }; }
|
|
157
146
|
function emptyRecent() { return { schema: ACCOUNTING_RECENT_SCHEMA, version: ACCOUNTING_STORE_VERSION, rows: [], details: {}, coverage: emptyCoverage() }; }
|
|
158
147
|
function newUsageSide() { return { seen: false, exact: true, value: 0 }; }
|
|
159
|
-
/**
|
|
160
|
-
* Fold one token cell into a side. Any uncertainty marker (unknown/lost/overflow, or a null
|
|
161
|
-
* value despite evidence) poisons the WHOLE side rather than being dropped: presenting a partial
|
|
162
|
-
* sum as a measurement is exactly what the provenance invariant forbids.
|
|
163
|
-
* Returns whether THIS cell carried a measured amount (drives the estimated-only detection below).
|
|
164
|
-
*/
|
|
165
148
|
function noteUsageCell(side, cell) {
|
|
166
|
-
// Only a MEASURED amount counts as usage. An all-unknown cell (the store's way of saying "no
|
|
167
|
-
// token facts arrived") is absence of evidence, not evidence of consumption — counting it
|
|
168
|
-
// would turn every unreported request into a phantom "mixed" reading.
|
|
169
149
|
if (!(cell.known > 0))
|
|
170
150
|
return false;
|
|
171
151
|
side.seen = true;
|
|
@@ -175,32 +155,15 @@ function noteUsageCell(side, cell) {
|
|
|
175
155
|
side.value += cell.value;
|
|
176
156
|
return true;
|
|
177
157
|
}
|
|
178
|
-
/** Input + output only: cache creation/read cells are deliberately excluded from this figure. */
|
|
179
158
|
function noteUsageTokens(side, input, output) {
|
|
180
159
|
const inputMeasured = noteUsageCell(side, input);
|
|
181
160
|
const outputMeasured = noteUsageCell(side, output);
|
|
182
161
|
return inputMeasured || outputMeasured;
|
|
183
162
|
}
|
|
184
|
-
/** Non-negative safe-integer counter, else 0 (callers convert a 0 total back to null). */
|
|
185
163
|
function windowCounter(value) {
|
|
186
164
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
187
165
|
}
|
|
188
166
|
const USED_WINDOW_EMPTY = { requests: null, tokens: null, basis: null };
|
|
189
|
-
/**
|
|
190
|
-
* Combine the sides under the contract's reported/estimated/mixed vocabulary.
|
|
191
|
-
*
|
|
192
|
-
* Decision (review fix 2026-08-22): a window holding BOTH bases never sums them into one
|
|
193
|
-
* scalar — the accounting invariant keeps reported and estimated accumulators separate precisely
|
|
194
|
-
* so no total can quietly blend them, and a lone figure tagged "mixed" names the mix without
|
|
195
|
-
* showing its split. Instead:
|
|
196
|
-
* - every measured request carried reported tokens ⇒ the REPORTED figure stands alone, labelled
|
|
197
|
-
* `reported` (the estimated cells on those same requests cover unreported halves of the same
|
|
198
|
-
* requests, so the reported figure is a true measurement, merely narrower);
|
|
199
|
-
* - any ESTIMATED-ONLY request sits beside reported ones ⇒ no number: `tokens: null`,
|
|
200
|
-
* `basis: "mixed"` — returning the reported figure alone would understate the window by every
|
|
201
|
-
* estimated-only request, and the sum would blend bases. Naming the mix without inventing the
|
|
202
|
-
* number is the provenance-safe direction.
|
|
203
|
-
*/
|
|
204
167
|
function usageReading(requests, reported, estimated, estimatedOnlyRequest) {
|
|
205
168
|
if (!reported.seen && !estimated.seen)
|
|
206
169
|
return { requests, tokens: null, basis: null };
|
|
@@ -227,8 +190,6 @@ function sourceCell(value) {
|
|
|
227
190
|
return {
|
|
228
191
|
value: safeCounter(source.value),
|
|
229
192
|
observedAt: validTimestamp(source.observedAt) ? source.observedAt : null,
|
|
230
|
-
// `isLoadableId`, not `isDashboardSafeId` — see `methodSnapshot`: the accept side must not
|
|
231
|
-
// admit what the loader will reject, or the whole day shard is quarantined on the next read.
|
|
232
193
|
method: typeof source.method === "string" && isLoadableId(source.method) && Buffer.byteLength(source.method, "utf8") <= 256 ? source.method : null,
|
|
233
194
|
};
|
|
234
195
|
}
|
|
@@ -399,12 +360,9 @@ function addRequest(target, event, outcome) {
|
|
|
399
360
|
exact &&= addMetric(target.latency, outcome === "cancelled" ? null : safeCounter(event.latencyMs), event.endedAt);
|
|
400
361
|
exact &&= addMetric(target.commit, outcome === "success" ? safeCounter(event.commitMs) : null, event.endedAt);
|
|
401
362
|
exact &&= addRawTokens(target.requestTokens, event.tokens, event.endedAt);
|
|
402
|
-
// Request spend mirrors request tokens: the WINNING serve only, so a
|
|
403
|
-
// retried-elsewhere request never double-counts its failed attempts.
|
|
404
363
|
if (event.spend !== null) {
|
|
405
364
|
target.requestSpend ??= emptySpend();
|
|
406
365
|
exact &&= addSpendIntoCells(target.requestSpend, event.spend);
|
|
407
|
-
// A partial figure (unpriced cache kinds etc.) is priced but a lower bound.
|
|
408
366
|
if (event.spend.coverage !== "full") {
|
|
409
367
|
exact &&= increase(target, "partiallyPricedRequests");
|
|
410
368
|
}
|
|
@@ -416,9 +374,6 @@ function addRequest(target, event, outcome) {
|
|
|
416
374
|
}
|
|
417
375
|
function addRootAttempt(target, attempt) {
|
|
418
376
|
let exact = increase(target, "attempts");
|
|
419
|
-
// Root aggregates accumulate BOTH attempt-side axes, exactly as the dimension
|
|
420
|
-
// attempt rows do: dropping the token fold made every root disagree with its
|
|
421
|
-
// own rows while the spend fold looked complete.
|
|
422
377
|
exact &&= addAggregateTokens(target.tokens, attempt.tokens);
|
|
423
378
|
exact &&= addAttemptSpend(target, attempt.spend);
|
|
424
379
|
return exact;
|
|
@@ -435,10 +390,6 @@ function addAttempt(target, attempt) {
|
|
|
435
390
|
function rowKey(row) {
|
|
436
391
|
return JSON.stringify([row.kind, row.role, row.provider, row.model, row.client, row.credentialId, row.attribution, row.outcome, row.failureKind]);
|
|
437
392
|
}
|
|
438
|
-
/**
|
|
439
|
-
* Dimension fields are fixed when a row is created (only aggregate counters move
|
|
440
|
-
* afterwards), so its serialized key is memoized rather than re-derived per scan.
|
|
441
|
-
*/
|
|
442
393
|
const rowKeys = new WeakMap();
|
|
443
394
|
function cachedRowKey(row) {
|
|
444
395
|
const existing = rowKeys.get(row);
|
|
@@ -448,10 +399,6 @@ function cachedRowKey(row) {
|
|
|
448
399
|
rowKeys.set(row, key);
|
|
449
400
|
return key;
|
|
450
401
|
}
|
|
451
|
-
/**
|
|
452
|
-
* In-memory membership index for a day's dedup array; the SORTED ARRAY remains
|
|
453
|
-
* the persisted form. Keyed weakly so an evicted or pruned day drops its index.
|
|
454
|
-
*/
|
|
455
402
|
const dedupMembership = new WeakMap();
|
|
456
403
|
function dedupSet(day) {
|
|
457
404
|
let ids = dedupMembership.get(day);
|
|
@@ -516,8 +463,6 @@ function makeAttempt(event) {
|
|
|
516
463
|
model: safeId(event.model),
|
|
517
464
|
credentialId: safeId(event.credentialId),
|
|
518
465
|
tokens: tokenSnapshot(event.tokens, event.endedAt),
|
|
519
|
-
// The lifecycle already validated this record's provenance fields; a
|
|
520
|
-
// re-validation through the packet guard below keeps that guarantee total.
|
|
521
466
|
spend: event.spend,
|
|
522
467
|
};
|
|
523
468
|
const parsed = parseAccountingAttemptPacketV1(value);
|
|
@@ -554,20 +499,11 @@ class AccountingStoreImpl {
|
|
|
554
499
|
recentReadState = "missing";
|
|
555
500
|
dirtyLifetime = false;
|
|
556
501
|
dirtyRecent = false;
|
|
557
|
-
/** Fixed snapshots were not safely loaded yet; terminal facts must queue. */
|
|
558
502
|
fixedLoadPending = false;
|
|
559
|
-
/** Quarantine/lower-bound evidence observed before both fixed reads complete. */
|
|
560
503
|
pendingFixedLoss = false;
|
|
561
504
|
pendingJournalLoss = false;
|
|
562
|
-
/** Losses observed before fixed snapshots load must not dirty empty state. */
|
|
563
505
|
deferredGlobalLosses = [];
|
|
564
|
-
/**
|
|
565
|
-
* Retention is its own post-fact transaction. Keep its intent separately so
|
|
566
|
-
* a fact commit followed by a failed tombstone commit cannot turn a retry
|
|
567
|
-
* into an apparently clean no-op.
|
|
568
|
-
*/
|
|
569
506
|
pendingRetention = null;
|
|
570
|
-
/** The exact pending journal shape, retained only to merge a later recovery. */
|
|
571
507
|
pendingRetentionPlan = null;
|
|
572
508
|
retryTimer = null;
|
|
573
509
|
retryDelay = RETRY_MIN_MS;
|
|
@@ -597,10 +533,6 @@ class AccountingStoreImpl {
|
|
|
597
533
|
maxTargets: 128,
|
|
598
534
|
...(options.ioHooks === undefined ? {} : { hooks: options.ioHooks }),
|
|
599
535
|
});
|
|
600
|
-
// A read-only store never takes the writer lease and never replays or quarantines
|
|
601
|
-
// anything — every one of those is a write against a directory a live relay may be
|
|
602
|
-
// committing to. It observes committed snapshots only; unflushed in-memory deltas of
|
|
603
|
-
// the running process stay invisible, which the CLI reports as lag rather than repairing.
|
|
604
536
|
if (this.readOnly) {
|
|
605
537
|
this.writer = { status: "released", error: null, retryable: false };
|
|
606
538
|
this.last = result("none", null, false, false);
|
|
@@ -649,8 +581,6 @@ class AccountingStoreImpl {
|
|
|
649
581
|
flush() {
|
|
650
582
|
if (this._closed)
|
|
651
583
|
return result("invalid", "closed");
|
|
652
|
-
// Read-only: flushing is a write. Nothing is ever dirty here anyway because no
|
|
653
|
-
// record path runs; keep the answer explicit rather than relying on that accident.
|
|
654
584
|
if (this.readOnly)
|
|
655
585
|
return result("none", null, false, false);
|
|
656
586
|
this.timer.clear();
|
|
@@ -668,10 +598,6 @@ class AccountingStoreImpl {
|
|
|
668
598
|
if (recovered.lowerBoundLoss || recovered.status === "recovery-loss")
|
|
669
599
|
this.consumeRecoveryLoss(recovered);
|
|
670
600
|
if (recovered.status === "recovered" && this.pendingRetentionPlan !== null) {
|
|
671
|
-
// Never replace current memory with the journal's saved lifetime: records
|
|
672
|
-
// may have arrived after the failed retention write. Reconcile only the
|
|
673
|
-
// durable pruning effects, then let the ordinary fact commit persist any
|
|
674
|
-
// newer in-memory facts.
|
|
675
601
|
this.reconcileRecoveredRetention(this.pendingRetentionPlan);
|
|
676
602
|
}
|
|
677
603
|
this.drainQueued();
|
|
@@ -699,9 +625,6 @@ class AccountingStoreImpl {
|
|
|
699
625
|
this.recentReadState = "ok";
|
|
700
626
|
this.retryDelay = RETRY_MIN_MS;
|
|
701
627
|
const retention = this.commitRetention(committedDates);
|
|
702
|
-
// A write-loaded shard stays outside the clean cache until this commit has
|
|
703
|
-
// made it durable. Re-admit it now, or evict it if every cache slot is
|
|
704
|
-
// still protected by dirty/in-flight facts.
|
|
705
628
|
for (const date of [...committedDates].sort())
|
|
706
629
|
this.rememberKnownDay(date);
|
|
707
630
|
return retention ?? written;
|
|
@@ -712,9 +635,6 @@ class AccountingStoreImpl {
|
|
|
712
635
|
this.timer.clear();
|
|
713
636
|
this.clearRetry();
|
|
714
637
|
const flushed = this.flush();
|
|
715
|
-
// Dirty facts and a recoverable journal still belong to this store. Keep
|
|
716
|
-
// it open so a later close can retry instead of irreversibly discarding
|
|
717
|
-
// the only in-process copy after one transient failure.
|
|
718
638
|
if (flushed.retryable)
|
|
719
639
|
return flushed;
|
|
720
640
|
const closed = this.io.close();
|
|
@@ -731,8 +651,6 @@ class AccountingStoreImpl {
|
|
|
731
651
|
const parsed = parsedDay(cached);
|
|
732
652
|
return parsed === null ? { status: "corrupt", value: null, error: "schema" } : { status: "ok", value: this.withDayLoss(parsed) };
|
|
733
653
|
}
|
|
734
|
-
// Quarantine renames a file; an out-of-process reader reports corruption instead of
|
|
735
|
-
// touching a shard a live relay may be about to rewrite.
|
|
736
654
|
const read = this.io.readJson(targetForDay(date), (value) => parseAccountingDayShardV1(value).ok, { quarantineCorrupt: !this.readOnly });
|
|
737
655
|
if (read.status === "missing")
|
|
738
656
|
return { status: "missing", value: null };
|
|
@@ -793,8 +711,6 @@ class AccountingStoreImpl {
|
|
|
793
711
|
if (parsed === null)
|
|
794
712
|
return { status: "corrupt", value: null, error: "schema" };
|
|
795
713
|
const requested = typeof options === "number" ? options : options?.limit;
|
|
796
|
-
// Unlike the constructor options, an EXPLICIT read limit is honoured exactly:
|
|
797
|
-
// 0 asks for zero rows, and only an omitted limit falls back to the default.
|
|
798
714
|
const limit = typeof requested === "number" && Number.isSafeInteger(requested)
|
|
799
715
|
? Math.min(Math.max(requested, 0), this.recentLimit)
|
|
800
716
|
: this.recentLimit;
|
|
@@ -814,24 +730,6 @@ class AccountingStoreImpl {
|
|
|
814
730
|
const packet = parsed.details[requestId];
|
|
815
731
|
return packet === undefined ? { status: "missing", value: null } : { status: "ok", value: frozenClone(packet) };
|
|
816
732
|
}
|
|
817
|
-
/**
|
|
818
|
-
* Local usage for one credential in the CURRENT period — the `localUsed` half of spec §5.1
|
|
819
|
-
* rung 2. Reads ONLY in-memory state and never touches disk:
|
|
820
|
-
*
|
|
821
|
-
* - minute/day → this process's in-memory day shard (`this.days`, which holds today's shard
|
|
822
|
-
* once any terminal request has been applied; a shard evicted from the read cache or not yet
|
|
823
|
-
* reloaded after restart is invisible ⇒ null, never 0).
|
|
824
|
-
* - month → in-memory lifetime months only. Reading 31 day shards would put disk IO on the
|
|
825
|
-
* availability path, so a month whose rollup was dropped by ACCOUNTING_MAX_MONTHS reports
|
|
826
|
-
* null rather than a partial sum.
|
|
827
|
-
*
|
|
828
|
-
* Included: every TERMINAL event already applied to memory, including ones still awaiting the
|
|
829
|
-
* write-behind flush. NOT included: in-flight requests that have not completed, and anything
|
|
830
|
-
* recorded before this store instance loaded. Tokens are reported input+output when every
|
|
831
|
-
* measured request carried a provider report, estimated-only otherwise; a window holding both
|
|
832
|
-
* bases reports NO number (`basis: "mixed"`, tokens null) — see usageReading. An uncertain cell
|
|
833
|
-
* poisons its whole side instead of being silently dropped from the sum.
|
|
834
|
-
*/
|
|
835
733
|
usedInWindow(options) {
|
|
836
734
|
const at = options.now ?? Date.now();
|
|
837
735
|
if (!Number.isFinite(at))
|
|
@@ -843,15 +741,10 @@ class AccountingStoreImpl {
|
|
|
843
741
|
let requests = null;
|
|
844
742
|
const reported = newUsageSide();
|
|
845
743
|
const estimated = newUsageSide();
|
|
846
|
-
// True when at least one counted request carried ONLY estimated tokens — the case that turns
|
|
847
|
-
// a reported figure into an understatement and forces basis "mixed" with no value.
|
|
848
744
|
let estimatedOnlyRequest = false;
|
|
849
745
|
if (options.period !== "month") {
|
|
850
|
-
// Request rows carry kind "request"; their aggregate counts requests exactly once per row.
|
|
851
746
|
const day = this.days.get(date);
|
|
852
747
|
if (day !== undefined) {
|
|
853
|
-
// Both sides are bare HH:MM strings, so lexicographic order is chronological. The day
|
|
854
|
-
// shard only contains THIS date's cells; the bound just drops clock-skewed future ones.
|
|
855
748
|
const minuteKeys = options.period === "minute" ? [minute] : Object.keys(day.cells).filter((cellMinute) => cellMinute <= minute);
|
|
856
749
|
let total = 0;
|
|
857
750
|
for (const cellMinute of minuteKeys) {
|
|
@@ -861,8 +754,6 @@ class AccountingStoreImpl {
|
|
|
861
754
|
for (const row of cell.rows) {
|
|
862
755
|
if (row.kind !== "request" || row.credentialId !== options.credentialId)
|
|
863
756
|
continue;
|
|
864
|
-
// Unattributable traffic cannot be claimed by a named credential; a model filter
|
|
865
|
-
// narrows further when given.
|
|
866
757
|
if (row.credentialId === null || (model !== null && row.model !== model))
|
|
867
758
|
continue;
|
|
868
759
|
total += windowCounter(row.requests);
|
|
@@ -877,18 +768,10 @@ class AccountingStoreImpl {
|
|
|
877
768
|
}
|
|
878
769
|
}
|
|
879
770
|
else {
|
|
880
|
-
// The lifetime month rollup carries ROOT aggregates across ALL credentials — it cannot be
|
|
881
|
-
// narrowed to one slot. Feeding it to a per-credential rung-2 subtraction would charge every
|
|
882
|
-
// key's traffic to the one credential whose ceiling is being resolved, so BOTH figures
|
|
883
|
-
// decline until the rollup is per-credential: requests as well as tokens. A whole-relay
|
|
884
|
-
// count on a per-slot row is not "exact with stated scope" — the wire row has no scope
|
|
885
|
-
// marker to state it with, and a reader cannot tell. Null is the honest reading here; same
|
|
886
|
-
// fail-safe direction as everywhere else in this method.
|
|
887
771
|
return USED_WINDOW_EMPTY;
|
|
888
772
|
}
|
|
889
773
|
return usageReading(requests, reported, estimated, estimatedOnlyRequest);
|
|
890
774
|
}
|
|
891
|
-
/** Read paths may touch arbitrary dates; retain only a deterministic bounded hint. */
|
|
892
775
|
rememberKnownDay(date) {
|
|
893
776
|
if (!this.days.has(date) || this.knownDays.has(date) || this.knownDaysCap < 1)
|
|
894
777
|
return;
|
|
@@ -903,10 +786,6 @@ class AccountingStoreImpl {
|
|
|
903
786
|
break;
|
|
904
787
|
}
|
|
905
788
|
if (!evicted) {
|
|
906
|
-
// Cache pressure may never discard a fact that is dirty or still has
|
|
907
|
-
// in-flight work. Those states are independently bounded by the
|
|
908
|
-
// transaction and pending-work caps; only a clean, unprotected read
|
|
909
|
-
// cache entry may be dropped here.
|
|
910
789
|
if (!this.dirtyDays.has(date) && !this.hasPendingDay(date))
|
|
911
790
|
this.days.delete(date);
|
|
912
791
|
return;
|
|
@@ -953,8 +832,6 @@ class AccountingStoreImpl {
|
|
|
953
832
|
onAttemptCompleted(event) {
|
|
954
833
|
const attempt = makeAttempt(event);
|
|
955
834
|
if (attempt === null) {
|
|
956
|
-
// A packet that fails validation is a measurement quietly disappearing
|
|
957
|
-
// unless the loss is announced like every other drop path.
|
|
958
835
|
this.markGlobalLoss("unknown", "truncated", "attempt_packet");
|
|
959
836
|
return;
|
|
960
837
|
}
|
|
@@ -990,10 +867,6 @@ class AccountingStoreImpl {
|
|
|
990
867
|
if (existing !== undefined)
|
|
991
868
|
return existing;
|
|
992
869
|
if (this.pending.size >= this.pendingRequestLimit) {
|
|
993
|
-
// Evict the OLDEST-STARTED in-flight request, not merely the first
|
|
994
|
-
// inserted: parallel subagent streams do not start in insertion order.
|
|
995
|
-
// An entry whose start event has not arrived carries the least identity,
|
|
996
|
-
// so it is treated as older than every dated entry.
|
|
997
870
|
let victim = null;
|
|
998
871
|
let victimStartedAt = null;
|
|
999
872
|
for (const [key, entry] of this.pending) {
|
|
@@ -1160,8 +1033,6 @@ class AccountingStoreImpl {
|
|
|
1160
1033
|
model: authoritative?.model ?? safeId(event.model),
|
|
1161
1034
|
credentialId: authoritative?.credentialId ?? safeId(event.credentialId),
|
|
1162
1035
|
tokens: authoritative?.tokens ?? tokenSnapshot(event.tokens, event.endedAt),
|
|
1163
|
-
// The WINNING serve's spend mirrors its tokens. Repair spend stays on the
|
|
1164
|
-
// repair attempt rows (C1) so a later roll-up can add it back exactly once.
|
|
1165
1036
|
spend: authoritative?.spend ?? null,
|
|
1166
1037
|
attempts: retained,
|
|
1167
1038
|
attemptMetadata: { total, stored: retained.length, dropped: total - retained.length },
|
|
@@ -1241,7 +1112,6 @@ class AccountingStoreImpl {
|
|
|
1241
1112
|
ids.add(requestId);
|
|
1242
1113
|
return true;
|
|
1243
1114
|
}
|
|
1244
|
-
/** Keep a fact transaction within the journal's bounded target set. */
|
|
1245
1115
|
ensureTransactionRoom(date) {
|
|
1246
1116
|
if (this.dirtyDays.has(date))
|
|
1247
1117
|
return true;
|
|
@@ -1250,14 +1120,10 @@ class AccountingStoreImpl {
|
|
|
1250
1120
|
return false;
|
|
1251
1121
|
if (this.dirtyDays.size + 3 <= maximum)
|
|
1252
1122
|
return true;
|
|
1253
|
-
// A terminal request owns a day + lifetime + recent atomically. Commit the
|
|
1254
|
-
// previous bounded batch before accepting a new day rather than allowing an
|
|
1255
|
-
// unrepresentable journal transaction to accumulate.
|
|
1256
1123
|
const flushed = this.flush();
|
|
1257
1124
|
return (flushed.status === "committed" || flushed.status === "recovered" || flushed.status === "none")
|
|
1258
1125
|
&& this.dirtyDays.size + 3 <= maximum;
|
|
1259
1126
|
}
|
|
1260
|
-
/** Read one named day through the namespace-restricted journal I/O only. */
|
|
1261
1127
|
loadDayForWrite(date) {
|
|
1262
1128
|
const cached = this.days.get(date);
|
|
1263
1129
|
if (cached !== undefined)
|
|
@@ -1269,9 +1135,6 @@ class AccountingStoreImpl {
|
|
|
1269
1135
|
return created;
|
|
1270
1136
|
}
|
|
1271
1137
|
if (read.status !== "ok" || read.value === null) {
|
|
1272
|
-
// A quarantined day is intentionally restarted as a lower bound on the
|
|
1273
|
-
// next queued pass. A transient read failure remains queued and cannot
|
|
1274
|
-
// overwrite an unreadable existing shard.
|
|
1275
1138
|
if (read.status === "corrupt" || read.status === "oversize") {
|
|
1276
1139
|
this.markGlobalLoss("corrupt_recovery", "corrupt", "day");
|
|
1277
1140
|
}
|
|
@@ -1286,7 +1149,6 @@ class AccountingStoreImpl {
|
|
|
1286
1149
|
this.days.set(date, mutable);
|
|
1287
1150
|
return mutable;
|
|
1288
1151
|
}
|
|
1289
|
-
/** Keep unread fixed snapshots immutable until a retry has loaded both safely. */
|
|
1290
1152
|
deferFixedLoad(value) {
|
|
1291
1153
|
this.fixedLoadPending = true;
|
|
1292
1154
|
this.lifetimeReadState = "corrupt";
|
|
@@ -1300,13 +1162,6 @@ class AccountingStoreImpl {
|
|
|
1300
1162
|
for (const loss of losses)
|
|
1301
1163
|
this.markGlobalLoss(loss.reason, loss.kind, loss.field, loss.count);
|
|
1302
1164
|
}
|
|
1303
|
-
/**
|
|
1304
|
-
* Read-only constructor load: read the two fixed snapshots WITHOUT the writer lease and
|
|
1305
|
-
* without any recovery/quarantine write. A corrupt or unreadable snapshot stays corrupt
|
|
1306
|
-
* (reported by `readLifetime`/`readRecent`) rather than being renamed aside. Day shards
|
|
1307
|
-
* are deliberately NOT preloaded — `readDay` reads them on demand through the same
|
|
1308
|
-
* no-write readJson path.
|
|
1309
|
-
*/
|
|
1310
1165
|
loadFixedSnapshotsReadOnly() {
|
|
1311
1166
|
const lifetime = this.io.readJson(LIFETIME_TARGET, (value) => parseAccountingLifetimeV1(value).ok, { quarantineCorrupt: false });
|
|
1312
1167
|
this.lifetimeReadState = lifetime.status === "ok" && lifetime.value !== null
|
|
@@ -1321,7 +1176,6 @@ class AccountingStoreImpl {
|
|
|
1321
1176
|
if (this.recentReadState === "ok")
|
|
1322
1177
|
this.recent = mutableRecent(recent.value);
|
|
1323
1178
|
}
|
|
1324
|
-
/** Recover a committed journal first, then load the two fixed snapshots. */
|
|
1325
1179
|
recoverAndLoad() {
|
|
1326
1180
|
const recovered = this.io.recover();
|
|
1327
1181
|
if (recovered.status === "failed" || recovered.status === "invalid") {
|
|
@@ -1397,7 +1251,6 @@ class AccountingStoreImpl {
|
|
|
1397
1251
|
this.queueRecoveredRetention();
|
|
1398
1252
|
this.last = recovered;
|
|
1399
1253
|
}
|
|
1400
|
-
/** Validate every outgoing canonical object before handing it to the journal. */
|
|
1401
1254
|
factSnapshots() {
|
|
1402
1255
|
const snapshots = {};
|
|
1403
1256
|
const encode = (name, value) => {
|
|
@@ -1432,15 +1285,9 @@ class AccountingStoreImpl {
|
|
|
1432
1285
|
}
|
|
1433
1286
|
return snapshots;
|
|
1434
1287
|
}
|
|
1435
|
-
/**
|
|
1436
|
-
* Retention never shares the fact transaction: a fact must be durable before
|
|
1437
|
-
* its day can become eligible for a tombstone transaction.
|
|
1438
|
-
*/
|
|
1439
1288
|
commitRetention(committedDates) {
|
|
1440
1289
|
if (this.retentionDays === null)
|
|
1441
1290
|
return null;
|
|
1442
|
-
// A clock that previously rolled back may have caught the durable frontier
|
|
1443
|
-
// since construction. Re-evaluate it on every flush, not only at startup.
|
|
1444
1291
|
this.queueRecoveredRetention();
|
|
1445
1292
|
const eligible = this.advanceTrustedDate(committedDates);
|
|
1446
1293
|
if (eligible !== null)
|
|
@@ -1448,8 +1295,6 @@ class AccountingStoreImpl {
|
|
|
1448
1295
|
const cutoff = this.pendingRetention;
|
|
1449
1296
|
if (cutoff === null)
|
|
1450
1297
|
return null;
|
|
1451
|
-
// A recovered or already-completed transaction can leave a stale in-memory
|
|
1452
|
-
// intent, but it must not cause an endless no-op retry.
|
|
1453
1298
|
if (!this.retentionNeedsWork(cutoff)) {
|
|
1454
1299
|
this.pendingRetention = null;
|
|
1455
1300
|
this.pendingRetentionPlan = null;
|
|
@@ -1465,17 +1310,12 @@ class AccountingStoreImpl {
|
|
|
1465
1310
|
const start = progress ?? dayFor(current.firstRequestAt ?? "");
|
|
1466
1311
|
const retention = this.retentionCandidates(start, cutoff, Math.max(0, maximum - 1));
|
|
1467
1312
|
const candidates = retention.dates;
|
|
1468
|
-
// One target is consumed by lifetime. Do not claim retention can make
|
|
1469
|
-
// progress if a caller has configured no room for a tombstone at all.
|
|
1470
1313
|
if (retention.truncated && candidates.length === 0) {
|
|
1471
1314
|
return this.fail(result("invalid", "retention-targets", false, this.lowerBoundLoss));
|
|
1472
1315
|
}
|
|
1473
1316
|
const next = mutableLifetime(current);
|
|
1474
1317
|
const lastCandidate = candidates.at(-1) ?? null;
|
|
1475
1318
|
const advanced = lastCandidate === null ? null : nextDate(lastCandidate);
|
|
1476
|
-
// `retentionFrom` is the durable cursor: it may reach the policy cutoff
|
|
1477
|
-
// only once every bounded tombstone batch before it has committed. This
|
|
1478
|
-
// lets a restarted store continue without discovering directory entries.
|
|
1479
1319
|
next.coverage.retentionFrom = advanced ?? (retention.truncated
|
|
1480
1320
|
? progress
|
|
1481
1321
|
: progress === null || progress < cutoff ? cutoff : progress);
|
|
@@ -1505,8 +1345,6 @@ class AccountingStoreImpl {
|
|
|
1505
1345
|
this.applyRetentionPlan(plan);
|
|
1506
1346
|
this.pendingRetentionPlan = null;
|
|
1507
1347
|
if (plan.truncated) {
|
|
1508
|
-
// A complete transaction can still have more bounded work. This is a
|
|
1509
|
-
// continuation, not a dirty fact write, so wake it explicitly.
|
|
1510
1348
|
this.pendingRetention = plan.cutoff;
|
|
1511
1349
|
this.retryDelay = RETRY_MIN_MS;
|
|
1512
1350
|
this.scheduleRetry();
|
|
@@ -1516,11 +1354,6 @@ class AccountingStoreImpl {
|
|
|
1516
1354
|
}
|
|
1517
1355
|
return written;
|
|
1518
1356
|
}
|
|
1519
|
-
/**
|
|
1520
|
-
* A restart cannot scan a directory to rediscover old shards. The durable
|
|
1521
|
-
* lifetime lower date lets us name a bounded, deterministic tombstone range
|
|
1522
|
-
* instead; absent days are harmless no-op tombstones.
|
|
1523
|
-
*/
|
|
1524
1357
|
retentionCandidates(start, cutoff, limit) {
|
|
1525
1358
|
const dates = new Set();
|
|
1526
1359
|
let truncated = false;
|
|
@@ -1544,15 +1377,11 @@ class AccountingStoreImpl {
|
|
|
1544
1377
|
}
|
|
1545
1378
|
}
|
|
1546
1379
|
if (start === null) {
|
|
1547
|
-
// Only this bounded fallback needs ordering. Normal retained data has a
|
|
1548
|
-
// lifetime first-request date, so it names its whole deterministic range
|
|
1549
|
-
// without retaining or sorting arbitrary read-path dates.
|
|
1550
1380
|
for (const date of [...this.knownDays].sort())
|
|
1551
1381
|
add(date);
|
|
1552
1382
|
}
|
|
1553
1383
|
return { dates: [...dates].sort(), truncated };
|
|
1554
1384
|
}
|
|
1555
|
-
/** Apply a retention transaction known to have committed in this process. */
|
|
1556
1385
|
applyRetentionPlan(plan) {
|
|
1557
1386
|
this.lifetime = plan.next;
|
|
1558
1387
|
this.lifetimeReadState = "ok";
|
|
@@ -1561,12 +1390,6 @@ class AccountingStoreImpl {
|
|
|
1561
1390
|
this.knownDays.delete(date);
|
|
1562
1391
|
}
|
|
1563
1392
|
}
|
|
1564
|
-
/**
|
|
1565
|
-
* A retry may recover a durable retention journal after subsequent records
|
|
1566
|
-
* have already changed memory. Merge only its coverage cursor and exact
|
|
1567
|
-
* tombstones; a later fact transaction writes the current aggregate rather
|
|
1568
|
-
* than replacing it with the journal's stale lifetime payload.
|
|
1569
|
-
*/
|
|
1570
1393
|
reconcileRecoveredRetention(plan) {
|
|
1571
1394
|
this.pendingRetentionPlan = null;
|
|
1572
1395
|
const current = parsedLifetime(this.lifetime);
|
|
@@ -1584,8 +1407,6 @@ class AccountingStoreImpl {
|
|
|
1584
1407
|
markCoverage(next.coverage, "unknown", "truncated", "retention");
|
|
1585
1408
|
this.lifetime = next;
|
|
1586
1409
|
this.lifetimeReadState = "ok";
|
|
1587
|
-
// It is harmless to rewrite an unchanged lifetime once; doing so is what
|
|
1588
|
-
// makes the recovered durable state and any unflushed newer facts converge.
|
|
1589
1410
|
this.dirtyLifetime = true;
|
|
1590
1411
|
for (const date of plan.candidates) {
|
|
1591
1412
|
if (this.dirtyDays.has(date))
|
|
@@ -1604,12 +1425,6 @@ class AccountingStoreImpl {
|
|
|
1604
1425
|
const progress = validDate(current.coverage.retentionFrom) ? current.coverage.retentionFrom : null;
|
|
1605
1426
|
return current.coverage.retentionDays !== this.retentionDays || progress === null || progress < cutoff;
|
|
1606
1427
|
}
|
|
1607
|
-
/**
|
|
1608
|
-
* Backfilling a shard behind the durable cursor invalidates that cursor,
|
|
1609
|
-
* including after a wall-clock rollback. Rewind coverage in the fact
|
|
1610
|
-
* transaction, then queue pruning when the shard is already expired under
|
|
1611
|
-
* the current clock.
|
|
1612
|
-
*/
|
|
1613
1428
|
revisitLateRetentionDate(date) {
|
|
1614
1429
|
if (this.retentionDays === null || !validDate(date))
|
|
1615
1430
|
return;
|
|
@@ -1630,18 +1445,11 @@ class AccountingStoreImpl {
|
|
|
1630
1445
|
return;
|
|
1631
1446
|
this.pendingRetention = latest(this.pendingRetention, cutoff);
|
|
1632
1447
|
}
|
|
1633
|
-
/**
|
|
1634
|
-
* `lastRequestAt` is durable evidence of the previous trusted frontier. Do
|
|
1635
|
-
* not derive it from a retention cursor: a partial cursor merely says where a
|
|
1636
|
-
* tombstone batch stopped. Keep a future frontier too, so a clock rollback
|
|
1637
|
-
* cannot let a late fact move retention backwards before the clock catches up.
|
|
1638
|
-
*/
|
|
1639
1448
|
seedTrustedDateFromLifetime() {
|
|
1640
1449
|
const last = dayFor(this.lifetime.lastRequestAt ?? "");
|
|
1641
1450
|
if (last !== null)
|
|
1642
1451
|
this.trustedDate = latest(this.trustedDate, last);
|
|
1643
1452
|
}
|
|
1644
|
-
/** Resume an incomplete durable cursor only when its fact frontier is clock-eligible. */
|
|
1645
1453
|
queueRecoveredRetention() {
|
|
1646
1454
|
if (this.retentionDays === null || this.trustedDate === null)
|
|
1647
1455
|
return;
|
|
@@ -1650,7 +1458,6 @@ class AccountingStoreImpl {
|
|
|
1650
1458
|
return;
|
|
1651
1459
|
this.queueRetentionForDate(this.trustedDate);
|
|
1652
1460
|
}
|
|
1653
|
-
/** A clock-gated, monotonic date: future and late facts cannot advance pruning. */
|
|
1654
1461
|
advanceTrustedDate(committedDates) {
|
|
1655
1462
|
const nowDate = this.clockDate();
|
|
1656
1463
|
if (nowDate === null)
|
|
@@ -1690,7 +1497,6 @@ class AccountingStoreImpl {
|
|
|
1690
1497
|
this.markGlobalLoss("corrupt_recovery", "corrupt", "journal");
|
|
1691
1498
|
this.scheduleFlush();
|
|
1692
1499
|
}
|
|
1693
|
-
/** Record a loss where it can be surfaced without claiming an exact total. */
|
|
1694
1500
|
markGlobalLoss(reason, kind, field, count = 1) {
|
|
1695
1501
|
if (this.fixedLoadPending) {
|
|
1696
1502
|
const existing = this.deferredGlobalLosses.find((loss) => loss.reason === reason && loss.kind === kind && loss.field === field);
|
|
@@ -1774,7 +1580,6 @@ class AccountingStoreImpl {
|
|
|
1774
1580
|
return value;
|
|
1775
1581
|
}
|
|
1776
1582
|
}
|
|
1777
|
-
/** Accounting shares the common write-behind cadence, exported for callers/tests. */
|
|
1778
1583
|
export const ACCOUNTING_FLUSH_DELAY_MS = DEFAULT_FLUSH_DELAY_MS;
|
|
1779
1584
|
export const ACCOUNTING_MAX_FLUSH_DELAY_MS = MAX_FLUSH_DELAY_MS;
|
|
1780
1585
|
export function createAccountingStore(options = {}) {
|