@remnic/core 9.3.595 → 9.3.596

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 (80) hide show
  1. package/dist/access-cli.js +14 -14
  2. package/dist/access-http.js +6 -6
  3. package/dist/access-mcp.js +5 -5
  4. package/dist/access-schema.d.ts +7 -7
  5. package/dist/access-service.js +4 -4
  6. package/dist/briefing.js +2 -2
  7. package/dist/causal-consolidation.js +3 -3
  8. package/dist/{chunk-ARY5OOLG.js → chunk-557IAFPD.js} +2 -2
  9. package/dist/{chunk-VFB2G5YL.js → chunk-5BUGGPBR.js} +4 -4
  10. package/dist/{chunk-USYGGIJZ.js → chunk-D2MMMTDV.js} +2 -2
  11. package/dist/{chunk-XM7BYXT7.js → chunk-D65TSG24.js} +2 -2
  12. package/dist/{chunk-FHBEL473.js → chunk-DOX2CG6Y.js} +54 -5
  13. package/dist/chunk-DOX2CG6Y.js.map +1 -0
  14. package/dist/{chunk-DARLGSFX.js → chunk-ELKI4BB6.js} +4 -4
  15. package/dist/{chunk-QRWZOCJN.js → chunk-F4LM4ULA.js} +12 -12
  16. package/dist/{chunk-JIBCUYIP.js → chunk-IEFHBIU2.js} +10 -10
  17. package/dist/{chunk-KDUFBSBF.js → chunk-IK34DVAC.js} +2 -2
  18. package/dist/{chunk-OPYFD6PD.js → chunk-IK7DCC5H.js} +2 -2
  19. package/dist/{chunk-574MU2Y3.js → chunk-JTDRJQ3K.js} +2 -2
  20. package/dist/{chunk-LAL7WBLY.js → chunk-LYPDMKUT.js} +3 -3
  21. package/dist/{chunk-GBXGCFRH.js → chunk-MA5MWGKP.js} +2 -2
  22. package/dist/{chunk-HQO5EBUC.js → chunk-MLT75J5S.js} +3 -3
  23. package/dist/{chunk-7X7TBJRX.js → chunk-NOMEVTUD.js} +2 -2
  24. package/dist/{chunk-SUTSSOYU.js → chunk-OD5LFAPZ.js} +2 -2
  25. package/dist/{chunk-XT7XVA53.js → chunk-OI27U2HT.js} +2 -2
  26. package/dist/{chunk-MQEIWDYW.js → chunk-QDDHYAKV.js} +2 -2
  27. package/dist/{chunk-ZY6UPHNY.js → chunk-TYICDVQW.js} +3 -3
  28. package/dist/{chunk-XRWTAEZM.js → chunk-W5O2FQTZ.js} +2 -2
  29. package/dist/{chunk-V3RXWQIE.js → chunk-WXACKLKP.js} +209 -59
  30. package/dist/chunk-WXACKLKP.js.map +1 -0
  31. package/dist/{chunk-IRFF6LSF.js → chunk-YFS5OEKO.js} +36 -1
  32. package/dist/chunk-YFS5OEKO.js.map +1 -0
  33. package/dist/cli.js +15 -15
  34. package/dist/compounding/engine.js +2 -2
  35. package/dist/connectors/codex-materialize-runner.js +2 -2
  36. package/dist/connectors/index.js +2 -2
  37. package/dist/entity-retrieval.js +2 -2
  38. package/dist/index.js +22 -22
  39. package/dist/maintenance/memory-governance.js +2 -2
  40. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +2 -2
  41. package/dist/maintenance/rebuild-memory-projection.js +3 -3
  42. package/dist/namespaces/migrate.js +3 -3
  43. package/dist/namespaces/storage.js +2 -2
  44. package/dist/operator-toolkit.js +5 -5
  45. package/dist/orchestrator.js +11 -11
  46. package/dist/retrieval-agents.js +2 -2
  47. package/dist/semantic-consolidation.js +3 -3
  48. package/dist/semantic-rule-promotion.js +2 -2
  49. package/dist/semantic-rule-verifier.js +2 -2
  50. package/dist/storage.d.ts +2 -0
  51. package/dist/storage.js +1 -1
  52. package/dist/temporal-index.js +1 -1
  53. package/dist/verified-recall.js +2 -2
  54. package/package.json +1 -1
  55. package/src/entity-retrieval.ts +64 -3
  56. package/src/storage.ts +40 -0
  57. package/src/temporal-index.test.ts +191 -0
  58. package/src/temporal-index.ts +291 -100
  59. package/dist/chunk-FHBEL473.js.map +0 -1
  60. package/dist/chunk-IRFF6LSF.js.map +0 -1
  61. package/dist/chunk-V3RXWQIE.js.map +0 -1
  62. /package/dist/{chunk-ARY5OOLG.js.map → chunk-557IAFPD.js.map} +0 -0
  63. /package/dist/{chunk-VFB2G5YL.js.map → chunk-5BUGGPBR.js.map} +0 -0
  64. /package/dist/{chunk-USYGGIJZ.js.map → chunk-D2MMMTDV.js.map} +0 -0
  65. /package/dist/{chunk-XM7BYXT7.js.map → chunk-D65TSG24.js.map} +0 -0
  66. /package/dist/{chunk-DARLGSFX.js.map → chunk-ELKI4BB6.js.map} +0 -0
  67. /package/dist/{chunk-QRWZOCJN.js.map → chunk-F4LM4ULA.js.map} +0 -0
  68. /package/dist/{chunk-JIBCUYIP.js.map → chunk-IEFHBIU2.js.map} +0 -0
  69. /package/dist/{chunk-KDUFBSBF.js.map → chunk-IK34DVAC.js.map} +0 -0
  70. /package/dist/{chunk-OPYFD6PD.js.map → chunk-IK7DCC5H.js.map} +0 -0
  71. /package/dist/{chunk-574MU2Y3.js.map → chunk-JTDRJQ3K.js.map} +0 -0
  72. /package/dist/{chunk-LAL7WBLY.js.map → chunk-LYPDMKUT.js.map} +0 -0
  73. /package/dist/{chunk-GBXGCFRH.js.map → chunk-MA5MWGKP.js.map} +0 -0
  74. /package/dist/{chunk-HQO5EBUC.js.map → chunk-MLT75J5S.js.map} +0 -0
  75. /package/dist/{chunk-7X7TBJRX.js.map → chunk-NOMEVTUD.js.map} +0 -0
  76. /package/dist/{chunk-SUTSSOYU.js.map → chunk-OD5LFAPZ.js.map} +0 -0
  77. /package/dist/{chunk-XT7XVA53.js.map → chunk-OI27U2HT.js.map} +0 -0
  78. /package/dist/{chunk-MQEIWDYW.js.map → chunk-QDDHYAKV.js.map} +0 -0
  79. /package/dist/{chunk-ZY6UPHNY.js.map → chunk-TYICDVQW.js.map} +0 -0
  80. /package/dist/{chunk-XRWTAEZM.js.map → chunk-W5O2FQTZ.js.map} +0 -0
@@ -15,8 +15,10 @@
15
15
  * - Both indexes are plain JSON; no external dependencies
16
16
  */
17
17
 
18
- import * as fs from "fs";
19
- import * as path from "path";
18
+ import { execFileSync } from "node:child_process";
19
+ import * as crypto from "node:crypto";
20
+ import * as fs from "node:fs";
21
+ import * as path from "node:path";
20
22
 
21
23
  export interface TemporalIndex {
22
24
  /** version bumped when schema changes */
@@ -46,6 +48,19 @@ const INDEX_VERSION = 1;
46
48
  const TEMPORAL_INDEX_FILE = "index_time.json";
47
49
  const TAG_INDEX_FILE = "index_tags.json";
48
50
  const TAG_INDEX_VERSION = 2;
51
+ const INDEX_LOCK_STALE_MS = 60_000;
52
+ const INDEX_LOCK_POLL_MS = 10;
53
+ const INDEX_PROCESS_START_TOLERANCE_MS = 2_000;
54
+ const INDEX_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(4));
55
+ const INDEX_PROCESS_STARTED_AT_MS = Date.now() - process.uptime() * 1000;
56
+
57
+ interface IndexLockOwner {
58
+ pid: number;
59
+ createdAt?: string;
60
+ processStartedAtMs?: number;
61
+ }
62
+
63
+ type IndexLockCleanupResult = "removed" | "wait" | "blocked";
49
64
 
50
65
  function stateDir(memoryDir: string): string {
51
66
  return path.join(memoryDir, "state");
@@ -83,22 +98,186 @@ function writeJsonSafe(filePath: string, data: unknown): void {
83
98
  }
84
99
  }
85
100
 
101
+ function sleepSync(ms: number): void {
102
+ Atomics.wait(INDEX_LOCK_SLEEP, 0, 0, ms);
103
+ }
104
+
105
+ function uniqueTempPath(filePath: string): string {
106
+ const dir = path.dirname(filePath);
107
+ const base = path.basename(filePath);
108
+ const nonce = crypto.randomBytes(6).toString("hex");
109
+ return path.join(dir, `.${base}.${process.pid}.${Date.now()}.${nonce}.tmp`);
110
+ }
111
+
112
+ function lockOwnerPath(lockDir: string): string {
113
+ return path.join(lockDir, "owner.json");
114
+ }
115
+
116
+ function writeIndexLockOwner(lockDir: string): void {
117
+ try {
118
+ fs.writeFileSync(
119
+ lockOwnerPath(lockDir),
120
+ JSON.stringify({
121
+ pid: process.pid,
122
+ createdAt: new Date().toISOString(),
123
+ processStartedAtMs: INDEX_PROCESS_STARTED_AT_MS,
124
+ }),
125
+ {
126
+ encoding: "utf8",
127
+ flag: "wx",
128
+ }
129
+ );
130
+ } catch {
131
+ // Fail silently — the directory lock is still the serialization primitive.
132
+ }
133
+ }
134
+
135
+ function readIndexLockOwner(lockDir: string): IndexLockOwner | null {
136
+ try {
137
+ const parsed = JSON.parse(fs.readFileSync(lockOwnerPath(lockDir), "utf8")) as { pid?: unknown };
138
+ if (!(typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0)) return null;
139
+ const owner: IndexLockOwner = { pid: parsed.pid };
140
+ if (
141
+ "createdAt" in parsed &&
142
+ typeof (parsed as { createdAt?: unknown }).createdAt === "string" &&
143
+ (parsed as { createdAt: string }).createdAt.length > 0
144
+ ) {
145
+ owner.createdAt = (parsed as { createdAt: string }).createdAt;
146
+ }
147
+ const processStartedAtMs = (parsed as { processStartedAtMs?: unknown }).processStartedAtMs;
148
+ if (typeof processStartedAtMs === "number" && Number.isFinite(processStartedAtMs) && processStartedAtMs > 0) {
149
+ owner.processStartedAtMs = processStartedAtMs;
150
+ }
151
+ return owner;
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+
157
+ function processIsAlive(pid: number): boolean {
158
+ try {
159
+ process.kill(pid, 0);
160
+ return true;
161
+ } catch (error) {
162
+ const code = (error as NodeJS.ErrnoException)?.code;
163
+ return code === "EPERM";
164
+ }
165
+ }
166
+
167
+ function readProcessStartedAtMs(pid: number): number | null {
168
+ try {
169
+ const output = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
170
+ encoding: "utf8",
171
+ stdio: ["ignore", "pipe", "ignore"],
172
+ timeout: 1_000,
173
+ }).trim();
174
+ if (!output) return null;
175
+ const startedAtMs = Date.parse(output);
176
+ return Number.isFinite(startedAtMs) ? startedAtMs : null;
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ function lockOwnerIsRunning(owner: IndexLockOwner): boolean {
183
+ if (!processIsAlive(owner.pid)) return false;
184
+ if (owner.processStartedAtMs === undefined) return true;
185
+ const runningStartedAtMs = readProcessStartedAtMs(owner.pid);
186
+ if (runningStartedAtMs === null) return true;
187
+ return runningStartedAtMs <= owner.processStartedAtMs + INDEX_PROCESS_START_TOLERANCE_MS;
188
+ }
189
+
190
+ function removeAbandonedIndexLock(lockDir: string): IndexLockCleanupResult {
191
+ try {
192
+ const info = fs.lstatSync(lockDir);
193
+ if (info.isSymbolicLink()) return "blocked";
194
+ if (!info.isDirectory()) {
195
+ fs.rmSync(lockDir, { force: true });
196
+ return "removed";
197
+ }
198
+ const owner = readIndexLockOwner(lockDir);
199
+ if (owner !== null && lockOwnerIsRunning(owner)) return "wait";
200
+ if (owner === null && Date.now() - info.mtimeMs < INDEX_LOCK_STALE_MS) return "wait";
201
+ fs.rmSync(lockDir, { recursive: true, force: true });
202
+ return "removed";
203
+ } catch {
204
+ // Fail silently — indexes are advisory only
205
+ return "blocked";
206
+ }
207
+ }
208
+
209
+ function withIndexFileLock(filePath: string, update: () => void): void {
210
+ const lockDir = `${filePath}.lock.d`;
211
+ let acquired = false;
212
+
213
+ while (!acquired) {
214
+ try {
215
+ fs.mkdirSync(lockDir);
216
+ writeIndexLockOwner(lockDir);
217
+ acquired = true;
218
+ } catch (error) {
219
+ const code = (error as NodeJS.ErrnoException)?.code;
220
+ if (code !== "EEXIST") return;
221
+ const cleanupResult = removeAbandonedIndexLock(lockDir);
222
+ if (cleanupResult === "blocked") return;
223
+ sleepSync(INDEX_LOCK_POLL_MS);
224
+ }
225
+ }
226
+
227
+ try {
228
+ update();
229
+ } finally {
230
+ try {
231
+ fs.rmSync(lockDir, { recursive: true, force: true });
232
+ } catch {
233
+ // Fail silently — indexes are advisory only
234
+ }
235
+ }
236
+ }
237
+
86
238
  /**
87
- * Atomic write: write to a `.tmp` sibling then rename so readers never
88
- * observe a partially-written file. Falls back to direct write on error.
239
+ * Atomic write: write to a unique `.tmp` sibling then rename so readers never
240
+ * observe a partially-written file.
89
241
  */
90
242
  function writeJsonAtomic(filePath: string, data: unknown): void {
91
- const tmp = `${filePath}.tmp`;
92
- try {
93
- fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
94
- fs.renameSync(tmp, filePath);
95
- } catch {
96
- // Attempt direct write as fallback; indexes are advisory only
97
- writeJsonSafe(filePath, data);
98
- try { fs.unlinkSync(tmp); } catch { /* ignore stale tmp */ }
243
+ const payload = JSON.stringify(data, null, 2);
244
+ for (let attempt = 0; attempt < 3; attempt += 1) {
245
+ const tmp = uniqueTempPath(filePath);
246
+ try {
247
+ fs.writeFileSync(tmp, payload, "utf8");
248
+ fs.renameSync(tmp, filePath);
249
+ return;
250
+ } catch {
251
+ try {
252
+ fs.unlinkSync(tmp);
253
+ } catch {
254
+ // Fail silently — indexes are advisory only
255
+ }
256
+ sleepSync(INDEX_LOCK_POLL_MS);
257
+ }
99
258
  }
100
259
  }
101
260
 
261
+ function updateTemporalIndex(memoryDir: string, update: (index: TemporalIndex) => void): void {
262
+ const indexPath = temporalIndexPath(memoryDir);
263
+ withIndexFileLock(indexPath, () => {
264
+ const index = readJsonSafe<TemporalIndex>(indexPath, { version: INDEX_VERSION, dates: {} });
265
+ update(index);
266
+ writeJsonAtomic(indexPath, index);
267
+ });
268
+ }
269
+
270
+ function updateTagIndex(memoryDir: string, update: (index: TagIndex) => void): void {
271
+ const indexPath = tagIndexPath(memoryDir);
272
+ withIndexFileLock(indexPath, () => {
273
+ const index = normalizeTagIndex(
274
+ readJsonSafe<TagIndex>(indexPath, { version: TAG_INDEX_VERSION, tags: {}, aliases: {} })
275
+ );
276
+ update(index);
277
+ writeJsonAtomic(indexPath, index);
278
+ });
279
+ }
280
+
102
281
  function isoDateFromTimestamp(isoString: string): string {
103
282
  if (typeof isoString !== "string" || isoString.length < 10) {
104
283
  // Malformed frontmatter — fall back to today so the memory is still indexed.
@@ -353,31 +532,22 @@ function promptContainsAlias(prompt: string, alias: string): boolean {
353
532
  * @param createdAt ISO timestamp of the memory's creation date
354
533
  * @param tags Array of tag strings from the memory's frontmatter
355
534
  */
356
- export function indexMemory(
357
- memoryDir: string,
358
- memoryPath: string,
359
- createdAt: string,
360
- tags: string[],
361
- ): void {
535
+ export function indexMemory(memoryDir: string, memoryPath: string, createdAt: string, tags: string[]): void {
362
536
  try {
363
537
  ensureStateDir(memoryDir);
364
538
 
365
- // Temporal index
366
- const tPath = temporalIndexPath(memoryDir);
367
- const tIndex = readJsonSafe<TemporalIndex>(tPath, { version: INDEX_VERSION, dates: {} });
368
539
  const dateKey = isoDateFromTimestamp(createdAt);
369
- addPathToSet(tIndex.dates, dateKey, memoryPath);
370
- writeJsonAtomic(tPath, tIndex);
540
+ updateTemporalIndex(memoryDir, (index) => {
541
+ addPathToSet(index.dates, dateKey, memoryPath);
542
+ });
371
543
 
372
- // Tag index
373
- const gPath = tagIndexPath(memoryDir);
374
- const gIndex = normalizeTagIndex(readJsonSafe<TagIndex>(gPath, { version: TAG_INDEX_VERSION, tags: {}, aliases: {} }));
375
- for (const tag of tags) {
376
- if (tag && typeof tag === "string") {
377
- addTagGraphEntry(gIndex, tag, memoryPath);
544
+ updateTagIndex(memoryDir, (index) => {
545
+ for (const tag of tags) {
546
+ if (tag && typeof tag === "string") {
547
+ addTagGraphEntry(index, tag, memoryPath);
548
+ }
378
549
  }
379
- }
380
- writeJsonAtomic(gPath, gIndex);
550
+ });
381
551
  } catch {
382
552
  // Fail silently
383
553
  }
@@ -386,29 +556,22 @@ export function indexMemory(
386
556
  /**
387
557
  * Remove a memory file from both indexes (called on deletion/archival).
388
558
  */
389
- export function deindexMemory(
390
- memoryDir: string,
391
- memoryPath: string,
392
- createdAt: string,
393
- tags: string[],
394
- ): void {
559
+ export function deindexMemory(memoryDir: string, memoryPath: string, createdAt: string, tags: string[]): void {
395
560
  try {
396
561
  ensureStateDir(memoryDir);
397
562
 
398
- const tPath = temporalIndexPath(memoryDir);
399
- const tIndex = readJsonSafe<TemporalIndex>(tPath, { version: INDEX_VERSION, dates: {} });
400
563
  const dateKey = isoDateFromTimestamp(createdAt);
401
- removePathFromSet(tIndex.dates, dateKey, memoryPath);
402
- writeJsonAtomic(tPath, tIndex);
564
+ updateTemporalIndex(memoryDir, (index) => {
565
+ removePathFromSet(index.dates, dateKey, memoryPath);
566
+ });
403
567
 
404
- const gPath = tagIndexPath(memoryDir);
405
- const gIndex = normalizeTagIndex(readJsonSafe<TagIndex>(gPath, { version: TAG_INDEX_VERSION, tags: {}, aliases: {} }));
406
- for (const tag of tags) {
407
- if (tag && typeof tag === "string") {
408
- removeTagGraphEntry(gIndex, tag, memoryPath);
568
+ updateTagIndex(memoryDir, (index) => {
569
+ for (const tag of tags) {
570
+ if (tag && typeof tag === "string") {
571
+ removeTagGraphEntry(index, tag, memoryPath);
572
+ }
409
573
  }
410
- }
411
- writeJsonAtomic(gPath, gIndex);
574
+ });
412
575
  } catch {
413
576
  // Fail silently
414
577
  }
@@ -422,8 +585,17 @@ export function deindexMemory(
422
585
  export function clearIndexes(memoryDir: string): void {
423
586
  try {
424
587
  ensureStateDir(memoryDir);
425
- writeJsonAtomic(temporalIndexPath(memoryDir), { version: INDEX_VERSION, dates: {} });
426
- writeJsonAtomic(tagIndexPath(memoryDir), { version: TAG_INDEX_VERSION, tags: {}, aliases: {} });
588
+ updateTemporalIndex(memoryDir, (index) => {
589
+ index.version = INDEX_VERSION;
590
+ index.lastRebuildAt = undefined;
591
+ index.dates = {};
592
+ });
593
+ updateTagIndex(memoryDir, (index) => {
594
+ index.version = TAG_INDEX_VERSION;
595
+ index.lastRebuildAt = undefined;
596
+ index.tags = {};
597
+ index.aliases = {};
598
+ });
427
599
  } catch {
428
600
  // Fail silently — indexes are advisory only
429
601
  }
@@ -435,10 +607,7 @@ export function clearIndexes(memoryDir: string): void {
435
607
  */
436
608
  export function indexesExist(memoryDir: string): boolean {
437
609
  try {
438
- return (
439
- fs.existsSync(temporalIndexPath(memoryDir)) &&
440
- fs.existsSync(tagIndexPath(memoryDir))
441
- );
610
+ return fs.existsSync(temporalIndexPath(memoryDir)) && fs.existsSync(tagIndexPath(memoryDir));
442
611
  } catch {
443
612
  return false;
444
613
  }
@@ -450,30 +619,28 @@ export function indexesExist(memoryDir: string): boolean {
450
619
  */
451
620
  export function indexMemoriesBatch(
452
621
  memoryDir: string,
453
- entries: Array<{ path: string; createdAt: string; tags: string[] }>,
622
+ entries: Array<{ path: string; createdAt: string; tags: string[] }>
454
623
  ): void {
455
624
  if (entries.length === 0) return;
456
625
  try {
457
626
  ensureStateDir(memoryDir);
458
627
 
459
- const tPath = temporalIndexPath(memoryDir);
460
- const tIndex = readJsonSafe<TemporalIndex>(tPath, { version: INDEX_VERSION, dates: {} });
461
-
462
- const gPath = tagIndexPath(memoryDir);
463
- const gIndex = normalizeTagIndex(readJsonSafe<TagIndex>(gPath, { version: TAG_INDEX_VERSION, tags: {}, aliases: {} }));
628
+ updateTemporalIndex(memoryDir, (index) => {
629
+ for (const entry of entries) {
630
+ const dateKey = isoDateFromTimestamp(entry.createdAt);
631
+ addPathToSet(index.dates, dateKey, entry.path);
632
+ }
633
+ });
464
634
 
465
- for (const entry of entries) {
466
- const dateKey = isoDateFromTimestamp(entry.createdAt);
467
- addPathToSet(tIndex.dates, dateKey, entry.path);
468
- for (const tag of entry.tags) {
469
- if (tag && typeof tag === "string") {
470
- addTagGraphEntry(gIndex, tag, entry.path);
635
+ updateTagIndex(memoryDir, (index) => {
636
+ for (const entry of entries) {
637
+ for (const tag of entry.tags) {
638
+ if (tag && typeof tag === "string") {
639
+ addTagGraphEntry(index, tag, entry.path);
640
+ }
471
641
  }
472
642
  }
473
- }
474
-
475
- writeJsonAtomic(tPath, tIndex);
476
- writeJsonAtomic(gPath, gIndex);
643
+ });
477
644
  } catch {
478
645
  // Fail silently
479
646
  }
@@ -497,7 +664,7 @@ export function indexMemoriesBatch(
497
664
  export async function queryByDateRangeAsync(
498
665
  memoryDir: string,
499
666
  fromDate: string,
500
- toDate?: string,
667
+ toDate?: string
501
668
  ): Promise<Set<string> | null> {
502
669
  try {
503
670
  const tPath = temporalIndexPath(memoryDir);
@@ -534,10 +701,7 @@ export async function queryByDateRangeAsync(
534
701
  * Async version of queryByTags — uses non-blocking fs.promises.readFile
535
702
  * to avoid blocking the Node.js event loop.
536
703
  */
537
- export async function queryByTagsAsync(
538
- memoryDir: string,
539
- tags: string[],
540
- ): Promise<Set<string> | null> {
704
+ export async function queryByTagsAsync(memoryDir: string, tags: string[]): Promise<Set<string> | null> {
541
705
  if (tags.length === 0) return null;
542
706
  try {
543
707
  const gPath = tagIndexPath(memoryDir);
@@ -593,7 +757,7 @@ export function extractTagsFromPrompt(prompt: string): string[] {
593
757
 
594
758
  export async function resolvePromptTagPrefilterAsync(
595
759
  memoryDir: string,
596
- prompt: string,
760
+ prompt: string
597
761
  ): Promise<{
598
762
  matchedTags: string[];
599
763
  expandedTags: string[];
@@ -635,7 +799,7 @@ export async function resolvePromptTagPrefilterAsync(
635
799
  */
636
800
  export function isTemporalQuery(prompt: string): boolean {
637
801
  return /\b(today|yesterday|this week|last week|this month|last month|recent(?:ly)?|lately|just now|earlier today|this morning|last night|last year|this year|\d+ days? ago|\d+ hours? ago|\d+ weeks? ago|\d+ months? ago|(?:in |on |during |since |before |after )?(?:january|february|march|april|may|june|july|august|september|october|november|december)(?:\s+\d{1,4})?|\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{2,4}|(?:spring|summer|fall|autumn|winter)\s+\d{4}|on the \d{1,2}(?:st|nd|rd|th)?|last (?:monday|tuesday|wednesday|thursday|friday|saturday|sunday))\b/i.test(
638
- prompt,
802
+ prompt
639
803
  );
640
804
  }
641
805
 
@@ -669,12 +833,26 @@ export function recencyWindowFromPrompt(prompt: string, nowMs: number = Date.now
669
833
  return jan1LastYear.toISOString().slice(0, 10);
670
834
  } else {
671
835
  // Try specific month references: "in March", "during January", "since February"
672
- const monthNames = ["january", "february", "march", "april", "may", "june",
673
- "july", "august", "september", "october", "november", "december"];
674
- const monthMatch = p.match(/\b(january|february|march|april|may|june|july|august|september|october|november|december)(?:\s+(\d{4}))?\b/);
836
+ const monthNames = [
837
+ "january",
838
+ "february",
839
+ "march",
840
+ "april",
841
+ "may",
842
+ "june",
843
+ "july",
844
+ "august",
845
+ "september",
846
+ "october",
847
+ "november",
848
+ "december",
849
+ ];
850
+ const monthMatch = p.match(
851
+ /\b(january|february|march|april|may|june|july|august|september|october|november|december)(?:\s+(\d{4}))?\b/
852
+ );
675
853
  if (monthMatch) {
676
854
  const monthIdx = monthNames.indexOf(monthMatch[1]);
677
- const year = monthMatch[2] ? parseInt(monthMatch[2], 10) : now.getFullYear();
855
+ const year = monthMatch[2] ? Number.parseInt(monthMatch[2], 10) : now.getFullYear();
678
856
  // "before <month>" means everything prior to that month: use 2-year lookback
679
857
  // as fromDate so the window isn't unbounded. toDate is set to the month start
680
858
  // in recencyWindowBoundsFromPrompt.
@@ -698,21 +876,21 @@ export function recencyWindowFromPrompt(prompt: string, nowMs: number = Date.now
698
876
  // Try "N weeks ago"
699
877
  const weekMatch = p.match(/(\d{1,5})\s*weeks?\s*ago/);
700
878
  if (weekMatch) {
701
- daysBack = Math.min(365, parseInt(weekMatch[1], 10) * 7);
879
+ daysBack = Math.min(365, Number.parseInt(weekMatch[1], 10) * 7);
702
880
  } else {
703
881
  // Try "N months ago"
704
882
  const monthsAgoMatch = p.match(/(\d{1,5})\s*months?\s*ago/);
705
883
  if (monthsAgoMatch) {
706
- daysBack = Math.min(730, parseInt(monthsAgoMatch[1], 10) * 31);
884
+ daysBack = Math.min(730, Number.parseInt(monthsAgoMatch[1], 10) * 31);
707
885
  } else {
708
886
  const numMatch = p.match(/(\d{1,5})\s*days?\s*ago/);
709
887
  if (numMatch) {
710
- daysBack = Math.min(365, parseInt(numMatch[1], 10)); // no off-by-one: "3 days ago" → 3
888
+ daysBack = Math.min(365, Number.parseInt(numMatch[1], 10)); // no off-by-one: "3 days ago" → 3
711
889
  } else {
712
890
  const hrMatch = p.match(/(\d{1,5})\s*hours?\s*ago/);
713
891
  if (hrMatch) {
714
892
  // Convert hours to days (ceiling); at least 1 day window
715
- daysBack = Math.max(1, Math.ceil(parseInt(hrMatch[1], 10) / 24));
893
+ daysBack = Math.max(1, Math.ceil(Number.parseInt(hrMatch[1], 10) / 24));
716
894
  }
717
895
  }
718
896
  }
@@ -725,7 +903,7 @@ export function recencyWindowFromPrompt(prompt: string, nowMs: number = Date.now
725
903
  }
726
904
  const usMatch = p.match(/(\d{1,2})\/(\d{1,2})\/(\d{2,4})/);
727
905
  if (usMatch) {
728
- const year = usMatch[3].length === 2 ? 2000 + parseInt(usMatch[3], 10) : parseInt(usMatch[3], 10);
906
+ const year = usMatch[3].length === 2 ? 2000 + Number.parseInt(usMatch[3], 10) : Number.parseInt(usMatch[3], 10);
729
907
  return `${year}-${usMatch[1].padStart(2, "0")}-${usMatch[2].padStart(2, "0")}`;
730
908
  }
731
909
 
@@ -735,7 +913,7 @@ export function recencyWindowFromPrompt(prompt: string, nowMs: number = Date.now
735
913
  const dayNames = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
736
914
  const targetDay = dayNames.indexOf(dayOfWeekMatch[1]);
737
915
  const currentDay = now.getDay();
738
- daysBack = ((currentDay - targetDay + 7) % 7) || 7; // at least 7 days back
916
+ daysBack = (currentDay - targetDay + 7) % 7 || 7; // at least 7 days back
739
917
  }
740
918
  }
741
919
 
@@ -766,7 +944,7 @@ export function recencyWindowFromPrompt(prompt: string, nowMs: number = Date.now
766
944
  */
767
945
  export function recencyWindowBoundsFromPrompt(
768
946
  prompt: string,
769
- nowMs: number = Date.now(),
947
+ nowMs: number = Date.now()
770
948
  ): { fromDate: string; toDate: string } {
771
949
  const fromDate = recencyWindowFromPrompt(prompt, nowMs);
772
950
  const p = prompt.toLowerCase();
@@ -794,15 +972,29 @@ export function recencyWindowBoundsFromPrompt(
794
972
  // working offset, then ISO/US/weekday patterns run AFTER ago patterns
795
973
  // and can override them — matching the priority ordering in recencyWindowFromPrompt.
796
974
 
797
- const monthNames = ["january", "february", "march", "april", "may", "june",
798
- "july", "august", "september", "october", "november", "december"];
799
- const monthMatch = p.match(/\b(january|february|march|april|may|june|july|august|september|october|november|december)(?:\s+(\d{4}))?\b/);
975
+ const monthNames = [
976
+ "january",
977
+ "february",
978
+ "march",
979
+ "april",
980
+ "may",
981
+ "june",
982
+ "july",
983
+ "august",
984
+ "september",
985
+ "october",
986
+ "november",
987
+ "december",
988
+ ];
989
+ const monthMatch = p.match(
990
+ /\b(january|february|march|april|may|june|july|august|september|october|november|december)(?:\s+(\d{4}))?\b/
991
+ );
800
992
  if (monthMatch) {
801
993
  // "since <month>" / "after <month>" — open-ended: everything from that month to now.
802
994
  // "before <month>" — closed upper bound: everything before that month starts.
803
995
  // Plain "<month>" — just that calendar month.
804
996
  const monthIdx = monthNames.indexOf(monthMatch[1]);
805
- const year = monthMatch[2] ? parseInt(monthMatch[2], 10) : now.getFullYear();
997
+ const year = monthMatch[2] ? Number.parseInt(monthMatch[2], 10) : now.getFullYear();
806
998
  const isSinceOrAfter = /\b(since|after)\b/.test(p);
807
999
  const isBefore = /\bbefore\b/.test(p);
808
1000
  if (isSinceOrAfter) {
@@ -822,17 +1014,17 @@ export function recencyWindowBoundsFromPrompt(
822
1014
  let toDaysBack = -1;
823
1015
  const weekMatch = p.match(/(\d{1,5})\s*weeks?\s*ago/);
824
1016
  if (weekMatch) {
825
- toDaysBack = Math.max(0, Math.min(52, parseInt(weekMatch[1], 10)) - 1) * 7;
1017
+ toDaysBack = Math.max(0, Math.min(52, Number.parseInt(weekMatch[1], 10)) - 1) * 7;
826
1018
  } else {
827
1019
  const monthsAgoMatch = p.match(/(\d{1,5})\s*months?\s*ago/);
828
1020
  if (monthsAgoMatch) {
829
- toDaysBack = Math.max(0, Math.min(24, parseInt(monthsAgoMatch[1], 10)) - 1) * 31;
1021
+ toDaysBack = Math.max(0, Math.min(24, Number.parseInt(monthsAgoMatch[1], 10)) - 1) * 31;
830
1022
  } else {
831
1023
  const numMatch = p.match(/(\d{1,5})\s*days?\s*ago/);
832
1024
  if (numMatch) {
833
1025
  // (N-1) mirrors the weeks/months ago formula: "3 days ago" → window [today-3, today-2]
834
1026
  // N=1 → toDaysBack=0 → toDate=today (exclusive) → window [yesterday, today) = 1 day. ✓
835
- toDaysBack = Math.max(0, Math.min(365, parseInt(numMatch[1], 10)) - 1);
1027
+ toDaysBack = Math.max(0, Math.min(365, Number.parseInt(numMatch[1], 10)) - 1);
836
1028
  } else {
837
1029
  const hrMatch = p.match(/(\d{1,5})\s*hours?\s*ago/);
838
1030
  if (hrMatch) {
@@ -854,7 +1046,8 @@ export function recencyWindowBoundsFromPrompt(
854
1046
  } else {
855
1047
  const usMatch = p.match(/(\d{1,2})\/(\d{1,2})\/(\d{2,4})/);
856
1048
  if (usMatch) {
857
- const year = usMatch[3].length === 2 ? 2000 + parseInt(usMatch[3], 10) : parseInt(usMatch[3], 10);
1049
+ const year =
1050
+ usMatch[3].length === 2 ? 2000 + Number.parseInt(usMatch[3], 10) : Number.parseInt(usMatch[3], 10);
858
1051
  // +1 day: exclusive upper bound includes the named date
859
1052
  const d = new Date(`${year}-${usMatch[1].padStart(2, "0")}-${usMatch[2].padStart(2, "0")}T00:00:00Z`);
860
1053
  toDate = new Date(d.getTime() + 86_400_000).toISOString().slice(0, 10);
@@ -864,7 +1057,7 @@ export function recencyWindowBoundsFromPrompt(
864
1057
  const dayNames = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
865
1058
  const targetDay = dayNames.indexOf(dayOfWeekMatch[1]);
866
1059
  const currentDay = now.getDay();
867
- const daysBack = ((currentDay - targetDay + 7) % 7) || 7;
1060
+ const daysBack = (currentDay - targetDay + 7) % 7 || 7;
868
1061
  // +1 day: exclusive upper bound includes the named weekday
869
1062
  toDate = new Date(nowMs - (daysBack - 1) * 86_400_000).toISOString().slice(0, 10);
870
1063
  } else {
@@ -872,9 +1065,7 @@ export function recencyWindowBoundsFromPrompt(
872
1065
  // toDaysBack=-1 means no pattern matched (or hours-ago): use tomorrow so today
873
1066
  // is included in the window. toDaysBack=0 means N=1 ago (e.g. "1 day ago"):
874
1067
  // toDate = today (exclusive) correctly creates a 1-day window [yesterday, today).
875
- toDate = toDaysBack < 0
876
- ? tomorrow
877
- : new Date(nowMs - toDaysBack * 86_400_000).toISOString().slice(0, 10);
1068
+ toDate = toDaysBack < 0 ? tomorrow : new Date(nowMs - toDaysBack * 86_400_000).toISOString().slice(0, 10);
878
1069
  }
879
1070
  }
880
1071
  }