@ngockhoale/ukit 3.0.6 → 3.0.8
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/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/scripts/bench/data-foundation.mjs +562 -0
- package/src/core/observability/adapters/common.js +75 -0
- package/src/core/observability/adapters/contextAdapter.js +55 -0
- package/src/core/observability/adapters/decisionAdapter.js +61 -0
- package/src/core/observability/adapters/routeAdapter.js +135 -0
- package/src/core/observability/analytics/digest.js +186 -0
- package/src/core/observability/analytics/fingerprints.js +126 -0
- package/src/core/observability/analytics/opportunities.js +329 -0
- package/src/core/observability/analytics/rebuild.js +56 -0
- package/src/core/observability/analytics/summary.js +298 -0
- package/src/core/observability/emit/config.js +29 -0
- package/src/core/observability/emit/recorder.js +297 -0
- package/src/core/observability/evaluation/aiPacket.js +230 -0
- package/src/core/observability/evaluation/optimizationKnowledge.js +172 -0
- package/src/core/observability/evaluation/replay.js +143 -0
- package/src/core/observability/evaluation/scorecard.js +445 -0
- package/src/core/observability/privacy/allowlist.js +185 -0
- package/src/core/observability/privacy/redaction.js +113 -0
- package/src/core/observability/privacy/sanitizeForSupport.js +133 -0
- package/src/core/observability/privacy/sanitizeObserved.js +134 -0
- package/src/core/observability/rollout.js +155 -0
- package/src/core/observability/schema/constants.js +66 -0
- package/src/core/observability/schema/registry.js +223 -0
- package/src/core/observability/schema/validate.js +227 -0
- package/src/core/observability/segments/internal.js +241 -0
- package/src/core/observability/segments/readSegments.js +215 -0
- package/src/core/observability/segments/recovery.js +123 -0
- package/src/core/observability/segments/retention.js +381 -0
- package/src/core/observability/support/import.js +402 -0
- package/src/core/observability/support/manifest.js +135 -0
- package/src/core/observability/support/paths.js +94 -0
- package/src/core/observability/support/projector.js +483 -0
- package/src/core/observability/support/renderer.js +130 -0
- package/src/core/observability/support/retention.js +155 -0
- package/template_project/.omp/RULES.md +6 -6
- package/template_project/.omp/config.yml +6 -0
- package/template_project/instructions/overlays/omp-rules.md +6 -6
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* retention.js (TASK-006, SPEC §5 DF-FR05 / §8)
|
|
3
|
+
*
|
|
4
|
+
* Write path, rotation, and retention for the append-only JSONL segment
|
|
5
|
+
* store. Layout under `root`:
|
|
6
|
+
*
|
|
7
|
+
* active.jsonl shared append target (O_APPEND — concurrent
|
|
8
|
+
* writers interleave whole rows, never bytes)
|
|
9
|
+
* seg-<seq>-<boot>-<writer>.jsonl sealed segment
|
|
10
|
+
* seg-<...>.meta.json sidecar: checksum, record/seq/importance
|
|
11
|
+
* stats, sealed_at_ms, recovered flag
|
|
12
|
+
* quarantine/ corrupt sealed segments (never falsified)
|
|
13
|
+
* _state.json next_segment_seq, expired/quarantined
|
|
14
|
+
* counters, disabled flag, active_since_ms
|
|
15
|
+
*
|
|
16
|
+
* Durability contract: appends are buffered writes — there is NO per-event
|
|
17
|
+
* fsync. A hard kill can lose the unflushed tail (bounded by the OS page
|
|
18
|
+
* cache, typically the last few KB); recoverTail() drops the partial final
|
|
19
|
+
* row and preserves the complete prefix. Never claim zero critical drops
|
|
20
|
+
* without fault evidence.
|
|
21
|
+
*
|
|
22
|
+
* Ordering contract: records are ordered by (boot_id, writer_id, sequence)
|
|
23
|
+
* and sealed segments by sealed_seq — never by wall clock, so clock jumps
|
|
24
|
+
* cannot reorder stored facts.
|
|
25
|
+
*
|
|
26
|
+
* Public surface (consumed by TASK-007 recorder):
|
|
27
|
+
* appendRecord(root, record, policy?) → { ok: true, written: true }
|
|
28
|
+
* | { ok: false, reason, errors? }
|
|
29
|
+
* rotateIfNeeded(root, policy?) → { ok: true, rotated, name? }
|
|
30
|
+
* | { ok: false, reason }
|
|
31
|
+
* enforceRetention(root, policy?) → { ok: true, expired, disabled }
|
|
32
|
+
* | { ok: false, reason }
|
|
33
|
+
*
|
|
34
|
+
* policy: { maxSegmentBytes, maxSegmentAgeMs, maxTotalBytes, ioTimeoutMs,
|
|
35
|
+
* now } — `now` is injectable for deterministic tests.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import fs from 'node:fs';
|
|
39
|
+
import path from 'node:path';
|
|
40
|
+
|
|
41
|
+
import { validateSemanticRecord } from '../schema/validate.js';
|
|
42
|
+
import { IMPORTANCE_LEVELS } from '../schema/constants.js';
|
|
43
|
+
import {
|
|
44
|
+
ACTIVE_FILE,
|
|
45
|
+
DEFAULT_IO_TIMEOUT_MS,
|
|
46
|
+
ioReason,
|
|
47
|
+
ioTimeoutMsOf,
|
|
48
|
+
listSegments,
|
|
49
|
+
metaPathFor,
|
|
50
|
+
nowOf,
|
|
51
|
+
policyOf,
|
|
52
|
+
readState,
|
|
53
|
+
resolveRoot,
|
|
54
|
+
sanitizeNameComponent,
|
|
55
|
+
sha256Buffer,
|
|
56
|
+
totalSegmentBytes,
|
|
57
|
+
withTimeout,
|
|
58
|
+
writeState,
|
|
59
|
+
} from './internal.js';
|
|
60
|
+
|
|
61
|
+
function importanceRank(level) {
|
|
62
|
+
const idx = IMPORTANCE_LEVELS.indexOf(level);
|
|
63
|
+
return idx >= 0 ? idx : 0; // unknown → lowest → evicted first under cap
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Seal the current active segment: parse stats, checksum the exact bytes,
|
|
68
|
+
* move it aside via link+unlink (atomic, never overwrites an existing seal),
|
|
69
|
+
* and write its meta sidecar. Concurrent rotators lose the race on ENOENT
|
|
70
|
+
* and simply report not-sealed.
|
|
71
|
+
*/
|
|
72
|
+
async function sealActive(root, state, policy) {
|
|
73
|
+
const timeoutMs = ioTimeoutMsOf(policy);
|
|
74
|
+
const activePath = path.join(root, ACTIVE_FILE);
|
|
75
|
+
|
|
76
|
+
let lst;
|
|
77
|
+
try {
|
|
78
|
+
lst = await withTimeout(fs.promises.lstat(activePath), timeoutMs);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err && err.code === 'ENOENT') return { ok: true, sealed: false };
|
|
81
|
+
return { ok: false, reason: ioReason(err) };
|
|
82
|
+
}
|
|
83
|
+
if (lst.isSymbolicLink()) return { ok: false, reason: 'unsafe_path' };
|
|
84
|
+
if (!lst.isFile() || lst.size === 0) {
|
|
85
|
+
state.active_since_ms = null;
|
|
86
|
+
await writeState(root, state, timeoutMs);
|
|
87
|
+
return { ok: true, sealed: false };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let buf;
|
|
91
|
+
try {
|
|
92
|
+
buf = await withTimeout(fs.promises.readFile(activePath), timeoutMs);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return { ok: false, reason: ioReason(err) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Stats from the sealed bytes themselves — never trusted from memory.
|
|
98
|
+
let recordCount = 0;
|
|
99
|
+
let corruptLines = 0;
|
|
100
|
+
let seqMin = null;
|
|
101
|
+
let seqMax = null;
|
|
102
|
+
let importanceMax = 'low';
|
|
103
|
+
let maxRank = -1;
|
|
104
|
+
let bootId = 'unknown';
|
|
105
|
+
let writerId = 'unknown';
|
|
106
|
+
const text = buf.toString('utf8');
|
|
107
|
+
const complete = text.endsWith('\n') ? text.slice(0, -1) : text;
|
|
108
|
+
for (const line of complete.split('\n')) {
|
|
109
|
+
if (line.length === 0) continue;
|
|
110
|
+
let parsed = null;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(line);
|
|
113
|
+
} catch {
|
|
114
|
+
corruptLines += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
recordCount += 1;
|
|
118
|
+
if (bootId === 'unknown' && typeof parsed.boot_id === 'string') bootId = parsed.boot_id;
|
|
119
|
+
if (writerId === 'unknown' && typeof parsed.writer_id === 'string') writerId = parsed.writer_id;
|
|
120
|
+
if (Number.isInteger(parsed.sequence)) {
|
|
121
|
+
seqMin = seqMin === null ? parsed.sequence : Math.min(seqMin, parsed.sequence);
|
|
122
|
+
seqMax = seqMax === null ? parsed.sequence : Math.max(seqMax, parsed.sequence);
|
|
123
|
+
}
|
|
124
|
+
const rank = importanceRank(parsed.importance);
|
|
125
|
+
if (rank > maxRank) {
|
|
126
|
+
maxRank = rank;
|
|
127
|
+
importanceMax = parsed.importance;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// link+unlink: fails atomically if the target seal already exists (another
|
|
132
|
+
// writer won the seq) — rename would silently overwrite it.
|
|
133
|
+
let seq = state.next_segment_seq;
|
|
134
|
+
let name = null;
|
|
135
|
+
let linked = false;
|
|
136
|
+
for (let attempt = 0; attempt < 64; attempt++) {
|
|
137
|
+
name = `seg-${String(seq).padStart(6, '0')}-${sanitizeNameComponent(bootId)}-${sanitizeNameComponent(writerId)}.jsonl`;
|
|
138
|
+
const target = path.join(root, name);
|
|
139
|
+
try {
|
|
140
|
+
await withTimeout(fs.promises.link(activePath, target), timeoutMs);
|
|
141
|
+
await withTimeout(fs.promises.unlink(activePath), timeoutMs);
|
|
142
|
+
linked = true;
|
|
143
|
+
break;
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (err && err.code === 'ENOENT') return { ok: true, sealed: false }; // lost the race
|
|
146
|
+
if (err && err.code === 'EEXIST') {
|
|
147
|
+
seq += 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
return { ok: false, reason: ioReason(err) };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Every attempt collided — never write meta for a name that was not sealed.
|
|
154
|
+
if (!linked) return { ok: false, reason: 'seal_conflict' };
|
|
155
|
+
|
|
156
|
+
const meta = {
|
|
157
|
+
sealed_seq: seq,
|
|
158
|
+
boot_id: bootId,
|
|
159
|
+
writer_id: writerId,
|
|
160
|
+
sealed_at_ms: nowOf(policy),
|
|
161
|
+
record_count: recordCount,
|
|
162
|
+
seq_min: seqMin,
|
|
163
|
+
seq_max: seqMax,
|
|
164
|
+
importance_max: importanceMax,
|
|
165
|
+
corrupt_lines: corruptLines,
|
|
166
|
+
recovered: false,
|
|
167
|
+
checksum: sha256Buffer(buf),
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
await withTimeout(
|
|
171
|
+
fs.promises.writeFile(metaPathFor(path.join(root, name)), JSON.stringify(meta, null, 2)),
|
|
172
|
+
timeoutMs,
|
|
173
|
+
);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return { ok: false, reason: ioReason(err) };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
state.next_segment_seq = Math.max(state.next_segment_seq, seq + 1);
|
|
179
|
+
state.active_since_ms = null;
|
|
180
|
+
await writeState(root, state, timeoutMs);
|
|
181
|
+
return { ok: true, sealed: true, name };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Append one validated record to the active segment. Validates the envelope
|
|
186
|
+
* (records reaching this layer are already sanitizeObserved output — this is
|
|
187
|
+
* the integrity gate, not the privacy gate). Rotates BEFORE writing when the
|
|
188
|
+
* line would push the active segment past maxSegmentBytes, then enforces
|
|
189
|
+
* retention. When the hard cap cannot be satisfied even after eviction the
|
|
190
|
+
* store flips to `disabled` and every subsequent append returns
|
|
191
|
+
* { ok: false, reason: 'disabled' } — never unbounded growth.
|
|
192
|
+
*/
|
|
193
|
+
export async function appendRecord(root, record, policy) {
|
|
194
|
+
const p = policyOf(policy);
|
|
195
|
+
const timeoutMs = ioTimeoutMsOf(p);
|
|
196
|
+
|
|
197
|
+
const validation = validateSemanticRecord(record);
|
|
198
|
+
if (!validation.ok) {
|
|
199
|
+
return { ok: false, reason: 'invalid_record', errors: validation.errors };
|
|
200
|
+
}
|
|
201
|
+
const line = JSON.stringify(record) + '\n';
|
|
202
|
+
const lineBytes = Buffer.byteLength(line, 'utf8');
|
|
203
|
+
|
|
204
|
+
const resolved = await resolveRoot(root, timeoutMs);
|
|
205
|
+
if (!resolved.ok) return { ok: false, reason: resolved.reason };
|
|
206
|
+
const realRoot = resolved.root;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await withTimeout(fs.promises.mkdir(realRoot, { recursive: true }), timeoutMs);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return { ok: false, reason: ioReason(err) };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let state = await readState(realRoot, timeoutMs);
|
|
215
|
+
if (state.disabled) return { ok: false, reason: 'disabled' };
|
|
216
|
+
|
|
217
|
+
// Rotate before writing when this line would overflow the active segment.
|
|
218
|
+
// A symlinked active.jsonl is rejected outright — appending through it
|
|
219
|
+
// would write outside the storage root (same rule as rotateIfNeeded).
|
|
220
|
+
try {
|
|
221
|
+
const st = await withTimeout(fs.promises.lstat(path.join(realRoot, ACTIVE_FILE)), timeoutMs);
|
|
222
|
+
if (st.isSymbolicLink()) return { ok: false, reason: 'unsafe_path' };
|
|
223
|
+
if (st.isFile() && st.size > 0 && st.size + lineBytes > p.maxSegmentBytes) {
|
|
224
|
+
const sealed = await sealActive(realRoot, state, p);
|
|
225
|
+
if (!sealed.ok) return { ok: false, reason: sealed.reason };
|
|
226
|
+
state = await readState(realRoot, timeoutMs);
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (!(err && err.code === 'ENOENT')) return { ok: false, reason: ioReason(err) };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// O_NOFOLLOW closes the lstat→append race: if active.jsonl was swapped to
|
|
233
|
+
// a symlink in between, open fails with ELOOP instead of writing through.
|
|
234
|
+
const appendFlags =
|
|
235
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND |
|
|
236
|
+
(fs.constants.O_NOFOLLOW || 0);
|
|
237
|
+
try {
|
|
238
|
+
await withTimeout(
|
|
239
|
+
fs.promises.appendFile(path.join(realRoot, ACTIVE_FILE), line, { flag: appendFlags }),
|
|
240
|
+
timeoutMs,
|
|
241
|
+
);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return { ok: false, reason: ioReason(err) };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (state.active_since_ms === null || state.active_since_ms === undefined) {
|
|
247
|
+
state.active_since_ms = nowOf(p);
|
|
248
|
+
await writeState(realRoot, state, timeoutMs);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const retention = await enforceRetention(realRoot, p);
|
|
252
|
+
if (!retention.ok) return { ok: false, reason: retention.reason };
|
|
253
|
+
if (retention.disabled) return { ok: false, reason: 'disabled' };
|
|
254
|
+
return { ok: true, written: true };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Seal the active segment when it has reached the size threshold or its age
|
|
259
|
+
* exceeds maxSegmentAgeMs. Safe under concurrent callers — losers of the
|
|
260
|
+
* seal race observe ENOENT and report rotated:false.
|
|
261
|
+
*/
|
|
262
|
+
export async function rotateIfNeeded(root, policy) {
|
|
263
|
+
const p = policyOf(policy);
|
|
264
|
+
const timeoutMs = ioTimeoutMsOf(p);
|
|
265
|
+
const resolved = await resolveRoot(root, timeoutMs);
|
|
266
|
+
if (!resolved.ok) return { ok: false, reason: resolved.reason };
|
|
267
|
+
if (!resolved.exists) return { ok: true, rotated: false };
|
|
268
|
+
|
|
269
|
+
const state = await readState(resolved.root, timeoutMs);
|
|
270
|
+
const activePath = path.join(resolved.root, ACTIVE_FILE);
|
|
271
|
+
let st;
|
|
272
|
+
try {
|
|
273
|
+
st = await withTimeout(fs.promises.lstat(activePath), timeoutMs);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
if (err && err.code === 'ENOENT') return { ok: true, rotated: false };
|
|
276
|
+
return { ok: false, reason: ioReason(err) };
|
|
277
|
+
}
|
|
278
|
+
if (st.isSymbolicLink()) return { ok: false, reason: 'unsafe_path' };
|
|
279
|
+
if (!st.isFile() || st.size === 0) return { ok: true, rotated: false };
|
|
280
|
+
|
|
281
|
+
const oversized = st.size >= p.maxSegmentBytes;
|
|
282
|
+
const aged =
|
|
283
|
+
state.active_since_ms !== null &&
|
|
284
|
+
state.active_since_ms !== undefined &&
|
|
285
|
+
nowOf(p) - state.active_since_ms >= p.maxSegmentAgeMs;
|
|
286
|
+
if (!oversized && !aged) return { ok: true, rotated: false };
|
|
287
|
+
|
|
288
|
+
const sealed = await sealActive(resolved.root, state, p);
|
|
289
|
+
if (!sealed.ok) return { ok: false, reason: sealed.reason };
|
|
290
|
+
return { ok: true, rotated: sealed.sealed, name: sealed.name };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function readMeta(segmentPath, timeoutMs) {
|
|
294
|
+
try {
|
|
295
|
+
const raw = await withTimeout(fs.promises.readFile(metaPathFor(segmentPath), 'utf8'), timeoutMs);
|
|
296
|
+
const meta = JSON.parse(raw);
|
|
297
|
+
return meta && typeof meta === 'object' ? meta : null;
|
|
298
|
+
} catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function removeSegment(root, name, timeoutMs) {
|
|
304
|
+
try {
|
|
305
|
+
await withTimeout(fs.promises.unlink(path.join(root, name)), timeoutMs);
|
|
306
|
+
} catch {
|
|
307
|
+
// Already gone (concurrent eviction) — fine.
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
await withTimeout(fs.promises.unlink(metaPathFor(path.join(root, name))), timeoutMs);
|
|
311
|
+
} catch {
|
|
312
|
+
// No sidecar — fine.
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Retention = age + size + importance under a hard total cap.
|
|
318
|
+
* 1. Age: sealed segments older than maxSegmentAgeMs expire.
|
|
319
|
+
* 2. Cap: while live .jsonl bytes exceed maxTotalBytes, evict the lowest
|
|
320
|
+
* importance_max segment first (ties → lowest sealed_seq = oldest).
|
|
321
|
+
* 3. If the cap is still unhittable (e.g. the active segment alone exceeds
|
|
322
|
+
* it), the store reports disabled — bounded, never silent growth.
|
|
323
|
+
* The active segment is never evicted; only sealed facts expire.
|
|
324
|
+
*/
|
|
325
|
+
export async function enforceRetention(root, policy) {
|
|
326
|
+
const p = policyOf(policy);
|
|
327
|
+
const timeoutMs = ioTimeoutMsOf(p);
|
|
328
|
+
const resolved = await resolveRoot(root, timeoutMs);
|
|
329
|
+
if (!resolved.ok) return { ok: false, reason: resolved.reason };
|
|
330
|
+
if (!resolved.exists) return { ok: true, expired: 0, disabled: false };
|
|
331
|
+
|
|
332
|
+
const state = await readState(resolved.root, timeoutMs);
|
|
333
|
+
const listing = await listSegments(resolved.root, timeoutMs);
|
|
334
|
+
if (!listing.ok) return { ok: false, reason: listing.reason };
|
|
335
|
+
|
|
336
|
+
const now = nowOf(p);
|
|
337
|
+
let expired = 0;
|
|
338
|
+
const live = [];
|
|
339
|
+
|
|
340
|
+
for (const seg of listing.sealed) {
|
|
341
|
+
const meta = await readMeta(seg.path, timeoutMs);
|
|
342
|
+
const sealedAt = meta && Number.isFinite(meta.sealed_at_ms) ? meta.sealed_at_ms : null;
|
|
343
|
+
let ageBasis = sealedAt;
|
|
344
|
+
if (ageBasis === null) {
|
|
345
|
+
try {
|
|
346
|
+
ageBasis = (await withTimeout(fs.promises.stat(seg.path), timeoutMs)).mtimeMs;
|
|
347
|
+
} catch {
|
|
348
|
+
ageBasis = now; // unknown age → treat as fresh, never evict blindly
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (now - ageBasis >= p.maxSegmentAgeMs) {
|
|
352
|
+
await removeSegment(resolved.root, seg.name, timeoutMs);
|
|
353
|
+
expired += 1;
|
|
354
|
+
} else {
|
|
355
|
+
live.push({
|
|
356
|
+
name: seg.name,
|
|
357
|
+
seq: seg.seq,
|
|
358
|
+
size: seg.size,
|
|
359
|
+
rank: meta ? importanceRank(meta.importance_max) : 0,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let usage = (listing.active ? listing.active.size : 0) + live.reduce((s, seg) => s + seg.size, 0);
|
|
365
|
+
|
|
366
|
+
// Hard cap: evict lowest-importance first, oldest (lowest seq) on ties.
|
|
367
|
+
live.sort((a, b) => a.rank - b.rank || a.seq - b.seq);
|
|
368
|
+
while (usage > p.maxTotalBytes && live.length > 0) {
|
|
369
|
+
const victim = live.shift();
|
|
370
|
+
await removeSegment(resolved.root, victim.name, timeoutMs);
|
|
371
|
+
usage -= victim.size;
|
|
372
|
+
expired += 1;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const disabled = usage > p.maxTotalBytes;
|
|
376
|
+
state.expired_segments = (state.expired_segments || 0) + expired;
|
|
377
|
+
if (disabled) state.disabled = true;
|
|
378
|
+
await writeState(resolved.root, state, timeoutMs);
|
|
379
|
+
|
|
380
|
+
return { ok: true, expired, disabled };
|
|
381
|
+
}
|