@remnic/core 9.3.700 → 9.3.701

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 (119) hide show
  1. package/dist/access-boundary.d.ts +3 -3
  2. package/dist/access-boundary.js +7 -7
  3. package/dist/access-cli.js +21 -21
  4. package/dist/access-http.d.ts +3 -3
  5. package/dist/access-http.js +10 -10
  6. package/dist/access-mcp.d.ts +3 -3
  7. package/dist/access-mcp.js +9 -9
  8. package/dist/access-operations.d.ts +3 -3
  9. package/dist/access-operations.js +8 -8
  10. package/dist/{access-service-Cte3ol0W.d.ts → access-service-CGVWK6lZ.d.ts} +1 -1
  11. package/dist/access-service.d.ts +3 -3
  12. package/dist/access-service.js +6 -6
  13. package/dist/access-surface-catalog.d.ts +3 -3
  14. package/dist/bootstrap.d.ts +2 -2
  15. package/dist/briefing.js +4 -4
  16. package/dist/{catalog-DN1PzThs.d.ts → catalog-DBIghceA.d.ts} +26 -52
  17. package/dist/causal-consolidation.js +5 -5
  18. package/dist/{chunk-GYVVQYA3.js → chunk-27LQPUMZ.js} +3 -3
  19. package/dist/{chunk-XJNBEDFE.js → chunk-3FAMU5TX.js} +31 -74
  20. package/dist/chunk-3FAMU5TX.js.map +1 -0
  21. package/dist/{chunk-PCZR32VL.js → chunk-3JJWNZTT.js} +2 -2
  22. package/dist/{chunk-GA5A6MJH.js → chunk-4HIAWLA2.js} +14 -14
  23. package/dist/{chunk-SEWF2O74.js → chunk-6W2D6FGG.js} +2 -2
  24. package/dist/{chunk-CHM274U6.js → chunk-7TAQEPLE.js} +2 -2
  25. package/dist/{chunk-G5PKTQ5J.js → chunk-DEDQXIDL.js} +2 -2
  26. package/dist/{chunk-3E5WRQNQ.js → chunk-FUCJAZ25.js} +4 -4
  27. package/dist/{chunk-6JDGADXK.js → chunk-HF4N43Q7.js} +2 -2
  28. package/dist/{chunk-NHBEO3F3.js → chunk-HXHKLVAS.js} +14 -14
  29. package/dist/{chunk-RC3CNIPK.js → chunk-IO5NQEGZ.js} +2 -2
  30. package/dist/{chunk-SMIVW7XC.js → chunk-ISLJ5WIM.js} +2 -2
  31. package/dist/{chunk-HDLC75NX.js → chunk-IYOPIG3E.js} +2 -2
  32. package/dist/{chunk-DR2JTSLZ.js → chunk-JKOKX3PS.js} +62 -163
  33. package/dist/chunk-JKOKX3PS.js.map +1 -0
  34. package/dist/{chunk-2NWHLAXX.js → chunk-JO3E5VGS.js} +2 -2
  35. package/dist/{chunk-CCOXIDRM.js → chunk-KF4TXW7Z.js} +4 -4
  36. package/dist/{chunk-JX3YZVII.js → chunk-KS7WQ4BZ.js} +3 -3
  37. package/dist/{chunk-YPR7DOPD.js → chunk-LTJAMRGI.js} +4 -4
  38. package/dist/{chunk-YPR7DOPD.js.map → chunk-LTJAMRGI.js.map} +1 -1
  39. package/dist/{chunk-ZDK2IW5F.js → chunk-MNU5G4TK.js} +2 -2
  40. package/dist/{chunk-PQG4T5V3.js → chunk-ODTWHSY2.js} +49 -45
  41. package/dist/chunk-ODTWHSY2.js.map +1 -0
  42. package/dist/{chunk-YMTGXDN6.js → chunk-OLOYQZFB.js} +4 -4
  43. package/dist/{chunk-JKW5XSWC.js → chunk-QP37KL5H.js} +2 -2
  44. package/dist/{chunk-X74FJSW7.js → chunk-SFMRLXIV.js} +2 -2
  45. package/dist/{chunk-RJ2THZ4H.js → chunk-TFVVONWD.js} +2 -2
  46. package/dist/{chunk-XY4WJTEX.js → chunk-WFEZUGU5.js} +2 -2
  47. package/dist/{chunk-O54DY26V.js → chunk-XTIRCSIH.js} +2 -2
  48. package/dist/{chunk-EOBJRBLC.js → chunk-YO4MBK3I.js} +2 -2
  49. package/dist/{chunk-YXIFA36P.js → chunk-ZT7B64BE.js} +2 -2
  50. package/dist/{chunk-33L6XHU2.js → chunk-ZYNMX6IU.js} +5 -5
  51. package/dist/{cli--yVN9yEV.d.ts → cli-D3XeenwN.d.ts} +2 -2
  52. package/dist/cli.d.ts +4 -4
  53. package/dist/cli.js +21 -21
  54. package/dist/compounding/engine.js +4 -4
  55. package/dist/connectors/codex-materialize-runner.js +4 -4
  56. package/dist/connectors/index.js +4 -4
  57. package/dist/entity-retrieval.js +4 -4
  58. package/dist/explicit-capture.d.ts +2 -2
  59. package/dist/{forget-BEXG5PQC.js → forget-6SOIPUMQ.js} +3 -3
  60. package/dist/index.d.ts +5 -5
  61. package/dist/index.js +29 -29
  62. package/dist/maintenance/memory-governance.js +4 -4
  63. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
  64. package/dist/maintenance/rebuild-memory-projection.js +5 -5
  65. package/dist/mcp-memory-inspector-app.d.ts +3 -3
  66. package/dist/namespaces/migrate.d.ts +1 -1
  67. package/dist/namespaces/migrate.js +5 -5
  68. package/dist/namespaces/storage.d.ts +13 -2
  69. package/dist/namespaces/storage.js +4 -4
  70. package/dist/operator-toolkit.js +9 -9
  71. package/dist/orchestration/maintenance.d.ts +1 -1
  72. package/dist/orchestration/maintenance.js +6 -6
  73. package/dist/{orchestrator-CJI4xdqV.d.ts → orchestrator-BzMCZlKn.d.ts} +1 -1
  74. package/dist/orchestrator.d.ts +2 -2
  75. package/dist/orchestrator.js +17 -17
  76. package/dist/semantic-consolidation.js +5 -5
  77. package/dist/semantic-rule-promotion.js +4 -4
  78. package/dist/semantic-rule-verifier.js +4 -4
  79. package/dist/storage.js +3 -3
  80. package/dist/summarizer.js +3 -2
  81. package/dist/summary-snapshot.js +2 -1
  82. package/dist/utils/serialize-mutations.js +1 -1
  83. package/dist/verified-recall.js +4 -4
  84. package/package.json +2 -2
  85. package/src/namespaces/catalog.test.ts +222 -14
  86. package/src/namespaces/catalog.ts +54 -187
  87. package/src/namespaces/storage.ts +87 -80
  88. package/src/summary-snapshot.test.ts +63 -1
  89. package/src/summary-snapshot.ts +61 -80
  90. package/src/utils/serialize-mutations.ts +10 -6
  91. package/dist/chunk-DR2JTSLZ.js.map +0 -1
  92. package/dist/chunk-PQG4T5V3.js.map +0 -1
  93. package/dist/chunk-XJNBEDFE.js.map +0 -1
  94. /package/dist/{chunk-GYVVQYA3.js.map → chunk-27LQPUMZ.js.map} +0 -0
  95. /package/dist/{chunk-PCZR32VL.js.map → chunk-3JJWNZTT.js.map} +0 -0
  96. /package/dist/{chunk-GA5A6MJH.js.map → chunk-4HIAWLA2.js.map} +0 -0
  97. /package/dist/{chunk-SEWF2O74.js.map → chunk-6W2D6FGG.js.map} +0 -0
  98. /package/dist/{chunk-CHM274U6.js.map → chunk-7TAQEPLE.js.map} +0 -0
  99. /package/dist/{chunk-G5PKTQ5J.js.map → chunk-DEDQXIDL.js.map} +0 -0
  100. /package/dist/{chunk-3E5WRQNQ.js.map → chunk-FUCJAZ25.js.map} +0 -0
  101. /package/dist/{chunk-6JDGADXK.js.map → chunk-HF4N43Q7.js.map} +0 -0
  102. /package/dist/{chunk-NHBEO3F3.js.map → chunk-HXHKLVAS.js.map} +0 -0
  103. /package/dist/{chunk-RC3CNIPK.js.map → chunk-IO5NQEGZ.js.map} +0 -0
  104. /package/dist/{chunk-SMIVW7XC.js.map → chunk-ISLJ5WIM.js.map} +0 -0
  105. /package/dist/{chunk-HDLC75NX.js.map → chunk-IYOPIG3E.js.map} +0 -0
  106. /package/dist/{chunk-2NWHLAXX.js.map → chunk-JO3E5VGS.js.map} +0 -0
  107. /package/dist/{chunk-CCOXIDRM.js.map → chunk-KF4TXW7Z.js.map} +0 -0
  108. /package/dist/{chunk-JX3YZVII.js.map → chunk-KS7WQ4BZ.js.map} +0 -0
  109. /package/dist/{chunk-ZDK2IW5F.js.map → chunk-MNU5G4TK.js.map} +0 -0
  110. /package/dist/{chunk-YMTGXDN6.js.map → chunk-OLOYQZFB.js.map} +0 -0
  111. /package/dist/{chunk-JKW5XSWC.js.map → chunk-QP37KL5H.js.map} +0 -0
  112. /package/dist/{chunk-X74FJSW7.js.map → chunk-SFMRLXIV.js.map} +0 -0
  113. /package/dist/{chunk-RJ2THZ4H.js.map → chunk-TFVVONWD.js.map} +0 -0
  114. /package/dist/{chunk-XY4WJTEX.js.map → chunk-WFEZUGU5.js.map} +0 -0
  115. /package/dist/{chunk-O54DY26V.js.map → chunk-XTIRCSIH.js.map} +0 -0
  116. /package/dist/{chunk-EOBJRBLC.js.map → chunk-YO4MBK3I.js.map} +0 -0
  117. /package/dist/{chunk-YXIFA36P.js.map → chunk-ZT7B64BE.js.map} +0 -0
  118. /package/dist/{chunk-33L6XHU2.js.map → chunk-ZYNMX6IU.js.map} +0 -0
  119. /package/dist/{forget-BEXG5PQC.js.map → forget-6SOIPUMQ.js.map} +0 -0
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-3UXOZBHV.js";
8
8
  import {
9
9
  StorageManager
10
- } from "./chunk-X74FJSW7.js";
10
+ } from "./chunk-SFMRLXIV.js";
11
11
  import {
12
12
  isSafeRouteNamespace
13
13
  } from "./chunk-U3PN77QT.js";
@@ -118,4 +118,4 @@ export {
118
118
  runCodexMaterialize,
119
119
  runPostConsolidationMaterialize
120
120
  };
121
- //# sourceMappingURL=chunk-SMIVW7XC.js.map
121
+ //# sourceMappingURL=chunk-ISLJ5WIM.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  StorageManager
3
- } from "./chunk-X74FJSW7.js";
3
+ } from "./chunk-SFMRLXIV.js";
4
4
 
5
5
  // src/semantic-rule-promotion.ts
6
6
  import { createHash, randomUUID } from "crypto";
@@ -529,4 +529,4 @@ export {
529
529
  setSemanticRulePromotionTestHooks,
530
530
  promoteSemanticRuleFromMemory
531
531
  };
532
- //# sourceMappingURL=chunk-HDLC75NX.js.map
532
+ //# sourceMappingURL=chunk-IYOPIG3E.js.map
@@ -1,12 +1,16 @@
1
1
  import {
2
2
  resolveDefaultNamespaceRoot,
3
3
  resolveNamespaceStorageRoot
4
- } from "./chunk-PQG4T5V3.js";
4
+ } from "./chunk-ODTWHSY2.js";
5
5
  import {
6
6
  namespaceIdentityFromToken,
7
7
  namespaceIdentityToken,
8
8
  normalizeNamespaceIdentity
9
9
  } from "./chunk-ZFXCQPNO.js";
10
+ import {
11
+ MutationSerializer,
12
+ withHeldFileLock
13
+ } from "./chunk-LTJAMRGI.js";
10
14
  import {
11
15
  displayErrorDetail
12
16
  } from "./chunk-6KYMPV2O.js";
@@ -19,19 +23,14 @@ import {
19
23
 
20
24
  // src/namespaces/catalog.ts
21
25
  import path from "path";
22
- import { randomUUID } from "crypto";
23
26
  import {
24
27
  appendFile,
25
28
  lstat,
26
29
  mkdir,
27
- open,
28
30
  readdir,
29
31
  readFile,
30
32
  realpath,
31
33
  rename,
32
- stat,
33
- unlink,
34
- utimes,
35
34
  writeFile
36
35
  } from "fs/promises";
37
36
  var NAMESPACE_KINDS = [
@@ -240,15 +239,16 @@ var NamespaceCatalog = class {
240
239
  stateDir;
241
240
  catalogPath;
242
241
  rebuildLockPath;
243
- // Per-INSTANCE lock owner id (round 6, codex P2 — NBsGP). The rebuild lock
244
- // file records this id, not just `process.pid`, so two NamespaceCatalog
245
- // instances in the SAME process sharing a memoryDir are NOT mistaken for each
246
- // other: a touch on instance B must still wait for instance A's rebuild lock
247
- // (different owner id, same PID) instead of skipping as "self-held".
248
- lockOwnerId = randomUUID();
249
- // Serialized write chain that recovers from rejection (CLAUDE.md rule #40)
250
- // so a single failed append cannot permanently poison subsequent writes.
251
- writeChain = Promise.resolve();
242
+ // In-process serialization for catalog mutations (issue #1524 adoption).
243
+ // Replaces the bespoke `writeChain` field: every touch/rebuild runs through
244
+ // this serializer so a single failed section never poisons subsequent ones
245
+ // (CLAUDE.md rule #40 recovery is the util's contract, not re-implemented
246
+ // here). Cross-process identity (the lock file's owner-uuid) is now per-CALL
247
+ // inside the shared util, which is STRONGER than the previous per-instance
248
+ // `lockOwnerId` two calls on the SAME instance get different ids, so
249
+ // neither mistakes the other's lock for self-held (round 6, codex P2 — NBsGP
250
+ // invariant preserved and tightened).
251
+ criticalSection = new MutationSerializer();
252
252
  // Test-only seam (round 7 — NEZkA): fires inside a touch's HELD-lock critical
253
253
  // section, after the lock is acquired but BEFORE the read→merge→append. A
254
254
  // deterministic concurrency test installs a hook here to widen the (otherwise
@@ -1091,24 +1091,23 @@ var NamespaceCatalog = class {
1091
1091
  * their respective critical sections — closing the check-then-append gap where a
1092
1092
  * polled-only touch could append into a rebuild's load→rename window.
1093
1093
  *
1094
- * Acquisition is atomic via `open(..., "wx")`. A lock older than
1095
- * `REBUILD_LOCK_STALE_MS` is treated as a crashed holder and broken. After
1096
- * `REBUILD_LOCK_MAX_WAIT_MS` of contention we proceed best-effort WITHOUT the
1097
- * lock rather than block forever. The lock is always released in `finally`.
1094
+ * Issue #1524 adoption: this is now a thin delegation to the shared
1095
+ * `withHeldFileLock` utility. The acquire loop, mtime heartbeat, stale-break
1096
+ * (NG7Bg replacement-safe), and ownership-checked release (NCzT6) all live in
1097
+ * ONE place the util so this module no longer re-implements them. The
1098
+ * catalog's `REBUILD_LOCK_*` constants and the `onBeforeBreakStaleUnlinkForTest`
1099
+ * seam flow straight through. The util generates a per-CALL owner uuid, which
1100
+ * is stricter than the previous per-instance `lockOwnerId` (two calls on the
1101
+ * same instance get different ids, so neither mistakes the other's lock as
1102
+ * self-held — the NBsGP invariant, preserved and tightened).
1098
1103
  *
1099
1104
  * IN-PROCESS SAFETY: every caller invokes this from inside (or wrapping) the
1100
1105
  * per-process `queueCritical` chain, which serializes all catalog mutations in
1101
- * THIS process. So within one process only one logical holder attempts OS-lock
1102
- * acquisition at a time — the file lock is never self-contended in-process, and
1103
- * the lock is acquired and released within a single in-process turn. The file
1104
- * lock adds only the missing CROSS-process exclusion.
1105
- *
1106
- * HEARTBEAT (round 5, cursor/codex Medium/P2): while WE hold the lock a timer
1107
- * refreshes its mtime every `REBUILD_LOCK_HEARTBEAT_MS`, so a legitimately long
1108
- * holder (> `REBUILD_LOCK_STALE_MS`) is not treated as a crashed holder and
1109
- * unlinked by another process — which would let overlapping windows lose
1110
- * appends. Heartbeat failures are swallowed; the timer is always cleared in
1111
- * `finally`.
1106
+ * THIS process (now via `MutationSerializer`). So within one process only one
1107
+ * logical holder attempts OS-lock acquisition at a time — the file lock is never
1108
+ * self-contended in-process, and the lock is acquired and released within a
1109
+ * single in-process turn. The file lock adds only the missing CROSS-process
1110
+ * exclusion.
1112
1111
  *
1113
1112
  * ACQUISITION RESULT (round 6, codex P2 — NBPmY): `fn` receives whether WE
1114
1113
  * actually hold the lock. When acquisition TIMED OUT (another holder is active),
@@ -1117,120 +1116,20 @@ var NamespaceCatalog = class {
1117
1116
  * caller uses `acquired` to run compute-only (rebuild) or DROP the append
1118
1117
  * (touch) when unlocked.
1119
1118
  */
1120
- async withHeldCatalogLock(fn) {
1121
- const acquired = await this.acquireRebuildLock();
1122
- let heartbeat;
1123
- if (acquired) {
1124
- heartbeat = setInterval(() => {
1125
- const now = /* @__PURE__ */ new Date();
1126
- utimes(this.rebuildLockPath, now, now).catch(() => void 0);
1127
- }, REBUILD_LOCK_HEARTBEAT_MS);
1128
- heartbeat.unref?.();
1129
- }
1130
- try {
1131
- return await fn(acquired);
1132
- } finally {
1133
- if (heartbeat) clearInterval(heartbeat);
1134
- if (acquired) {
1135
- try {
1136
- if (await this.rebuildLockHeldBySelf()) {
1137
- await unlink(this.rebuildLockPath);
1138
- }
1139
- } catch {
1140
- }
1141
- }
1142
- }
1143
- }
1144
- /** Try to acquire the rebuild lock; returns true if WE created it. */
1145
- async acquireRebuildLock() {
1146
- const deadline = Date.now() + REBUILD_LOCK_MAX_WAIT_MS;
1147
- await mkdir(this.stateDir, { recursive: true });
1148
- for (; ; ) {
1149
- try {
1150
- const handle = await open(this.rebuildLockPath, "wx");
1151
- try {
1152
- await handle.writeFile(
1153
- `${process.pid} ${this.lockOwnerId} ${(/* @__PURE__ */ new Date()).toISOString()}
1154
- `,
1155
- "utf8"
1156
- );
1157
- } catch {
1158
- } finally {
1159
- await handle.close();
1160
- }
1161
- return true;
1162
- } catch (err) {
1163
- if (err?.code !== "EEXIST") {
1164
- return false;
1165
- }
1166
- await this.breakStaleRebuildLock();
1167
- if (Date.now() >= deadline) return false;
1168
- await new Promise((r) => setTimeout(r, REBUILD_LOCK_POLL_MS));
1169
- }
1170
- }
1171
- }
1172
- /**
1173
- * Remove the lock file if its mtime is older than the stale threshold.
1174
- *
1175
- * REPLACEMENT-SAFE (NG7Bg, codex P2): a plain `stat` → `unlink` has a TOCTOU
1176
- * window — two processes can both observe the SAME stale lock; one removes it and
1177
- * creates a FRESH lock, and the other's later `unlink` then deletes that fresh
1178
- * holder's ACTIVE lock based on the stale identity it read earlier, leaving the
1179
- * fresh holder running its critical section with no visible lock and reopening the
1180
- * lost-update race the mutex prevents. We therefore capture the lock's IDENTITY
1181
- * (its full content line: `<pid> <owner-uuid> <iso>`) when we judge it stale, then
1182
- * RE-READ immediately before unlinking and only remove it when the content is
1183
- * byte-identical AND still stale. A replacement lock has a different owner id /
1184
- * timestamp, so its content differs and we leave it untouched. We never unlink a
1185
- * lock whose mtime is now fresh (a heartbeat refreshed it) or whose identity
1186
- * changed (a replacement was created). This is best-effort: any mismatch/vanish
1187
- * simply skips the break and the caller polls again.
1188
- */
1189
- async breakStaleRebuildLock() {
1190
- let staleIdentity;
1191
- try {
1192
- const info = await stat(this.rebuildLockPath);
1193
- if (Date.now() - info.mtimeMs <= REBUILD_LOCK_STALE_MS) {
1194
- return;
1195
- }
1196
- staleIdentity = await readFile(this.rebuildLockPath, "utf8");
1197
- } catch {
1198
- return;
1199
- }
1200
- if (this.onBeforeBreakStaleUnlinkForTest) {
1201
- await this.onBeforeBreakStaleUnlinkForTest();
1202
- }
1203
- try {
1204
- const current = await readFile(this.rebuildLockPath, "utf8");
1205
- if (current !== staleIdentity) return;
1206
- const recheck = await stat(this.rebuildLockPath);
1207
- if (Date.now() - recheck.mtimeMs <= REBUILD_LOCK_STALE_MS) return;
1208
- await unlink(this.rebuildLockPath).catch(() => void 0);
1209
- } catch {
1210
- }
1211
- }
1212
- /**
1213
- * Whether the rebuild lock file was written by THIS instance (round 6, codex
1214
- * P2 — NBsGP). Matches the per-instance owner id, NOT just `process.pid`: two
1215
- * NamespaceCatalog instances in the same process share a PID, so a PID-only
1216
- * check would wrongly treat instance A's lock as self-held by instance B and
1217
- * let B's touch skip the wait and append into A's rebuild window. Falls back to
1218
- * the legacy PID-only form for lock files written before owner ids existed.
1219
- */
1220
- async rebuildLockHeldBySelf() {
1221
- try {
1222
- const body = await readFile(this.rebuildLockPath, "utf8");
1223
- const parts = body.trim().split(/\s+/);
1224
- const pid = Number.parseInt(parts[0] ?? "", 10);
1225
- const ownerId = parts[1];
1226
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1227
- if (ownerId && UUID_RE.test(ownerId)) {
1228
- return ownerId === this.lockOwnerId;
1229
- }
1230
- return Number.isFinite(pid) && pid === process.pid;
1231
- } catch {
1232
- return false;
1233
- }
1119
+ withHeldCatalogLock(fn) {
1120
+ return withHeldFileLock(
1121
+ this.rebuildLockPath,
1122
+ {
1123
+ staleMs: REBUILD_LOCK_STALE_MS,
1124
+ maxWaitMs: REBUILD_LOCK_MAX_WAIT_MS,
1125
+ pollMs: REBUILD_LOCK_POLL_MS,
1126
+ heartbeatMs: REBUILD_LOCK_HEARTBEAT_MS,
1127
+ // NG7Bg seam: fires inside the util's breakStaleLock after it judges the
1128
+ // lock stale and captures its identity, before the atomic rename+verify.
1129
+ onBeforeBreakStaleUnlinkForTest: this.onBeforeBreakStaleUnlinkForTest
1130
+ },
1131
+ fn
1132
+ );
1234
1133
  }
1235
1134
  /**
1236
1135
  * Merge a prior record's preserved metadata (timestamps, principal hints)
@@ -1288,20 +1187,20 @@ var NamespaceCatalog = class {
1288
1187
  }
1289
1188
  /**
1290
1189
  * Serialize an arbitrary read-modify-write critical section through the single
1291
- * write chain. Every catalog mutation (touch read+merge+append, full rewrite)
1292
- * runs through this so they are mutually exclusive: a touch always reads the
1293
- * latest persisted state before appending, and a rebuild rewrite cannot
1294
- * interleave with a touch's append. The chain recovers from rejection
1295
- * (CLAUDE.md rule #40) — one failed section never poisons subsequent ones —
1296
- * while still surfacing the error to that section's awaited promise.
1190
+ * per-instance chain. Every catalog mutation (touch read+merge+append, full
1191
+ * rewrite) runs through this so they are mutually exclusive: a touch always
1192
+ * reads the latest persisted state before appending, and a rebuild rewrite
1193
+ * cannot interleave with a touch's append.
1194
+ *
1195
+ * Issue #1524 adoption: delegates to the shared `MutationSerializer` (stored
1196
+ * as `criticalSection`). The util owns the rejection-recovery invariant
1197
+ * (CLAUDE.md rule #40 — one failed section never poisons subsequent ones, but
1198
+ * the failing section's error still surfaces to ITS awaited promise) and the
1199
+ * no-unbounded-growth cleanup. The key is constant: the catalog has ONE
1200
+ * logical mutation queue (touches and rebuilds mutually exclude in-process).
1297
1201
  */
1298
1202
  queueCritical(fn) {
1299
- const run = this.writeChain.then(fn);
1300
- this.writeChain = run.then(
1301
- () => void 0,
1302
- () => void 0
1303
- );
1304
- return run;
1203
+ return this.criticalSection.serialize("catalog", fn);
1305
1204
  }
1306
1205
  /**
1307
1206
  * Append a single record to the JSONL log WITHOUT re-serializing through the
@@ -1336,14 +1235,14 @@ function isPathInside(root, child) {
1336
1235
  }
1337
1236
 
1338
1237
  // src/maintenance/namespace-planner.ts
1339
- import { createHash, randomUUID as randomUUID2 } from "crypto";
1340
- import { lstat as lstat2, mkdir as mkdir2, open as open2, readFile as readFile2, readdir as readdir2, rename as rename2, rm, rmdir, utimes as utimes2, writeFile as writeFile2 } from "fs/promises";
1238
+ import { createHash, randomUUID } from "crypto";
1239
+ import { lstat as lstat2, mkdir as mkdir2, open, readFile as readFile2, readdir as readdir2, rename as rename2, rm, rmdir, utimes, writeFile as writeFile2 } from "fs/promises";
1341
1240
  import path2 from "path";
1342
1241
  var DEFAULT_MAX_NAMESPACES_PER_CYCLE = 20;
1343
1242
  var DEFAULT_LOCK_STALE_MS = 10 * 6e4;
1344
1243
  var LOCK_BASE = "maintenance-locks";
1345
1244
  var STATUS_BASE = "namespace-maintenance-status";
1346
- var namespaceMaintenanceFs = { open: open2, rm };
1245
+ var namespaceMaintenanceFs = { open, rm };
1347
1246
  function configuredNamespaces(config) {
1348
1247
  return Array.from(
1349
1248
  new Set(
@@ -1589,7 +1488,7 @@ async function tryAcquireNamespaceMaintenanceLock(config, jobName, namespace) {
1589
1488
  const filePath = lockPath(config, jobName, namespace);
1590
1489
  await mkdir2(path2.dirname(filePath), { recursive: true });
1591
1490
  try {
1592
- const lockId = randomUUID2();
1491
+ const lockId = randomUUID();
1593
1492
  await mkdir2(filePath);
1594
1493
  const ownerPath = path2.join(filePath, `${lockId}.json`);
1595
1494
  let handle;
@@ -1620,8 +1519,8 @@ async function tryAcquireNamespaceMaintenanceLock(config, jobName, namespace) {
1620
1519
  const parsed = JSON.parse(await readFile2(ownerPath, "utf8"));
1621
1520
  if (parsed.lockId === lockId) {
1622
1521
  const now = /* @__PURE__ */ new Date();
1623
- await utimes2(ownerPath, now, now);
1624
- await utimes2(filePath, now, now);
1522
+ await utimes(ownerPath, now, now);
1523
+ await utimes(filePath, now, now);
1625
1524
  }
1626
1525
  } catch {
1627
1526
  }
@@ -2051,4 +1950,4 @@ export {
2051
1950
  runNamespaceMaintenancePlan,
2052
1951
  runNamespaceMaintenanceBatchPlan
2053
1952
  };
2054
- //# sourceMappingURL=chunk-DR2JTSLZ.js.map
1953
+ //# sourceMappingURL=chunk-JKOKX3PS.js.map