@remnic/core 10.0.93 → 10.0.94
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/access-admin-ops-surface.js +29 -29
- package/dist/access-cli.js +29 -29
- package/dist/access-extraction-force-flush.js +29 -29
- package/dist/access-http-briefing.js +29 -29
- package/dist/access-http-query.js +29 -29
- package/dist/access-http.js +29 -29
- package/dist/access-identity-continuity-surface.js +29 -29
- package/dist/access-lcm-surface.js +29 -29
- package/dist/access-mcp.js +29 -29
- package/dist/access-observe-write-surface.js +29 -29
- package/dist/access-operations-batch.js +29 -29
- package/dist/access-operations.js +29 -29
- package/dist/access-recall-concurrency.js +29 -29
- package/dist/access-recall-response.js +29 -29
- package/dist/access-recall-surface.js +29 -29
- package/dist/access-service.js +29 -29
- package/dist/active-recall.js +2 -2
- package/dist/briefing.js +3 -3
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-2LMA22UU.js → chunk-4EZWSDFL.js} +3 -3
- package/dist/{chunk-PRAUN6PN.js → chunk-BN2EBCLB.js} +2 -2
- package/dist/{chunk-B33YWUJV.js → chunk-BPK32FRM.js} +2 -2
- package/dist/{chunk-NAQ4A5IX.js → chunk-CL4JU3DF.js} +3 -3
- package/dist/{chunk-YKJBSMMP.js → chunk-D5S6RL32.js} +24 -24
- package/dist/{chunk-BGHCSLE5.js → chunk-D5XG2KPS.js} +2 -2
- package/dist/{chunk-EFL24KOI.js → chunk-DNY63QWJ.js} +2 -2
- package/dist/{chunk-5YT2IIFX.js → chunk-FQAQ63QA.js} +2 -2
- package/dist/{chunk-WQURALGV.js → chunk-FWRXW7CZ.js} +7 -3
- package/dist/chunk-FWRXW7CZ.js.map +1 -0
- package/dist/{chunk-XIYLWEXC.js → chunk-FZZMLGWQ.js} +2 -2
- package/dist/{chunk-ZSE774RV.js → chunk-GBJUTX44.js} +2 -2
- package/dist/{chunk-ATG2PQ4V.js → chunk-IL3RHBWM.js} +3 -3
- package/dist/{chunk-OJXJWNFY.js → chunk-ILY5SSOM.js} +2 -2
- package/dist/{chunk-XRDYRWZI.js → chunk-IQ55QGY5.js} +2 -2
- package/dist/{chunk-BTI4C4MA.js → chunk-J7BIFOYP.js} +2 -2
- package/dist/{chunk-WKSQTMU7.js → chunk-LNGELD2M.js} +2 -2
- package/dist/{chunk-SGQPR5SX.js → chunk-OUVYYKYU.js} +2 -2
- package/dist/{chunk-CGJYL633.js → chunk-QHARXWTC.js} +2 -2
- package/dist/{chunk-SVT5IA2H.js → chunk-QMFAMOT2.js} +2 -2
- package/dist/{chunk-NT7F7WJQ.js → chunk-RFUL2ZIQ.js} +291 -93
- package/dist/chunk-RFUL2ZIQ.js.map +1 -0
- package/dist/{chunk-622V2AZA.js → chunk-RKABLU5M.js} +7 -7
- package/dist/{chunk-MH632KPO.js → chunk-RNG7S3FY.js} +2 -2
- package/dist/{chunk-GQXOBKPL.js → chunk-RUTN5MMC.js} +3 -3
- package/dist/{chunk-MUX7IL77.js → chunk-SUTEBYGI.js} +2 -2
- package/dist/{chunk-FMKILENU.js → chunk-WGGRZYSQ.js} +7 -7
- package/dist/{chunk-ETBVCRVL.js → chunk-X5HGU2EM.js} +2 -2
- package/dist/{chunk-TJ4F6H4H.js → chunk-XJ2S5UBF.js} +2 -2
- package/dist/{chunk-TRQE3RYR.js → chunk-YIXJSW5M.js} +2 -2
- package/dist/{chunk-3CYOCE6U.js → chunk-Z3FTSHT5.js} +2 -2
- package/dist/{chunk-RM2YUFH6.js → chunk-ZDV5YWID.js} +2 -2
- package/dist/cli.js +29 -29
- package/dist/coding/export-okf-codegraph.js +2 -2
- package/dist/compounding/engine.js +4 -4
- package/dist/config.js +2 -2
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/index.js +3 -3
- package/dist/convergence-refresh.js +5 -5
- package/dist/corpus-watermark.js +5 -5
- package/dist/entity-retrieval.js +3 -3
- package/dist/explicit-capture.js +29 -29
- package/dist/extraction.js +3 -3
- package/dist/harmonic-retrieval.js +3 -3
- package/dist/index.js +29 -29
- package/dist/location/index.js +4 -4
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection-runner.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/storage.js +3 -3
- package/dist/operator-doctor-corpus.js +6 -6
- package/dist/operator-toolkit.js +9 -9
- package/dist/orchestration/backlog-scheduler.js +30 -30
- package/dist/orchestration/buffer-drain-runner.js +30 -30
- package/dist/orchestration/maintenance.js +11 -11
- package/dist/orchestrator.js +29 -29
- package/dist/recall-navigation.js +29 -29
- package/dist/resume-bundles.js +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/source-agent-qualifier.js +3 -3
- package/dist/standup/build.js +3 -3
- package/dist/storage/buffer-drain-claims.d.ts +97 -12
- package/dist/storage/buffer-drain-claims.d.ts.map +1 -1
- package/dist/storage/buffer-drain-claims.js +1 -1
- package/dist/storage/buffer-session-files.d.ts +4 -0
- package/dist/storage/buffer-session-files.d.ts.map +1 -1
- package/dist/storage/buffer-session-files.js +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +2 -2
- package/dist/transfer/export-okf.js +3 -3
- package/dist/verified-recall.js +3 -3
- package/dist/who-knows-cli.js +4 -4
- package/dist/who-knows.js +3 -3
- package/package.json +2 -2
- package/src/buffer-drain-handover.test.ts +5 -1
- package/src/storage/buffer-drain-claims.test.ts +385 -1
- package/src/storage/buffer-drain-claims.ts +380 -97
- package/src/storage/buffer-session-files.ts +4 -0
- package/src/storage.ts +5 -1
- package/dist/chunk-NT7F7WJQ.js.map +0 -1
- package/dist/chunk-WQURALGV.js.map +0 -1
- /package/dist/{chunk-2LMA22UU.js.map → chunk-4EZWSDFL.js.map} +0 -0
- /package/dist/{chunk-PRAUN6PN.js.map → chunk-BN2EBCLB.js.map} +0 -0
- /package/dist/{chunk-B33YWUJV.js.map → chunk-BPK32FRM.js.map} +0 -0
- /package/dist/{chunk-NAQ4A5IX.js.map → chunk-CL4JU3DF.js.map} +0 -0
- /package/dist/{chunk-YKJBSMMP.js.map → chunk-D5S6RL32.js.map} +0 -0
- /package/dist/{chunk-BGHCSLE5.js.map → chunk-D5XG2KPS.js.map} +0 -0
- /package/dist/{chunk-EFL24KOI.js.map → chunk-DNY63QWJ.js.map} +0 -0
- /package/dist/{chunk-5YT2IIFX.js.map → chunk-FQAQ63QA.js.map} +0 -0
- /package/dist/{chunk-XIYLWEXC.js.map → chunk-FZZMLGWQ.js.map} +0 -0
- /package/dist/{chunk-ZSE774RV.js.map → chunk-GBJUTX44.js.map} +0 -0
- /package/dist/{chunk-ATG2PQ4V.js.map → chunk-IL3RHBWM.js.map} +0 -0
- /package/dist/{chunk-OJXJWNFY.js.map → chunk-ILY5SSOM.js.map} +0 -0
- /package/dist/{chunk-XRDYRWZI.js.map → chunk-IQ55QGY5.js.map} +0 -0
- /package/dist/{chunk-BTI4C4MA.js.map → chunk-J7BIFOYP.js.map} +0 -0
- /package/dist/{chunk-WKSQTMU7.js.map → chunk-LNGELD2M.js.map} +0 -0
- /package/dist/{chunk-SGQPR5SX.js.map → chunk-OUVYYKYU.js.map} +0 -0
- /package/dist/{chunk-CGJYL633.js.map → chunk-QHARXWTC.js.map} +0 -0
- /package/dist/{chunk-SVT5IA2H.js.map → chunk-QMFAMOT2.js.map} +0 -0
- /package/dist/{chunk-622V2AZA.js.map → chunk-RKABLU5M.js.map} +0 -0
- /package/dist/{chunk-MH632KPO.js.map → chunk-RNG7S3FY.js.map} +0 -0
- /package/dist/{chunk-GQXOBKPL.js.map → chunk-RUTN5MMC.js.map} +0 -0
- /package/dist/{chunk-MUX7IL77.js.map → chunk-SUTEBYGI.js.map} +0 -0
- /package/dist/{chunk-FMKILENU.js.map → chunk-WGGRZYSQ.js.map} +0 -0
- /package/dist/{chunk-ETBVCRVL.js.map → chunk-X5HGU2EM.js.map} +0 -0
- /package/dist/{chunk-TJ4F6H4H.js.map → chunk-XJ2S5UBF.js.map} +0 -0
- /package/dist/{chunk-TRQE3RYR.js.map → chunk-YIXJSW5M.js.map} +0 -0
- /package/dist/{chunk-3CYOCE6U.js.map → chunk-Z3FTSHT5.js.map} +0 -0
- /package/dist/{chunk-RM2YUFH6.js.map → chunk-ZDV5YWID.js.map} +0 -0
|
@@ -9,15 +9,19 @@ import {
|
|
|
9
9
|
SecureStoreDecryptError,
|
|
10
10
|
SecureStoreLockedError
|
|
11
11
|
} from "./chunk-EYGOM6AI.js";
|
|
12
|
+
import {
|
|
13
|
+
isErrnoCode
|
|
14
|
+
} from "./chunk-KNA3BXXL.js";
|
|
12
15
|
import {
|
|
13
16
|
log
|
|
14
17
|
} from "./chunk-3WCASNQZ.js";
|
|
15
18
|
|
|
16
19
|
// src/storage/buffer-drain-claims.ts
|
|
20
|
+
import { randomUUID } from "crypto";
|
|
17
21
|
import { hostname } from "os";
|
|
18
22
|
import { z } from "zod";
|
|
19
23
|
import { existsSync } from "fs";
|
|
20
|
-
import { mkdir, readdir as readdir2, rename, unlink as unlink2, writeFile } from "fs/promises";
|
|
24
|
+
import { mkdir, readdir as readdir2, readFile, rename, stat, unlink as unlink2, writeFile } from "fs/promises";
|
|
21
25
|
import path2 from "path";
|
|
22
26
|
|
|
23
27
|
// src/storage/buffer-session-files.ts
|
|
@@ -89,16 +93,16 @@ var BufferSessionFiles = class {
|
|
|
89
93
|
*/
|
|
90
94
|
async assertDirSafe() {
|
|
91
95
|
if (this.dirVerified) return;
|
|
92
|
-
let
|
|
96
|
+
let stat2 = null;
|
|
93
97
|
try {
|
|
94
|
-
|
|
98
|
+
stat2 = await lstat(this.dir);
|
|
95
99
|
} catch (err) {
|
|
96
100
|
if (!(err instanceof Error) || !isMissing(asFsError(err))) throw err;
|
|
97
101
|
}
|
|
98
|
-
if (
|
|
102
|
+
if (stat2 && (stat2.isSymbolicLink() || !stat2.isDirectory())) {
|
|
99
103
|
throw new Error(`buffer session directory must be a real directory, not a symlink or file: ${this.dir}`);
|
|
100
104
|
}
|
|
101
|
-
this.dirVerified =
|
|
105
|
+
this.dirVerified = stat2 !== null;
|
|
102
106
|
}
|
|
103
107
|
/** Persist ONE session atomically. Compact JSON: machine-read state, and
|
|
104
108
|
* pretty-printing a large buffer costs CPU and bytes on every observe.
|
|
@@ -258,17 +262,19 @@ function turnCounts(turns) {
|
|
|
258
262
|
}
|
|
259
263
|
return counts;
|
|
260
264
|
}
|
|
261
|
-
function
|
|
262
|
-
const merged = [...existing];
|
|
263
|
-
const existingCounts = turnCounts(existing);
|
|
265
|
+
function appendNewOccurrences(target, counts, incoming) {
|
|
264
266
|
const incomingCounts = /* @__PURE__ */ new Map();
|
|
267
|
+
const added = [];
|
|
265
268
|
for (const turn of incoming) {
|
|
266
269
|
const key = bufferTurnIdentityKey(turn);
|
|
267
270
|
const count = (incomingCounts.get(key) ?? 0) + 1;
|
|
268
271
|
incomingCounts.set(key, count);
|
|
269
|
-
if (count > (
|
|
272
|
+
if (count > (counts.get(key) ?? 0)) {
|
|
273
|
+
target.push(turn);
|
|
274
|
+
added.push(key);
|
|
275
|
+
}
|
|
270
276
|
}
|
|
271
|
-
|
|
277
|
+
for (const key of added) counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
272
278
|
}
|
|
273
279
|
function preserveSourceOccurrences(target, source, identity) {
|
|
274
280
|
const targetCount = turnCounts(target).get(identity) ?? 0;
|
|
@@ -297,22 +303,28 @@ function removeOneByIdentity(turns, identity) {
|
|
|
297
303
|
function takeByIdentity(source, identity, count) {
|
|
298
304
|
return source.filter((turn) => bufferTurnIdentityKey(turn) === identity).slice(0, count);
|
|
299
305
|
}
|
|
300
|
-
function
|
|
306
|
+
function foldRecords(claim, records) {
|
|
307
|
+
const consumed = new Set(claim.foldedRecords);
|
|
308
|
+
const fresh = records.filter((record) => !consumed.has(record.id));
|
|
309
|
+
if (fresh.length === 0) return claim;
|
|
310
|
+
const turns = [...claim.entry.turns];
|
|
311
|
+
const retainedTurns = [...claim.entry.retainedTurns ?? []];
|
|
312
|
+
const activeCounts = turnCounts(turns);
|
|
313
|
+
const retainedCounts = turnCounts(retainedTurns);
|
|
301
314
|
const entry = emptyEntry();
|
|
302
|
-
entry.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
entry.extractionCount = Math.max(claim.entry.extractionCount, incoming.extractionCount);
|
|
315
|
+
entry.lastExtractionAt = claim.entry.lastExtractionAt;
|
|
316
|
+
entry.extractionCount = claim.entry.extractionCount;
|
|
317
|
+
for (const { entry: incoming } of fresh) {
|
|
318
|
+
appendNewOccurrences(turns, activeCounts, subtractBufferTurns(incoming.turns, claim.drained ?? []));
|
|
319
|
+
appendNewOccurrences(retainedTurns, retainedCounts, subtractBufferTurns(incoming.retainedTurns ?? [], claim.drainedRetained ?? []));
|
|
320
|
+
const stamps = [entry.lastExtractionAt, incoming.lastExtractionAt].filter(
|
|
321
|
+
(value) => value !== null && value !== void 0
|
|
322
|
+
);
|
|
323
|
+
entry.lastExtractionAt = stamps.sort().at(-1) ?? null;
|
|
324
|
+
entry.extractionCount = Math.max(entry.extractionCount, incoming.extractionCount);
|
|
325
|
+
}
|
|
326
|
+
entry.turns = turns;
|
|
327
|
+
if (retainedTurns.length > 0) entry.retainedTurns = retainedTurns;
|
|
316
328
|
return { ...claim, entry };
|
|
317
329
|
}
|
|
318
330
|
function isOwnerAlive(lease) {
|
|
@@ -344,7 +356,8 @@ var claimBodySchema = z.object({
|
|
|
344
356
|
claimedAt: z.string().optional(),
|
|
345
357
|
drained: z.array(z.unknown()).optional(),
|
|
346
358
|
drainedRetained: z.array(z.unknown()).optional(),
|
|
347
|
-
movedTurns: z.array(z.unknown()).optional()
|
|
359
|
+
movedTurns: z.array(z.unknown()).optional(),
|
|
360
|
+
foldedRecords: z.array(z.string()).optional()
|
|
348
361
|
});
|
|
349
362
|
function parseBody(raw) {
|
|
350
363
|
let body;
|
|
@@ -376,8 +389,49 @@ function parseBody(raw) {
|
|
|
376
389
|
if (Array.isArray(body.movedTurns)) {
|
|
377
390
|
claim.movedTurns = body.movedTurns;
|
|
378
391
|
}
|
|
392
|
+
if (body.foldedRecords) claim.foldedRecords = body.foldedRecords;
|
|
379
393
|
return claim;
|
|
380
394
|
}
|
|
395
|
+
var inboxRecordSchema = z.object({
|
|
396
|
+
id: z.string().min(1),
|
|
397
|
+
turns: z.array(z.unknown()),
|
|
398
|
+
retainedTurns: z.array(z.unknown()).optional(),
|
|
399
|
+
lastExtractionAt: z.string().nullable().optional(),
|
|
400
|
+
extractionCount: z.number().optional()
|
|
401
|
+
});
|
|
402
|
+
function parseInbox(raw) {
|
|
403
|
+
const records = [];
|
|
404
|
+
for (const line of raw.split("\n")) {
|
|
405
|
+
if (line.length === 0) continue;
|
|
406
|
+
let record;
|
|
407
|
+
try {
|
|
408
|
+
record = inboxRecordSchema.parse(JSON.parse(line));
|
|
409
|
+
} catch {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const turns = record.turns;
|
|
413
|
+
const entry = {
|
|
414
|
+
turns,
|
|
415
|
+
lastExtractionAt: record.lastExtractionAt ?? null,
|
|
416
|
+
extractionCount: Number.isFinite(record.extractionCount) ? Number(record.extractionCount) : 0
|
|
417
|
+
};
|
|
418
|
+
if (record.retainedTurns) {
|
|
419
|
+
entry.retainedTurns = record.retainedTurns;
|
|
420
|
+
}
|
|
421
|
+
records.push({ id: record.id, entry });
|
|
422
|
+
}
|
|
423
|
+
return records;
|
|
424
|
+
}
|
|
425
|
+
function pendingTurns(entry) {
|
|
426
|
+
return entry.turns.length + (entry.retainedTurns?.length ?? 0);
|
|
427
|
+
}
|
|
428
|
+
var summarySchema = z.object({
|
|
429
|
+
key: z.string().min(1),
|
|
430
|
+
size: z.number(),
|
|
431
|
+
mtimeMs: z.number(),
|
|
432
|
+
pending: z.number(),
|
|
433
|
+
drained: z.number()
|
|
434
|
+
});
|
|
381
435
|
var leaseSchema = z.object({
|
|
382
436
|
ownerId: z.string().min(1),
|
|
383
437
|
pid: z.number().optional(),
|
|
@@ -405,6 +459,8 @@ function parseLease(raw) {
|
|
|
405
459
|
function leaseBelongsToProcess(lease, ownerId) {
|
|
406
460
|
return lease !== null && lease.ownerId === ownerId && lease.pid === process.pid && lease.host === hostname();
|
|
407
461
|
}
|
|
462
|
+
var INBOX_SUFFIX = ".inbox.jsonl";
|
|
463
|
+
var COUNT_SUFFIX = ".count";
|
|
408
464
|
var LIST_PAGE_SCAN_FACTOR = 8;
|
|
409
465
|
var BufferDrainClaims = class {
|
|
410
466
|
constructor(stateDir, io) {
|
|
@@ -425,6 +481,10 @@ var BufferDrainClaims = class {
|
|
|
425
481
|
lockFileFor(key) {
|
|
426
482
|
return `${this.fileFor(key)}.lock`;
|
|
427
483
|
}
|
|
484
|
+
/** Append-only hand-in log next to the body (see the file header). */
|
|
485
|
+
inboxFor(key) {
|
|
486
|
+
return `${this.fileFor(key)}${INBOX_SUFFIX}`;
|
|
487
|
+
}
|
|
428
488
|
/** Sync existence check for hot-path gates (`SmartBuffer.isSessionClaimed`). */
|
|
429
489
|
isClaimed(key) {
|
|
430
490
|
return existsSync(this.fileFor(key));
|
|
@@ -440,13 +500,100 @@ var BufferDrainClaims = class {
|
|
|
440
500
|
return null;
|
|
441
501
|
}
|
|
442
502
|
}
|
|
443
|
-
/**
|
|
444
|
-
|
|
445
|
-
|
|
503
|
+
/**
|
|
504
|
+
* Every inbox record, oldest first. Only "no inbox" reads as empty: any
|
|
505
|
+
* other failure (a locked or undecryptable store) propagates, because a
|
|
506
|
+
* compaction that mistook an unreadable inbox for an empty one would delete
|
|
507
|
+
* the handed-in turns.
|
|
508
|
+
*/
|
|
509
|
+
async readInbox(key) {
|
|
510
|
+
try {
|
|
511
|
+
return parseInbox(await this.io.read(this.inboxFor(key)));
|
|
512
|
+
} catch (err) {
|
|
513
|
+
if (isErrnoCode(err, "ENOENT")) return [];
|
|
514
|
+
throw err;
|
|
515
|
+
}
|
|
446
516
|
}
|
|
447
|
-
|
|
517
|
+
/**
|
|
518
|
+
* The body with its inbox folded in, plus the ids of every record the inbox
|
|
519
|
+
* holds (what a compaction must list as consumed). The inbox is read BEFORE
|
|
520
|
+
* the body: a compaction landing between the two reads leaves records the
|
|
521
|
+
* newer body already lists, which the fold skips by id, whereas the reverse
|
|
522
|
+
* order could pair an old body with a deleted inbox and lose their turns.
|
|
523
|
+
*/
|
|
524
|
+
async readFolded(key) {
|
|
525
|
+
const records = await this.readInbox(key);
|
|
526
|
+
const body = await this.readClaimBody(key);
|
|
527
|
+
if (!body) return null;
|
|
528
|
+
return { claim: foldRecords(body, records), consumed: records.map((record) => record.id) };
|
|
529
|
+
}
|
|
530
|
+
/** The claim content for `key` (body plus inbox), or null when it is not claimed. */
|
|
531
|
+
async load(key) {
|
|
532
|
+
return (await this.readFolded(key))?.claim ?? null;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Rewrite the body from a folded claim and consume the inbox it folded.
|
|
536
|
+
* Order is the crash contract: body first (it lists `consumed` so a replay
|
|
537
|
+
* skips them), then the summary, then the inbox delete. Callers hold the
|
|
538
|
+
* claim lock, so no append can land between the read and the delete.
|
|
539
|
+
*/
|
|
540
|
+
async writeClaimBody(key, claim, consumed = []) {
|
|
448
541
|
await mkdir(this.dir, { recursive: true });
|
|
449
|
-
|
|
542
|
+
const bodyPath = this.fileFor(key);
|
|
543
|
+
const foldedRecords = consumed.length > 0 ? [...consumed] : void 0;
|
|
544
|
+
await this.io.write(bodyPath, JSON.stringify({ ...claim, key, foldedRecords }));
|
|
545
|
+
await stat(bodyPath).then((identity) => this.writeSummary(bodyPath, claim, identity), () => {
|
|
546
|
+
});
|
|
547
|
+
await unlink2(this.inboxFor(key)).catch(() => {
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Best-effort: a missing or stale summary only costs one body parse.
|
|
552
|
+
* `identity` MUST come from a stat taken before `claim` was read (or while
|
|
553
|
+
* the lock holds the body still), never after, or the counts could be
|
|
554
|
+
* tagged with a newer body's identity.
|
|
555
|
+
*/
|
|
556
|
+
async writeSummary(bodyPath, claim, identity) {
|
|
557
|
+
const summary = {
|
|
558
|
+
key: claim.key,
|
|
559
|
+
size: identity.size,
|
|
560
|
+
mtimeMs: identity.mtimeMs,
|
|
561
|
+
pending: pendingTurns(claim.entry),
|
|
562
|
+
drained: (claim.drained?.length ?? 0) + (claim.drainedRetained?.length ?? 0)
|
|
563
|
+
};
|
|
564
|
+
try {
|
|
565
|
+
await writeFile(`${bodyPath}${COUNT_SUFFIX}`, JSON.stringify(summary));
|
|
566
|
+
} catch (err) {
|
|
567
|
+
log.debug(`buffer drain: claim summary write skipped for ${claim.key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
568
|
+
}
|
|
569
|
+
return summary;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Turn counts of the BODY at `bodyPath` without parsing it: the summary file
|
|
573
|
+
* is trusted only while the body's size and mtime still match the ones it
|
|
574
|
+
* was written against; otherwise the body is parsed once and the summary
|
|
575
|
+
* refreshed. Null when the claim body is absent or unreadable.
|
|
576
|
+
*/
|
|
577
|
+
async readSummary(bodyPath) {
|
|
578
|
+
let identity;
|
|
579
|
+
try {
|
|
580
|
+
identity = await stat(bodyPath);
|
|
581
|
+
} catch {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
const cached = summarySchema.parse(JSON.parse(await readFile(`${bodyPath}${COUNT_SUFFIX}`, "utf8")));
|
|
586
|
+
if (cached.size === identity.size && cached.mtimeMs === identity.mtimeMs) return cached;
|
|
587
|
+
} catch {
|
|
588
|
+
}
|
|
589
|
+
let claim;
|
|
590
|
+
try {
|
|
591
|
+
claim = parseBody(await this.io.read(bodyPath));
|
|
592
|
+
} catch (err) {
|
|
593
|
+
log.debug(`buffer drain: skipping unreadable claim file ${path2.basename(bodyPath)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
return claim ? await this.writeSummary(bodyPath, claim, identity) : null;
|
|
450
597
|
}
|
|
451
598
|
async readLease(key) {
|
|
452
599
|
try {
|
|
@@ -487,6 +634,8 @@ var BufferDrainClaims = class {
|
|
|
487
634
|
async (acquired) => {
|
|
488
635
|
if (!acquired) return "already_claimed";
|
|
489
636
|
if (this.isClaimed(key)) return "already_claimed";
|
|
637
|
+
await unlink2(this.inboxFor(key)).catch(() => {
|
|
638
|
+
});
|
|
490
639
|
try {
|
|
491
640
|
await rename(sessionPath, this.fileFor(key));
|
|
492
641
|
} catch (err) {
|
|
@@ -497,11 +646,13 @@ var BufferDrainClaims = class {
|
|
|
497
646
|
throw err;
|
|
498
647
|
}
|
|
499
648
|
const claim = await this.readClaimBody(key);
|
|
500
|
-
if (!claim ||
|
|
649
|
+
if (!claim || pendingTurns(claim.entry) === 0) {
|
|
501
650
|
await unlink2(this.fileFor(key)).catch(() => {
|
|
502
651
|
});
|
|
503
652
|
return "empty";
|
|
504
653
|
}
|
|
654
|
+
await stat(this.fileFor(key)).then((identity) => this.writeSummary(this.fileFor(key), claim, identity), () => {
|
|
655
|
+
});
|
|
505
656
|
await this.writeLease(key, lease);
|
|
506
657
|
return "claimed";
|
|
507
658
|
}
|
|
@@ -545,20 +696,21 @@ var BufferDrainClaims = class {
|
|
|
545
696
|
);
|
|
546
697
|
}
|
|
547
698
|
/**
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
699
|
+
* Hand a live owner's in-memory entry to the claim (the path a buffer owner
|
|
700
|
+
* takes instead of persisting a claimed session): ONE append to the inbox,
|
|
701
|
+
* O(incoming), never a read or rewrite of the claim body. Readers fold the
|
|
702
|
+
* inbox over the body with the identity-aware merge and the drainer's next
|
|
703
|
+
* progress write compacts it. The lock serializes the append against that
|
|
704
|
+
* compaction, so a record can never land between its read and its delete.
|
|
551
705
|
*/
|
|
552
706
|
async mergeIntoClaim(key, incoming) {
|
|
553
707
|
await withHeldFileLock(this.lockFileFor(key), { staleMs: CLAIM_LOCK_STALE_MS, maxWaitMs: CLAIM_LOCK_MAX_WAIT_MS }, async (acquired) => {
|
|
554
708
|
if (!acquired) {
|
|
555
709
|
throw new Error(`buffer drain claim for ${key} is busy; retrying the save later`);
|
|
556
710
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
await this.writeClaimBody(key, mergeEntries(claim, incoming));
|
|
711
|
+
if (!this.isClaimed(key)) return;
|
|
712
|
+
await this.io.append(this.inboxFor(key), `
|
|
713
|
+
${JSON.stringify({ id: randomUUID(), key, ...incoming })}`);
|
|
562
714
|
});
|
|
563
715
|
}
|
|
564
716
|
/** Drainer progress: replace the claim content with what is left to do.
|
|
@@ -582,8 +734,9 @@ var BufferDrainClaims = class {
|
|
|
582
734
|
if (!leaseBelongsToProcess(lease, ownerId)) {
|
|
583
735
|
throw new Error(`buffer drain claim for ${key} is no longer owned by ${ownerId}; progress write skipped`);
|
|
584
736
|
}
|
|
585
|
-
const
|
|
586
|
-
if (!
|
|
737
|
+
const folded = await this.readFolded(key);
|
|
738
|
+
if (!folded) return;
|
|
739
|
+
const { claim, consumed } = folded;
|
|
587
740
|
const seenTurns = seen?.turns ?? claim.entry.turns;
|
|
588
741
|
const seenRetained = seen?.retainedTurns ?? claim.entry.retainedTurns ?? [];
|
|
589
742
|
const entryRetained = entry.retainedTurns ?? [];
|
|
@@ -660,7 +813,7 @@ var BufferDrainClaims = class {
|
|
|
660
813
|
drained,
|
|
661
814
|
drainedRetained,
|
|
662
815
|
movedTurns: moved.length > 0 ? moved : void 0
|
|
663
|
-
});
|
|
816
|
+
}, consumed);
|
|
664
817
|
});
|
|
665
818
|
}
|
|
666
819
|
/** Extend the lease. "lost" means another drainer took the claim over.
|
|
@@ -702,13 +855,14 @@ var BufferDrainClaims = class {
|
|
|
702
855
|
if (!acquired) return "busy";
|
|
703
856
|
const lease = await this.readLease(key);
|
|
704
857
|
if (!leaseBelongsToProcess(lease, ownerId)) return "busy";
|
|
705
|
-
const
|
|
706
|
-
if (!
|
|
858
|
+
const folded = await this.readFolded(key);
|
|
859
|
+
if (!folded) {
|
|
707
860
|
await unlink2(this.leaseFileFor(key)).catch(() => {
|
|
708
861
|
});
|
|
709
862
|
return "acked";
|
|
710
863
|
}
|
|
711
|
-
if (
|
|
864
|
+
if (folded.consumed.length > 0) await this.writeClaimBody(key, folded.claim, folded.consumed);
|
|
865
|
+
if (pendingTurns(folded.claim.entry) > 0) return "not_empty";
|
|
712
866
|
await unlink2(this.leaseFileFor(key)).catch(() => {
|
|
713
867
|
});
|
|
714
868
|
return "acked";
|
|
@@ -744,39 +898,60 @@ var BufferDrainClaims = class {
|
|
|
744
898
|
* merge into it, never release it.
|
|
745
899
|
*/
|
|
746
900
|
async isDrained(key) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
return
|
|
901
|
+
if (await this.readLease(key) !== null) return false;
|
|
902
|
+
const summary = await this.readSummary(this.fileFor(key));
|
|
903
|
+
if (!summary || summary.drained === 0 || summary.pending > 0) return false;
|
|
904
|
+
const claim = await this.load(key);
|
|
905
|
+
return claim !== null && (claim.drained?.length ?? 0) + (claim.drainedRetained?.length ?? 0) > 0 && pendingTurns(claim.entry) === 0;
|
|
752
906
|
}
|
|
753
|
-
/** Drop the claim
|
|
907
|
+
/** Drop the claim entirely (its owner consumed the tombstone), inbox and summary included. */
|
|
754
908
|
async release(key) {
|
|
755
|
-
|
|
756
|
-
})
|
|
757
|
-
|
|
758
|
-
|
|
909
|
+
const bodyPath = this.fileFor(key);
|
|
910
|
+
for (const file of [this.inboxFor(key), bodyPath, `${bodyPath}${COUNT_SUFFIX}`, this.leaseFileFor(key)]) {
|
|
911
|
+
await unlink2(file).catch(() => {
|
|
912
|
+
});
|
|
913
|
+
}
|
|
759
914
|
}
|
|
760
|
-
/**
|
|
761
|
-
async
|
|
762
|
-
let names;
|
|
915
|
+
/** Claim body file names in the drain directory (no leases, inboxes, summaries or locks). */
|
|
916
|
+
async claimNames() {
|
|
763
917
|
try {
|
|
764
|
-
|
|
918
|
+
return (await readdir2(this.dir)).filter((name) => name.endsWith(".json") && !name.endsWith(".lease.json")).sort();
|
|
765
919
|
} catch (err) {
|
|
766
|
-
if (err?.code === "ENOENT") return
|
|
920
|
+
if (err?.code === "ENOENT") return null;
|
|
767
921
|
throw err;
|
|
768
922
|
}
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* One claim's listing from its summary plus its inbox: the body is never
|
|
926
|
+
* parsed for a count. While an inbox is unfolded the turn count is an UPPER
|
|
927
|
+
* bound (records can repeat turns the body holds or already drained); the
|
|
928
|
+
* drain that picks the claim up folds exactly and its ack compacts the
|
|
929
|
+
* inbox, so an over-count costs one cheap drain run, never a lost turn.
|
|
930
|
+
*/
|
|
931
|
+
async describeClaim(name) {
|
|
932
|
+
const summary = await this.readSummary(path2.join(this.dir, name));
|
|
933
|
+
if (!summary) return null;
|
|
934
|
+
let inboxTurns = 0;
|
|
935
|
+
try {
|
|
936
|
+
for (const record of await this.readInbox(summary.key)) inboxTurns += pendingTurns(record.entry);
|
|
937
|
+
} catch (err) {
|
|
938
|
+
log.debug(`buffer drain: skipping claim ${name} with an unreadable inbox: ${err instanceof Error ? err.message : String(err)}`);
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
const lease = await this.readLease(summary.key);
|
|
942
|
+
return {
|
|
943
|
+
key: summary.key,
|
|
944
|
+
turnCount: summary.pending + inboxTurns,
|
|
945
|
+
leased: lease !== null,
|
|
946
|
+
leaseExpired: lease !== null ? isLeaseExpired(lease) : true
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
/** Every claim with its lease state (CLI scan + summaries). */
|
|
950
|
+
async list() {
|
|
769
951
|
const out = [];
|
|
770
|
-
for (const name of
|
|
771
|
-
const
|
|
772
|
-
if (
|
|
773
|
-
const lease = await this.readLease(claim.key);
|
|
774
|
-
out.push({
|
|
775
|
-
key: claim.key,
|
|
776
|
-
turnCount: claim.entry.turns.length + (claim.entry.retainedTurns?.length ?? 0),
|
|
777
|
-
leased: lease !== null,
|
|
778
|
-
leaseExpired: lease !== null ? isLeaseExpired(lease) : true
|
|
779
|
-
});
|
|
952
|
+
for (const name of await this.claimNames() ?? []) {
|
|
953
|
+
const listing = await this.describeClaim(name);
|
|
954
|
+
if (listing) out.push(listing);
|
|
780
955
|
}
|
|
781
956
|
return out;
|
|
782
957
|
}
|
|
@@ -786,20 +961,14 @@ var BufferDrainClaims = class {
|
|
|
786
961
|
* `nextCursor` by a previous page — file-name order is stable, so a
|
|
787
962
|
* rotating cursor visits every claim fairly without ever materializing
|
|
788
963
|
* the whole directory's bodies). Empty tombstones never count toward the
|
|
789
|
-
* limit; the number of
|
|
964
|
+
* limit; the number of claims examined per call is bounded separately
|
|
790
965
|
* (`LIST_PAGE_SCAN_FACTOR` per requested entry), and the cursor advances
|
|
791
966
|
* past every scanned name so a long tombstone run is crossed over several
|
|
792
967
|
* pages instead of starving the pending claims behind it.
|
|
793
968
|
*/
|
|
794
969
|
async listPage(afterName, limit) {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
names = (await readdir2(this.dir)).filter((name) => name.endsWith(".json") && !name.endsWith(".lease.json")).sort();
|
|
798
|
-
} catch (err) {
|
|
799
|
-
const errno = err?.code;
|
|
800
|
-
if (errno === "ENOENT") return { entries: [] };
|
|
801
|
-
throw err;
|
|
802
|
-
}
|
|
970
|
+
const names = await this.claimNames();
|
|
971
|
+
if (names === null) return { entries: [] };
|
|
803
972
|
const start = afterName === void 0 ? 0 : names.findIndex((name) => name > afterName);
|
|
804
973
|
const out = [];
|
|
805
974
|
let cursor;
|
|
@@ -810,21 +979,50 @@ var BufferDrainClaims = class {
|
|
|
810
979
|
const name = names[index];
|
|
811
980
|
scanned += 1;
|
|
812
981
|
cursor = name;
|
|
813
|
-
const
|
|
814
|
-
if (
|
|
815
|
-
const turnCount = claim.entry.turns.length + (claim.entry.retainedTurns?.length ?? 0);
|
|
816
|
-
if (turnCount <= 0) continue;
|
|
817
|
-
const lease = await this.readLease(claim.key);
|
|
818
|
-
out.push({
|
|
819
|
-
key: claim.key,
|
|
820
|
-
turnCount,
|
|
821
|
-
leased: lease !== null,
|
|
822
|
-
leaseExpired: lease !== null ? isLeaseExpired(lease) : true
|
|
823
|
-
});
|
|
982
|
+
const listing = await this.describeClaim(name);
|
|
983
|
+
if (listing && listing.turnCount > 0) out.push(listing);
|
|
824
984
|
}
|
|
825
985
|
return { entries: out, nextCursor: cursor };
|
|
826
986
|
}
|
|
827
|
-
|
|
987
|
+
/**
|
|
988
|
+
* Rollback helper: fold every claim's inbox into its body and delete the
|
|
989
|
+
* inboxes, so a build that predates the inbox (it never reads one) sees every
|
|
990
|
+
* handed-in turn. Run it with the daemon stopped, BEFORE downgrading; the
|
|
991
|
+
* same fold is what compaction does, so it is safe to run on a live store.
|
|
992
|
+
* Returns how many inboxes were folded.
|
|
993
|
+
*/
|
|
994
|
+
async foldAllInboxes() {
|
|
995
|
+
let names;
|
|
996
|
+
try {
|
|
997
|
+
names = (await readdir2(this.dir)).filter((name) => name.endsWith(INBOX_SUFFIX));
|
|
998
|
+
} catch (err) {
|
|
999
|
+
if (isErrnoCode(err, "ENOENT")) return 0;
|
|
1000
|
+
throw err;
|
|
1001
|
+
}
|
|
1002
|
+
let folded = 0;
|
|
1003
|
+
for (const name of names) {
|
|
1004
|
+
const claim = await this.readClaimBodyByName(name.slice(0, -INBOX_SUFFIX.length));
|
|
1005
|
+
if (!claim) continue;
|
|
1006
|
+
folded += await withHeldFileLock(
|
|
1007
|
+
this.lockFileFor(claim.key),
|
|
1008
|
+
{ staleMs: CLAIM_LOCK_STALE_MS, maxWaitMs: CLAIM_LOCK_MAX_WAIT_MS },
|
|
1009
|
+
async (acquired) => {
|
|
1010
|
+
if (!acquired) throw new Error(`buffer drain claim for ${claim.key} is busy; its inbox was not folded`);
|
|
1011
|
+
const current = await this.readFolded(claim.key);
|
|
1012
|
+
if (!current) return 0;
|
|
1013
|
+
if (current.consumed.length === 0) {
|
|
1014
|
+
await unlink2(this.inboxFor(claim.key)).catch(() => {
|
|
1015
|
+
});
|
|
1016
|
+
return 0;
|
|
1017
|
+
}
|
|
1018
|
+
await this.writeClaimBody(claim.key, current.claim, current.consumed);
|
|
1019
|
+
return 1;
|
|
1020
|
+
}
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
return folded;
|
|
1024
|
+
}
|
|
1025
|
+
async readClaimBodyByName(name) {
|
|
828
1026
|
try {
|
|
829
1027
|
return parseBody(await this.io.read(path2.join(this.dir, name)));
|
|
830
1028
|
} catch (err) {
|
|
@@ -840,4 +1038,4 @@ export {
|
|
|
840
1038
|
legacyStateToEntries,
|
|
841
1039
|
BufferSessionFiles
|
|
842
1040
|
};
|
|
843
|
-
//# sourceMappingURL=chunk-
|
|
1041
|
+
//# sourceMappingURL=chunk-RFUL2ZIQ.js.map
|