@lmzhen/dsh-evolution-state-json 0.3.81 → 0.3.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/lib/index.js +73 -20
  3. package/package.json +7 -7
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @deepseek-ai/dsh-evolution-state-json
1
+ # @lmzhen/dsh-evolution-state-json
2
2
 
3
3
  JSON-file evolution state provider over the IO seam
4
4
 
@@ -9,7 +9,7 @@ JSON-file evolution state provider over the IO seam
9
9
 
10
10
  #### What the model sees
11
11
 
12
- `@deepseek-ai/dsh-evolution-state-json` registers no direct prompt or tool schema itself. Model-visible effects are owned by the packages that consume this service.
12
+ `@lmzhen/dsh-evolution-state-json` registers no direct prompt or tool schema itself. Model-visible effects are owned by the packages that consume this service.
13
13
 
14
14
  #### Token effect
15
15
 
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { evolutionHome, makeSerialQueue, transactIo } from "@lmzhen/dsh-evolution-core";
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";
2
+ import { evolutionHome, makeSerialQueue, transactIo, transactTaskGuard } from "@lmzhen/dsh-evolution-core";
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, cloneRecord, 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
6
6
  /**
@@ -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
- await jsonTransact(ctx, io, root, REVIEW_STATE_FILE, (current) => {
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,22 +530,27 @@ function apply(ctx, rawConfig = {}) {
481
530
  },
482
531
  async saveCuratorState(record) {
483
532
  await mutate(async () => {
484
- await jsonTransact(ctx, io, root, CURATOR_STATE_FILE, (current) => ({
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) {
491
542
  await mutate(async () => {
492
- await jsonTransact(ctx, io, root, CURATOR_STATE_FILE, (current) => {
493
- const next = task(current?.[CURATOR_STATE_KEY] ?? null);
543
+ const guard = transactTaskGuard(`curator state transact (${CURATOR_STATE_FILE})`);
544
+ await jsonTransact(ctx, io, root, CURATOR_STATE_FILE, guard.wrap((current) => {
545
+ const stored = current?.[CURATOR_STATE_KEY] ?? null;
546
+ const next = task(stored === null ? null : cloneRecord(stored));
494
547
  if (next === null) return current;
495
548
  return {
496
549
  ...current ?? {},
497
550
  [CURATOR_STATE_KEY]: next
498
551
  };
499
- });
552
+ }));
553
+ guard.assertInvoked();
500
554
  });
501
555
  },
502
556
  async listPending(status = "pending") {
@@ -507,25 +561,22 @@ function apply(ctx, rawConfig = {}) {
507
561
  },
508
562
  async savePending(record) {
509
563
  await mutate(async () => {
510
- await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (current) => {
564
+ const guard = transactTaskGuard(`pending record "${record.id}" (${PENDING_STATE_FILE})`);
565
+ await pendingTransact(guard.wrap(async (current) => {
511
566
  const map = {
512
567
  ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}),
513
568
  [record.id]: record
514
569
  };
515
- const liveCount = Object.values(map).filter((entry) => entry.status === "pending" || entry.status === "executing").length;
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;
570
+ warnPendingGrowth(map, "save");
521
571
  return map;
522
- });
572
+ }));
573
+ guard.assertInvoked();
523
574
  });
524
575
  },
525
576
  async claimPending(id, claimId) {
526
577
  return await mutate(async () => {
527
578
  const slot = { claimed: null };
528
- await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (current) => {
579
+ await pendingTransact(async (current) => {
529
580
  const map = { ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}) };
530
581
  const record = map[id] ?? null;
531
582
  if (record === null || !canClaimPending(record.status)) return map;
@@ -539,12 +590,13 @@ function apply(ctx, rawConfig = {}) {
539
590
  map[id] = slot.claimed;
540
591
  return map;
541
592
  });
593
+ warnPendingGrowth(await loadPendingMap(), "claim");
542
594
  return slot.claimed ? { ...slot.claimed } : null;
543
595
  });
544
596
  },
545
597
  async releasePendingClaim(id, claimId) {
546
598
  await mutate(async () => {
547
- await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (current) => {
599
+ await pendingTransact(async (current) => {
548
600
  const map = { ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}) };
549
601
  const record = map[id];
550
602
  if (!record || record.claimedBy !== claimId) return map;
@@ -562,7 +614,7 @@ function apply(ctx, rawConfig = {}) {
562
614
  record: null,
563
615
  applied: false
564
616
  };
565
- await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (current) => {
617
+ await pendingTransact(async (current) => {
566
618
  const map = { ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}) };
567
619
  const record = map[id] ?? null;
568
620
  if (record === null || !canResolvePending(record.status)) {
@@ -602,6 +654,7 @@ function apply(ctx, rawConfig = {}) {
602
654
  }
603
655
  return pruned.map;
604
656
  });
657
+ warnPendingGrowth(await loadPendingMap(), "resolve");
605
658
  return result;
606
659
  });
607
660
  }
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.81",
4
+ "version": "0.3.83",
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.81"
30
+ "@lmzhen/dsh-evolution-core": "^0.3.83"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@deepseek-ai/cordis": "^4.0.1",
34
- "@lmzhen/dsh-evolution-io": "^0.3.81",
35
- "@lmzhen/dsh-evolution-state-storage": "^0.3.81"
34
+ "@lmzhen/dsh-evolution-io": "^0.3.83",
35
+ "@lmzhen/dsh-evolution-state-storage": "^0.3.83"
36
36
  },
37
37
  "devDependencies": {
38
- "@lmzhen/dsh-evolution-io": "^0.3.81",
39
- "@lmzhen/dsh-evolution-state-storage": "^0.3.81",
40
- "@lmzhen/dsh-evolution-io-node": "^0.3.81"
38
+ "@lmzhen/dsh-evolution-io": "^0.3.83",
39
+ "@lmzhen/dsh-evolution-state-storage": "^0.3.83",
40
+ "@lmzhen/dsh-evolution-io-node": "^0.3.83"
41
41
  }
42
42
  }