@lmzhen/dsh-evolution-state-json 0.3.81 → 0.3.82
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/lib/index.js +66 -16
- package/package.json +7 -7
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { evolutionHome, makeSerialQueue, transactIo } from "@lmzhen/dsh-evolution-core";
|
|
2
|
+
import { evolutionHome, makeSerialQueue, transactIo, transactTaskGuard } from "@lmzhen/dsh-evolution-core";
|
|
3
3
|
import { CURATOR_STATE_FILE, CURATOR_STATE_KEY, CURATOR_STATE_TABLE, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, PENDING_TABLE, PROVIDER_JSON, REVIEW_STATE_FILE, REVIEW_STATE_SESSION_CAP, REVIEW_STATE_TABLE, assertCloneable, canClaimPending, canResolvePending, recordIssue, releasedStatus, selectPendingOverflow, selectSessionOverflow } from "@lmzhen/dsh-evolution-state-storage";
|
|
4
4
|
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
5
5
|
//#region lib/types/index.js
|
|
@@ -24,6 +24,12 @@ const PENDING_RESOLVED_CAP$1 = PENDING_RESOLVED_CAP;
|
|
|
24
24
|
* `.bak` sidecar, so the file — and the full-array rewrite on every append —
|
|
25
25
|
* never grows without bound. */
|
|
26
26
|
const ARCHIVE_RESOLVED_CAP = 5e3;
|
|
27
|
+
/** S2-12 (FLOW5-3): the staged table's byte budget for the args it carries.
|
|
28
|
+
* Lives here rather than on the seam because it bounds THIS medium: json
|
|
29
|
+
* rewrites the whole state file per pending mutation, so bytes — not just row
|
|
30
|
+
* count — decide the cost. The domain provider writes rows individually and
|
|
31
|
+
* needs no equivalent (declared asymmetry, not an oversight). */
|
|
32
|
+
const PENDING_ARGS_BYTES_WARN = 1e6;
|
|
27
33
|
/** 0.3.27 (V4-01): an archive entry's dedupe identity. The same audit record
|
|
28
34
|
* (id + status + resolvedAt) must never appear twice; the read-only legacy
|
|
29
35
|
* `pending.json` merge used to re-introduce an evicted record on the next
|
|
@@ -223,7 +229,22 @@ function apply(ctx, rawConfig = {}) {
|
|
|
223
229
|
gateDroppedCurrent.clear();
|
|
224
230
|
for (const id of ids) gateDroppedCurrent.add(id);
|
|
225
231
|
};
|
|
232
|
+
/**
|
|
233
|
+
* v43 audit (S1-9, F-2's deeper half): every pending-state mutation goes
|
|
234
|
+
* through jsonTransact, and only the RETIREMENT path used to hand the
|
|
235
|
+
* gate-drop reporter over. The other paths read `current` with their own
|
|
236
|
+
* field-gate pass, so a record dropped there stayed invisible to
|
|
237
|
+
* `gateDroppedCurrent` — and `mergedWithFilteredLegacy` then resurrected the
|
|
238
|
+
* legacy pending twin of a record whose fields went bad (an approve replays an
|
|
239
|
+
* already-landed write). Binding the reporter to the FILE here means no call
|
|
240
|
+
* site can forget it; jsonTransact reports the drops before the task runs, so
|
|
241
|
+
* the merged basis inside the task sees them.
|
|
242
|
+
* @param task - the read-modify-write body, exactly as jsonTransact takes it.
|
|
243
|
+
* @returns jsonTransact's own result.
|
|
244
|
+
*/
|
|
245
|
+
const pendingTransact = (task) => jsonTransact(ctx, io, root, PENDING_STATE_FILE, task, { onGateDrop: noteGateDrop });
|
|
226
246
|
let warnedPendingCapacity = false;
|
|
247
|
+
let warnedPendingArgsBytes = false;
|
|
227
248
|
async function readJson(file) {
|
|
228
249
|
const raw = await io().readText(pathOf(file));
|
|
229
250
|
if (raw === null) return null;
|
|
@@ -444,6 +465,32 @@ function apply(ctx, rawConfig = {}) {
|
|
|
444
465
|
return false;
|
|
445
466
|
}
|
|
446
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* S2-12 (FLOW5-3/5-4): the staged table's two growth signals, evaluated on
|
|
470
|
+
* EVERY write path that can move it. Before this, only `savePending` warned
|
|
471
|
+
* about the record count, so a deployment whose approvals arrive through
|
|
472
|
+
* claim/resolve grew silently; and nothing measured the staged ARGS at all.
|
|
473
|
+
* Both warns flip once and re-arm when the table comes back under the bound.
|
|
474
|
+
*
|
|
475
|
+
* @param map - the post-write pending map.
|
|
476
|
+
* @param where - the path that wrote it, named in the warning.
|
|
477
|
+
*/
|
|
478
|
+
const warnPendingGrowth = (map, where) => {
|
|
479
|
+
const live = Object.values(map).filter((entry) => entry.status === "pending" || entry.status === "executing");
|
|
480
|
+
if (live.length > PENDING_RESOLVED_CAP$1) {
|
|
481
|
+
if (!warnedPendingCapacity) {
|
|
482
|
+
warnedPendingCapacity = true;
|
|
483
|
+
ctx.logger.warn(`evolution-state-json: ${live.length} pending/executing staged records exceed the resolved cap (${PENDING_RESOLVED_CAP$1}) after ${where} — they are never trimmed by design; resolve or reject them (/evolution pending) or the file keeps growing`);
|
|
484
|
+
}
|
|
485
|
+
} else warnedPendingCapacity = false;
|
|
486
|
+
const argsBytes = live.reduce((total, entry) => total + Buffer.byteLength(JSON.stringify(entry.args ?? null), "utf8"), 0);
|
|
487
|
+
if (argsBytes > PENDING_ARGS_BYTES_WARN) {
|
|
488
|
+
if (!warnedPendingArgsBytes) {
|
|
489
|
+
warnedPendingArgsBytes = true;
|
|
490
|
+
ctx.logger.warn(`evolution-state-json: ${argsBytes} bytes of staged args across ${live.length} pending/executing record(s) exceed ${PENDING_ARGS_BYTES_WARN} after ${where} — every pending mutation rewrites this file in full; resolve or drop the oversized records (/evolution pending)`);
|
|
491
|
+
}
|
|
492
|
+
} else warnedPendingArgsBytes = false;
|
|
493
|
+
};
|
|
447
494
|
const provider = {
|
|
448
495
|
name: PROVIDER_JSON,
|
|
449
496
|
async loadReviewState(sessionId) {
|
|
@@ -456,7 +503,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
456
503
|
},
|
|
457
504
|
async saveReviewState(sessionId, record) {
|
|
458
505
|
await mutate(async () => {
|
|
459
|
-
|
|
506
|
+
const guard = transactTaskGuard(`review state for session "${sessionId}" (${REVIEW_STATE_FILE})`);
|
|
507
|
+
await jsonTransact(ctx, io, root, REVIEW_STATE_FILE, guard.wrap((current) => {
|
|
460
508
|
const stamped = { ...current ?? {} };
|
|
461
509
|
stamped[sessionId] = {
|
|
462
510
|
...record,
|
|
@@ -471,7 +519,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
471
519
|
const pruned = {};
|
|
472
520
|
for (const [id, row] of Object.entries(stamped)) if (!evict.has(id)) pruned[id] = row;
|
|
473
521
|
return pruned;
|
|
474
|
-
});
|
|
522
|
+
}));
|
|
523
|
+
guard.assertInvoked();
|
|
475
524
|
});
|
|
476
525
|
},
|
|
477
526
|
async loadCuratorState() {
|
|
@@ -481,10 +530,12 @@ function apply(ctx, rawConfig = {}) {
|
|
|
481
530
|
},
|
|
482
531
|
async saveCuratorState(record) {
|
|
483
532
|
await mutate(async () => {
|
|
484
|
-
|
|
533
|
+
const guard = transactTaskGuard(`curator state (${CURATOR_STATE_FILE})`);
|
|
534
|
+
await jsonTransact(ctx, io, root, CURATOR_STATE_FILE, guard.wrap((current) => ({
|
|
485
535
|
...current ?? {},
|
|
486
536
|
[CURATOR_STATE_KEY]: record
|
|
487
|
-
}));
|
|
537
|
+
})));
|
|
538
|
+
guard.assertInvoked();
|
|
488
539
|
});
|
|
489
540
|
},
|
|
490
541
|
async transactCuratorState(task) {
|
|
@@ -507,25 +558,22 @@ function apply(ctx, rawConfig = {}) {
|
|
|
507
558
|
},
|
|
508
559
|
async savePending(record) {
|
|
509
560
|
await mutate(async () => {
|
|
510
|
-
|
|
561
|
+
const guard = transactTaskGuard(`pending record "${record.id}" (${PENDING_STATE_FILE})`);
|
|
562
|
+
await pendingTransact(guard.wrap(async (current) => {
|
|
511
563
|
const map = {
|
|
512
564
|
...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}),
|
|
513
565
|
[record.id]: record
|
|
514
566
|
};
|
|
515
|
-
|
|
516
|
-
if (liveCount > PENDING_RESOLVED_CAP$1 && !warnedPendingCapacity) {
|
|
517
|
-
warnedPendingCapacity = true;
|
|
518
|
-
ctx.logger.warn(`evolution-state-json: ${liveCount} pending/executing staged records exceed the resolved cap (${PENDING_RESOLVED_CAP$1}) — they are never trimmed by design; resolve or reject them (/evolution pending) or the file keeps growing`);
|
|
519
|
-
}
|
|
520
|
-
if (liveCount <= PENDING_RESOLVED_CAP$1) warnedPendingCapacity = false;
|
|
567
|
+
warnPendingGrowth(map, "save");
|
|
521
568
|
return map;
|
|
522
|
-
});
|
|
569
|
+
}));
|
|
570
|
+
guard.assertInvoked();
|
|
523
571
|
});
|
|
524
572
|
},
|
|
525
573
|
async claimPending(id, claimId) {
|
|
526
574
|
return await mutate(async () => {
|
|
527
575
|
const slot = { claimed: null };
|
|
528
|
-
await
|
|
576
|
+
await pendingTransact(async (current) => {
|
|
529
577
|
const map = { ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}) };
|
|
530
578
|
const record = map[id] ?? null;
|
|
531
579
|
if (record === null || !canClaimPending(record.status)) return map;
|
|
@@ -539,12 +587,13 @@ function apply(ctx, rawConfig = {}) {
|
|
|
539
587
|
map[id] = slot.claimed;
|
|
540
588
|
return map;
|
|
541
589
|
});
|
|
590
|
+
warnPendingGrowth(await loadPendingMap(), "claim");
|
|
542
591
|
return slot.claimed ? { ...slot.claimed } : null;
|
|
543
592
|
});
|
|
544
593
|
},
|
|
545
594
|
async releasePendingClaim(id, claimId) {
|
|
546
595
|
await mutate(async () => {
|
|
547
|
-
await
|
|
596
|
+
await pendingTransact(async (current) => {
|
|
548
597
|
const map = { ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}) };
|
|
549
598
|
const record = map[id];
|
|
550
599
|
if (!record || record.claimedBy !== claimId) return map;
|
|
@@ -562,7 +611,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
562
611
|
record: null,
|
|
563
612
|
applied: false
|
|
564
613
|
};
|
|
565
|
-
await
|
|
614
|
+
await pendingTransact(async (current) => {
|
|
566
615
|
const map = { ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}) };
|
|
567
616
|
const record = map[id] ?? null;
|
|
568
617
|
if (record === null || !canResolvePending(record.status)) {
|
|
@@ -602,6 +651,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
602
651
|
}
|
|
603
652
|
return pruned.map;
|
|
604
653
|
});
|
|
654
|
+
warnPendingGrowth(await loadPendingMap(), "resolve");
|
|
605
655
|
return result;
|
|
606
656
|
});
|
|
607
657
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-state-json",
|
|
3
3
|
"description": "JSON-file evolution state provider over the IO seam (community build)",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.82",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -27,16 +27,16 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
30
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
30
|
+
"@lmzhen/dsh-evolution-core": "^0.3.82"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
34
|
-
"@lmzhen/dsh-evolution-io": "^0.3.
|
|
35
|
-
"@lmzhen/dsh-evolution-state-storage": "^0.3.
|
|
34
|
+
"@lmzhen/dsh-evolution-io": "^0.3.82",
|
|
35
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.82"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@lmzhen/dsh-evolution-io": "^0.3.
|
|
39
|
-
"@lmzhen/dsh-evolution-state-storage": "^0.3.
|
|
40
|
-
"@lmzhen/dsh-evolution-io-node": "^0.3.
|
|
38
|
+
"@lmzhen/dsh-evolution-io": "^0.3.82",
|
|
39
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.82",
|
|
40
|
+
"@lmzhen/dsh-evolution-io-node": "^0.3.82"
|
|
41
41
|
}
|
|
42
42
|
}
|