@remnic/core 9.3.688 → 9.3.690

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 (163) hide show
  1. package/dist/access-boundary.d.ts +5 -4
  2. package/dist/access-boundary.js +12 -11
  3. package/dist/access-cli.js +90 -26
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +4 -3
  6. package/dist/access-http.js +15 -14
  7. package/dist/access-mcp.d.ts +11 -3
  8. package/dist/access-mcp.js +14 -13
  9. package/dist/access-operations.d.ts +11 -5
  10. package/dist/access-operations.js +15 -12
  11. package/dist/{access-service-DmCHJ4cH.d.ts → access-service-Dujr3MTm.d.ts} +62 -2
  12. package/dist/access-service.d.ts +4 -3
  13. package/dist/access-service.js +11 -10
  14. package/dist/access-surface-catalog.d.ts +4 -3
  15. package/dist/access-surface-catalog.js +2 -0
  16. package/dist/access-surface-catalog.js.map +1 -1
  17. package/dist/bootstrap.d.ts +3 -2
  18. package/dist/briefing.d.ts +1 -0
  19. package/dist/briefing.js +3 -2
  20. package/dist/buffer.d.ts +1 -0
  21. package/dist/{catalog-COqWZlZ6.d.ts → catalog-CKPtJ114.d.ts} +1 -1
  22. package/dist/causal-consolidation.js +4 -3
  23. package/dist/causal-consolidation.js.map +1 -1
  24. package/dist/{chunk-B4XVLHJA.js → chunk-2N6UNJSX.js} +2 -2
  25. package/dist/{chunk-NN7QYW5W.js → chunk-46URPRE6.js} +2 -2
  26. package/dist/chunk-46URPRE6.js.map +1 -0
  27. package/dist/{chunk-4N3TFFPH.js → chunk-54PVJDO5.js} +2 -2
  28. package/dist/{chunk-PH3HOKYW.js → chunk-67MMWC74.js} +2 -2
  29. package/dist/{chunk-STOEE37X.js → chunk-BKAMHZYR.js} +2 -2
  30. package/dist/{chunk-B43NZNMG.js → chunk-CE6CBRCV.js} +432 -2240
  31. package/dist/chunk-CE6CBRCV.js.map +1 -0
  32. package/dist/{chunk-RVYD6LR3.js → chunk-CP2NZQLT.js} +15 -4
  33. package/dist/chunk-CP2NZQLT.js.map +1 -0
  34. package/dist/{chunk-BLIWOONZ.js → chunk-CUNTLEJP.js} +4 -12
  35. package/dist/chunk-CUNTLEJP.js.map +1 -0
  36. package/dist/{chunk-2SJCWLQD.js → chunk-CWE74HRG.js} +3 -3
  37. package/dist/{chunk-VX6OBUDW.js → chunk-GR77Z2BM.js} +2 -2
  38. package/dist/{chunk-CTOQEZSN.js → chunk-LXIEXSHU.js} +2 -2
  39. package/dist/{chunk-KFBOZYME.js → chunk-NSVXK7M5.js} +33 -4
  40. package/dist/chunk-NSVXK7M5.js.map +1 -0
  41. package/dist/{chunk-473JIN2U.js → chunk-OMKUJTVJ.js} +37 -5
  42. package/dist/chunk-OMKUJTVJ.js.map +1 -0
  43. package/dist/{chunk-GS55WYRL.js → chunk-PHZKALOE.js} +2 -2
  44. package/dist/{chunk-6O6A6YUO.js → chunk-RGNEARXW.js} +3 -3
  45. package/dist/{chunk-4FE2K57M.js → chunk-RTFAZOIR.js} +2 -2
  46. package/dist/{chunk-UTYBJR7M.js → chunk-SB6CQTKP.js} +2 -2
  47. package/dist/chunk-SVOZFLIQ.js +16 -0
  48. package/dist/chunk-SVOZFLIQ.js.map +1 -0
  49. package/dist/{chunk-2KAYTPPT.js → chunk-TYF3D4MS.js} +5 -5
  50. package/dist/{chunk-WIHPNY65.js → chunk-UD5OKH4J.js} +2 -2
  51. package/dist/{chunk-QANVLERJ.js → chunk-UPIBE2DK.js} +2 -2
  52. package/dist/{chunk-CCWHPGT4.js → chunk-WROKD3XC.js} +45 -30
  53. package/dist/chunk-WROKD3XC.js.map +1 -0
  54. package/dist/{chunk-ZPTISBQU.js → chunk-X5J3JZR3.js} +7 -7
  55. package/dist/{chunk-OV4D5T7V.js → chunk-X7RLU5CR.js} +2 -2
  56. package/dist/{chunk-S2OU5DZY.js → chunk-XKUKJIOY.js} +30 -7
  57. package/dist/chunk-XKUKJIOY.js.map +1 -0
  58. package/dist/{chunk-FUCUR2OZ.js → chunk-Z7KILAOU.js} +603 -9
  59. package/dist/chunk-Z7KILAOU.js.map +1 -0
  60. package/dist/chunk-ZU7N3S6V.js +2190 -0
  61. package/dist/chunk-ZU7N3S6V.js.map +1 -0
  62. package/dist/{chunk-DCWIQFNA.js → chunk-ZUDM75KG.js} +4 -4
  63. package/dist/{cli-D8nZ2MPH.d.ts → cli-BkDp6WNi.d.ts} +2 -2
  64. package/dist/cli.d.ts +5 -4
  65. package/dist/cli.js +32 -30
  66. package/dist/compounding/engine.d.ts +1 -0
  67. package/dist/compounding/engine.js +3 -2
  68. package/dist/connectors/codex-materialize-runner.js +3 -2
  69. package/dist/connectors/index.js +3 -2
  70. package/dist/consolidation-provenance-check.d.ts +1 -0
  71. package/dist/consolidation-undo.d.ts +1 -0
  72. package/dist/contradiction/index.d.ts +1 -0
  73. package/dist/conversation-index/backend.js +2 -2
  74. package/dist/entity-retrieval.d.ts +1 -0
  75. package/dist/entity-retrieval.js +3 -2
  76. package/dist/explicit-capture.d.ts +3 -2
  77. package/dist/index.d.ts +6 -5
  78. package/dist/index.js +47 -44
  79. package/dist/index.js.map +1 -1
  80. package/dist/lcm/engine.js +3 -3
  81. package/dist/lcm/index.js +5 -5
  82. package/dist/maintenance/memory-governance.js +3 -2
  83. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -2
  84. package/dist/maintenance/rebuild-memory-projection.js +4 -3
  85. package/dist/mcp-memory-inspector-app.d.ts +4 -3
  86. package/dist/memory-worth-outcomes.d.ts +1 -0
  87. package/dist/namespaces/migrate.d.ts +2 -1
  88. package/dist/namespaces/migrate.js +10 -9
  89. package/dist/namespaces/search.js +6 -6
  90. package/dist/namespaces/storage.d.ts +2 -1
  91. package/dist/namespaces/storage.js +3 -2
  92. package/dist/operator-toolkit.d.ts +1 -0
  93. package/dist/operator-toolkit.js +13 -11
  94. package/dist/{orchestrator-CA6ouzBn.d.ts → orchestrator-B7ixmUkP.d.ts} +145 -1
  95. package/dist/orchestrator.d.ts +3 -2
  96. package/dist/orchestrator.js +22 -20
  97. package/dist/recall-pipeline-stages.d.ts +167 -0
  98. package/dist/recall-pipeline-stages.js +61 -0
  99. package/dist/recall-pipeline-stages.js.map +1 -0
  100. package/dist/schemas.d.ts +10 -10
  101. package/dist/search/factory.js +5 -5
  102. package/dist/search/index.js +9 -9
  103. package/dist/semantic-consolidation.js +4 -3
  104. package/dist/semantic-rule-promotion.js +3 -2
  105. package/dist/semantic-rule-verifier.js +3 -2
  106. package/dist/storage.d.ts +3 -27
  107. package/dist/storage.js +5 -3
  108. package/dist/structured-attributes.d.ts +29 -0
  109. package/dist/structured-attributes.js +8 -0
  110. package/dist/structured-attributes.js.map +1 -0
  111. package/dist/temporal-supersession.d.ts +1 -0
  112. package/dist/tier-migration.d.ts +1 -0
  113. package/dist/verified-recall.js +3 -2
  114. package/package.json +2 -2
  115. package/src/access-boundary.ts +2 -1
  116. package/src/access-cli.test.ts +40 -0
  117. package/src/access-cli.ts +90 -2
  118. package/src/access-http.ts +35 -6
  119. package/src/access-mcp.ts +34 -0
  120. package/src/access-operations.ts +45 -0
  121. package/src/access-service.ts +60 -0
  122. package/src/access-surface-catalog.test.ts +1 -1
  123. package/src/access-surface-catalog.ts +2 -0
  124. package/src/cli.ts +18 -0
  125. package/src/coding/architecture-card.test.ts +544 -0
  126. package/src/coding/architecture-card.ts +687 -0
  127. package/src/coding/architecture-surfaces.test.ts +579 -0
  128. package/src/coding/architecture-surfaces.ts +457 -0
  129. package/src/maintenance/namespace-maintenance-fanout.test.ts +595 -0
  130. package/src/maintenance/namespace-maintenance-fanout.ts +318 -0
  131. package/src/maintenance/namespace-planner.ts +74 -16
  132. package/src/operator-toolkit.ts +25 -0
  133. package/src/orchestrator.ts +144 -0
  134. package/src/recall-pipeline-stages.test.ts +269 -0
  135. package/src/recall-pipeline-stages.ts +294 -0
  136. package/src/storage.ts +6 -20
  137. package/src/structured-attributes.ts +39 -0
  138. package/dist/chunk-473JIN2U.js.map +0 -1
  139. package/dist/chunk-B43NZNMG.js.map +0 -1
  140. package/dist/chunk-BLIWOONZ.js.map +0 -1
  141. package/dist/chunk-CCWHPGT4.js.map +0 -1
  142. package/dist/chunk-FUCUR2OZ.js.map +0 -1
  143. package/dist/chunk-KFBOZYME.js.map +0 -1
  144. package/dist/chunk-NN7QYW5W.js.map +0 -1
  145. package/dist/chunk-RVYD6LR3.js.map +0 -1
  146. package/dist/chunk-S2OU5DZY.js.map +0 -1
  147. /package/dist/{chunk-B4XVLHJA.js.map → chunk-2N6UNJSX.js.map} +0 -0
  148. /package/dist/{chunk-4N3TFFPH.js.map → chunk-54PVJDO5.js.map} +0 -0
  149. /package/dist/{chunk-PH3HOKYW.js.map → chunk-67MMWC74.js.map} +0 -0
  150. /package/dist/{chunk-STOEE37X.js.map → chunk-BKAMHZYR.js.map} +0 -0
  151. /package/dist/{chunk-2SJCWLQD.js.map → chunk-CWE74HRG.js.map} +0 -0
  152. /package/dist/{chunk-VX6OBUDW.js.map → chunk-GR77Z2BM.js.map} +0 -0
  153. /package/dist/{chunk-CTOQEZSN.js.map → chunk-LXIEXSHU.js.map} +0 -0
  154. /package/dist/{chunk-GS55WYRL.js.map → chunk-PHZKALOE.js.map} +0 -0
  155. /package/dist/{chunk-6O6A6YUO.js.map → chunk-RGNEARXW.js.map} +0 -0
  156. /package/dist/{chunk-4FE2K57M.js.map → chunk-RTFAZOIR.js.map} +0 -0
  157. /package/dist/{chunk-UTYBJR7M.js.map → chunk-SB6CQTKP.js.map} +0 -0
  158. /package/dist/{chunk-2KAYTPPT.js.map → chunk-TYF3D4MS.js.map} +0 -0
  159. /package/dist/{chunk-WIHPNY65.js.map → chunk-UD5OKH4J.js.map} +0 -0
  160. /package/dist/{chunk-QANVLERJ.js.map → chunk-UPIBE2DK.js.map} +0 -0
  161. /package/dist/{chunk-ZPTISBQU.js.map → chunk-X5J3JZR3.js.map} +0 -0
  162. /package/dist/{chunk-OV4D5T7V.js.map → chunk-X7RLU5CR.js.map} +0 -0
  163. /package/dist/{chunk-DCWIQFNA.js.map → chunk-ZUDM75KG.js.map} +0 -0
@@ -19,7 +19,7 @@ import {
19
19
  } from "./chunk-666A3MOW.js";
20
20
  import {
21
21
  LcmEngine
22
- } from "./chunk-ZPTISBQU.js";
22
+ } from "./chunk-X5J3JZR3.js";
23
23
  import {
24
24
  chunkTranscriptEntries
25
25
  } from "./chunk-TECVW3JP.js";
@@ -29,7 +29,7 @@ import {
29
29
  import {
30
30
  CompoundingEngine,
31
31
  defaultTierMigrationCycleBudget
32
- } from "./chunk-STOEE37X.js";
32
+ } from "./chunk-BKAMHZYR.js";
33
33
  import {
34
34
  SharedContextManager
35
35
  } from "./chunk-UFS7OXGL.js";
@@ -183,27 +183,27 @@ import {
183
183
  buildEntityRecallSection,
184
184
  entityRecentTranscriptLookbackHours,
185
185
  readRecentEntityTranscriptEntries
186
- } from "./chunk-B4XVLHJA.js";
186
+ } from "./chunk-2N6UNJSX.js";
187
187
  import {
188
188
  formatDaySummaryMemories
189
189
  } from "./chunk-EO5QWINU.js";
190
- import {
191
- normalizeReplaySessionKey
192
- } from "./chunk-2PRQG7PV.js";
193
190
  import {
194
191
  RoutingRulesStore
195
192
  } from "./chunk-WIWPSQYU.js";
193
+ import {
194
+ normalizeReplaySessionKey
195
+ } from "./chunk-2PRQG7PV.js";
196
196
  import {
197
197
  compareVerifiedEpisodeResults,
198
198
  searchVerifiedEpisodes
199
- } from "./chunk-4FE2K57M.js";
199
+ } from "./chunk-RTFAZOIR.js";
200
200
  import {
201
201
  ThreadingManager
202
202
  } from "./chunk-QRDOSYOR.js";
203
203
  import {
204
204
  compareVerifiedSemanticRuleResults,
205
205
  searchVerifiedSemanticRules
206
- } from "./chunk-PH3HOKYW.js";
206
+ } from "./chunk-67MMWC74.js";
207
207
  import {
208
208
  searchWorkProductLedgerEntries
209
209
  } from "./chunk-ZRWB5D4H.js";
@@ -214,10 +214,15 @@ import {
214
214
  PolicyRuntimeManager
215
215
  } from "./chunk-33JBK2XP.js";
216
216
  import {
217
- NamespaceStorageRouter,
218
- resolveDefaultNamespaceRoot,
219
- resolveNamespaceStorageRoot
220
- } from "./chunk-WIHPNY65.js";
217
+ NamespaceCatalog,
218
+ planNamespaceMaintenance,
219
+ runNamespaceMaintenanceBatchPlan,
220
+ runNamespaceMaintenanceFanout,
221
+ summarizeNamespaceMaintenanceHealth
222
+ } from "./chunk-ZU7N3S6V.js";
223
+ import {
224
+ NamespaceStorageRouter
225
+ } from "./chunk-UD5OKH4J.js";
221
226
  import {
222
227
  isAboveImportanceThreshold,
223
228
  scoreImportance
@@ -262,7 +267,7 @@ import {
262
267
  materializeAfterSemanticConsolidation,
263
268
  parseConsolidationResponse,
264
269
  parseOperatorAwareConsolidationResponse
265
- } from "./chunk-GS55WYRL.js";
270
+ } from "./chunk-PHZKALOE.js";
266
271
  import {
267
272
  buildBehaviorSignalsForMemory,
268
273
  dedupeBehaviorSignalsByMemoryAndHash
@@ -327,11 +332,11 @@ import {
327
332
  } from "./chunk-6HMYUWXR.js";
328
333
  import {
329
334
  NamespaceSearchRouter
330
- } from "./chunk-CTOQEZSN.js";
335
+ } from "./chunk-LXIEXSHU.js";
331
336
  import {
332
337
  createConversationIndexRuntime,
333
338
  createSearchBackend
334
- } from "./chunk-2KAYTPPT.js";
339
+ } from "./chunk-TYF3D4MS.js";
335
340
  import {
336
341
  NoopSearchBackend
337
342
  } from "./chunk-CYEPCZN5.js";
@@ -363,16 +368,15 @@ import {
363
368
  } from "./chunk-YN4ZT4CW.js";
364
369
  import {
365
370
  buildChainFollowupGenerator
366
- } from "./chunk-OV4D5T7V.js";
371
+ } from "./chunk-X7RLU5CR.js";
367
372
  import {
368
373
  ContentHashIndex,
369
374
  StorageManager,
370
375
  compareEntityTimestamps,
371
376
  fingerprintEntityStructuredFacts,
372
377
  normalizeAttributePairs,
373
- parseEntityFile,
374
- stripAttributesSuffix
375
- } from "./chunk-BLIWOONZ.js";
378
+ parseEntityFile
379
+ } from "./chunk-CUNTLEJP.js";
376
380
  import {
377
381
  isValidTranscriptDate,
378
382
  loadSpeakerRegistry,
@@ -383,6 +387,9 @@ import {
383
387
  import {
384
388
  assertPathInsideRoot
385
389
  } from "./chunk-5GPPACXK.js";
390
+ import {
391
+ stripAttributesSuffix
392
+ } from "./chunk-SVOZFLIQ.js";
386
393
  import {
387
394
  attachCitation,
388
395
  hasCitationForTemplate,
@@ -391,9 +398,6 @@ import {
391
398
  import {
392
399
  confidenceTier
393
400
  } from "./chunk-NGFEWFNK.js";
394
- import {
395
- displayErrorDetail
396
- } from "./chunk-6KYMPV2O.js";
397
401
  import {
398
402
  buildQmdRecallCacheKey,
399
403
  getCachedQmdRecall,
@@ -421,7 +425,6 @@ import {
421
425
  parseFlexibleIsoTimestamp
422
426
  } from "./chunk-P7FMDTKL.js";
423
427
  import {
424
- ALL_CATEGORY_DIRS,
425
428
  RECALL_FALLBACK_DIRS,
426
429
  categoryDirName
427
430
  } from "./chunk-VS2IYZRU.js";
@@ -455,19 +458,19 @@ import {
455
458
  } from "./chunk-PVGDJXVK.js";
456
459
 
457
460
  // src/orchestrator.ts
458
- import path4 from "path";
461
+ import path2 from "path";
459
462
  import os from "os";
460
- import { createHash as createHash2, randomBytes } from "crypto";
463
+ import { createHash, randomBytes } from "crypto";
461
464
  import { existsSync, readFileSync } from "fs";
462
465
  import {
463
- lstat as lstat3,
464
- mkdir as mkdir4,
465
- readdir as readdir3,
466
- readFile as readFile4,
467
- realpath as realpath2,
468
- stat as stat3,
469
- unlink as unlink2,
470
- writeFile as writeFile4
466
+ lstat,
467
+ mkdir as mkdir2,
468
+ readdir,
469
+ readFile as readFile2,
470
+ realpath,
471
+ stat,
472
+ unlink,
473
+ writeFile as writeFile2
471
474
  } from "fs/promises";
472
475
 
473
476
  // src/procedural/procedure-recall.ts
@@ -1404,2089 +1407,166 @@ Install it alongside Remnic:
1404
1407
  }
1405
1408
  return results;
1406
1409
  }
1407
- /**
1408
- * Memories created from wearable transcripts, filterable by source
1409
- * and/or day. Includes pending_review candidates — the whole point of
1410
- * review mode is seeing what's queued.
1411
- */
1412
- async transcriptMemories(options = {}) {
1413
- if (options.date !== void 0 && !isValidTranscriptDate(options.date)) {
1414
- throw new WearablesInputError(`invalid date '${options.date}' \u2014 expected YYYY-MM-DD`);
1415
- }
1416
- if (options.source !== void 0) assertValidSourceId(options.source);
1417
- const limit = clampLimit(
1418
- options.limit,
1419
- MEMORY_LIST_DEFAULT_LIMIT,
1420
- MEMORY_LIST_MAX_LIMIT,
1421
- "limit"
1422
- );
1423
- const storage = await this.deps.getStorage();
1424
- const memories = await storage.readAllMemories();
1425
- const results = [];
1426
- for (const memory of memories) {
1427
- const source = memory.frontmatter.source;
1428
- if (typeof source !== "string" || !source.startsWith(`${WEARABLE_SOURCE_PREFIX}:`)) {
1429
- continue;
1430
- }
1431
- const attrs = memory.frontmatter.structuredAttributes ?? {};
1432
- const sourceId = attrs.wearableSource;
1433
- if (options.source !== void 0) {
1434
- if (sourceId !== options.source && source !== wearableSourceLabel(options.source) && source !== `${wearableSourceLabel(options.source)}:native`) {
1435
- continue;
1436
- }
1437
- }
1438
- if (options.date !== void 0 && attrs.wearableDate !== options.date) {
1439
- continue;
1440
- }
1441
- results.push({
1442
- id: memory.frontmatter.id,
1443
- source: sourceId ?? source,
1444
- date: attrs.wearableDate,
1445
- conversationId: attrs.wearableConversationId,
1446
- status: memory.frontmatter.status,
1447
- content: memory.content,
1448
- created: memory.frontmatter.created
1449
- });
1450
- }
1451
- results.sort((a, b) => {
1452
- if (a.created > b.created) return -1;
1453
- if (a.created < b.created) return 1;
1454
- if (a.id < b.id) return -1;
1455
- if (a.id > b.id) return 1;
1456
- return 0;
1457
- });
1458
- return results.slice(0, limit);
1459
- }
1460
- // -- speakers -------------------------------------------------------------
1461
- async listSpeakers() {
1462
- const storage = await this.deps.getStorage();
1463
- return loadSpeakerRegistry(storage.dir);
1464
- }
1465
- async setSpeaker(sourceId, speakerKey, name, opts = {}) {
1466
- if (typeof name !== "string" || name.trim().length === 0) {
1467
- throw new WearablesInputError("speaker name must be a non-empty string");
1468
- }
1469
- if (typeof speakerKey !== "string" || speakerKey.trim().length === 0) {
1470
- throw new WearablesInputError("speaker key must be a non-empty string");
1471
- }
1472
- const storage = await this.deps.getStorage();
1473
- const registry = await loadSpeakerRegistry(storage.dir);
1474
- registry.speakers[speakerRegistryKey(sourceId, speakerKey.trim())] = {
1475
- name: name.trim(),
1476
- ...opts.isSelf === true ? { isSelf: true } : {},
1477
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1478
- };
1479
- await saveSpeakerRegistry(storage.dir, registry);
1480
- return registry;
1481
- }
1482
- async setSelfName(name) {
1483
- if (typeof name !== "string" || name.trim().length === 0) {
1484
- throw new WearablesInputError("self name must be a non-empty string");
1485
- }
1486
- const storage = await this.deps.getStorage();
1487
- const registry = await loadSpeakerRegistry(storage.dir);
1488
- registry.selfName = name.trim();
1489
- await saveSpeakerRegistry(storage.dir, registry);
1490
- return registry;
1491
- }
1492
- async removeSpeaker(sourceId, speakerKey) {
1493
- const storage = await this.deps.getStorage();
1494
- const registry = await loadSpeakerRegistry(storage.dir);
1495
- const key = speakerRegistryKey(sourceId, speakerKey.trim());
1496
- if (!(key in registry.speakers)) {
1497
- throw new WearablesInputError(`no speaker override stored for '${key}'`);
1498
- }
1499
- delete registry.speakers[key];
1500
- await saveSpeakerRegistry(storage.dir, registry);
1501
- return registry;
1502
- }
1503
- // -- corrections ----------------------------------------------------------
1504
- async listCorrections() {
1505
- const storage = await this.deps.getStorage();
1506
- return {
1507
- fromConfig: this.deps.config.corrections,
1508
- fromState: await loadCorrectionsFile(storage.dir),
1509
- stateFilePath: correctionsFilePath(storage.dir)
1510
- };
1511
- }
1512
- async addCorrection(rule) {
1513
- compileCorrectionRule(rule, "correction");
1514
- const storage = await this.deps.getStorage();
1515
- const rules = await loadCorrectionsFile(storage.dir);
1516
- const duplicate = rules.some(
1517
- (existing) => existing.match === rule.match && existing.replace === rule.replace && existing.regex === true === (rule.regex === true)
1518
- );
1519
- if (duplicate) {
1520
- throw new WearablesInputError(
1521
- `an identical correction rule already exists (match: ${JSON.stringify(rule.match)})`
1522
- );
1523
- }
1524
- rules.push(rule);
1525
- await saveCorrectionsFile(storage.dir, rules);
1526
- }
1527
- async removeCorrection(index) {
1528
- if (!Number.isInteger(index) || index < 0) {
1529
- throw new WearablesInputError(`invalid correction index '${index}'`);
1530
- }
1531
- const storage = await this.deps.getStorage();
1532
- const rules = await loadCorrectionsFile(storage.dir);
1533
- if (index >= rules.length) {
1534
- throw new WearablesInputError(
1535
- `correction index ${index} is out of range (have ${rules.length} state rule${rules.length === 1 ? "" : "s"})`
1536
- );
1537
- }
1538
- const [removed] = rules.splice(index, 1);
1539
- await saveCorrectionsFile(storage.dir, rules);
1540
- return removed;
1541
- }
1542
- };
1543
- function clampLimit(value, fallback, max, label) {
1544
- if (value === void 0) return fallback;
1545
- if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1 || value > max) {
1546
- throw new WearablesInputError(
1547
- `invalid ${label} '${value}' \u2014 expected an integer between 1 and ${max}`
1548
- );
1549
- }
1550
- return value;
1551
- }
1552
- function locateTranscriptPath(hitPath) {
1553
- const normalized = hitPath.replace(/\\/g, "/");
1554
- const match = normalized.match(
1555
- /(?:^|\/)wearables\/([a-z][a-z0-9-]{0,63})\/(\d{4}-\d{2}-\d{2})\.md$/
1556
- );
1557
- if (!match) return null;
1558
- if (!isValidTranscriptDate(match[2])) return null;
1559
- return { source: match[1], date: match[2] };
1560
- }
1561
- function extractSnippet(body, index, matchLength) {
1562
- const start = Math.max(0, index - 80);
1563
- const end = Math.min(body.length, index + matchLength + 80);
1564
- const prefix = start > 0 ? "\u2026" : "";
1565
- const suffix = end < body.length ? "\u2026" : "";
1566
- return `${prefix}${body.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
1567
- }
1568
-
1569
- // src/namespaces/catalog.ts
1570
- import path2 from "path";
1571
- import { randomUUID } from "crypto";
1572
- import {
1573
- appendFile,
1574
- lstat,
1575
- mkdir as mkdir2,
1576
- open,
1577
- readdir,
1578
- readFile as readFile2,
1579
- realpath,
1580
- rename,
1581
- stat,
1582
- unlink,
1583
- utimes,
1584
- writeFile as writeFile2
1585
- } from "fs/promises";
1586
- var NAMESPACE_KINDS = [
1587
- "default",
1588
- "self",
1589
- "shared",
1590
- "project",
1591
- "branch",
1592
- "team-project",
1593
- "explicit",
1594
- "legacy"
1595
- ];
1596
- var NAMESPACE_DISCOVERY_SOURCES = [
1597
- "config",
1598
- "write",
1599
- "read",
1600
- "scan",
1601
- "migration"
1602
- ];
1603
- var CATALOG_FILE = "namespaces.jsonl";
1604
- var STATE_DIR = "state";
1605
- var REBUILD_LOCK_FILE = "namespaces.rebuild.lock";
1606
- var REBUILD_LOCK_STALE_MS = 3e4;
1607
- var REBUILD_LOCK_MAX_WAIT_MS = 5e3;
1608
- var REBUILD_LOCK_POLL_MS = 50;
1609
- var REBUILD_LOCK_HEARTBEAT_MS = 1e4;
1610
- var MEMORY_DATA_CHILDREN = [
1611
- ...ALL_CATEGORY_DIRS,
1612
- "entities",
1613
- "artifacts",
1614
- "identity",
1615
- "config",
1616
- "summaries",
1617
- "profile.md",
1618
- "state"
1619
- ];
1620
- function isCatalogEnabled(config) {
1621
- if (config.namespacesEnabled !== true) return false;
1622
- return config.namespaceCatalogEnabled !== false;
1623
- }
1624
- var FILE_MEMORY_DATA_CHILDREN = /* @__PURE__ */ new Set(["profile.md"]);
1625
- function isNotFoundError(err) {
1626
- return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
1627
- }
1628
- async function inspectMemoryDataMarker(rootDir, child) {
1629
- const childPath = path2.join(rootDir, child);
1630
- let entry;
1631
- try {
1632
- entry = await lstat(childPath);
1633
- } catch (err) {
1634
- return isNotFoundError(err) ? { state: "absent" } : { state: "invalid", detail: `${child}: ${err instanceof Error ? err.message : String(err)}` };
1635
- }
1636
- if (entry.isSymbolicLink()) return { state: "invalid", detail: `${child}: symlink` };
1637
- if (FILE_MEMORY_DATA_CHILDREN.has(child)) {
1638
- return entry.isFile() ? { state: "valid" } : { state: "invalid", detail: `${child}: expected file` };
1639
- }
1640
- if (!entry.isDirectory()) return { state: "invalid", detail: `${child}: expected directory` };
1641
- try {
1642
- const rootReal = await realpath(rootDir);
1643
- const childReal = await realpath(childPath);
1644
- return isPathInside(rootReal, childReal) ? { state: "valid" } : { state: "invalid", detail: `${child}: escapes namespace root` };
1645
- } catch (err) {
1646
- return { state: "invalid", detail: `${child}: ${err instanceof Error ? err.message : String(err)}` };
1647
- }
1648
- }
1649
- async function inspectMemoryDataRoot(rootDir) {
1650
- let hasData = false;
1651
- for (const child of MEMORY_DATA_CHILDREN) {
1652
- const marker = await inspectMemoryDataMarker(rootDir, child);
1653
- if (marker.state === "invalid") {
1654
- return { hasData: false, invalidMarker: marker.detail };
1655
- }
1656
- if (marker.state === "valid") {
1657
- hasData = true;
1658
- }
1659
- }
1660
- return { hasData };
1661
- }
1662
- async function hasMemoryData(rootDir) {
1663
- return (await inspectMemoryDataRoot(rootDir)).hasData;
1664
- }
1665
- function isValidIsoTimestamp(value) {
1666
- const ms = Date.parse(value);
1667
- return Number.isFinite(ms);
1668
- }
1669
- function isNamespaceKind(value) {
1670
- return typeof value === "string" && NAMESPACE_KINDS.includes(value);
1671
- }
1672
- function isNamespaceDiscoverySource(value) {
1673
- return typeof value === "string" && NAMESPACE_DISCOVERY_SOURCES.includes(value);
1674
- }
1675
- function coerceRecord(value) {
1676
- if (typeof value !== "object" || value === null) return null;
1677
- const v = value;
1678
- if (typeof v.namespace !== "string") return null;
1679
- const namespace = normalizeNamespaceIdentity(v.namespace);
1680
- if (namespace.length === 0) return null;
1681
- if (typeof v.identityToken !== "string" || v.identityToken.length === 0) return null;
1682
- const expectedIdentityToken = namespaceIdentityToken(namespace);
1683
- if (v.identityToken !== expectedIdentityToken) return null;
1684
- if (typeof v.storageDir !== "string" || v.storageDir.length === 0) return null;
1685
- if (typeof v.createdAt !== "string" || v.createdAt.length === 0) return null;
1686
- if (!isValidIsoTimestamp(v.createdAt)) return null;
1687
- const kind = v.kind === void 0 ? "explicit" : isNamespaceKind(v.kind) ? v.kind : null;
1688
- if (!kind) return null;
1689
- const discoveredBy = v.discoveredBy === void 0 ? "scan" : isNamespaceDiscoverySource(v.discoveredBy) ? v.discoveredBy : null;
1690
- if (!discoveredBy) return null;
1691
- const record = {
1692
- namespace,
1693
- identityToken: expectedIdentityToken,
1694
- kind,
1695
- createdAt: v.createdAt,
1696
- storageDir: v.storageDir,
1697
- discoveredBy
1698
- };
1699
- if (typeof v.principal === "string") record.principal = v.principal;
1700
- if (typeof v.projectId === "string") record.projectId = v.projectId;
1701
- if (typeof v.branch === "string") record.branch = v.branch;
1702
- if (typeof v.parentNamespace === "string") record.parentNamespace = v.parentNamespace;
1703
- if (typeof v.lastReadAt === "string" && isValidIsoTimestamp(v.lastReadAt)) {
1704
- record.lastReadAt = v.lastReadAt;
1705
- }
1706
- if (typeof v.lastWriteAt === "string" && isValidIsoTimestamp(v.lastWriteAt)) {
1707
- record.lastWriteAt = v.lastWriteAt;
1708
- }
1709
- if (v.lastMaintenanceAt && typeof v.lastMaintenanceAt === "object") {
1710
- const out = {};
1711
- for (const [k, val] of Object.entries(v.lastMaintenanceAt)) {
1712
- if (typeof val === "string" && isValidIsoTimestamp(val)) out[k] = val;
1713
- }
1714
- if (Object.keys(out).length > 0) record.lastMaintenanceAt = out;
1715
- }
1716
- return record;
1717
- }
1718
- function laterIso(a, b) {
1719
- if (!a) return b;
1720
- if (!b) return a;
1721
- const am = Date.parse(a);
1722
- const bm = Date.parse(b);
1723
- if (!Number.isFinite(am)) return b;
1724
- if (!Number.isFinite(bm)) return a;
1725
- return bm > am ? b : a;
1726
- }
1727
- function mergeNewerTouchFields(base, fresh) {
1728
- const merged = { ...base };
1729
- const lr = laterIso(base.lastReadAt, fresh.lastReadAt);
1730
- if (lr) merged.lastReadAt = lr;
1731
- const lw = laterIso(base.lastWriteAt, fresh.lastWriteAt);
1732
- if (lw) merged.lastWriteAt = lw;
1733
- if (base.lastMaintenanceAt || fresh.lastMaintenanceAt) {
1734
- const jobs = { ...base.lastMaintenanceAt ?? {} };
1735
- for (const [job, ts] of Object.entries(fresh.lastMaintenanceAt ?? {})) {
1736
- const latest = laterIso(jobs[job], ts);
1737
- if (latest) jobs[job] = latest;
1738
- }
1739
- if (Object.keys(jobs).length > 0) merged.lastMaintenanceAt = jobs;
1740
- }
1741
- return merged;
1742
- }
1743
- function serializeRecord(record) {
1744
- const ordered = {};
1745
- const source = record;
1746
- for (const key of Object.keys(source).sort()) {
1747
- const value = source[key];
1748
- if (value === void 0) continue;
1749
- if (key === "lastMaintenanceAt" && value && typeof value === "object") {
1750
- const sortedJobs = {};
1751
- for (const jobKey of Object.keys(value).sort()) {
1752
- sortedJobs[jobKey] = value[jobKey];
1753
- }
1754
- ordered[key] = sortedJobs;
1755
- continue;
1756
- }
1757
- ordered[key] = value;
1758
- }
1759
- return JSON.stringify(ordered);
1760
- }
1761
- function inferKind(namespace, config) {
1762
- if (namespace === normalizeNamespaceIdentity(config.defaultNamespace)) return "default";
1763
- if (namespace === normalizeNamespaceIdentity(config.sharedNamespace)) return "shared";
1764
- if (config.namespacePolicies.some((p) => normalizeNamespaceIdentity(p.name) === namespace)) {
1765
- return "explicit";
1766
- }
1767
- if (/-branch-|^project-[^-]+-branch-/.test(namespace) || namespace.includes("-branch-")) {
1768
- return "branch";
1769
- }
1770
- if (/^team-.*-project-/.test(namespace) || /^team-.*project-/.test(namespace)) {
1771
- return "team-project";
1772
- }
1773
- if (/^project-/.test(namespace) || /-project-/.test(namespace)) {
1774
- return "project";
1775
- }
1776
- return "explicit";
1777
- }
1778
- var NamespaceCatalog = class {
1779
- constructor(config) {
1780
- this.config = config;
1781
- this.memoryDir = config.memoryDir;
1782
- this.stateDir = path2.join(this.memoryDir, STATE_DIR);
1783
- this.catalogPath = path2.join(this.stateDir, CATALOG_FILE);
1784
- this.rebuildLockPath = path2.join(this.stateDir, REBUILD_LOCK_FILE);
1785
- this.defaultNamespaceIdentity = normalizeNamespaceIdentity(config.defaultNamespace);
1786
- }
1787
- config;
1788
- memoryDir;
1789
- stateDir;
1790
- catalogPath;
1791
- rebuildLockPath;
1792
- // Per-INSTANCE lock owner id (round 6, codex P2 — NBsGP). The rebuild lock
1793
- // file records this id, not just `process.pid`, so two NamespaceCatalog
1794
- // instances in the SAME process sharing a memoryDir are NOT mistaken for each
1795
- // other: a touch on instance B must still wait for instance A's rebuild lock
1796
- // (different owner id, same PID) instead of skipping as "self-held".
1797
- lockOwnerId = randomUUID();
1798
- // Serialized write chain that recovers from rejection (CLAUDE.md rule #40)
1799
- // so a single failed append cannot permanently poison subsequent writes.
1800
- writeChain = Promise.resolve();
1801
- // Test-only seam (round 7 — NEZkA): fires inside a touch's HELD-lock critical
1802
- // section, after the lock is acquired but BEFORE the read→merge→append. A
1803
- // deterministic concurrency test installs a hook here to widen the (otherwise
1804
- // microscopic) window and prove that a cross-process rebuild CANNOT run its
1805
- // load→rename while a touch holds the lock. Never set in production code.
1806
- onTouchCriticalSectionForTest;
1807
- // Test-only seam (round 7 — NEZkA): fires inside a mutating rebuild's HELD-lock
1808
- // critical section, after the final cross-process re-merge `loadCompacted()` and
1809
- // BEFORE the atomic `rename()`. This is the EXACT window in which a check-then-
1810
- // append touch (the old bug) would clobber its append. A deterministic test
1811
- // installs a hook here to attempt a cross-instance touch in this window and
1812
- // assert the held mutex blocks it. Never set in production code.
1813
- onRebuildBeforeRenameForTest;
1814
- // Test-only seam (NFgCT, codex P2): fires AFTER the lockless disk scan but
1815
- // BEFORE the rebuild acquires the cross-process file lock for its final
1816
- // load→merge→rename window. A deterministic test installs a hook here to attempt
1817
- // a cross-instance touch DURING the scan window and assert it is NOT blocked or
1818
- // dropped — proving the scan no longer holds the mutex. Never set in production.
1819
- onRebuildAfterScanForTest;
1820
- // Test-only seam (NG7Bg, codex P2): fires inside `breakStaleRebuildLock` AFTER it
1821
- // has judged the lock stale and captured its identity, but BEFORE the final
1822
- // re-validation+unlink. A deterministic test installs a hook here to REPLACE the
1823
- // lock file (a fresh holder created a new lock in the race window) and assert the
1824
- // break is skipped — the replacement's active lock is not deleted. Never set in
1825
- // production.
1826
- onBeforeBreakStaleUnlinkForTest;
1827
- // Normalized (trimmed) default namespace identity (NH-FH, cursor Medium).
1828
- // Catalog records key namespaces by their NORMALIZED identity
1829
- // (`normalizeNamespaceIdentity`), but several default-namespace exemptions and
1830
- // memoryDir-ownership checks compared against the RAW `config.defaultNamespace`.
1831
- // If the configured default name carries surrounding whitespace the record key
1832
- // is trimmed while the comparison string is not, so the default row is
1833
- // misclassified, dropped at read time, or given the wrong storage root. Compare
1834
- // against this normalized form everywhere instead.
1835
- defaultNamespaceIdentity;
1836
- /** Whether the catalog is active (namespaces enabled and catalog not opted out). */
1837
- get enabled() {
1838
- return isCatalogEnabled(this.config);
1839
- }
1840
- // ── Public enumeration API ──────────────────────────────────────────────
1841
- /**
1842
- * Sanitize a record at the enumeration boundary (round 5, cursor Medium + codex
1843
- * P2; round 6 — NDXHe). Reads return whatever is in `namespaces.jsonl` after
1844
- * schema checks only, so a tampered or pre-fix row could surface unsafe data to
1845
- * maintenance/QMD until a rewrite occurs. Two distinct defenses:
1846
- *
1847
- * 1. UNSAFE NAMESPACE NAME (NGZqr, codex P2): an unsafe non-default namespace
1848
- * (e.g. `../evil`, a name with separators, or >64 chars) is REJECTED outright
1849
- * — return `null` so the caller drops it. The disk SCAN and the hot touch
1850
- * path both reject such names with the SAME default-exempt `isSafeRouteNamespace`
1851
- * gate, so the read boundary MUST agree, or `listNamespaces()`/`getNamespaceRecord()`
1852
- * would expose a namespace those paths reject (note `isStorageDirForNamespace`
1853
- * can still build a tokenized root even for `../evil`, so storageDir sanitation
1854
- * alone does not catch it). The default namespace is exempt (it may be a
1855
- * non-route literal), matching every other validation site.
1856
- *
1857
- * 2. UNSAFE storageDir: for an otherwise-valid namespace, apply the SAME contract
1858
- * as the write path — full containment (`isContainedStorageDir`: lexical +
1859
- * symlink/realpath) AND namespace ownership (`isStorageDirForNamespace`). When
1860
- * a record fails EITHER check we substitute the trusted resolved-and-safe root
1861
- * for that namespace (rule 42: read and write stay symmetric).
1862
- */
1863
- async sanitizeRecordForRead(record) {
1864
- if (record.namespace !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(record.namespace)) {
1865
- return null;
1866
- }
1867
- if (await this.isContainedStorageDir(record.storageDir) && await this.isStorageDirForNamespace(record.namespace, record.storageDir)) {
1868
- return record;
1869
- }
1870
- const safe = await this.resolveSafeStorageDir(record.namespace);
1871
- return { ...record, storageDir: safe };
1872
- }
1873
- storageRootOwnershipRank(record, resolvedStorageDir, configured) {
1874
- if (resolvedStorageDir === path2.resolve(this.memoryDir)) {
1875
- return record.namespace === this.defaultNamespaceIdentity ? 0 : 3;
1876
- }
1877
- const leaf = path2.basename(resolvedStorageDir);
1878
- const tokenOwnsRoot = namespaceIdentityToken(record.namespace) === leaf;
1879
- if (tokenOwnsRoot && configured.has(record.namespace)) {
1880
- return 0;
1881
- }
1882
- if (record.namespace === leaf) return 1;
1883
- if (tokenOwnsRoot) return 2;
1884
- return 3;
1885
- }
1886
- configuredNamespaceIdentities() {
1887
- return new Set(
1888
- [
1889
- this.config.defaultNamespace,
1890
- this.config.sharedNamespace,
1891
- ...this.config.namespacePolicies.map((p) => p.name)
1892
- ].map((n) => normalizeNamespaceIdentity(n)).filter((n) => n.length > 0)
1893
- );
1894
- }
1895
- preferStorageRootOwner(current, candidate, resolvedStorageDir, configured) {
1896
- const currentRank = this.storageRootOwnershipRank(current, resolvedStorageDir, configured);
1897
- const candidateRank = this.storageRootOwnershipRank(candidate, resolvedStorageDir, configured);
1898
- if (candidateRank < currentRank) return candidate;
1899
- if (candidateRank > currentRank) return current;
1900
- const byName = candidate.namespace.localeCompare(current.namespace);
1901
- if (byName < 0) return candidate;
1902
- if (byName > 0) return current;
1903
- return candidate.identityToken.localeCompare(current.identityToken) < 0 ? candidate : current;
1904
- }
1905
- dropDuplicateStorageRootAliases(records) {
1906
- const byStorageDir = /* @__PURE__ */ new Map();
1907
- const configured = this.configuredNamespaceIdentities();
1908
- for (const record of records) {
1909
- const resolvedStorageDir = path2.resolve(record.storageDir);
1910
- const current = byStorageDir.get(resolvedStorageDir);
1911
- if (!current) {
1912
- byStorageDir.set(resolvedStorageDir, record);
1913
- continue;
1914
- }
1915
- const owner = this.preferStorageRootOwner(current, record, resolvedStorageDir, configured);
1916
- const alias = owner === current ? record : current;
1917
- byStorageDir.set(resolvedStorageDir, mergeNewerTouchFields(owner, alias));
1918
- }
1919
- return [...byStorageDir.values()];
1920
- }
1921
- async loadSanitizedRecords() {
1922
- const records = await this.loadCompacted();
1923
- const sanitized = await Promise.all(
1924
- [...records.values()].map((r) => this.sanitizeRecordForRead(r))
1925
- );
1926
- return this.dropDuplicateStorageRootAliases(
1927
- sanitized.filter((r) => r !== null)
1928
- );
1929
- }
1930
- async listNamespaces(filter) {
1931
- if (!this.enabled) return [];
1932
- let out = await this.loadSanitizedRecords();
1933
- if (filter?.kind) out = out.filter((r) => r.kind === filter.kind);
1934
- if (filter?.discoveredBy) out = out.filter((r) => r.discoveredBy === filter.discoveredBy);
1935
- if (filter?.writtenSince) {
1936
- const sinceMs = filter.writtenSince.getTime();
1937
- out = out.filter((r) => {
1938
- if (!r.lastWriteAt) return false;
1939
- const ms = Date.parse(r.lastWriteAt);
1940
- return Number.isFinite(ms) && ms >= sinceMs;
1941
- });
1942
- }
1943
- return out.sort((a, b) => {
1944
- const byName = a.namespace.localeCompare(b.namespace);
1945
- if (byName !== 0) return byName;
1946
- return a.identityToken.localeCompare(b.identityToken);
1947
- });
1948
- }
1949
- async getNamespaceRecord(namespace) {
1950
- if (!this.enabled) return null;
1951
- const ns = normalizeNamespaceIdentity(namespace);
1952
- return (await this.loadSanitizedRecords()).find((record) => record.namespace === ns) ?? null;
1953
- }
1954
- // ── Touch API (cheap, failure-tolerant) ─────────────────────────────────
1955
- async markRead(namespace, metadata) {
1956
- await this.touch(namespace, "read", metadata);
1957
- }
1958
- async markWrite(namespace, metadata) {
1959
- await this.touch(namespace, "write", metadata);
1960
- }
1961
- async markMaintenance(namespace, jobName, at) {
1962
- if (typeof jobName !== "string" || jobName.trim().length === 0) {
1963
- throw new Error("markMaintenance requires a non-empty jobName");
1964
- }
1965
- await this.touch(namespace, "maintenance", { at }, jobName.trim());
1966
- }
1967
- /**
1968
- * Register namespaces known purely from config (default, shared, explicit
1969
- * policies). Source `config`. Cheap and idempotent.
1970
- */
1971
- async registerConfiguredNamespaces() {
1972
- if (!this.enabled) return;
1973
- const names = /* @__PURE__ */ new Set([
1974
- this.config.defaultNamespace,
1975
- this.config.sharedNamespace,
1976
- ...this.config.namespacePolicies.map((p) => p.name)
1977
- ]);
1978
- for (const ns of names) {
1979
- if (!ns) continue;
1980
- if (normalizeNamespaceIdentity(ns) !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {
1981
- continue;
1982
- }
1983
- try {
1984
- await this.register(ns, { discoveredBy: "config" });
1985
- } catch {
1986
- }
1987
- }
1988
- }
1989
- /**
1990
- * Register a namespace whose storage was just resolved by the router. Used as
1991
- * the router's integration hook (`discoveredBy: config`). Storage dir is
1992
- * provided so we do not re-resolve it. Failure-tolerant. Returns whether the
1993
- * registration actually APPENDED (round 6, codex P2 — NEFoX), so the router's
1994
- * resolve-hook dedup only marks a namespace notified when it truly persisted —
1995
- * a dropped append (disabled catalog or rebuild-lock-timeout drop) returns
1996
- * `false` and is retried on the next resolve.
1997
- */
1998
- async registerResolved(namespace, storageDir) {
1999
- if (!this.enabled) return false;
2000
- return this.register(namespace, { discoveredBy: "config", storageDir });
2001
- }
2002
- /**
2003
- * Generic register/touch without changing read/write timestamps unless the
2004
- * source implies it. Validates the namespace and resolves a storage dir.
2005
- * Returns whether the touch actually appended.
2006
- */
2007
- async register(namespace, metadata) {
2008
- return this.touch(namespace, "register", metadata);
2009
- }
2010
- validateNamespace(namespace) {
2011
- const ns = normalizeNamespaceIdentity(namespace);
2012
- if (ns.length === 0) throw new Error("empty namespace");
2013
- if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {
2014
- throw new Error(`unsafe namespace: ${ns}`);
2015
- }
2016
- return ns;
2017
- }
2018
- /**
2019
- * Resolve the on-disk storage dir for a namespace WITHOUT trusting caller
2020
- * input. The default namespace may use the legacy memoryDir root; everything
2021
- * else lives under `<memoryDir>/namespaces/<token>`. Containment is enforced
2022
- * by rejecting separators/parent-refs in the token.
2023
- */
2024
- resolveStorageDir(namespace) {
2025
- if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {
2026
- return this.memoryDir;
2027
- }
2028
- const token = namespaceIdentityToken(namespace);
2029
- return this.namespaceTokenDir(token);
2030
- }
2031
- namespaceTokenDir(token) {
2032
- if (token.length === 0 || token.includes("/") || token.includes("\\") || token.includes("..") || path2.isAbsolute(token)) {
2033
- throw new Error(`unsafe namespace token: ${token}`);
2034
- }
2035
- return path2.join(this.memoryDir, "namespaces", token);
2036
- }
2037
- /**
2038
- * Whether a candidate storage dir is LEXICALLY contained: it is either the
2039
- * legacy default root (`memoryDir`) or a strict descendant of
2040
- * `<memoryDir>/namespaces/`. The router legitimately resolves a namespace to
2041
- * EITHER the tokenized dir or a legacy raw-name dir under `namespaces/`, so we
2042
- * accept any contained child rather than a single exact token path. This is a
2043
- * pure string check — symlink escape is checked separately via realpath.
2044
- */
2045
- isLexicallyContained(candidate) {
2046
- const resolved = path2.resolve(candidate);
2047
- if (resolved === path2.resolve(this.memoryDir)) return true;
2048
- const nsBase = path2.resolve(path2.join(this.memoryDir, "namespaces"));
2049
- const rel = path2.relative(nsBase, resolved);
2050
- return rel.length > 0 && !rel.startsWith("..") && !path2.isAbsolute(rel);
2051
- }
2052
- /**
2053
- * Whether a candidate storage dir satisfies the catalog containment contract,
2054
- * including SYMLINK-escape rejection (round 5, codex P2). A lexically-contained
2055
- * path that is actually a symlink to an outside directory would let maintenance
2056
- * or QMD follow it outside `memoryDir`. We mirror `rebuildFromDisk`'s posture:
2057
- * the path must be lexically contained AND, if it exists on disk, neither the
2058
- * path itself a symlink nor its realpath escaping the memory root. Non-existent
2059
- * paths pass the realpath stage (nothing to follow yet) but still must be
2060
- * lexically contained.
2061
- */
2062
- async isContainedStorageDir(candidate) {
2063
- if (!this.isLexicallyContained(candidate)) return false;
2064
- if (path2.resolve(candidate) === path2.resolve(this.memoryDir)) return true;
2065
- let memoryReal;
2066
- try {
2067
- memoryReal = await realpath(this.memoryDir);
2068
- } catch {
2069
- memoryReal = path2.resolve(this.memoryDir);
2070
- }
2071
- if (await this.hasSymlinkedAncestor(candidate)) return false;
2072
- try {
2073
- const stat4 = await lstat(candidate);
2074
- if (stat4.isSymbolicLink()) return false;
2075
- if (!stat4.isDirectory()) return false;
2076
- } catch {
2077
- return this.isNearestExistingAncestorContained(candidate, memoryReal);
2078
- }
2079
- try {
2080
- const real = await realpath(candidate);
2081
- return isPathInside(memoryReal, real);
2082
- } catch {
2083
- return false;
2084
- }
2085
- }
2086
- /**
2087
- * Reject a candidate whose path crosses a SYMLINKED ancestor strictly between
2088
- * memoryDir and the leaf (codex NVuq5). `realpath`-based containment accepts a
2089
- * symlinked `<memoryDir>/namespaces` that currently resolves back inside
2090
- * memoryDir, but the disk scanner rejects such a root and a later retarget would
2091
- * escape the memory tree — so refuse it here too. The leaf itself is
2092
- * symlink-checked by the caller; this walks only the intermediate ancestors.
2093
- */
2094
- async hasSymlinkedAncestor(candidate) {
2095
- const stopAt = path2.resolve(this.memoryDir);
2096
- let dir = path2.dirname(path2.resolve(candidate));
2097
- const root = path2.parse(dir).root;
2098
- while (dir !== stopAt && dir !== root && dir !== path2.dirname(dir)) {
2099
- try {
2100
- if ((await lstat(dir)).isSymbolicLink()) return true;
2101
- } catch {
2102
- }
2103
- dir = path2.dirname(dir);
2104
- }
2105
- return false;
2106
- }
2107
- /**
2108
- * Walk up from a not-yet-existing candidate to the nearest ancestor that exists
2109
- * on disk and verify its realpath stays inside `memoryReal` (round 6, codex P2
2110
- * — NDo79). Rejects a non-existent leaf whose existing parent chain escapes
2111
- * memoryDir via a symlink. Stops at memoryDir's resolved root.
2112
- *
2113
- * The nearest existing ancestor must also be a DIRECTORY (NHIdt, codex P2): if
2114
- * an existing parent such as `<memoryDir>/namespaces` is a regular FILE (or
2115
- * socket/fifo), `realpath(parent)` still succeeds and resolves inside memoryDir,
2116
- * so a containment-only check would ACCEPT a leaf that can never be created — you
2117
- * cannot mkdir a child under a file. We `lstat` the nearest existing ancestor and
2118
- * reject when it is not a directory, mirroring the leaf non-directory rejection
2119
- * (NF21i) and the disk scan, so every containment consumer agrees.
2120
- */
2121
- async isNearestExistingAncestorContained(candidate, memoryReal) {
2122
- let dir = path2.resolve(candidate);
2123
- const root = path2.parse(dir).root;
2124
- for (; ; ) {
2125
- const parent = path2.dirname(dir);
2126
- if (parent === dir || dir === root) return false;
2127
- let real;
2128
- try {
2129
- real = await realpath(parent);
2130
- } catch {
2131
- dir = parent;
2132
- continue;
2133
- }
2134
- if (!(isPathInside(memoryReal, real) || real === memoryReal)) return false;
2135
- try {
2136
- const stat4 = await lstat(real);
2137
- return stat4.isDirectory();
2138
- } catch {
2139
- return false;
2140
- }
2141
- }
2142
- }
2143
- /**
2144
- * Resolve the storage dir to persist for a touch, validating any caller-
2145
- * provided `metadata.storageDir` against the catalog containment contract
2146
- * (round 4 + round 5, codex P2). `markWrite`/`registerResolved` accept an
2147
- * explicit storageDir, but persisting it verbatim would let a bad hook or
2148
- * external consumer write an arbitrary path — including one outside `memoryDir`
2149
- * or a symlink that escapes it — into the catalog, handing maintenance/QMD an
2150
- * unsafe root. We accept an explicit (or previously-stored) dir ONLY when it
2151
- * stays contained under memoryDir (lexically AND via realpath); otherwise we
2152
- * drop it and fall back to the trusted resolved dir.
2153
- */
2154
- async resolveTouchStorageDir(namespace, explicit, existingDir) {
2155
- if (explicit !== void 0 && await this.isContainedStorageDir(explicit) && await this.isStorageDirForNamespace(namespace, explicit)) {
2156
- return explicit;
2157
- }
2158
- if (existingDir !== void 0 && await this.isContainedStorageDir(existingDir) && await this.isStorageDirForNamespace(namespace, existingDir)) {
2159
- return existingDir;
2160
- }
2161
- return this.resolveSafeStorageDir(namespace);
2162
- }
2163
- /**
2164
- * Whether `candidate` is a legitimate storage root FOR `namespace` (round 6,
2165
- * codex P2 — NDATT). Accepts the namespace's router-resolved root, its canonical
2166
- * lexical tokenized dir, and (for the default namespace only) memoryDir. This
2167
- * prevents a contained-but-CROSS-NAMESPACE path — another namespace's tree, or
2168
- * memoryDir for a non-default namespace — from being persisted as this
2169
- * namespace's root. Compared on resolved (absolute) paths.
2170
- */
2171
- async isStorageDirForNamespace(namespace, candidate) {
2172
- const resolvedCandidate = path2.resolve(candidate);
2173
- const valid = /* @__PURE__ */ new Set();
2174
- try {
2175
- valid.add(path2.resolve(this.namespaceTokenDir(namespaceIdentityToken(namespace))));
2176
- } catch {
2177
- }
2178
- try {
2179
- valid.add(path2.resolve(this.namespaceTokenDir(namespace)));
2180
- } catch {
2181
- }
2182
- try {
2183
- valid.add(path2.resolve(await resolveNamespaceStorageRoot(this.config, namespace)));
2184
- } catch {
2185
- }
2186
- if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {
2187
- valid.add(path2.resolve(this.memoryDir));
2188
- try {
2189
- valid.add(path2.resolve(await resolveDefaultNamespaceRoot(this.config)));
2190
- } catch {
2191
- }
2192
- }
2193
- return valid.has(resolvedCandidate);
2194
- }
2195
- /**
2196
- * Resolve the canonical storage dir for a namespace as the LIVE ROUTER would,
2197
- * but NEVER return a path that escapes the memory root.
2198
- *
2199
- * Router alignment (round 4, cursor Medium): a read/register touch with no
2200
- * explicit storageDir previously used the lexical `resolveStorageDir`, which
2201
- * always picks `<memoryDir>/namespaces/<token>` (or `memoryDir` for the
2202
- * default). That diverges from `NamespaceStorageRouter`, which can route to a
2203
- * legacy raw-name dir or a migrated default root — so a recall touch could
2204
- * record a contained-but-WRONG root that maintenance/rebuild then targets. We
2205
- * now delegate to the shared `resolveNamespaceStorageRoot` (the very helper the
2206
- * router uses) so the catalog records the same on-disk root the router serves.
2207
- *
2208
- * Containment (round 5, codex P2): the resolved path can still be a symlink
2209
- * escaping memoryDir, so we run the full (lexical + realpath) containment
2210
- * contract. When it FAILS we fall back to a NAMESPACE-SPECIFIC safe root, NOT
2211
- * a blanket `memoryDir`. Recording `memoryDir` for a non-default namespace
2212
- * would point enumeration/maintenance at the DEFAULT namespace's tree (round 5,
2213
- * cursor/codex Medium/P2) — a cross-namespace fanout error. The correct safe
2214
- * root is the namespace's own lexical tokenized dir
2215
- * (`<memoryDir>/namespaces/<token>`), which is always contained and is that
2216
- * namespace's canonical location (we record the lexical PATH as metadata; we do
2217
- * not follow the escaping symlink). Only the default namespace — or a token so
2218
- * unsafe even the lexical dir cannot be built — falls back to `memoryDir`.
2219
- */
2220
- async resolveSafeStorageDir(namespace) {
2221
- let resolved;
2222
- try {
2223
- resolved = await resolveNamespaceStorageRoot(this.config, namespace);
2224
- } catch {
2225
- return this.safeFallbackStorageDir(namespace);
2226
- }
2227
- if (await this.isContainedStorageDir(resolved)) return resolved;
2228
- return this.safeFallbackStorageDir(namespace);
2229
- }
2230
- /**
2231
- * The namespace-specific contained fallback root, used when the router-resolved
2232
- * root fails containment (round 5, cursor/codex Medium/P2).
2233
- *
2234
- * Preference order:
2235
- * 1. The namespace's OWN lexical tokenized dir (`namespaces/<token>`) — so a
2236
- * non-default namespace is NOT pointed at the DEFAULT namespace's `memoryDir`
2237
- * tree (which would misdirect maintenance fanout). Returned only when the
2238
- * token dir itself stays CONTAINED (it is not a symlink, and its realpath
2239
- * does not escape memoryDir — e.g. via a symlinked `namespaces/` parent).
2240
- * 2. `memoryDir` as a LAST resort — for the default namespace, an unsafe token
2241
- * that cannot build a contained path, OR the irreparable case where the
2242
- * token dir's realpath escapes the root (so even its lexical path resolves
2243
- * outside). NF21m note (codex P2): we deliberately do NOT record the lexical
2244
- * token dir in that irreparable case — its realpath escapes memoryDir, and
2245
- * the NDo79 contract REQUIRES that an escaping path is never persisted (a
2246
- * later mkdir/maintenance/QMD op would follow it outside the root). Since no
2247
- * contained namespace-specific path exists, containment wins: `memoryDir` is
2248
- * the only safe root left. A namespace whose token dir's realpath escapes is
2249
- * an irreparable on-disk state; recording the contained default root is
2250
- * strictly safer than persisting an escaping one. The common case where the
2251
- * token dir IS contained is handled by branch 1, so a healthy non-default
2252
- * namespace never reaches `memoryDir`.
2253
- */
2254
- async safeFallbackStorageDir(namespace) {
2255
- if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) return this.memoryDir;
2256
- let tokenDir;
2257
- try {
2258
- tokenDir = this.namespaceTokenDir(namespaceIdentityToken(namespace));
2259
- } catch {
2260
- return this.memoryDir;
2261
- }
2262
- if (await this.isContainedStorageDir(tokenDir)) return tokenDir;
2263
- return this.memoryDir;
2264
- }
2265
- /**
2266
- * Re-check, NOW, whether a namespace's storage root currently EXISTS on disk
2267
- * with the SAME safety the directory scan uses (NFJV8, codex P2).
2268
- *
2269
- * The rebuild's final re-merge runs under the held lock and folds the freshly
2270
- * re-read log (`latest`) into the scanned `rebuilt` set. A namespace present in
2271
- * `latest` (a live touch row) but ABSENT from `rebuilt` is normally PURGED as
2272
- * deleted (the NATqU "disk scan is authoritative" rule). But there is a TOCTOU
2273
- * window: a dynamic namespace can be CREATED on disk AFTER `rebuildFromDisk()`
2274
- * already enumerated `namespaces/` but BEFORE this re-merge. The scan snapshot
2275
- * missed its new root, yet a gateway `markWrite` already appended a row for it.
2276
- * Blindly purging that row would rewrite the catalog WITHOUT a live namespace
2277
- * that now has data on disk, so `writtenSince`/maintenance/QMD consumers miss
2278
- * it until another touch or rebuild.
2279
- *
2280
- * So before purging, we re-resolve the namespace's safe storage root (the same
2281
- * router-aligned, containment-checked path the scan would have catalogued) and
2282
- * confirm it is a real, contained, non-symlink directory that actually holds
2283
- * memory data RIGHT NOW. If so the namespace was created-after-scan and is LIVE
2284
- * — KEEP its row. This is the precise inverse of NATqU and does NOT reintroduce
2285
- * it: a touch on a REMOVED root re-checks as ABSENT (no data on disk) and is
2286
- * still purged; only a root that EXISTS on a fresh re-check is kept.
2287
- *
2288
- * Mirrors the per-entry scan checks (symlink rejection + realpath containment +
2289
- * `hasMemoryData`) so a symlinked/escaping root is never resurrected.
2290
- */
2291
- async liveStorageRootExistsForRebuild(namespace, memoryReal) {
2292
- let root;
2293
- try {
2294
- root = await this.resolveSafeStorageDir(namespace);
2295
- } catch {
2296
- return false;
2297
- }
2298
- if (normalizeNamespaceIdentity(namespace) !== this.defaultNamespaceIdentity && path2.resolve(root) === path2.resolve(this.memoryDir)) {
2299
- return false;
2300
- }
2301
- let stat4;
2302
- try {
2303
- stat4 = await lstat(root);
2304
- } catch {
2305
- return false;
2306
- }
2307
- if (stat4.isSymbolicLink()) return false;
2308
- if (!stat4.isDirectory()) return false;
2309
- try {
2310
- const real = await realpath(root);
2311
- if (memoryReal && !isPathInside(memoryReal, real)) return false;
2312
- } catch {
2313
- return false;
2314
- }
2315
- return hasMemoryData(root);
2316
- }
2317
- /**
2318
- * Record a namespace touch. Returns whether the touch actually APPENDED to the
2319
- * log (round 6, codex P2 — NEFoX): a disabled catalog or a dropped append (the
2320
- * NAUf7 rebuild-lock-timeout drop) returns `false`, so callers (e.g. the router
2321
- * resolve-hook dedup) can avoid marking a dropped registration as completed and
2322
- * suppressing its retry.
2323
- */
2324
- async touch(namespace, kind, metadata, jobName) {
2325
- if (!this.enabled) return false;
2326
- const ns = this.validateNamespace(namespace);
2327
- const nowIso = (metadata?.at ?? /* @__PURE__ */ new Date()).toISOString();
2328
- return this.queueCritical(
2329
- async () => this.withHeldCatalogLock(async (acquired) => {
2330
- if (!acquired) return false;
2331
- if (this.onTouchCriticalSectionForTest) {
2332
- await this.onTouchCriticalSectionForTest();
2333
- }
2334
- const records = await this.loadCompacted();
2335
- const existing = records.get(ns);
2336
- const storageDir = await this.resolveTouchStorageDir(
2337
- ns,
2338
- metadata?.storageDir,
2339
- existing?.storageDir
2340
- );
2341
- const record = existing ? { ...existing } : {
2342
- namespace: ns,
2343
- identityToken: namespaceIdentityToken(ns),
2344
- kind: metadata?.kind ?? inferKind(ns, this.config),
2345
- createdAt: nowIso,
2346
- storageDir,
2347
- discoveredBy: metadata?.discoveredBy ?? (kind === "register" ? "config" : kind === "maintenance" ? "scan" : kind)
2348
- };
2349
- record.storageDir = storageDir;
2350
- if (metadata?.kind) record.kind = metadata.kind;
2351
- if (metadata?.principal !== void 0) record.principal = metadata.principal;
2352
- if (metadata?.projectId !== void 0) record.projectId = metadata.projectId;
2353
- if (metadata?.branch !== void 0) record.branch = metadata.branch;
2354
- if (metadata?.parentNamespace !== void 0)
2355
- record.parentNamespace = metadata.parentNamespace;
2356
- if (kind === "write" && existing && record.discoveredBy === "config") {
2357
- record.discoveredBy = "write";
2358
- }
2359
- if (kind === "read") record.lastReadAt = nowIso;
2360
- if (kind === "write") record.lastWriteAt = nowIso;
2361
- if (kind === "maintenance" && jobName) {
2362
- record.lastMaintenanceAt = { ...record.lastMaintenanceAt ?? {}, [jobName]: nowIso };
2363
- }
2364
- await this.appendUnchained(record);
2365
- return true;
2366
- })
2367
- );
2368
- }
2369
- // ── Rebuild from disk ────────────────────────────────────────────────────
2370
- async rebuildFromDisk(options) {
2371
- const dryRun = options?.dryRun === true;
2372
- if (!this.enabled) {
2373
- return { dryRun, records: [], skipped: [], applied: false };
2374
- }
2375
- if (dryRun) {
2376
- return this.queueCritical(async () => this.rebuildInsideChain(dryRun, false));
2377
- }
2378
- return this.queueCritical(async () => this.rebuildInsideChain(dryRun, true));
2379
- }
2380
- /**
2381
- * Body of `rebuildFromDisk`, run inside a single `queueCritical` turn. MUST
2382
- * only be invoked from within the serialized chain so the load and the
2383
- * rewrite are atomic with respect to concurrent touches (in-process).
2384
- *
2385
- * `wantMutate` is true for an `--apply` (the caller intends to rewrite). The
2386
- * cross-process file lock is acquired LATE — only around the final
2387
- * load→merge→rename window (NFgCT, codex P2) — never across the disk scan, so a
2388
- * long scan does not force concurrent gateway touches to wait (and drop their
2389
- * append). Whether the rewrite actually happened is reported via the result's
2390
- * `applied`: true only when `wantMutate` AND the lock was acquired.
2391
- */
2392
- async rebuildInsideChain(dryRun, wantMutate) {
2393
- const existing = await this.loadCompacted();
2394
- const skipped = [];
2395
- const rebuilt = /* @__PURE__ */ new Map();
2396
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2397
- let memoryReal = null;
2398
- try {
2399
- memoryReal = await realpath(this.memoryDir);
2400
- } catch {
2401
- memoryReal = this.memoryDir;
2402
- }
2403
- const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
2404
- const configured = new Set(
2405
- [
2406
- this.config.defaultNamespace,
2407
- this.config.sharedNamespace,
2408
- ...this.config.namespacePolicies.map((p) => p.name)
2409
- ].map((n) => normalizeNamespaceIdentity(n)).filter((n) => n.length > 0)
2410
- );
2411
- const resolvedDefaultRoot = await resolveDefaultNamespaceRoot(this.config);
2412
- const defaultStorageDir = await this.isContainedStorageDir(resolvedDefaultRoot) ? resolvedDefaultRoot : this.memoryDir;
2413
- const legacyDefaultHasData = defaultStorageDir === this.memoryDir;
2414
- for (const ns of configured) {
2415
- if (!ns) continue;
2416
- if (ns !== defaultNs && !isSafeRouteNamespace(ns)) {
2417
- let token;
2418
- try {
2419
- token = namespaceIdentityToken(ns);
2420
- } catch {
2421
- token = ns;
2422
- }
2423
- skipped.push({ token, reason: "unsafe", detail: ns });
2424
- continue;
2425
- }
2426
- let storageDir;
2427
- if (ns === defaultNs) {
2428
- storageDir = defaultStorageDir;
2429
- } else {
2430
- try {
2431
- storageDir = await resolveNamespaceStorageRoot(this.config, ns);
2432
- } catch {
2433
- storageDir = this.namespaceTokenDir(namespaceIdentityToken(ns));
2434
- }
2435
- }
2436
- if (!await this.isContainedStorageDir(storageDir)) {
2437
- if (ns === defaultNs) {
2438
- storageDir = this.memoryDir;
2439
- } else {
2440
- skipped.push({ token: namespaceIdentityToken(ns), reason: "escape", detail: storageDir });
2441
- continue;
2442
- }
2443
- }
2444
- rebuilt.set(
2445
- ns,
2446
- this.mergeForRebuild(existing.get(ns), {
2447
- namespace: ns,
2448
- identityToken: namespaceIdentityToken(ns),
2449
- kind: inferKind(ns, this.config),
2450
- createdAt: existing.get(ns)?.createdAt ?? nowIso,
2451
- storageDir,
2452
- discoveredBy: "config"
2453
- })
2454
- );
2455
- }
2456
- const namespacesDir = path2.join(this.memoryDir, "namespaces");
2457
- let entries = [];
2458
- let namespacesDirSafe = true;
2459
- try {
2460
- const rootStat = await lstat(namespacesDir);
2461
- if (rootStat.isSymbolicLink()) {
2462
- namespacesDirSafe = false;
2463
- } else {
2464
- const realNamespacesDir = await realpath(namespacesDir);
2465
- if (memoryReal && !isPathInside(memoryReal, realNamespacesDir)) {
2466
- namespacesDirSafe = false;
2467
- }
2468
- }
2469
- } catch {
2470
- namespacesDirSafe = true;
2471
- }
2472
- if (!namespacesDirSafe) {
2473
- skipped.push({ token: "namespaces", reason: "symlink", detail: namespacesDir });
2474
- } else {
2475
- try {
2476
- entries = await readdir(namespacesDir, { withFileTypes: true });
2477
- } catch {
2478
- entries = [];
2479
- }
2480
- }
2481
- const scannedFromTokenized = /* @__PURE__ */ new Set();
2482
- for (const entry of entries) {
2483
- const token = entry.name;
2484
- const fullPath = path2.join(namespacesDir, token);
2485
- let stat4;
2486
- try {
2487
- stat4 = await lstat(fullPath);
2488
- } catch (err) {
2489
- skipped.push({ token, reason: "error", detail: err instanceof Error ? err.message : String(err) });
2490
- continue;
2491
- }
2492
- if (stat4.isSymbolicLink()) {
2493
- skipped.push({ token, reason: "symlink", detail: fullPath });
2494
- continue;
2495
- }
2496
- if (!stat4.isDirectory()) continue;
2497
- try {
2498
- const real = await realpath(fullPath);
2499
- if (memoryReal && !isPathInside(memoryReal, real)) {
2500
- skipped.push({ token, reason: "escape", detail: real });
2501
- continue;
2502
- }
2503
- } catch (err) {
2504
- skipped.push({ token, reason: "error", detail: err instanceof Error ? err.message : String(err) });
2505
- continue;
2506
- }
2507
- const literalRecord = existing.get(token);
2508
- const literalOwnsRoot = configured.has(token) || literalRecord !== void 0 && path2.resolve(literalRecord.storageDir) === path2.resolve(fullPath);
2509
- const tokenDecoded = literalOwnsRoot ? null : namespaceIdentityFromToken(token);
2510
- const rawDecoded = tokenDecoded && tokenDecoded.length > 0 ? tokenDecoded : token;
2511
- const decoded = normalizeNamespaceIdentity(rawDecoded);
2512
- if (decoded.length === 0 || rawDecoded !== decoded) {
2513
- skipped.push({ token, reason: "unsafe", detail: rawDecoded });
2514
- continue;
2515
- }
2516
- if (decoded !== defaultNs && !isSafeRouteNamespace(decoded)) {
2517
- skipped.push({ token, reason: "unsafe", detail: decoded });
2518
- continue;
2519
- }
2520
- const memoryData = await inspectMemoryDataRoot(fullPath);
2521
- if (memoryData.invalidMarker) {
2522
- skipped.push({
2523
- token,
2524
- reason: "unsafe",
2525
- detail: `invalid memory marker: ${memoryData.invalidMarker}`
2526
- });
2527
- continue;
2528
- }
2529
- if (!memoryData.hasData) continue;
2530
- if (decoded === defaultNs) {
2531
- const def = rebuilt.get(defaultNs);
2532
- if (def) {
2533
- def.storageDir = defaultStorageDir;
2534
- def.kind = "default";
2535
- }
2536
- continue;
2537
- }
2538
- const isTokenizedEntry = token === namespaceIdentityToken(decoded);
2539
- if (rebuilt.has(decoded) && scannedFromTokenized.has(decoded) && !isTokenizedEntry) {
2540
- continue;
2541
- }
2542
- if (isTokenizedEntry) scannedFromTokenized.add(decoded);
2543
- const prior = existing.get(decoded);
2544
- rebuilt.set(
2545
- decoded,
2546
- this.mergeForRebuild(prior, {
2547
- namespace: decoded,
2548
- identityToken: namespaceIdentityToken(decoded),
2549
- kind: inferKind(decoded, this.config),
2550
- createdAt: prior?.createdAt ?? nowIso,
2551
- storageDir: fullPath,
2552
- // Configured-and-present namespaces keep config provenance; purely
2553
- // discovered ones are scan.
2554
- discoveredBy: configured.has(decoded) ? "config" : prior?.discoveredBy ?? "scan"
2555
- })
2556
- );
2557
- }
2558
- if (legacyDefaultHasData && defaultStorageDir === this.memoryDir) {
2559
- const def = rebuilt.get(defaultNs);
2560
- if (def) def.kind = "default";
2561
- }
2562
- if (!wantMutate) {
2563
- return this.finishRebuild(rebuilt, skipped, dryRun, false, memoryReal, nowIso);
2564
- }
2565
- if (this.onRebuildAfterScanForTest) {
2566
- await this.onRebuildAfterScanForTest();
2567
- }
2568
- return this.withHeldCatalogLock(
2569
- (acquired) => this.finishRebuild(rebuilt, skipped, dryRun, acquired, memoryReal, nowIso)
2570
- );
2571
- }
2572
- /**
2573
- * Final load→merge→rename window of a rebuild, factored out so the caller can
2574
- * run it WITHIN the cross-process file lock (NFgCT, codex P2) without holding
2575
- * that lock across the preceding disk scan. Re-reads the latest on-disk state,
2576
- * folds concurrent touches, then (when `canMutate`) atomically rewrites the log.
2577
- *
2578
- * `canMutate` records that the cross-process lock was actually held. The
2579
- * re-merge + rewrite run only when it is true — a dry-run, or an unlocked apply
2580
- * (lock-acquisition timeout), computes records but does NOT rename, so it can
2581
- * never clobber a concurrent lock holder's window. `applied` mirrors `canMutate`.
2582
- */
2583
- async finishRebuild(rebuilt, skipped, dryRun, canMutate, memoryReal, nowIso) {
2584
- if (canMutate) {
2585
- const latest = await this.loadCompacted();
2586
- for (const [ns, fresh] of latest) {
2587
- const current = rebuilt.get(ns);
2588
- if (!current) {
2589
- if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {
2590
- continue;
2591
- }
2592
- if (await this.liveStorageRootExistsForRebuild(ns, memoryReal)) {
2593
- const safeDir = await this.resolveSafeStorageDir(ns);
2594
- const resolvedSafe = path2.resolve(safeDir);
2595
- let owningNamespace = null;
2596
- for (const [otherNs, otherRec] of rebuilt) {
2597
- if (otherNs !== ns && path2.resolve(otherRec.storageDir) === resolvedSafe) {
2598
- owningNamespace = otherNs;
2599
- break;
2600
- }
2601
- }
2602
- if (owningNamespace) {
2603
- const owner = rebuilt.get(owningNamespace);
2604
- if (owner) rebuilt.set(owningNamespace, mergeNewerTouchFields(owner, fresh));
2605
- continue;
2606
- }
2607
- rebuilt.set(ns, {
2608
- ...fresh,
2609
- storageDir: safeDir,
2610
- identityToken: namespaceIdentityToken(ns),
2611
- kind: fresh.kind ?? inferKind(ns, this.config),
2612
- createdAt: fresh.createdAt ?? nowIso
2613
- });
2614
- continue;
2615
- }
2616
- continue;
2617
- }
2618
- rebuilt.set(ns, mergeNewerTouchFields(current, fresh));
2619
- }
2620
- }
2621
- const records = [...rebuilt.values()].sort((a, b) => {
2622
- const byName = a.namespace.localeCompare(b.namespace);
2623
- if (byName !== 0) return byName;
2624
- return a.identityToken.localeCompare(b.identityToken);
2625
- });
2626
- if (canMutate) {
2627
- if (this.onRebuildBeforeRenameForTest) {
2628
- await this.onRebuildBeforeRenameForTest();
2629
- }
2630
- await this.rewriteUnchained(records);
2631
- }
2632
- return { dryRun, records, skipped, applied: canMutate };
2633
- }
2634
- // ── Cross-process catalog write lock (held mutex) ────────────────────────
2635
- /**
2636
- * Run `fn` while HOLDING the shared cross-process advisory lock (round 5, codex
2637
- * P2; generalized round 7 — NEZkA). This is the SINGLE mutex shared by BOTH the
2638
- * touch read→merge→append window AND the rebuild final load→merge→rename window,
2639
- * so a touch and a rebuild in different processes are mutually exclusive over
2640
- * their respective critical sections — closing the check-then-append gap where a
2641
- * polled-only touch could append into a rebuild's load→rename window.
2642
- *
2643
- * Acquisition is atomic via `open(..., "wx")`. A lock older than
2644
- * `REBUILD_LOCK_STALE_MS` is treated as a crashed holder and broken. After
2645
- * `REBUILD_LOCK_MAX_WAIT_MS` of contention we proceed best-effort WITHOUT the
2646
- * lock rather than block forever. The lock is always released in `finally`.
2647
- *
2648
- * IN-PROCESS SAFETY: every caller invokes this from inside (or wrapping) the
2649
- * per-process `queueCritical` chain, which serializes all catalog mutations in
2650
- * THIS process. So within one process only one logical holder attempts OS-lock
2651
- * acquisition at a time — the file lock is never self-contended in-process, and
2652
- * the lock is acquired and released within a single in-process turn. The file
2653
- * lock adds only the missing CROSS-process exclusion.
2654
- *
2655
- * HEARTBEAT (round 5, cursor/codex Medium/P2): while WE hold the lock a timer
2656
- * refreshes its mtime every `REBUILD_LOCK_HEARTBEAT_MS`, so a legitimately long
2657
- * holder (> `REBUILD_LOCK_STALE_MS`) is not treated as a crashed holder and
2658
- * unlinked by another process — which would let overlapping windows lose
2659
- * appends. Heartbeat failures are swallowed; the timer is always cleared in
2660
- * `finally`.
2661
- *
2662
- * ACQUISITION RESULT (round 6, codex P2 — NBPmY): `fn` receives whether WE
2663
- * actually hold the lock. When acquisition TIMED OUT (another holder is active),
2664
- * a MUTATING rebuild must NOT perform its load/rename window unlocked, and a
2665
- * touch must NOT append unlocked — both would recreate the lost-append race. The
2666
- * caller uses `acquired` to run compute-only (rebuild) or DROP the append
2667
- * (touch) when unlocked.
2668
- */
2669
- async withHeldCatalogLock(fn) {
2670
- const acquired = await this.acquireRebuildLock();
2671
- let heartbeat;
2672
- if (acquired) {
2673
- heartbeat = setInterval(() => {
2674
- const now = /* @__PURE__ */ new Date();
2675
- utimes(this.rebuildLockPath, now, now).catch(() => void 0);
2676
- }, REBUILD_LOCK_HEARTBEAT_MS);
2677
- heartbeat.unref?.();
2678
- }
2679
- try {
2680
- return await fn(acquired);
2681
- } finally {
2682
- if (heartbeat) clearInterval(heartbeat);
2683
- if (acquired) {
2684
- try {
2685
- if (await this.rebuildLockHeldBySelf()) {
2686
- await unlink(this.rebuildLockPath);
2687
- }
2688
- } catch {
2689
- }
2690
- }
2691
- }
2692
- }
2693
- /** Try to acquire the rebuild lock; returns true if WE created it. */
2694
- async acquireRebuildLock() {
2695
- const deadline = Date.now() + REBUILD_LOCK_MAX_WAIT_MS;
2696
- await mkdir2(this.stateDir, { recursive: true });
2697
- for (; ; ) {
2698
- try {
2699
- const handle = await open(this.rebuildLockPath, "wx");
2700
- try {
2701
- await handle.writeFile(
2702
- `${process.pid} ${this.lockOwnerId} ${(/* @__PURE__ */ new Date()).toISOString()}
2703
- `,
2704
- "utf8"
2705
- );
2706
- } catch {
2707
- } finally {
2708
- await handle.close();
2709
- }
2710
- return true;
2711
- } catch (err) {
2712
- if (err?.code !== "EEXIST") {
2713
- return false;
2714
- }
2715
- await this.breakStaleRebuildLock();
2716
- if (Date.now() >= deadline) return false;
2717
- await new Promise((r) => setTimeout(r, REBUILD_LOCK_POLL_MS));
2718
- }
2719
- }
2720
- }
2721
- /**
2722
- * Remove the lock file if its mtime is older than the stale threshold.
2723
- *
2724
- * REPLACEMENT-SAFE (NG7Bg, codex P2): a plain `stat` → `unlink` has a TOCTOU
2725
- * window — two processes can both observe the SAME stale lock; one removes it and
2726
- * creates a FRESH lock, and the other's later `unlink` then deletes that fresh
2727
- * holder's ACTIVE lock based on the stale identity it read earlier, leaving the
2728
- * fresh holder running its critical section with no visible lock and reopening the
2729
- * lost-update race the mutex prevents. We therefore capture the lock's IDENTITY
2730
- * (its full content line: `<pid> <owner-uuid> <iso>`) when we judge it stale, then
2731
- * RE-READ immediately before unlinking and only remove it when the content is
2732
- * byte-identical AND still stale. A replacement lock has a different owner id /
2733
- * timestamp, so its content differs and we leave it untouched. We never unlink a
2734
- * lock whose mtime is now fresh (a heartbeat refreshed it) or whose identity
2735
- * changed (a replacement was created). This is best-effort: any mismatch/vanish
2736
- * simply skips the break and the caller polls again.
2737
- */
2738
- async breakStaleRebuildLock() {
2739
- let staleIdentity;
2740
- try {
2741
- const info = await stat(this.rebuildLockPath);
2742
- if (Date.now() - info.mtimeMs <= REBUILD_LOCK_STALE_MS) {
2743
- return;
2744
- }
2745
- staleIdentity = await readFile2(this.rebuildLockPath, "utf8");
2746
- } catch {
2747
- return;
2748
- }
2749
- if (this.onBeforeBreakStaleUnlinkForTest) {
2750
- await this.onBeforeBreakStaleUnlinkForTest();
2751
- }
2752
- try {
2753
- const current = await readFile2(this.rebuildLockPath, "utf8");
2754
- if (current !== staleIdentity) return;
2755
- const recheck = await stat(this.rebuildLockPath);
2756
- if (Date.now() - recheck.mtimeMs <= REBUILD_LOCK_STALE_MS) return;
2757
- await unlink(this.rebuildLockPath).catch(() => void 0);
2758
- } catch {
2759
- }
2760
- }
2761
- /**
2762
- * Whether the rebuild lock file was written by THIS instance (round 6, codex
2763
- * P2 — NBsGP). Matches the per-instance owner id, NOT just `process.pid`: two
2764
- * NamespaceCatalog instances in the same process share a PID, so a PID-only
2765
- * check would wrongly treat instance A's lock as self-held by instance B and
2766
- * let B's touch skip the wait and append into A's rebuild window. Falls back to
2767
- * the legacy PID-only form for lock files written before owner ids existed.
2768
- */
2769
- async rebuildLockHeldBySelf() {
2770
- try {
2771
- const body = await readFile2(this.rebuildLockPath, "utf8");
2772
- const parts = body.trim().split(/\s+/);
2773
- const pid = Number.parseInt(parts[0] ?? "", 10);
2774
- const ownerId = parts[1];
2775
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2776
- if (ownerId && UUID_RE.test(ownerId)) {
2777
- return ownerId === this.lockOwnerId;
2778
- }
2779
- return Number.isFinite(pid) && pid === process.pid;
2780
- } catch {
2781
- return false;
2782
- }
2783
- }
2784
- /**
2785
- * Merge a prior record's preserved metadata (timestamps, principal hints)
2786
- * onto a freshly-discovered record. Disk-derived fields (storageDir, kind)
2787
- * take precedence from the new record.
2788
- *
2789
- * PROVENANCE (round 3, cursor Low): `discoveredBy` and `createdAt` are
2790
- * CREATION-ONLY — identical to the touch path's invariant. A rebuild must NOT
2791
- * reset a namespace first seen via a `write`/`read` touch back to `config`
2792
- * just because it is also listed in policies. So when a prior record exists we
2793
- * carry its `discoveredBy` forward; only brand-new records keep the fresh
2794
- * (config/scan) provenance.
2795
- */
2796
- mergeForRebuild(prior, fresh) {
2797
- if (!prior) return fresh;
2798
- const merged = {
2799
- ...fresh,
2800
- createdAt: prior.createdAt ?? fresh.createdAt,
2801
- discoveredBy: prior.discoveredBy ?? fresh.discoveredBy
2802
- };
2803
- if (prior.lastReadAt) merged.lastReadAt = prior.lastReadAt;
2804
- if (prior.lastWriteAt) merged.lastWriteAt = prior.lastWriteAt;
2805
- if (prior.lastMaintenanceAt) merged.lastMaintenanceAt = { ...prior.lastMaintenanceAt };
2806
- if (prior.principal !== void 0) merged.principal = prior.principal;
2807
- if (prior.projectId !== void 0) merged.projectId = prior.projectId;
2808
- if (prior.branch !== void 0) merged.branch = prior.branch;
2809
- if (prior.parentNamespace !== void 0) merged.parentNamespace = prior.parentNamespace;
2810
- return merged;
2811
- }
2812
- // ── Persistence ──────────────────────────────────────────────────────────
2813
- /** Load the JSONL log and fold it into current state (last-record-wins). */
2814
- async loadCompacted() {
2815
- const records = /* @__PURE__ */ new Map();
2816
- let raw;
2817
- try {
2818
- raw = await readFile2(this.catalogPath, "utf8");
2819
- } catch {
2820
- return records;
2821
- }
2822
- for (const line of raw.split("\n")) {
2823
- const trimmed = line.trim();
2824
- if (trimmed.length === 0) continue;
2825
- let parsed;
2826
- try {
2827
- parsed = JSON.parse(trimmed);
2828
- } catch {
2829
- continue;
2830
- }
2831
- const record = coerceRecord(parsed);
2832
- if (!record) continue;
2833
- const prior = records.get(record.namespace);
2834
- records.set(record.namespace, prior ? mergeNewerTouchFields(record, prior) : record);
2835
- }
2836
- return records;
2837
- }
2838
- /**
2839
- * Serialize an arbitrary read-modify-write critical section through the single
2840
- * write chain. Every catalog mutation (touch read+merge+append, full rewrite)
2841
- * runs through this so they are mutually exclusive: a touch always reads the
2842
- * latest persisted state before appending, and a rebuild rewrite cannot
2843
- * interleave with a touch's append. The chain recovers from rejection
2844
- * (CLAUDE.md rule #40) — one failed section never poisons subsequent ones —
2845
- * while still surfacing the error to that section's awaited promise.
2846
- */
2847
- queueCritical(fn) {
2848
- const run = this.writeChain.then(fn);
2849
- this.writeChain = run.then(
2850
- () => void 0,
2851
- () => void 0
2852
- );
2853
- return run;
2854
- }
2855
- /**
2856
- * Append a single record to the JSONL log WITHOUT re-serializing through the
2857
- * write chain. MUST only be called from inside a `queueCritical(...)` section
2858
- * (which already holds the serialized turn); calling it directly would bypass
2859
- * the read-before-append ordering that prevents lost-field races.
2860
- */
2861
- async appendUnchained(record) {
2862
- const line = serializeRecord(record) + "\n";
2863
- await mkdir2(this.stateDir, { recursive: true });
2864
- await appendFile(this.catalogPath, line, "utf8");
2865
- }
2866
- /**
2867
- * Atomic temp-file + rename rewrite (CLAUDE.md rule #54: write temp, then
2868
- * rename — never delete-before-write) WITHOUT re-entering the write chain.
2869
- * MUST only be called from inside a `queueCritical(...)` turn (e.g. the
2870
- * rebuild critical section, which already holds the serialized turn so its
2871
- * load and rewrite are atomic against concurrent touches). Re-entering the
2872
- * chain from within a held turn would deadlock.
2873
- */
2874
- async rewriteUnchained(records) {
2875
- const body = records.map((r) => serializeRecord(r)).join("\n") + (records.length > 0 ? "\n" : "");
2876
- await mkdir2(this.stateDir, { recursive: true });
2877
- const tmp = `${this.catalogPath}.${process.pid}.${Date.now()}.tmp`;
2878
- await writeFile2(tmp, body, "utf8");
2879
- await rename(tmp, this.catalogPath);
2880
- }
2881
- };
2882
- function isPathInside(root, child) {
2883
- const relative = path2.relative(root, child);
2884
- return relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative);
2885
- }
2886
-
2887
- // src/maintenance/namespace-planner.ts
2888
- import { createHash, randomUUID as randomUUID2 } from "crypto";
2889
- import { lstat as lstat2, mkdir as mkdir3, open as open2, readFile as readFile3, readdir as readdir2, rename as rename2, rm, rmdir, utimes as utimes2, writeFile as writeFile3 } from "fs/promises";
2890
- import path3 from "path";
2891
- var DEFAULT_MAX_NAMESPACES_PER_CYCLE = 20;
2892
- var DEFAULT_LOCK_STALE_MS = 10 * 6e4;
2893
- var LOCK_BASE = "maintenance-locks";
2894
- var STATUS_BASE = "namespace-maintenance-status";
2895
- var namespaceMaintenanceFs = { open: open2, rm };
2896
- function configuredNamespaces(config) {
2897
- return Array.from(
2898
- new Set(
2899
- [config.defaultNamespace, config.sharedNamespace, ...config.namespacePolicies.map((policy) => policy.name)].map((value) => value.trim()).filter(Boolean)
2900
- )
2901
- );
2902
- }
2903
- function inferConfiguredKind(config, namespace) {
2904
- if (namespace === config.defaultNamespace.trim()) return "default";
2905
- if (namespace === config.sharedNamespace.trim()) return "shared";
2906
- return "explicit";
2907
- }
2908
- function maxNamespacesPerCycle(config) {
2909
- return Math.max(
2910
- 1,
2911
- Math.floor(
2912
- typeof config.maintenanceMaxNamespacesPerCycle === "number" && Number.isFinite(config.maintenanceMaxNamespacesPerCycle) ? config.maintenanceMaxNamespacesPerCycle : DEFAULT_MAX_NAMESPACES_PER_CYCLE
2913
- )
2914
- );
2915
- }
2916
- function namespaceKindAllowed(config, kind) {
2917
- switch (kind) {
2918
- case "branch":
2919
- return config.maintenanceIncludeBranchNamespaces === true;
2920
- case "project":
2921
- return config.maintenanceIncludeProjectNamespaces !== false;
2922
- case "team-project":
2923
- return config.maintenanceIncludeTeamProjectNamespaces !== false;
2924
- default:
2925
- return true;
2926
- }
2927
- }
2928
- function disabledReasonForKind(kind) {
2929
- if (kind === "branch") return "branch_disabled";
2930
- if (kind === "project") return "project_disabled";
2931
- if (kind === "team-project") return "team_project_disabled";
2932
- return "fanout_disabled";
2933
- }
2934
- async function catalogRootIsLive(config, record) {
2935
- if (typeof record.storageDir !== "string" || record.storageDir.length === 0) {
2936
- return false;
2937
- }
2938
- try {
2939
- const liveRoot = await resolveNamespaceStorageRoot(config, record.namespace);
2940
- if (path3.resolve(liveRoot) !== path3.resolve(record.storageDir)) return false;
2941
- return hasMemoryData(liveRoot);
2942
- } catch {
2943
- return false;
2944
- }
2945
- }
2946
- function candidateSortKey(candidate) {
2947
- const write = candidate.lastWriteAt ?? "";
2948
- return `${write}\0${candidate.namespace}`;
2949
- }
2950
- function candidatePriority(candidate) {
2951
- if (candidate.kind === "default") return 0;
2952
- if (candidate.kind === "shared") return 1;
2953
- if (candidate.source === "configured") return 2;
2954
- if (candidate.kind === "team-project") return 3;
2955
- if (candidate.kind === "project") return 4;
2956
- if (candidate.kind === "self") return 5;
2957
- if (candidate.kind === "legacy") return 6;
2958
- if (candidate.kind === "branch") return 8;
2959
- return 7;
2960
- }
2961
- function sortCandidates(a, b) {
2962
- const priority = candidatePriority(a) - candidatePriority(b);
2963
- if (priority !== 0) return priority;
2964
- const am = Date.parse(a.lastMaintenanceAt ?? "");
2965
- const bm = Date.parse(b.lastMaintenanceAt ?? "");
2966
- const aMaintained = Number.isFinite(am);
2967
- const bMaintained = Number.isFinite(bm);
2968
- if (aMaintained && bMaintained && am !== bm) return am - bm;
2969
- if (aMaintained !== bMaintained) return aMaintained ? 1 : -1;
2970
- const aw = Date.parse(a.lastWriteAt ?? "");
2971
- const bw = Date.parse(b.lastWriteAt ?? "");
2972
- const aValid = Number.isFinite(aw);
2973
- const bValid = Number.isFinite(bw);
2974
- if (aValid && bValid && aw !== bw) return bw - aw;
2975
- if (aValid !== bValid) return aValid ? -1 : 1;
2976
- const byKey = candidateSortKey(a).localeCompare(candidateSortKey(b));
2977
- if (byKey !== 0) return byKey;
2978
- return a.namespace.localeCompare(b.namespace);
2979
- }
2980
- async function planNamespaceMaintenance(config, options) {
2981
- const generatedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
2982
- const configured = configuredNamespaces(config);
2983
- const byNamespace = /* @__PURE__ */ new Map();
2984
- const skipped = [];
2985
- for (const namespace of configured) {
2986
- const kind = inferConfiguredKind(config, namespace);
2987
- byNamespace.set(namespace, {
2988
- namespace,
2989
- kind,
2990
- source: "configured"
2991
- });
2992
- }
2993
- if (config.namespacesEnabled && config.maintenanceNamespaceFanoutEnabled !== false) {
2994
- const configuredSet = new Set(configured);
2995
- try {
2996
- const records = options.catalog?.enabled ? await options.catalog.listNamespaces() : [];
2997
- for (const record of records) {
2998
- const namespace = record.namespace.trim();
2999
- if (!namespace) continue;
3000
- const isConfigured = configuredSet.has(namespace);
3001
- const kind = isConfigured ? inferConfiguredKind(config, namespace) : record.kind;
3002
- if (!namespaceKindAllowed(config, kind)) {
3003
- skipped.push({
3004
- namespace,
3005
- kind,
3006
- reason: disabledReasonForKind(kind)
3007
- });
3008
- continue;
3009
- }
3010
- if (!isConfigured && !await catalogRootIsLive(config, record)) {
3011
- skipped.push({
3012
- namespace,
3013
- kind,
3014
- reason: "unsafe_or_stale_root"
3015
- });
3016
- continue;
3017
- }
3018
- byNamespace.set(namespace, {
3019
- namespace,
3020
- kind,
3021
- storageDir: record.storageDir,
3022
- source: isConfigured ? "configured" : "catalog",
3023
- lastWriteAt: record.lastWriteAt,
3024
- lastMaintenanceAt: record.lastMaintenanceAt?.[options.jobName]
3025
- });
3026
- }
3027
- } catch (error) {
3028
- skipped.push({
3029
- namespace: "*",
3030
- reason: "catalog_read_failed",
3031
- detail: error instanceof Error ? error.message : String(error)
3032
- });
3033
- }
3034
- } else if (config.namespacesEnabled) {
3035
- skipped.push({
3036
- namespace: "*",
3037
- reason: "fanout_disabled"
3038
- });
3039
- }
3040
- if (options.budgetMode !== "unbounded") {
3041
- const latestStatusAtByNamespace = await readLatestStatusAtByNamespace(config, options.jobName);
3042
- for (const candidate of byNamespace.values()) {
3043
- if (!candidate.lastMaintenanceAt) {
3044
- candidate.lastMaintenanceAt = latestStatusAtByNamespace.get(candidate.namespace);
3045
- }
3046
- }
3047
- }
3048
- const candidates = [...byNamespace.values()].filter((candidate) => namespaceKindAllowed(config, candidate.kind)).sort(sortCandidates);
3049
- const max = maxNamespacesPerCycle(config);
3050
- const applyCycleBudget = options.budgetMode !== "unbounded";
3051
- const selected = applyCycleBudget ? candidates.slice(0, max) : candidates;
3052
- if (applyCycleBudget) {
3053
- for (const candidate of candidates.slice(max)) {
3054
- skipped.push({
3055
- namespace: candidate.namespace,
3056
- kind: candidate.kind,
3057
- reason: "budget_exhausted"
3058
- });
3059
- }
3060
- }
3061
- return {
3062
- jobName: options.jobName,
3063
- generatedAt,
3064
- namespaces: selected,
3065
- skipped,
3066
- budget: {
3067
- maxNamespacesPerCycle: max,
3068
- selected: selected.length
3069
- }
3070
- };
3071
- }
3072
- function stablePathSegment(value) {
3073
- const sanitized = value.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 128) || "unnamed";
3074
- if (sanitized.length <= 128 && sanitized === value) return sanitized;
3075
- return `${sanitized.slice(0, 80)}-${createHash("sha256").update(value).digest("hex").slice(0, 16)}`;
3076
- }
3077
- function namespacePathSegment(namespace) {
3078
- const token = namespaceIdentityToken(namespace);
3079
- if (token.length <= 160) return token;
3080
- return `ns-${createHash("sha256").update(namespace).digest("hex")}`;
3081
- }
3082
- function lockPath(config, jobName, namespace) {
3083
- return path3.join(
3084
- config.memoryDir,
3085
- "state",
3086
- LOCK_BASE,
3087
- stablePathSegment(jobName),
3088
- `${namespacePathSegment(namespace)}.lock`
3089
- );
3090
- }
3091
- function namespaceMaintenanceLockStaleMs(config) {
3092
- if (typeof config.maintenanceNamespaceLockStaleMs === "number" && Number.isFinite(config.maintenanceNamespaceLockStaleMs) && config.maintenanceNamespaceLockStaleMs > 0) {
3093
- return Math.floor(config.maintenanceNamespaceLockStaleMs);
3094
- }
3095
- return DEFAULT_LOCK_STALE_MS;
3096
- }
3097
- function namespaceMaintenanceLockHeartbeatMs(config) {
3098
- const staleMs = namespaceMaintenanceLockStaleMs(config);
3099
- return Math.max(1, Math.min(3e4, Math.floor(staleMs / 3) || 1));
3100
- }
3101
- function errorCode(error) {
3102
- return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
3103
- }
3104
- async function withNamespaceMaintenanceLockHeartbeat(config, locks, task) {
3105
- const activeLocks = Array.isArray(locks) ? locks : [locks];
3106
- const interval = setInterval(() => {
3107
- for (const lock of activeLocks) {
3108
- void lock.touch().catch(() => void 0);
3109
- }
3110
- }, namespaceMaintenanceLockHeartbeatMs(config));
3111
- interval.unref?.();
3112
- try {
3113
- return await task();
3114
- } finally {
3115
- clearInterval(interval);
3116
- }
3117
- }
3118
- async function removeStaleLockDirectory(filePath) {
3119
- let entries;
3120
- try {
3121
- entries = await readdir2(filePath, { withFileTypes: true });
3122
- } catch (error) {
3123
- if (errorCode(error) === "ENOENT") return;
3124
- throw error;
3125
- }
3126
- for (const entry of entries) {
3127
- if (!entry.isFile()) continue;
3128
- await namespaceMaintenanceFs.rm(path3.join(filePath, entry.name), { force: true });
3129
- }
3130
- try {
3131
- await rmdir(filePath);
3132
- } catch (error) {
3133
- if (errorCode(error) === "ENOENT") return;
3134
- throw error;
3135
- }
3136
- }
3137
- async function tryAcquireNamespaceMaintenanceLock(config, jobName, namespace) {
3138
- const filePath = lockPath(config, jobName, namespace);
3139
- await mkdir3(path3.dirname(filePath), { recursive: true });
3140
- try {
3141
- const lockId = randomUUID2();
3142
- await mkdir3(filePath);
3143
- const ownerPath = path3.join(filePath, `${lockId}.json`);
3144
- let handle;
3145
- try {
3146
- handle = await namespaceMaintenanceFs.open(ownerPath, "wx");
3147
- await handle.writeFile(
3148
- `${JSON.stringify({
3149
- lockId,
3150
- pid: process.pid,
3151
- jobName,
3152
- namespace,
3153
- acquiredAt: (/* @__PURE__ */ new Date()).toISOString()
3154
- })}
3155
- `,
3156
- "utf8"
3157
- );
3158
- await handle.close();
3159
- } catch (setupError) {
3160
- await handle?.close().catch(() => void 0);
3161
- await namespaceMaintenanceFs.rm(ownerPath, { force: true }).catch(() => void 0);
3162
- await rmdir(filePath).catch(() => void 0);
3163
- throw setupError;
3164
- }
3165
- return {
3166
- path: filePath,
3167
- async touch() {
3168
- try {
3169
- const parsed = JSON.parse(await readFile3(ownerPath, "utf8"));
3170
- if (parsed.lockId === lockId) {
3171
- const now = /* @__PURE__ */ new Date();
3172
- await utimes2(ownerPath, now, now);
3173
- await utimes2(filePath, now, now);
3174
- }
3175
- } catch {
3176
- }
3177
- },
3178
- async release() {
3179
- try {
3180
- const parsed = JSON.parse(await readFile3(ownerPath, "utf8"));
3181
- if (parsed.lockId === lockId) {
3182
- await namespaceMaintenanceFs.rm(ownerPath, { force: true });
3183
- await rmdir(filePath).catch(() => void 0);
3184
- }
3185
- } catch {
3186
- }
1410
+ /**
1411
+ * Memories created from wearable transcripts, filterable by source
1412
+ * and/or day. Includes pending_review candidates — the whole point of
1413
+ * review mode is seeing what's queued.
1414
+ */
1415
+ async transcriptMemories(options = {}) {
1416
+ if (options.date !== void 0 && !isValidTranscriptDate(options.date)) {
1417
+ throw new WearablesInputError(`invalid date '${options.date}' \u2014 expected YYYY-MM-DD`);
1418
+ }
1419
+ if (options.source !== void 0) assertValidSourceId(options.source);
1420
+ const limit = clampLimit(
1421
+ options.limit,
1422
+ MEMORY_LIST_DEFAULT_LIMIT,
1423
+ MEMORY_LIST_MAX_LIMIT,
1424
+ "limit"
1425
+ );
1426
+ const storage = await this.deps.getStorage();
1427
+ const memories = await storage.readAllMemories();
1428
+ const results = [];
1429
+ for (const memory of memories) {
1430
+ const source = memory.frontmatter.source;
1431
+ if (typeof source !== "string" || !source.startsWith(`${WEARABLE_SOURCE_PREFIX}:`)) {
1432
+ continue;
3187
1433
  }
3188
- };
3189
- } catch (error) {
3190
- if (errorCode(error) === "EEXIST") {
3191
- const staleMs = namespaceMaintenanceLockStaleMs(config);
3192
- try {
3193
- const s = await lstat2(filePath);
3194
- if (s.isSymbolicLink()) {
3195
- return null;
3196
- }
3197
- if ((s.isFile() || s.isDirectory()) && Date.now() - s.mtimeMs > staleMs) {
3198
- try {
3199
- if (s.isDirectory()) {
3200
- await removeStaleLockDirectory(filePath);
3201
- } else {
3202
- await namespaceMaintenanceFs.rm(filePath, { force: true });
3203
- }
3204
- } catch (removeError) {
3205
- if (errorCode(removeError) === "ENOENT") {
3206
- return tryAcquireNamespaceMaintenanceLock(config, jobName, namespace);
3207
- }
3208
- if (errorCode(removeError) === "ENOTEMPTY") {
3209
- return null;
3210
- }
3211
- throw removeError;
3212
- }
3213
- return tryAcquireNamespaceMaintenanceLock(config, jobName, namespace);
3214
- }
3215
- } catch (statError) {
3216
- if (errorCode(statError) === "ENOENT") {
3217
- return tryAcquireNamespaceMaintenanceLock(config, jobName, namespace);
1434
+ const attrs = memory.frontmatter.structuredAttributes ?? {};
1435
+ const sourceId = attrs.wearableSource;
1436
+ if (options.source !== void 0) {
1437
+ if (sourceId !== options.source && source !== wearableSourceLabel(options.source) && source !== `${wearableSourceLabel(options.source)}:native`) {
1438
+ continue;
3218
1439
  }
3219
- throw statError;
3220
1440
  }
3221
- return null;
1441
+ if (options.date !== void 0 && attrs.wearableDate !== options.date) {
1442
+ continue;
1443
+ }
1444
+ results.push({
1445
+ id: memory.frontmatter.id,
1446
+ source: sourceId ?? source,
1447
+ date: attrs.wearableDate,
1448
+ conversationId: attrs.wearableConversationId,
1449
+ status: memory.frontmatter.status,
1450
+ content: memory.content,
1451
+ created: memory.frontmatter.created
1452
+ });
3222
1453
  }
3223
- throw error;
3224
- }
3225
- }
3226
- function statusBasePath(config) {
3227
- return path3.join(config.memoryDir, "state", STATUS_BASE);
3228
- }
3229
- function statusPath(config, jobName, namespace) {
3230
- return path3.join(statusBasePath(config), stablePathSegment(jobName), `${namespacePathSegment(namespace)}.json`);
3231
- }
3232
- function lastRanStatusPath(config, jobName, namespace) {
3233
- return path3.join(statusBasePath(config), stablePathSegment(jobName), `${namespacePathSegment(namespace)}.last-ran.json`);
3234
- }
3235
- function parseStatus(value) {
3236
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
3237
- const v = value;
3238
- if (typeof v.namespace === "string" && typeof v.jobName === "string" && (v.state === "ran" || v.state === "skipped" || v.state === "failed") && typeof v.startedAt === "string" && typeof v.completedAt === "string") {
3239
- return v;
3240
- }
3241
- return null;
3242
- }
3243
- async function readLatestStatusAtByNamespace(config, jobName) {
3244
- const latest = /* @__PURE__ */ new Map();
3245
- const latestMs = /* @__PURE__ */ new Map();
3246
- for (const status of [...await readStatusFiles(config), ...await readLastRanStatusFiles(config)]) {
3247
- if (status.state !== "ran") continue;
3248
- if (status.jobName !== jobName) continue;
3249
- const completedAtMs = Date.parse(status.completedAt);
3250
- if (!Number.isFinite(completedAtMs)) continue;
3251
- const previousMs = latestMs.get(status.namespace);
3252
- if (previousMs !== void 0 && previousMs >= completedAtMs) continue;
3253
- latestMs.set(status.namespace, completedAtMs);
3254
- latest.set(status.namespace, status.completedAt);
3255
- }
3256
- return latest;
3257
- }
3258
- async function readStatusFile(filePath) {
3259
- try {
3260
- const parsed = JSON.parse(await readFile3(filePath, "utf8"));
3261
- return parseStatus(parsed);
3262
- } catch {
3263
- return null;
3264
- }
3265
- }
3266
- async function readStatusFiles(config) {
3267
- if (typeof config.memoryDir !== "string" || config.memoryDir.length === 0) {
3268
- return [];
1454
+ results.sort((a, b) => {
1455
+ if (a.created > b.created) return -1;
1456
+ if (a.created < b.created) return 1;
1457
+ if (a.id < b.id) return -1;
1458
+ if (a.id > b.id) return 1;
1459
+ return 0;
1460
+ });
1461
+ return results.slice(0, limit);
3269
1462
  }
3270
- const root = statusBasePath(config);
3271
- const statuses = [];
3272
- let jobDirs;
3273
- try {
3274
- jobDirs = await readdir2(root, { withFileTypes: true });
3275
- } catch {
3276
- return statuses;
1463
+ // -- speakers -------------------------------------------------------------
1464
+ async listSpeakers() {
1465
+ const storage = await this.deps.getStorage();
1466
+ return loadSpeakerRegistry(storage.dir);
3277
1467
  }
3278
- for (const jobDir of jobDirs) {
3279
- if (!jobDir.isDirectory()) continue;
3280
- let files;
3281
- try {
3282
- files = await readdir2(path3.join(root, jobDir.name), { withFileTypes: true });
3283
- } catch {
3284
- continue;
1468
+ async setSpeaker(sourceId, speakerKey, name, opts = {}) {
1469
+ if (typeof name !== "string" || name.trim().length === 0) {
1470
+ throw new WearablesInputError("speaker name must be a non-empty string");
3285
1471
  }
3286
- for (const file of files) {
3287
- if (!file.isFile() || !file.name.endsWith(".json")) continue;
3288
- if (file.name.endsWith(".last-ran.json")) continue;
3289
- const status = await readStatusFile(path3.join(root, jobDir.name, file.name));
3290
- if (status) statuses.push(status);
1472
+ if (typeof speakerKey !== "string" || speakerKey.trim().length === 0) {
1473
+ throw new WearablesInputError("speaker key must be a non-empty string");
3291
1474
  }
1475
+ const storage = await this.deps.getStorage();
1476
+ const registry = await loadSpeakerRegistry(storage.dir);
1477
+ registry.speakers[speakerRegistryKey(sourceId, speakerKey.trim())] = {
1478
+ name: name.trim(),
1479
+ ...opts.isSelf === true ? { isSelf: true } : {},
1480
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1481
+ };
1482
+ await saveSpeakerRegistry(storage.dir, registry);
1483
+ return registry;
3292
1484
  }
3293
- return statuses;
3294
- }
3295
- async function readLastRanStatusFiles(config) {
3296
- if (typeof config.memoryDir !== "string" || config.memoryDir.length === 0) {
3297
- return [];
3298
- }
3299
- const root = statusBasePath(config);
3300
- const statuses = [];
3301
- let jobDirs;
3302
- try {
3303
- jobDirs = await readdir2(root, { withFileTypes: true });
3304
- } catch {
3305
- return statuses;
3306
- }
3307
- for (const jobDir of jobDirs) {
3308
- if (!jobDir.isDirectory()) continue;
3309
- let files;
3310
- try {
3311
- files = await readdir2(path3.join(root, jobDir.name), { withFileTypes: true });
3312
- } catch {
3313
- continue;
3314
- }
3315
- for (const file of files) {
3316
- if (!file.isFile() || !file.name.endsWith(".last-ran.json")) continue;
3317
- const status = await readStatusFile(path3.join(root, jobDir.name, file.name));
3318
- if (status) statuses.push(status);
1485
+ async setSelfName(name) {
1486
+ if (typeof name !== "string" || name.trim().length === 0) {
1487
+ throw new WearablesInputError("self name must be a non-empty string");
3319
1488
  }
1489
+ const storage = await this.deps.getStorage();
1490
+ const registry = await loadSpeakerRegistry(storage.dir);
1491
+ registry.selfName = name.trim();
1492
+ await saveSpeakerRegistry(storage.dir, registry);
1493
+ return registry;
3320
1494
  }
3321
- return statuses;
3322
- }
3323
- async function writeStatusPayload(target, status) {
3324
- const dir = path3.dirname(target);
3325
- await mkdir3(dir, { recursive: true });
3326
- const temp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
3327
- const payload = {
3328
- version: 1,
3329
- ...status
3330
- };
3331
- await writeFile3(temp, `${JSON.stringify(payload, null, 2)}
3332
- `, "utf8");
3333
- await rename2(temp, target);
3334
- }
3335
- async function writeStatusFile(config, status) {
3336
- await writeStatusPayload(statusPath(config, status.jobName, status.namespace), status);
3337
- if (status.state === "ran") {
3338
- await writeStatusPayload(lastRanStatusPath(config, status.jobName, status.namespace), status);
3339
- }
3340
- }
3341
- async function recordNamespaceMaintenanceStatusSafely(config, status) {
3342
- try {
3343
- await writeStatusFile(config, status);
3344
- } catch {
3345
- }
3346
- }
3347
- function maintenanceErrorDetail(error) {
3348
- return displayErrorDetail(error) || "Error";
3349
- }
3350
- async function runNamespaceMaintenanceBatchPlan(config, plan, runner, catalog, options = {}) {
3351
- const statuses = [];
3352
- for (const skipped of plan.skipped) {
3353
- if (skipped.namespace === "*") continue;
3354
- const now = (/* @__PURE__ */ new Date()).toISOString();
3355
- const status = {
3356
- namespace: skipped.namespace,
3357
- jobName: plan.jobName,
3358
- state: "skipped",
3359
- reason: skipped.reason,
3360
- startedAt: now,
3361
- completedAt: now
3362
- };
3363
- statuses.push(status);
3364
- await recordNamespaceMaintenanceStatusSafely(config, status);
3365
- }
3366
- const acquired = [];
3367
- try {
3368
- for (const candidate of plan.namespaces) {
3369
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3370
- const lock = await tryAcquireNamespaceMaintenanceLock(config, plan.jobName, candidate.namespace);
3371
- if (!lock) {
3372
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
3373
- const status = {
3374
- namespace: candidate.namespace,
3375
- jobName: plan.jobName,
3376
- state: "skipped",
3377
- reason: "lock_held",
3378
- startedAt,
3379
- completedAt
3380
- };
3381
- statuses.push(status);
3382
- await recordNamespaceMaintenanceStatusSafely(config, status);
3383
- continue;
3384
- }
3385
- acquired.push({ candidate, lock, startedAt });
3386
- }
3387
- } catch (error) {
3388
- await Promise.all(acquired.map(({ lock }) => lock.release().catch(() => void 0)));
3389
- throw error;
3390
- }
3391
- if (options.requireAllLocks && acquired.length > 0 && acquired.length < plan.namespaces.length) {
3392
- for (const { candidate, startedAt } of acquired) {
3393
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
3394
- const status = {
3395
- namespace: candidate.namespace,
3396
- jobName: plan.jobName,
3397
- state: "skipped",
3398
- reason: "batch_lock_incomplete",
3399
- startedAt,
3400
- completedAt
3401
- };
3402
- statuses.push(status);
3403
- await recordNamespaceMaintenanceStatusSafely(config, status);
1495
+ async removeSpeaker(sourceId, speakerKey) {
1496
+ const storage = await this.deps.getStorage();
1497
+ const registry = await loadSpeakerRegistry(storage.dir);
1498
+ const key = speakerRegistryKey(sourceId, speakerKey.trim());
1499
+ if (!(key in registry.speakers)) {
1500
+ throw new WearablesInputError(`no speaker override stored for '${key}'`);
3404
1501
  }
3405
- await Promise.all(acquired.map(({ lock }) => lock.release().catch(() => void 0)));
3406
- return {
3407
- jobName: plan.jobName,
3408
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3409
- ran: statuses.filter((s) => s.state === "ran").length,
3410
- skipped: statuses.filter((s) => s.state === "skipped").length,
3411
- failed: statuses.filter((s) => s.state === "failed").length,
3412
- statuses
3413
- };
1502
+ delete registry.speakers[key];
1503
+ await saveSpeakerRegistry(storage.dir, registry);
1504
+ return registry;
3414
1505
  }
3415
- if (acquired.length === 0) {
1506
+ // -- corrections ----------------------------------------------------------
1507
+ async listCorrections() {
1508
+ const storage = await this.deps.getStorage();
3416
1509
  return {
3417
- jobName: plan.jobName,
3418
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3419
- ran: statuses.filter((s) => s.state === "ran").length,
3420
- skipped: statuses.filter((s) => s.state === "skipped").length,
3421
- failed: statuses.filter((s) => s.state === "failed").length,
3422
- statuses
1510
+ fromConfig: this.deps.config.corrections,
1511
+ fromState: await loadCorrectionsFile(storage.dir),
1512
+ stateFilePath: correctionsFilePath(storage.dir)
3423
1513
  };
3424
1514
  }
3425
- try {
3426
- const result = await withNamespaceMaintenanceLockHeartbeat(
3427
- config,
3428
- acquired.map(({ lock }) => lock),
3429
- () => runner(acquired.map(({ candidate }) => candidate))
1515
+ async addCorrection(rule) {
1516
+ compileCorrectionRule(rule, "correction");
1517
+ const storage = await this.deps.getStorage();
1518
+ const rules = await loadCorrectionsFile(storage.dir);
1519
+ const duplicate = rules.some(
1520
+ (existing) => existing.match === rule.match && existing.replace === rule.replace && existing.regex === true === (rule.regex === true)
3430
1521
  );
3431
- for (const { candidate, startedAt } of acquired) {
3432
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
3433
- const status = {
3434
- namespace: candidate.namespace,
3435
- jobName: plan.jobName,
3436
- state: "ran",
3437
- startedAt,
3438
- completedAt,
3439
- itemCount: itemCountForNamespace(result, candidate.namespace)
3440
- };
3441
- statuses.push(status);
3442
- await recordNamespaceMaintenanceStatusSafely(config, status);
3443
- try {
3444
- await catalog?.markMaintenance(candidate.namespace, plan.jobName, new Date(completedAt));
3445
- } catch {
3446
- }
1522
+ if (duplicate) {
1523
+ throw new WearablesInputError(
1524
+ `an identical correction rule already exists (match: ${JSON.stringify(rule.match)})`
1525
+ );
3447
1526
  }
3448
- } catch (error) {
3449
- const skipReason = options.skipReasonForError?.(error);
3450
- for (const { candidate, startedAt } of acquired) {
3451
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
3452
- const status = skipReason ? {
3453
- namespace: candidate.namespace,
3454
- jobName: plan.jobName,
3455
- state: "skipped",
3456
- reason: skipReason,
3457
- startedAt,
3458
- completedAt
3459
- } : {
3460
- namespace: candidate.namespace,
3461
- jobName: plan.jobName,
3462
- state: "failed",
3463
- reason: "job_failed",
3464
- startedAt,
3465
- completedAt,
3466
- error: maintenanceErrorDetail(error)
3467
- };
3468
- statuses.push(status);
3469
- await recordNamespaceMaintenanceStatusSafely(config, status);
1527
+ rules.push(rule);
1528
+ await saveCorrectionsFile(storage.dir, rules);
1529
+ }
1530
+ async removeCorrection(index) {
1531
+ if (!Number.isInteger(index) || index < 0) {
1532
+ throw new WearablesInputError(`invalid correction index '${index}'`);
3470
1533
  }
3471
- } finally {
3472
- await Promise.all(acquired.map(({ lock }) => lock.release().catch(() => void 0)));
1534
+ const storage = await this.deps.getStorage();
1535
+ const rules = await loadCorrectionsFile(storage.dir);
1536
+ if (index >= rules.length) {
1537
+ throw new WearablesInputError(
1538
+ `correction index ${index} is out of range (have ${rules.length} state rule${rules.length === 1 ? "" : "s"})`
1539
+ );
1540
+ }
1541
+ const [removed] = rules.splice(index, 1);
1542
+ await saveCorrectionsFile(storage.dir, rules);
1543
+ return removed;
3473
1544
  }
3474
- return {
3475
- jobName: plan.jobName,
3476
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3477
- ran: statuses.filter((s) => s.state === "ran").length,
3478
- skipped: statuses.filter((s) => s.state === "skipped").length,
3479
- failed: statuses.filter((s) => s.state === "failed").length,
3480
- statuses
3481
- };
3482
- }
3483
- function itemCountForNamespace(result, namespace) {
3484
- const itemCounts = result?.itemCounts;
3485
- if (itemCounts instanceof Map) return itemCounts.get(namespace);
3486
- if (itemCounts && Object.prototype.hasOwnProperty.call(itemCounts, namespace)) {
3487
- return itemCounts[namespace];
1545
+ };
1546
+ function clampLimit(value, fallback, max, label) {
1547
+ if (value === void 0) return fallback;
1548
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1 || value > max) {
1549
+ throw new WearablesInputError(
1550
+ `invalid ${label} '${value}' \u2014 expected an integer between 1 and ${max}`
1551
+ );
3488
1552
  }
3489
- return result?.itemCount;
1553
+ return value;
1554
+ }
1555
+ function locateTranscriptPath(hitPath) {
1556
+ const normalized = hitPath.replace(/\\/g, "/");
1557
+ const match = normalized.match(
1558
+ /(?:^|\/)wearables\/([a-z][a-z0-9-]{0,63})\/(\d{4}-\d{2}-\d{2})\.md$/
1559
+ );
1560
+ if (!match) return null;
1561
+ if (!isValidTranscriptDate(match[2])) return null;
1562
+ return { source: match[1], date: match[2] };
1563
+ }
1564
+ function extractSnippet(body, index, matchLength) {
1565
+ const start = Math.max(0, index - 80);
1566
+ const end = Math.min(body.length, index + matchLength + 80);
1567
+ const prefix = start > 0 ? "\u2026" : "";
1568
+ const suffix = end < body.length ? "\u2026" : "";
1569
+ return `${prefix}${body.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
3490
1570
  }
3491
1571
 
3492
1572
  // src/scopes/scope-plan.ts
@@ -3625,7 +1705,7 @@ function flattenStructuredSectionEvidence(sections) {
3625
1705
  );
3626
1706
  }
3627
1707
  function fingerprintEntitySynthesisEvidence(entity) {
3628
- const fingerprint = createHash2("sha256");
1708
+ const fingerprint = createHash("sha256");
3629
1709
  const timelineEntries = entity.timeline.map((entry) => [
3630
1710
  entry.timestamp,
3631
1711
  entry.source ?? "",
@@ -3675,7 +1755,7 @@ async function raceRecallAbort(promise, signal, message = "recall aborted") {
3675
1755
  }
3676
1756
  }
3677
1757
  function qmdCollectionPathParts(resultPath) {
3678
- if (!resultPath || path4.isAbsolute(resultPath)) return null;
1758
+ if (!resultPath || path2.isAbsolute(resultPath)) return null;
3679
1759
  const normalized = resultPath.replace(/\\/g, "/").replace(/^\/+/, "");
3680
1760
  const slashIndex = normalized.indexOf("/");
3681
1761
  if (slashIndex <= 0 || slashIndex >= normalized.length - 1) return null;
@@ -3688,9 +1768,9 @@ function qmdCollectionPathParts(resultPath) {
3688
1768
  }
3689
1769
  function qmdResultPathCandidates(storageDir, resultPath) {
3690
1770
  const candidates = /* @__PURE__ */ new Set();
3691
- const storageRoot = path4.resolve(storageDir);
1771
+ const storageRoot = path2.resolve(storageDir);
3692
1772
  const addCandidate = (candidate) => {
3693
- const resolved = path4.resolve(candidate);
1773
+ const resolved = path2.resolve(candidate);
3694
1774
  if (isPathInsideStorageRoot(storageRoot, resolved)) {
3695
1775
  candidates.add(resolved);
3696
1776
  }
@@ -3698,12 +1778,12 @@ function qmdResultPathCandidates(storageDir, resultPath) {
3698
1778
  const addRelativeCandidates = (relativePath) => {
3699
1779
  const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
3700
1780
  if (!normalized) return;
3701
- addCandidate(path4.join(storageRoot, normalized));
1781
+ addCandidate(path2.join(storageRoot, normalized));
3702
1782
  if (/^\d{4}-\d{2}-\d{2}\//.test(normalized)) {
3703
- addCandidate(path4.join(storageRoot, "facts", normalized));
1783
+ addCandidate(path2.join(storageRoot, "facts", normalized));
3704
1784
  }
3705
1785
  };
3706
- if (path4.isAbsolute(resultPath)) {
1786
+ if (path2.isAbsolute(resultPath)) {
3707
1787
  addCandidate(resultPath);
3708
1788
  } else {
3709
1789
  addRelativeCandidates(resultPath);
@@ -3820,11 +1900,11 @@ async function qmdStartupCollectionCheckWithTimeout(promise, controller, label)
3820
1900
  return await Promise.race([checkedPromise, timeoutPromise]);
3821
1901
  }
3822
1902
  function defaultWorkspaceDir() {
3823
- return path4.join(os.homedir(), ".openclaw", "workspace");
1903
+ return path2.join(os.homedir(), ".openclaw", "workspace");
3824
1904
  }
3825
1905
  function sanitizeSessionKeyForFilename(sessionKey) {
3826
1906
  const readable = sessionKey.replace(/[^a-zA-Z0-9._-]/g, "_");
3827
- const hash = createHash2("sha256").update(sessionKey).digest("hex").slice(0, 12);
1907
+ const hash = createHash("sha256").update(sessionKey).digest("hex").slice(0, 12);
3828
1908
  return `${readable}-${hash}`;
3829
1909
  }
3830
1910
  function latestSourceValidAtFromTurns(turns) {
@@ -4127,11 +2207,11 @@ function mergeGraphExpandedResults(primary, expanded) {
4127
2207
  return Array.from(mergedByPath.values());
4128
2208
  }
4129
2209
  function graphPathRelativeToStorage(storageDir, candidatePath) {
4130
- const absolutePath = path4.isAbsolute(candidatePath) ? candidatePath : path4.resolve(storageDir, candidatePath);
4131
- const rel = path4.relative(storageDir, absolutePath);
2210
+ const absolutePath = path2.isAbsolute(candidatePath) ? candidatePath : path2.resolve(storageDir, candidatePath);
2211
+ const rel = path2.relative(storageDir, absolutePath);
4132
2212
  if (!rel || rel === ".") return null;
4133
2213
  if (rel.startsWith("..")) return null;
4134
- return rel.split(path4.sep).join("/");
2214
+ return rel.split(path2.sep).join("/");
4135
2215
  }
4136
2216
  function normalizeGraphActivationScore(score) {
4137
2217
  const bounded = Number.isFinite(score) && score > 0 ? score : 0;
@@ -4273,7 +2353,7 @@ function buildMemoryPathById(allMemsForGraph, storageDir) {
4273
2353
  for (const mem of allMemsForGraph ?? []) {
4274
2354
  const id = mem.frontmatter.id;
4275
2355
  if (!id) continue;
4276
- pathById.set(id, path4.relative(storageDir, mem.path));
2356
+ pathById.set(id, path2.relative(storageDir, mem.path));
4277
2357
  }
4278
2358
  return pathById;
4279
2359
  }
@@ -4281,7 +2361,7 @@ function appendMemoryToGraphContext(options) {
4281
2361
  if (!Array.isArray(options.allMemsForGraph)) return;
4282
2362
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4283
2363
  options.allMemsForGraph.push({
4284
- path: path4.join(options.storageDir, options.memoryRelPath),
2364
+ path: path2.join(options.storageDir, options.memoryRelPath),
4285
2365
  content: options.content,
4286
2366
  frontmatter: {
4287
2367
  id: options.memoryId,
@@ -4301,16 +2381,16 @@ function resolvePersistedMemoryRelativePath(options) {
4301
2381
  const persisted = options.pathById.get(options.memoryId);
4302
2382
  if (persisted) return persisted;
4303
2383
  if (options.category === "correction") {
4304
- return path4.join("corrections", `${options.memoryId}.md`);
2384
+ return path2.join("corrections", `${options.memoryId}.md`);
4305
2385
  }
4306
2386
  const subtree = categoryDirName(options.category);
4307
2387
  const idParts = options.memoryId.split("-");
4308
2388
  const maybeTimestamp = Number(idParts[1]);
4309
2389
  if (Number.isFinite(maybeTimestamp) && maybeTimestamp > 0) {
4310
2390
  const day = new Date(maybeTimestamp).toISOString().slice(0, 10);
4311
- return path4.join(subtree, day, `${options.memoryId}.md`);
2391
+ return path2.join(subtree, day, `${options.memoryId}.md`);
4312
2392
  }
4313
- return path4.join(subtree, `${options.memoryId}.md`);
2393
+ return path2.join(subtree, `${options.memoryId}.md`);
4314
2394
  }
4315
2395
  function qmdMaintenanceSkipReasonForError(error) {
4316
2396
  const message = error instanceof Error ? error.message : String(error);
@@ -4745,7 +2825,7 @@ var Orchestrator = class _Orchestrator {
4745
2825
  const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
4746
2826
  if (ns !== defaultNs && !isSafeRouteNamespace(ns)) return;
4747
2827
  if (!this.storageDirMatchesNamespaceHint(ns, storageDir)) return;
4748
- const resolvedStorageDir = path4.resolve(storageDir);
2828
+ const resolvedStorageDir = path2.resolve(storageDir);
4749
2829
  let hints = this.namespaceStorageDirHints.get(resolvedStorageDir);
4750
2830
  if (!hints) {
4751
2831
  hints = /* @__PURE__ */ new Set();
@@ -4756,21 +2836,21 @@ var Orchestrator = class _Orchestrator {
4756
2836
  storageDirMatchesNamespaceHint(namespace, storageDir) {
4757
2837
  const ns = normalizeNamespaceIdentity(namespace);
4758
2838
  if (!ns) return false;
4759
- const resolvedStorageDir = path4.resolve(storageDir);
4760
- const resolvedMemoryDir = path4.resolve(this.config.memoryDir);
2839
+ const resolvedStorageDir = path2.resolve(storageDir);
2840
+ const resolvedMemoryDir = path2.resolve(this.config.memoryDir);
4761
2841
  const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
4762
2842
  if (resolvedStorageDir === resolvedMemoryDir) return ns === defaultNs;
4763
- const resolvedNamespacesDir = path4.join(resolvedMemoryDir, "namespaces");
2843
+ const resolvedNamespacesDir = path2.join(resolvedMemoryDir, "namespaces");
4764
2844
  if (!isPathInsideStorageRoot(resolvedNamespacesDir, resolvedStorageDir)) return false;
4765
- const rawRoot = path4.resolve(resolvedNamespacesDir, ns);
4766
- const tokenRoot = path4.resolve(resolvedNamespacesDir, namespaceIdentityToken(ns));
2845
+ const rawRoot = path2.resolve(resolvedNamespacesDir, ns);
2846
+ const tokenRoot = path2.resolve(resolvedNamespacesDir, namespaceIdentityToken(ns));
4767
2847
  return resolvedStorageDir === rawRoot || resolvedStorageDir === tokenRoot;
4768
2848
  }
4769
2849
  namespaceStorageDirHintOwnershipRank(record, resolvedStorageDir, configured) {
4770
- if (resolvedStorageDir === path4.resolve(this.config.memoryDir)) {
2850
+ if (resolvedStorageDir === path2.resolve(this.config.memoryDir)) {
4771
2851
  return record.namespace === normalizeNamespaceIdentity(this.config.defaultNamespace) ? 0 : 3;
4772
2852
  }
4773
- const leaf = path4.basename(resolvedStorageDir);
2853
+ const leaf = path2.basename(resolvedStorageDir);
4774
2854
  const tokenOwnsRoot = namespaceIdentityToken(record.namespace) === leaf;
4775
2855
  if (tokenOwnsRoot && configured.has(record.namespace)) return 0;
4776
2856
  if (record.namespace === leaf) return 1;
@@ -4798,7 +2878,7 @@ var Orchestrator = class _Orchestrator {
4798
2878
  loadNamespaceStorageDirHintsFromCatalog() {
4799
2879
  if (this.namespaceStorageDirHintsLoaded || !this.namespaceCatalog.enabled) return;
4800
2880
  this.namespaceStorageDirHintsLoaded = true;
4801
- const catalogPath = path4.join(this.config.memoryDir, "state", "namespaces.jsonl");
2881
+ const catalogPath = path2.join(this.config.memoryDir, "state", "namespaces.jsonl");
4802
2882
  if (!existsSync(catalogPath)) return;
4803
2883
  let body;
4804
2884
  try {
@@ -4835,7 +2915,7 @@ var Orchestrator = class _Orchestrator {
4835
2915
  if (!this.storageDirMatchesNamespaceHint(record.namespace, record.storageDir)) {
4836
2916
  continue;
4837
2917
  }
4838
- const resolvedStorageDir = path4.resolve(record.storageDir);
2918
+ const resolvedStorageDir = path2.resolve(record.storageDir);
4839
2919
  const current = preferredByStorageDir.get(resolvedStorageDir);
4840
2920
  preferredByStorageDir.set(
4841
2921
  resolvedStorageDir,
@@ -4874,6 +2954,36 @@ var Orchestrator = class _Orchestrator {
4874
2954
  });
4875
2955
  return plan.namespaces.map((candidate) => candidate.namespace);
4876
2956
  }
2957
+ /**
2958
+ * Fan out a maintenance job across all maintained namespaces (issue #1500).
2959
+ *
2960
+ * Delegates to the namespace-maintenance-fanout coordinator, which plans
2961
+ * namespace discovery (configured + catalog), applies the cycle budget,
2962
+ * acquires per-job+namespace locks, records status files, and touches the
2963
+ * catalog's `lastMaintenanceAt`. The runner receives a per-namespace
2964
+ * candidate and a storage resolver wired to `this.getStorage(namespace)`.
2965
+ *
2966
+ * When namespaces are disabled the planner returns only the default
2967
+ * namespace, so the job runs exactly once — preserving single-user behavior.
2968
+ */
2969
+ async runNamespaceMaintenanceFanoutForJob(jobName, runner, options = {}) {
2970
+ return runNamespaceMaintenanceFanout({
2971
+ config: this.config,
2972
+ catalog: this.namespaceCatalog,
2973
+ jobName,
2974
+ runner,
2975
+ resolveStorage: (namespace) => this.getStorage(namespace),
2976
+ enabled: options.enabled
2977
+ });
2978
+ }
2979
+ /**
2980
+ * Read-only namespace maintenance health summary for doctor / dashboard /
2981
+ * CLI (issue #1500). Aggregates all per-namespace maintenance status files
2982
+ * into one report without running any maintenance.
2983
+ */
2984
+ async readNamespaceMaintenanceHealth() {
2985
+ return summarizeNamespaceMaintenanceHealth(this.config);
2986
+ }
4877
2987
  buildConfiguredQmdSearchOptions(queryText) {
4878
2988
  const intentHint = this.config.qmdIntentHintsEnabled ? buildQmdIntentHint(inferIntentFromText(queryText)) : void 0;
4879
2989
  const explain = this.config.qmdExplainEnabled === true;
@@ -5002,7 +3112,7 @@ var Orchestrator = class _Orchestrator {
5002
3112
  this.config = config;
5003
3113
  this.profiler = new ProfilingCollector({
5004
3114
  enabled: config.profilingEnabled,
5005
- storageDir: config.profilingStorageDir || path4.join(config.memoryDir, "profiling"),
3115
+ storageDir: config.profilingStorageDir || path2.join(config.memoryDir, "profiling"),
5006
3116
  maxTraces: config.profilingMaxTraces
5007
3117
  });
5008
3118
  this.namespaceCatalog = new NamespaceCatalog(config);
@@ -5048,7 +3158,7 @@ var Orchestrator = class _Orchestrator {
5048
3158
  this.compounding = config.compoundingEnabled ? new CompoundingEngine(config, this.storage) : void 0;
5049
3159
  this.buffer = new SmartBuffer(config, this.storage);
5050
3160
  this.transcript = new TranscriptManager(config);
5051
- this.conversationIndexDir = path4.join(
3161
+ this.conversationIndexDir = path2.join(
5052
3162
  config.memoryDir,
5053
3163
  "conversation-index",
5054
3164
  "chunks"
@@ -5105,7 +3215,7 @@ var Orchestrator = class _Orchestrator {
5105
3215
  this.modelRegistry
5106
3216
  );
5107
3217
  this.threading = new ThreadingManager(
5108
- path4.join(config.memoryDir, "threads"),
3218
+ path2.join(config.memoryDir, "threads"),
5109
3219
  config.threadingGapMinutes
5110
3220
  );
5111
3221
  this.tmtBuilder = new TmtBuilder(config.memoryDir, {
@@ -5184,7 +3294,7 @@ var Orchestrator = class _Orchestrator {
5184
3294
  utilityPromoteThresholdDelta: this.utilityRuntimeValues?.promoteThresholdDelta ?? 0,
5185
3295
  utilityDemoteThresholdDelta: this.utilityRuntimeValues?.demoteThresholdDelta ?? 0
5186
3296
  };
5187
- return createHash2("sha256").update(JSON.stringify(payload)).digest("hex").slice(0, 12);
3297
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex").slice(0, 12);
5188
3298
  }
5189
3299
  effectiveLifecycleThresholds() {
5190
3300
  const archiveDecayThreshold = this.config.lifecycleArchiveDecayThreshold;
@@ -5404,13 +3514,13 @@ var Orchestrator = class _Orchestrator {
5404
3514
  if (this.config.compactionResetEnabled) {
5405
3515
  try {
5406
3516
  const wsDir = this.config.workspaceDir || defaultWorkspaceDir();
5407
- const files = await readdir3(wsDir).catch(() => []);
3517
+ const files = await readdir(wsDir).catch(() => []);
5408
3518
  for (const f of files) {
5409
3519
  if (!f.startsWith(".compaction-reset-signal-")) continue;
5410
- const fp = path4.join(wsDir, f);
5411
- const s = await stat3(fp).catch(() => null);
3520
+ const fp = path2.join(wsDir, f);
3521
+ const s = await stat(fp).catch(() => null);
5412
3522
  if (s && Date.now() - s.mtimeMs >= COMPACTION_SIGNAL_MAX_AGE_MS) {
5413
- await unlink2(fp).catch(() => {
3523
+ await unlink(fp).catch(() => {
5414
3524
  });
5415
3525
  log.debug(`initialize: removed stale compaction signal ${f}`);
5416
3526
  }
@@ -5686,9 +3796,9 @@ var Orchestrator = class _Orchestrator {
5686
3796
  `wearables auto-sync started: every ${this.config.wearables.autoSyncIntervalMinutes}m over ${this.config.wearables.autoSyncDays}d (deep ${this.config.wearables.autoSyncDeepDays}d daily)`
5687
3797
  );
5688
3798
  } catch (err) {
5689
- const { displayErrorDetail: displayErrorDetail2 } = await import("./runtime/better-sqlite.js");
3799
+ const { displayErrorDetail } = await import("./runtime/better-sqlite.js");
5690
3800
  log.warn(
5691
- `wearables auto-sync failed to start (non-fatal): ${displayErrorDetail2(err)}`
3801
+ `wearables auto-sync failed to start (non-fatal): ${displayErrorDetail(err)}`
5692
3802
  );
5693
3803
  }
5694
3804
  }
@@ -5801,7 +3911,7 @@ var Orchestrator = class _Orchestrator {
5801
3911
  */
5802
3912
  async autoRegisterDaySummaryCron() {
5803
3913
  const home = resolveHomeDir();
5804
- const jobsPath = path4.join(home, ".openclaw", "cron", "jobs.json");
3914
+ const jobsPath = path2.join(home, ".openclaw", "cron", "jobs.json");
5805
3915
  try {
5806
3916
  if (!existsSync(jobsPath)) {
5807
3917
  log.debug(
@@ -5854,7 +3964,7 @@ var Orchestrator = class _Orchestrator {
5854
3964
  }
5855
3965
  async autoRegisterNightlyGovernanceCron() {
5856
3966
  const home = resolveHomeDir();
5857
- const jobsPath = path4.join(home, ".openclaw", "cron", "jobs.json");
3967
+ const jobsPath = path2.join(home, ".openclaw", "cron", "jobs.json");
5858
3968
  try {
5859
3969
  if (!existsSync(jobsPath)) {
5860
3970
  log.debug("nightly governance cron: jobs.json not found, skipping auto-register");
@@ -5876,7 +3986,7 @@ var Orchestrator = class _Orchestrator {
5876
3986
  }
5877
3987
  async autoRegisterProceduralMiningCron() {
5878
3988
  const home = resolveHomeDir();
5879
- const jobsPath = path4.join(home, ".openclaw", "cron", "jobs.json");
3989
+ const jobsPath = path2.join(home, ".openclaw", "cron", "jobs.json");
5880
3990
  try {
5881
3991
  if (!existsSync(jobsPath)) {
5882
3992
  log.debug("procedural mining cron: jobs.json not found, skipping auto-register");
@@ -5896,7 +4006,7 @@ var Orchestrator = class _Orchestrator {
5896
4006
  }
5897
4007
  async autoRegisterContradictionScanCron() {
5898
4008
  const home = resolveHomeDir();
5899
- const jobsPath = path4.join(home, ".openclaw", "cron", "jobs.json");
4009
+ const jobsPath = path2.join(home, ".openclaw", "cron", "jobs.json");
5900
4010
  try {
5901
4011
  if (!existsSync(jobsPath)) {
5902
4012
  log.debug("contradiction scan cron: jobs.json not found, skipping auto-register");
@@ -5916,7 +4026,7 @@ var Orchestrator = class _Orchestrator {
5916
4026
  }
5917
4027
  async autoRegisterPatternReinforcementCron() {
5918
4028
  const home = resolveHomeDir();
5919
- const jobsPath = path4.join(home, ".openclaw", "cron", "jobs.json");
4029
+ const jobsPath = path2.join(home, ".openclaw", "cron", "jobs.json");
5920
4030
  try {
5921
4031
  if (!existsSync(jobsPath)) {
5922
4032
  log.debug("pattern reinforcement cron: jobs.json not found, skipping auto-register");
@@ -5976,9 +4086,92 @@ var Orchestrator = class _Orchestrator {
5976
4086
  );
5977
4087
  return { ran: true, result, namespace: cadenceKey };
5978
4088
  }
4089
+ /**
4090
+ * Fan out pattern reinforcement across all maintained namespaces (issue #1500).
4091
+ * Delegates per-namespace execution to {@link runPatternReinforcement} while
4092
+ * the planner handles discovery, budgeting, locking, and status recording.
4093
+ * When namespaces are disabled, runs once against default storage.
4094
+ */
4095
+ async runPatternReinforcementFanout(options = {}) {
4096
+ return this.runNamespaceMaintenanceFanoutForJob(
4097
+ "pattern-reinforcement",
4098
+ async (ctx) => {
4099
+ const result = await this.runPatternReinforcement({
4100
+ namespace: ctx.candidate.namespace,
4101
+ force: options.force
4102
+ });
4103
+ if (!result.ran) {
4104
+ return {
4105
+ skipped: true,
4106
+ skipReason: result.skippedReason ?? "throttled"
4107
+ };
4108
+ }
4109
+ return result.result ? { itemCount: result.result.clustersFound } : { itemCount: 0 };
4110
+ },
4111
+ { enabled: this.config.patternReinforcementEnabled }
4112
+ );
4113
+ }
4114
+ /**
4115
+ * Fan out lifecycle/governance policy across all maintained namespaces
4116
+ * (issue #1500). Each namespace gets its own lifecycle pass against its
4117
+ * namespace-scoped storage. When namespaces are disabled, runs once against
4118
+ * default storage.
4119
+ */
4120
+ async runLifecyclePolicyFanout() {
4121
+ return this.runNamespaceMaintenanceFanoutForJob(
4122
+ "lifecycle",
4123
+ async (ctx) => {
4124
+ const storage = await this.getStorage(ctx.candidate.namespace);
4125
+ const corpus = await storage.readAllMemories();
4126
+ const assessed = await this.runLifecyclePolicyPass(corpus, storage);
4127
+ return { itemCount: assessed };
4128
+ },
4129
+ { enabled: this.config.lifecyclePolicyEnabled }
4130
+ );
4131
+ }
4132
+ /**
4133
+ * Fan out semantic consolidation across all maintained namespaces (issue #1500).
4134
+ * Each namespace gets its own consolidation pass against its namespace-scoped
4135
+ * storage. When namespaces are disabled, runs once against default storage.
4136
+ */
4137
+ async runSemanticConsolidationFanout(options = {}) {
4138
+ return this.runNamespaceMaintenanceFanoutForJob(
4139
+ "semantic-consolidation",
4140
+ async (ctx) => {
4141
+ const storage = await this.getStorage(ctx.candidate.namespace);
4142
+ const result = await this.runSemanticConsolidation({
4143
+ dryRun: options.dryRun,
4144
+ thresholdOverride: void 0,
4145
+ force: true,
4146
+ storage
4147
+ });
4148
+ return { itemCount: result.clustersFound };
4149
+ },
4150
+ { enabled: this.config.semanticConsolidationEnabled }
4151
+ );
4152
+ }
4153
+ /**
4154
+ * Fan out deep-sleep governance across all maintained namespaces (issue #1500).
4155
+ * Each namespace gets its own governance scan against its namespace-scoped
4156
+ * storage. When namespaces are disabled, runs once against default storage.
4157
+ */
4158
+ async runDeepSleepGovernanceFanout(options = {}) {
4159
+ return this.runNamespaceMaintenanceFanoutForJob(
4160
+ "governance",
4161
+ async (ctx) => {
4162
+ const storage = await this.getStorage(ctx.candidate.namespace);
4163
+ const result = await this.runDeepSleepGovernanceNow({
4164
+ dryRun: options.dryRun,
4165
+ storage
4166
+ });
4167
+ return { itemCount: result.scannedMemories };
4168
+ },
4169
+ { enabled: this.config.dreamsPhases.deepSleep.enabled }
4170
+ );
4171
+ }
5979
4172
  async autoRegisterGraphEdgeDecayCron() {
5980
4173
  const home = resolveHomeDir();
5981
- const jobsPath = path4.join(home, ".openclaw", "cron", "jobs.json");
4174
+ const jobsPath = path2.join(home, ".openclaw", "cron", "jobs.json");
5982
4175
  try {
5983
4176
  if (!existsSync(jobsPath)) {
5984
4177
  log.debug("graph edge decay cron: jobs.json not found, skipping auto-register");
@@ -6035,15 +4228,15 @@ ${doc.content}` : doc.content,
6035
4228
  this.lastFileHygieneRunAtMs = now;
6036
4229
  if (hygiene.rotateEnabled) {
6037
4230
  for (const rel of hygiene.rotatePaths) {
6038
- const abs = path4.isAbsolute(rel) ? rel : path4.join(this.config.workspaceDir, rel);
4231
+ const abs = path2.isAbsolute(rel) ? rel : path2.join(this.config.workspaceDir, rel);
6039
4232
  try {
6040
- const raw = await readFile4(abs, "utf-8");
4233
+ const raw = await readFile2(abs, "utf-8");
6041
4234
  if (raw.length > hygiene.rotateMaxBytes) {
6042
- const archiveDir = path4.join(
4235
+ const archiveDir = path2.join(
6043
4236
  this.config.workspaceDir,
6044
4237
  hygiene.archiveDir
6045
4238
  );
6046
- const base = path4.basename(abs);
4239
+ const base = path2.basename(abs);
6047
4240
  const prefix = base.toUpperCase().replace(/\.MD$/i, "").replace(/[^A-Z0-9]+/g, "-") || "FILE";
6048
4241
  const { newContent } = await rotateMarkdownFileToArchive({
6049
4242
  filePath: abs,
@@ -6051,7 +4244,7 @@ ${doc.content}` : doc.content,
6051
4244
  archivePrefix: prefix,
6052
4245
  keepTailChars: hygiene.rotateKeepTailChars
6053
4246
  });
6054
- await writeFile4(abs, newContent, "utf-8");
4247
+ await writeFile2(abs, newContent, "utf-8");
6055
4248
  }
6056
4249
  } catch {
6057
4250
  }
@@ -6068,8 +4261,8 @@ ${doc.content}` : doc.content,
6068
4261
  log.warn(w.message);
6069
4262
  }
6070
4263
  if (hygiene.warningsLogEnabled && warnings.length > 0) {
6071
- const fp = path4.join(this.config.memoryDir, hygiene.warningsLogPath);
6072
- await mkdir4(path4.dirname(fp), { recursive: true });
4264
+ const fp = path2.join(this.config.memoryDir, hygiene.warningsLogPath);
4265
+ await mkdir2(path2.dirname(fp), { recursive: true });
6073
4266
  const stamp = (/* @__PURE__ */ new Date()).toISOString();
6074
4267
  const block = `
6075
4268
 
@@ -6078,11 +4271,11 @@ ${doc.content}` : doc.content,
6078
4271
  ` + warnings.map((w) => `- ${w.message}`).join("\n") + "\n";
6079
4272
  let existing = "";
6080
4273
  try {
6081
- existing = await readFile4(fp, "utf-8");
4274
+ existing = await readFile2(fp, "utf-8");
6082
4275
  } catch {
6083
4276
  existing = "# Engram File Hygiene Warnings\n";
6084
4277
  }
6085
- await writeFile4(fp, existing + block, "utf-8");
4278
+ await writeFile2(fp, existing + block, "utf-8");
6086
4279
  }
6087
4280
  }
6088
4281
  }
@@ -6547,26 +4740,26 @@ ${evidenceText}`
6547
4740
  const facts = [];
6548
4741
  let memoryRootReal = null;
6549
4742
  try {
6550
- memoryRootReal = await realpath2(storage.dir);
4743
+ memoryRootReal = await realpath(storage.dir);
6551
4744
  } catch {
6552
4745
  memoryRootReal = null;
6553
4746
  }
6554
4747
  for (const categoryDir of RECALL_FALLBACK_DIRS) {
6555
4748
  if (memoryRootReal === null) break;
6556
4749
  for (const date of datesToScan) {
6557
- const dateDir = path4.join(storage.dir, categoryDir, date);
4750
+ const dateDir = path2.join(storage.dir, categoryDir, date);
6558
4751
  try {
6559
- const dirStat = await lstat3(dateDir);
4752
+ const dirStat = await lstat(dateDir);
6560
4753
  if (dirStat.isSymbolicLink() || !dirStat.isDirectory()) continue;
6561
- assertPathInsideRoot(memoryRootReal, await realpath2(dateDir), dateDir);
6562
- const entries = await readdir3(dateDir, { withFileTypes: true });
4754
+ assertPathInsideRoot(memoryRootReal, await realpath(dateDir), dateDir);
4755
+ const entries = await readdir(dateDir, { withFileTypes: true });
6563
4756
  for (const entry of entries) {
6564
4757
  if (entry.isSymbolicLink()) continue;
6565
4758
  if (!entry.name.endsWith(".md")) continue;
6566
- const fullPath = path4.join(dateDir, entry.name);
4759
+ const fullPath = path2.join(dateDir, entry.name);
6567
4760
  try {
6568
- assertPathInsideRoot(memoryRootReal, await realpath2(fullPath), fullPath);
6569
- const raw = await readFile4(fullPath, "utf-8");
4761
+ assertPathInsideRoot(memoryRootReal, await realpath(fullPath), fullPath);
4762
+ const raw = await readFile2(fullPath, "utf-8");
6570
4763
  const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
6571
4764
  if (!fmMatch) continue;
6572
4765
  const fmBlock = fmMatch[1];
@@ -6585,7 +4778,7 @@ ${evidenceText}`
6585
4778
  facts.push({
6586
4779
  path: fullPath,
6587
4780
  frontmatter: {
6588
- id: fm.id || path4.basename(entry.name, ".md"),
4781
+ id: fm.id || path2.basename(entry.name, ".md"),
6589
4782
  category: fm.category || "fact",
6590
4783
  created,
6591
4784
  updated: fm.updated || created,
@@ -6608,15 +4801,15 @@ ${evidenceText}`
6608
4801
  return a.frontmatter.created < b.frontmatter.created ? -1 : 1;
6609
4802
  });
6610
4803
  const hourlySummaries = [];
6611
- const hourlyBaseDir = path4.join(storage.dir, "summaries", "hourly");
4804
+ const hourlyBaseDir = path2.join(storage.dir, "summaries", "hourly");
6612
4805
  try {
6613
- const sessionKeys = await readdir3(hourlyBaseDir, { withFileTypes: true });
4806
+ const sessionKeys = await readdir(hourlyBaseDir, { withFileTypes: true });
6614
4807
  for (const sk of sessionKeys) {
6615
4808
  if (!sk.isDirectory()) continue;
6616
4809
  for (const date of datesToScan) {
6617
- const summaryFile = path4.join(hourlyBaseDir, sk.name, `${date}.md`);
4810
+ const summaryFile = path2.join(hourlyBaseDir, sk.name, `${date}.md`);
6618
4811
  try {
6619
- const raw = await readFile4(summaryFile, "utf-8");
4812
+ const raw = await readFile2(summaryFile, "utf-8");
6620
4813
  const filtered = filterHourlySummaryMarkdownForLocalDay(
6621
4814
  raw,
6622
4815
  date,
@@ -6718,13 +4911,13 @@ ${evidenceText}`
6718
4911
  }
6719
4912
  async getLastGraphRecallSnapshot(namespace) {
6720
4913
  const storage = await this.getStorage(namespace);
6721
- const snapshotPath = path4.join(
4914
+ const snapshotPath = path2.join(
6722
4915
  storage.dir,
6723
4916
  "state",
6724
4917
  "last_graph_recall.json"
6725
4918
  );
6726
4919
  try {
6727
- const raw = await readFile4(snapshotPath, "utf-8");
4920
+ const raw = await readFile2(snapshotPath, "utf-8");
6728
4921
  const parsed = JSON.parse(raw);
6729
4922
  if (!parsed || typeof parsed !== "object") return null;
6730
4923
  return {
@@ -6757,9 +4950,9 @@ ${evidenceText}`
6757
4950
  }
6758
4951
  async getLastIntentSnapshot(namespace) {
6759
4952
  const storage = await this.getStorage(namespace);
6760
- const snapshotPath = path4.join(storage.dir, "state", "last_intent.json");
4953
+ const snapshotPath = path2.join(storage.dir, "state", "last_intent.json");
6761
4954
  try {
6762
- const raw = await readFile4(snapshotPath, "utf-8");
4955
+ const raw = await readFile2(snapshotPath, "utf-8");
6763
4956
  const parsed = JSON.parse(raw);
6764
4957
  if (!parsed || typeof parsed !== "object") return null;
6765
4958
  const graphDecision = parsed.graphDecision && typeof parsed.graphDecision === "object" ? parsed.graphDecision : void 0;
@@ -6790,13 +4983,13 @@ ${evidenceText}`
6790
4983
  }
6791
4984
  async getLastQmdRecallSnapshot(namespace) {
6792
4985
  const storage = await this.getStorage(namespace);
6793
- const snapshotPath = path4.join(
4986
+ const snapshotPath = path2.join(
6794
4987
  storage.dir,
6795
4988
  "state",
6796
4989
  "last_qmd_recall.json"
6797
4990
  );
6798
4991
  try {
6799
- const raw = await readFile4(snapshotPath, "utf-8");
4992
+ const raw = await readFile2(snapshotPath, "utf-8");
6800
4993
  const parsed = JSON.parse(raw);
6801
4994
  if (!parsed || typeof parsed !== "object") return null;
6802
4995
  return {
@@ -6940,10 +5133,10 @@ ${r.snippet.trim()}
6940
5133
  }
6941
5134
  async countConversationChunkDocs(dir) {
6942
5135
  try {
6943
- const entries = await readdir3(dir, { withFileTypes: true });
5136
+ const entries = await readdir(dir, { withFileTypes: true });
6944
5137
  let total = 0;
6945
5138
  for (const entry of entries) {
6946
- const fullPath = path4.join(dir, entry.name);
5139
+ const fullPath = path2.join(dir, entry.name);
6947
5140
  if (entry.isDirectory()) {
6948
5141
  total += await this.countConversationChunkDocs(fullPath);
6949
5142
  continue;
@@ -7667,7 +5860,7 @@ ${r.snippet.trim()}
7667
5860
  if (!options.onDebugSnapshot) return;
7668
5861
  await options.onDebugSnapshot({
7669
5862
  recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
7670
- queryHash: createHash2("sha256").update(prompt).digest("hex"),
5863
+ queryHash: createHash("sha256").update(prompt).digest("hex"),
7671
5864
  queryLength: prompt.length,
7672
5865
  collection: options.collection,
7673
5866
  namespaces: options.recallNamespaces,
@@ -7849,7 +6042,7 @@ ${r.snippet.trim()}
7849
6042
  resolvedPath = resolvedCold.result.path;
7850
6043
  resolvedResult = resolvedCold.result;
7851
6044
  }
7852
- if (!path4.isAbsolute(resolvedPath)) {
6045
+ if (!path2.isAbsolute(resolvedPath)) {
7853
6046
  resolvedAmbiguousSeeds.set(result.path, null);
7854
6047
  return null;
7855
6048
  }
@@ -7874,7 +6067,7 @@ ${r.snippet.trim()}
7874
6067
  }
7875
6068
  continue;
7876
6069
  }
7877
- if (path4.isAbsolute(result.path)) {
6070
+ if (path2.isAbsolute(result.path)) {
7878
6071
  const resolved = await resolveAmbiguousSeedOwner(result, null);
7879
6072
  if (resolved) {
7880
6073
  addResultForNamespace(resolved.namespace, resolved.result);
@@ -7917,7 +6110,7 @@ ${r.snippet.trim()}
7917
6110
  0
7918
6111
  );
7919
6112
  seedPaths.push(
7920
- ...seedRelativePaths.map((rel) => path4.join(storage.dir, rel))
6113
+ ...seedRelativePaths.map((rel) => path2.join(storage.dir, rel))
7921
6114
  );
7922
6115
  const seedSet = new Set(seedRelativePaths);
7923
6116
  const expanded = await this.graphIndexFor(storage).spreadingActivation(
@@ -7933,7 +6126,7 @@ ${r.snippet.trim()}
7933
6126
  for (const candidate of expanded.slice(0, perNamespaceExpandedCap)) {
7934
6127
  if (deadlineExpired()) break;
7935
6128
  if (seedSet.has(candidate.path)) continue;
7936
- const memoryPath = path4.resolve(storage.dir, candidate.path);
6129
+ const memoryPath = path2.resolve(storage.dir, candidate.path);
7937
6130
  const memory = await storage.readMemoryByPath(memoryPath);
7938
6131
  if (deadlineExpired()) break;
7939
6132
  if (!memory) continue;
@@ -7958,7 +6151,7 @@ ${r.snippet.trim()}
7958
6151
  path: memory.path,
7959
6152
  score,
7960
6153
  namespace,
7961
- seed: path4.resolve(storage.dir, candidate.seed),
6154
+ seed: path2.resolve(storage.dir, candidate.seed),
7962
6155
  hopDepth: candidate.hopDepth,
7963
6156
  decayedWeight: candidate.decayedWeight,
7964
6157
  graphType: candidate.graphType,
@@ -7979,12 +6172,12 @@ ${r.snippet.trim()}
7979
6172
  }
7980
6173
  async recordLastGraphRecallSnapshot(options) {
7981
6174
  try {
7982
- const snapshotPath = path4.join(
6175
+ const snapshotPath = path2.join(
7983
6176
  options.storage.dir,
7984
6177
  "state",
7985
6178
  "last_graph_recall.json"
7986
6179
  );
7987
- await mkdir4(path4.dirname(snapshotPath), { recursive: true });
6180
+ await mkdir2(path2.dirname(snapshotPath), { recursive: true });
7988
6181
  const now = (/* @__PURE__ */ new Date()).toISOString();
7989
6182
  const totalSeedCount = options.seedPaths.length;
7990
6183
  const totalExpandedCount = options.expandedPaths.length;
@@ -7996,7 +6189,7 @@ ${r.snippet.trim()}
7996
6189
  const payload = {
7997
6190
  recordedAt: now,
7998
6191
  mode: options.recallMode,
7999
- queryHash: createHash2("sha256").update(options.prompt).digest("hex"),
6192
+ queryHash: createHash("sha256").update(options.prompt).digest("hex"),
8000
6193
  queryLength: options.prompt.length,
8001
6194
  namespaces: options.recallNamespaces,
8002
6195
  seedCount: totalSeedCount,
@@ -8011,20 +6204,20 @@ ${r.snippet.trim()}
8011
6204
  finalResults: (options.finalResults ?? []).slice(0, 64),
8012
6205
  shadowComparison: options.shadowComparison
8013
6206
  };
8014
- await writeFile4(snapshotPath, JSON.stringify(payload, null, 2), "utf-8");
6207
+ await writeFile2(snapshotPath, JSON.stringify(payload, null, 2), "utf-8");
8015
6208
  } catch (err) {
8016
6209
  log.debug(`last graph recall write failed: ${err}`);
8017
6210
  }
8018
6211
  }
8019
6212
  async recordLastIntentSnapshot(options) {
8020
6213
  try {
8021
- const snapshotPath = path4.join(
6214
+ const snapshotPath = path2.join(
8022
6215
  options.storage.dir,
8023
6216
  "state",
8024
6217
  "last_intent.json"
8025
6218
  );
8026
- await mkdir4(path4.dirname(snapshotPath), { recursive: true });
8027
- await writeFile4(
6219
+ await mkdir2(path2.dirname(snapshotPath), { recursive: true });
6220
+ await writeFile2(
8028
6221
  snapshotPath,
8029
6222
  JSON.stringify(options.snapshot, null, 2),
8030
6223
  "utf-8"
@@ -8035,13 +6228,13 @@ ${r.snippet.trim()}
8035
6228
  }
8036
6229
  async recordLastQmdRecallSnapshot(options) {
8037
6230
  try {
8038
- const snapshotPath = path4.join(
6231
+ const snapshotPath = path2.join(
8039
6232
  options.storage.dir,
8040
6233
  "state",
8041
6234
  "last_qmd_recall.json"
8042
6235
  );
8043
- await mkdir4(path4.dirname(snapshotPath), { recursive: true });
8044
- await writeFile4(
6236
+ await mkdir2(path2.dirname(snapshotPath), { recursive: true });
6237
+ await writeFile2(
8045
6238
  snapshotPath,
8046
6239
  JSON.stringify(options.snapshot, null, 2),
8047
6240
  "utf-8"
@@ -8055,9 +6248,9 @@ ${r.snippet.trim()}
8055
6248
  const stateDir = await this.resolveStateDirForNamespace(
8056
6249
  options.namespace
8057
6250
  );
8058
- const snapshotPath = path4.join(stateDir, "last_intent.json");
8059
- await mkdir4(path4.dirname(snapshotPath), { recursive: true });
8060
- await writeFile4(
6251
+ const snapshotPath = path2.join(stateDir, "last_intent.json");
6252
+ await mkdir2(path2.dirname(snapshotPath), { recursive: true });
6253
+ await writeFile2(
8061
6254
  snapshotPath,
8062
6255
  JSON.stringify(options.snapshot, null, 2),
8063
6256
  "utf-8"
@@ -8068,24 +6261,24 @@ ${r.snippet.trim()}
8068
6261
  }
8069
6262
  async resolveStateDirForNamespace(namespace) {
8070
6263
  if (!this.config.namespacesEnabled) {
8071
- return path4.join(this.config.memoryDir, "state");
6264
+ return path2.join(this.config.memoryDir, "state");
8072
6265
  }
8073
6266
  if (namespace !== this.config.defaultNamespace) {
8074
- return path4.join(this.config.memoryDir, "namespaces", namespace, "state");
6267
+ return path2.join(this.config.memoryDir, "namespaces", namespace, "state");
8075
6268
  }
8076
- const candidate = path4.join(
6269
+ const candidate = path2.join(
8077
6270
  this.config.memoryDir,
8078
6271
  "namespaces",
8079
6272
  this.config.defaultNamespace
8080
6273
  );
8081
6274
  try {
8082
- const candidateStat = await stat3(candidate);
6275
+ const candidateStat = await stat(candidate);
8083
6276
  if (candidateStat.isDirectory()) {
8084
- return path4.join(candidate, "state");
6277
+ return path2.join(candidate, "state");
8085
6278
  }
8086
6279
  } catch {
8087
6280
  }
8088
- return path4.join(this.config.memoryDir, "state");
6281
+ return path2.join(this.config.memoryDir, "state");
8089
6282
  }
8090
6283
  buildGraphRecallRankedResults(results, sourceLabelResolver, limit = 64) {
8091
6284
  return results.slice(0, limit).map((result) => ({
@@ -8338,8 +6531,8 @@ ${r.snippet.trim()}
8338
6531
  timings,
8339
6532
  logger: log
8340
6533
  });
8341
- const promptHash = createHash2("sha256").update(prompt).digest("hex");
8342
- const traceId = createHash2("sha256").update(`${sessionKey ?? "default"}:${recallStart}:${promptHash}`).digest("hex").slice(0, 16);
6534
+ const promptHash = createHash("sha256").update(prompt).digest("hex");
6535
+ const traceId = createHash("sha256").update(`${sessionKey ?? "default"}:${recallStart}:${promptHash}`).digest("hex").slice(0, 16);
8343
6536
  const sectionBuckets = /* @__PURE__ */ new Map();
8344
6537
  const queryPolicy = buildRecallQueryPolicy(prompt, sessionKey, {
8345
6538
  cronRecallPolicyEnabled: this.config.cronRecallPolicyEnabled,
@@ -8348,7 +6541,7 @@ ${r.snippet.trim()}
8348
6541
  cronConversationRecallMode: this.config.cronConversationRecallMode
8349
6542
  });
8350
6543
  const retrievalQuery = queryPolicy.retrievalQuery || prompt;
8351
- const retrievalQueryHash = createHash2("sha256").update(retrievalQuery).digest("hex");
6544
+ const retrievalQueryHash = createHash("sha256").update(retrievalQuery).digest("hex");
8352
6545
  const policyVersion = this.currentPolicyVersion();
8353
6546
  let impressionRecorded = false;
8354
6547
  let recallSource = "none";
@@ -8483,7 +6676,7 @@ ${r.snippet.trim()}
8483
6676
  const graphExpandedResultPaths = /* @__PURE__ */ new Set();
8484
6677
  const graphSourceLabelsForPath = (resultPath) => {
8485
6678
  const labels = [];
8486
- const normalizedPath = resultPath.split(path4.sep).join("/");
6679
+ const normalizedPath = resultPath.split(path2.sep).join("/");
8487
6680
  const isEntityPath = normalizedPath.startsWith("entities/") || normalizedPath.includes("/entities/");
8488
6681
  if (graphBaselinePaths.has(resultPath)) labels.push("baseline");
8489
6682
  if (graphExpandedResultPaths.has(resultPath))
@@ -8633,7 +6826,7 @@ ${r.snippet.trim()}
8633
6826
  profileStorageNamespaces.map((namespace) => this.storageRouter.storageFor(namespace))
8634
6827
  );
8635
6828
  const emptyProfileStorage = new Proxy(
8636
- { dir: path4.join(this.config.memoryDir, ".empty-scope-profile") },
6829
+ { dir: path2.join(this.config.memoryDir, ".empty-scope-profile") },
8637
6830
  {
8638
6831
  get(target, prop) {
8639
6832
  if (prop in target) return target[prop];
@@ -10109,16 +8302,16 @@ ${formatted}`;
10109
8302
  if (!this.config.compactionResetEnabled) return null;
10110
8303
  const workspaceDir = compactionWorkspaceDir || this.config.workspaceDir || defaultWorkspaceDir();
10111
8304
  const safeSessionKey = sanitizeSessionKeyForFilename(effectiveSessionKey);
10112
- const signalPath = path4.join(
8305
+ const signalPath = path2.join(
10113
8306
  workspaceDir,
10114
8307
  `.compaction-reset-signal-${safeSessionKey}`
10115
8308
  );
10116
- const bootPath = path4.join(workspaceDir, "BOOT.md");
8309
+ const bootPath = path2.join(workspaceDir, "BOOT.md");
10117
8310
  try {
10118
- const signalStat = await stat3(signalPath).catch(() => null);
8311
+ const signalStat = await stat(signalPath).catch(() => null);
10119
8312
  if (!signalStat) return null;
10120
8313
  const signalAge = Date.now() - signalStat.mtimeMs;
10121
- const signalData = JSON.parse(await readFile4(signalPath, "utf-8"));
8314
+ const signalData = JSON.parse(await readFile2(signalPath, "utf-8"));
10122
8315
  if (signalData.sessionKey !== effectiveSessionKey) {
10123
8316
  log.debug(
10124
8317
  `recall: compaction signal is for ${signalData.sessionKey}, not ${effectiveSessionKey} \u2014 skipping`
@@ -10129,7 +8322,7 @@ ${formatted}`;
10129
8322
  log.debug(
10130
8323
  `recall: stale compaction signal (${Math.round(signalAge / 1e3)}s old), skipping`
10131
8324
  );
10132
- await unlink2(signalPath).catch(() => {
8325
+ await unlink(signalPath).catch(() => {
10133
8326
  });
10134
8327
  return null;
10135
8328
  }
@@ -10138,7 +8331,7 @@ ${formatted}`;
10138
8331
 
10139
8332
  `;
10140
8333
  try {
10141
- const bootContent = await readFile4(bootPath, "utf-8");
8334
+ const bootContent = await readFile2(bootPath, "utf-8");
10142
8335
  section += "### BOOT.md (working state before compaction)\n\n";
10143
8336
  section += bootContent + "\n";
10144
8337
  } catch {
@@ -10149,12 +8342,12 @@ ${formatted}`;
10149
8342
  log.info(
10150
8343
  `recall: injected compaction reset context for ${effectiveSessionKey}`
10151
8344
  );
10152
- await unlink2(signalPath).catch(() => {
8345
+ await unlink(signalPath).catch(() => {
10153
8346
  });
10154
8347
  return section;
10155
8348
  } catch (err) {
10156
8349
  log.debug("recall: compaction signal check failed:", err);
10157
- await unlink2(signalPath).catch(() => {
8350
+ await unlink(signalPath).catch(() => {
10158
8351
  });
10159
8352
  return null;
10160
8353
  }
@@ -12293,7 +10486,7 @@ _Context: ${topQuestion.context}_`
12293
10486
  const shouldUseStableBatchKey = turns.some(
12294
10487
  (turn) => turn.persistProcessedFingerprint === true || typeof turn.turnFingerprint === "string" && turn.turnFingerprint.length > 0
12295
10488
  );
12296
- const stableBatchFingerprint = shouldUseStableBatchKey ? createHash2("sha256").update(
10489
+ const stableBatchFingerprint = shouldUseStableBatchKey ? createHash("sha256").update(
12297
10490
  turns.map(
12298
10491
  (turn) => [
12299
10492
  turn.role,
@@ -12550,7 +10743,7 @@ _Context: ${topQuestion.context}_`
12550
10743
  buildExtractionFingerprint(turns, bufferKey) {
12551
10744
  const normalized = this.normalizeExtractionFingerprintTurns(turns).join("\n");
12552
10745
  if (!normalized) return null;
12553
- return createHash2("sha256").update(`${bufferKey}
10746
+ return createHash("sha256").update(`${bufferKey}
12554
10747
  ${normalized}`).digest("hex");
12555
10748
  }
12556
10749
  shouldQueueExtraction(turns, options = {}) {
@@ -13075,7 +11268,7 @@ ${normalized}`).digest("hex");
13075
11268
  );
13076
11269
  this.tierMigrationInFlight = true;
13077
11270
  try {
13078
- const coldStorage = new StorageManager(path4.join(storage.dir, "cold"));
11271
+ const coldStorage = new StorageManager(path2.join(storage.dir, "cold"));
13079
11272
  const [hotMemories, coldMemories] = await Promise.all([
13080
11273
  storage.readAllMemories(),
13081
11274
  coldStorage.readAllMemories()
@@ -14491,7 +12684,7 @@ ${normalized}`).digest("hex");
14491
12684
  const allMems = allMemsForGraph ?? [];
14492
12685
  for (const m of allMems) {
14493
12686
  if (m.frontmatter.entityRef === entityRef) {
14494
- const rel = path4.relative(storage.dir, m.path);
12687
+ const rel = path2.relative(storage.dir, m.path);
14495
12688
  if (rel !== memoryRelPath) entitySiblings.push(rel);
14496
12689
  }
14497
12690
  }
@@ -14813,14 +13006,14 @@ ${normalized}`).digest("hex");
14813
13006
  }
14814
13007
  if (this.config.semanticConsolidationEnabled) {
14815
13008
  try {
14816
- const stateFilePath = path4.join(
13009
+ const stateFilePath = path2.join(
14817
13010
  this.config.memoryDir,
14818
13011
  "state",
14819
13012
  "semantic-consolidation-last-run.json"
14820
13013
  );
14821
13014
  let shouldRun = true;
14822
13015
  try {
14823
- const stateRaw = await readFile4(stateFilePath, "utf-8");
13016
+ const stateRaw = await readFile2(stateFilePath, "utf-8");
14824
13017
  const stateData = JSON.parse(stateRaw);
14825
13018
  if (stateData.lastRunAt) {
14826
13019
  const lastRunMs = new Date(stateData.lastRunAt).getTime();
@@ -14861,9 +13054,9 @@ ${normalized}`).digest("hex");
14861
13054
  );
14862
13055
  }
14863
13056
  if (semResult.errors === 0 || semResult.memoriesArchived > 0) {
14864
- const stateDir = path4.join(this.config.memoryDir, "state");
14865
- await mkdir4(stateDir, { recursive: true });
14866
- await writeFile4(
13057
+ const stateDir = path2.join(this.config.memoryDir, "state");
13058
+ await mkdir2(stateDir, { recursive: true });
13059
+ await writeFile2(
14867
13060
  stateFilePath,
14868
13061
  JSON.stringify({ lastRunAt: (/* @__PURE__ */ new Date()).toISOString() }),
14869
13062
  "utf-8"
@@ -15059,7 +13252,7 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
15059
13252
  }
15060
13253
  });
15061
13254
  const content = renderCompressionGuidelinesMarkdown(refinedCandidate);
15062
- const contentHash = createHash2("sha256").update(content).digest("hex");
13255
+ const contentHash = createHash("sha256").update(content).digest("hex");
15063
13256
  const semanticRefinementApplied = JSON.stringify(refinedCandidate.ruleUpdates) !== JSON.stringify(candidate.ruleUpdates);
15064
13257
  const changedRules = refinedCandidate.ruleUpdates.filter(
15065
13258
  (rule) => rule.delta !== 0
@@ -15361,13 +13554,13 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
15361
13554
  protectedCategories: this.config.lifecycleProtectedCategories
15362
13555
  }
15363
13556
  };
15364
- const metricsPath = path4.join(
13557
+ const metricsPath = path2.join(
15365
13558
  storage.dir,
15366
13559
  "state",
15367
13560
  "lifecycle-metrics.json"
15368
13561
  );
15369
- await mkdir4(path4.dirname(metricsPath), { recursive: true });
15370
- await writeFile4(metricsPath, JSON.stringify(metrics, null, 2), "utf-8");
13562
+ await mkdir2(path2.dirname(metricsPath), { recursive: true });
13563
+ await writeFile2(metricsPath, JSON.stringify(metrics, null, 2), "utf-8");
15371
13564
  return updatedCount;
15372
13565
  }
15373
13566
  /**
@@ -15890,7 +14083,7 @@ ${lines.join("\n\n")}`;
15890
14083
  const seenStorageDirs = /* @__PURE__ */ new Set();
15891
14084
  const addStorage = (storage) => {
15892
14085
  const storageDir = storageDirFor(storage);
15893
- const storageKey = storageDir ? path4.resolve(storageDir) : `storage-without-dir-${storages.length}`;
14086
+ const storageKey = storageDir ? path2.resolve(storageDir) : `storage-without-dir-${storages.length}`;
15894
14087
  if (seenStorageDirs.has(storageKey)) return;
15895
14088
  seenStorageDirs.add(storageKey);
15896
14089
  storages.push(storage);
@@ -15920,7 +14113,7 @@ ${lines.join("\n\n")}`;
15920
14113
  continue;
15921
14114
  }
15922
14115
  try {
15923
- const coldRoot = path4.join(storageDir, "cold");
14116
+ const coldRoot = path2.join(storageDir, "cold");
15924
14117
  for (const candidate of qmdResultPathCandidates(
15925
14118
  coldRoot,
15926
14119
  parts.relativePath
@@ -15961,7 +14154,7 @@ ${lines.join("\n\n")}`;
15961
14154
  return null;
15962
14155
  }
15963
14156
  }
15964
- if (path4.isAbsolute(resultPath)) {
14157
+ if (path2.isAbsolute(resultPath)) {
15965
14158
  if (!fallbackStorageDir) {
15966
14159
  return await fallbackStorage.readMemoryByPath(resultPath);
15967
14160
  }
@@ -16000,7 +14193,7 @@ ${lines.join("\n\n")}`;
16000
14193
  );
16001
14194
  if (!memory) return null;
16002
14195
  let ownerNamespace = null;
16003
- if (path4.isAbsolute(memory.path)) {
14196
+ if (path2.isAbsolute(memory.path)) {
16004
14197
  const ownerStorage = await this.storageForAbsoluteQmdResultPath(
16005
14198
  memory.path,
16006
14199
  fallbackStorage,
@@ -16024,16 +14217,16 @@ ${lines.join("\n\n")}`;
16024
14217
  };
16025
14218
  }
16026
14219
  async storageForAbsoluteQmdResultPath(resultPath, fallbackStorage, recallNamespaces = []) {
16027
- const resolvedPath = path4.resolve(resultPath);
16028
- const memoryRoot = path4.resolve(this.config.memoryDir);
16029
- const namespacesRoot = path4.join(memoryRoot, "namespaces");
14220
+ const resolvedPath = path2.resolve(resultPath);
14221
+ const memoryRoot = path2.resolve(this.config.memoryDir);
14222
+ const namespacesRoot = path2.join(memoryRoot, "namespaces");
16030
14223
  const fallbackStorageDir = typeof fallbackStorage.dir === "string" && fallbackStorage.dir ? fallbackStorage.dir : null;
16031
14224
  const matches = [];
16032
14225
  const seenDirs = /* @__PURE__ */ new Set();
16033
14226
  const maybeAddStorage = (storage, namespace) => {
16034
14227
  const storageDir = typeof storage.dir === "string" && storage.dir ? storage.dir : null;
16035
14228
  if (!storageDir) return;
16036
- const candidateRoot = path4.resolve(storageDir);
14229
+ const candidateRoot = path2.resolve(storageDir);
16037
14230
  if (seenDirs.has(candidateRoot)) return;
16038
14231
  if (!isPathInsideStorageRoot(candidateRoot, resolvedPath)) return;
16039
14232
  if (candidateRoot === memoryRoot && isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
@@ -16051,7 +14244,7 @@ ${lines.join("\n\n")}`;
16051
14244
  candidateNamespaces.add(ns);
16052
14245
  }
16053
14246
  if (isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
16054
- const relativeToNamespaces = path4.relative(namespacesRoot, resolvedPath);
14247
+ const relativeToNamespaces = path2.relative(namespacesRoot, resolvedPath);
16055
14248
  const [namespaceSegment] = relativeToNamespaces.split(/[\\/]/);
16056
14249
  if (namespaceSegment) {
16057
14250
  candidateNamespaces.add(
@@ -16096,7 +14289,7 @@ ${lines.join("\n\n")}`;
16096
14289
  nsMap = buildMemoryWorthCounterMap(memories);
16097
14290
  this.memoryWorthCounterCache.set(ns, { at: nowMs, counters: nsMap });
16098
14291
  }
16099
- for (const [path5, c] of nsMap) counters.set(path5, c);
14292
+ for (const [path3, c] of nsMap) counters.set(path3, c);
16100
14293
  } catch (err) {
16101
14294
  log.debug("memory-worth: failed to read namespace, skipping", {
16102
14295
  namespace: ns,
@@ -16267,12 +14460,12 @@ ${lines.join("\n\n")}`;
16267
14460
  */
16268
14461
  semanticDedupScopeFor(targetStorage) {
16269
14462
  if (!this.config.namespacesEnabled) return {};
16270
- const memoryDir = path4.resolve(this.config.memoryDir);
16271
- const storageDir = path4.resolve(targetStorage.dir);
14463
+ const memoryDir = path2.resolve(this.config.memoryDir);
14464
+ const storageDir = path2.resolve(targetStorage.dir);
16272
14465
  if (storageDir === memoryDir) {
16273
14466
  return { pathExcludePrefixes: ["namespaces/"] };
16274
14467
  }
16275
- let rel = path4.relative(memoryDir, storageDir);
14468
+ let rel = path2.relative(memoryDir, storageDir);
16276
14469
  if (!rel || rel.startsWith("..")) {
16277
14470
  log.debug(
16278
14471
  `semantic dedup: target storage dir ${storageDir} is outside memoryDir ${memoryDir}; scoping lookup to absolute path prefix`
@@ -16291,7 +14484,7 @@ ${lines.join("\n\n")}`;
16291
14484
  if (hits.length === 0) return [];
16292
14485
  const results = [];
16293
14486
  for (const hit of hits) {
16294
- const fullPath = path4.isAbsolute(hit.path) ? hit.path : path4.join(this.config.memoryDir, hit.path);
14487
+ const fullPath = path2.isAbsolute(hit.path) ? hit.path : path2.join(this.config.memoryDir, hit.path);
16295
14488
  const memory = await this.storage.readMemoryByPath(fullPath);
16296
14489
  if (!memory) continue;
16297
14490
  results.push({
@@ -16495,7 +14688,7 @@ ${lines.join("\n\n")}`;
16495
14688
  const storage = await this.storageRouter.storageFor(namespace);
16496
14689
  const storageDir = typeof storage.dir === "string" && storage.dir ? storage.dir : null;
16497
14690
  if (!storageDir) continue;
16498
- const recallRoot = path4.resolve(storageDir);
14691
+ const recallRoot = path2.resolve(storageDir);
16499
14692
  if (seenRecallRoots.has(recallRoot)) continue;
16500
14693
  seenRecallRoots.add(recallRoot);
16501
14694
  recallRoots.push(recallRoot);
@@ -16519,8 +14712,8 @@ ${lines.join("\n\n")}`;
16519
14712
  if (resolvedCold) scopedResults.push(resolvedCold.result);
16520
14713
  continue;
16521
14714
  }
16522
- if (path4.isAbsolute(result.path)) {
16523
- const resolvedPath = path4.resolve(result.path);
14715
+ if (path2.isAbsolute(result.path)) {
14716
+ const resolvedPath = path2.resolve(result.path);
16524
14717
  if (recallRoots.some(
16525
14718
  (recallRoot) => isPathInsideStorageRoot(recallRoot, resolvedPath)
16526
14719
  )) {
@@ -17262,8 +15455,8 @@ ${lines.join("\n\n")}`;
17262
15455
  }
17263
15456
  namespaceFromStorageDir(storageDir) {
17264
15457
  if (!this.config.namespacesEnabled) return this.config.defaultNamespace;
17265
- const resolvedStorageDir = path4.resolve(storageDir);
17266
- const resolvedMemoryDir = path4.resolve(this.config.memoryDir);
15458
+ const resolvedStorageDir = path2.resolve(storageDir);
15459
+ const resolvedMemoryDir = path2.resolve(this.config.memoryDir);
17267
15460
  if (resolvedStorageDir === resolvedMemoryDir)
17268
15461
  return this.config.defaultNamespace;
17269
15462
  const m = resolvedStorageDir.match(/[\\/]namespaces[\\/]([^\\/]+)$/);
@@ -17343,7 +15536,6 @@ export {
17343
15536
  ensureBuiltInWearableConnectors,
17344
15537
  WearablesService,
17345
15538
  locateTranscriptPath,
17346
- NamespaceCatalog,
17347
15539
  BulkImportBatchPartialFailureError,
17348
15540
  dedupeEntitySynthesisEvidenceEntries,
17349
15541
  defaultWorkspaceDir,
@@ -17374,4 +15566,4 @@ export {
17374
15566
  resolvePersistedMemoryRelativePath,
17375
15567
  Orchestrator
17376
15568
  };
17377
- //# sourceMappingURL=chunk-B43NZNMG.js.map
15569
+ //# sourceMappingURL=chunk-CE6CBRCV.js.map