@remnic/core 9.3.595 → 9.3.597
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-cli.js +14 -14
- package/dist/access-http.js +6 -6
- package/dist/access-mcp.js +5 -5
- package/dist/access-schema.d.ts +7 -7
- package/dist/access-service.js +4 -4
- package/dist/briefing.js +2 -2
- package/dist/causal-consolidation.js +3 -3
- package/dist/{chunk-ARY5OOLG.js → chunk-557IAFPD.js} +2 -2
- package/dist/{chunk-VFB2G5YL.js → chunk-5BUGGPBR.js} +4 -4
- package/dist/{chunk-USYGGIJZ.js → chunk-D2MMMTDV.js} +2 -2
- package/dist/{chunk-XM7BYXT7.js → chunk-D65TSG24.js} +2 -2
- package/dist/{chunk-FHBEL473.js → chunk-DOX2CG6Y.js} +54 -5
- package/dist/chunk-DOX2CG6Y.js.map +1 -0
- package/dist/{chunk-DARLGSFX.js → chunk-ELKI4BB6.js} +4 -4
- package/dist/{chunk-QRWZOCJN.js → chunk-F4LM4ULA.js} +12 -12
- package/dist/{chunk-JIBCUYIP.js → chunk-IEFHBIU2.js} +10 -10
- package/dist/{chunk-KDUFBSBF.js → chunk-IK34DVAC.js} +2 -2
- package/dist/{chunk-OPYFD6PD.js → chunk-IK7DCC5H.js} +2 -2
- package/dist/{chunk-574MU2Y3.js → chunk-JTDRJQ3K.js} +2 -2
- package/dist/{chunk-LAL7WBLY.js → chunk-LYPDMKUT.js} +3 -3
- package/dist/{chunk-GBXGCFRH.js → chunk-MA5MWGKP.js} +2 -2
- package/dist/{chunk-HQO5EBUC.js → chunk-MLT75J5S.js} +3 -3
- package/dist/{chunk-7X7TBJRX.js → chunk-NOMEVTUD.js} +2 -2
- package/dist/{chunk-SUTSSOYU.js → chunk-OD5LFAPZ.js} +2 -2
- package/dist/{chunk-XT7XVA53.js → chunk-OI27U2HT.js} +2 -2
- package/dist/{chunk-MQEIWDYW.js → chunk-QDDHYAKV.js} +2 -2
- package/dist/{chunk-ZY6UPHNY.js → chunk-TYICDVQW.js} +3 -3
- package/dist/{chunk-XRWTAEZM.js → chunk-W5O2FQTZ.js} +2 -2
- package/dist/{chunk-V3RXWQIE.js → chunk-WXACKLKP.js} +209 -59
- package/dist/chunk-WXACKLKP.js.map +1 -0
- package/dist/{chunk-IRFF6LSF.js → chunk-YFS5OEKO.js} +36 -1
- package/dist/chunk-YFS5OEKO.js.map +1 -0
- package/dist/cli.js +15 -15
- package/dist/compounding/engine.js +2 -2
- package/dist/connectors/codex-materialize-runner.js +2 -2
- package/dist/connectors/index.js +2 -2
- package/dist/entity-retrieval.js +2 -2
- package/dist/index.js +22 -22
- package/dist/maintenance/memory-governance.js +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +2 -2
- package/dist/maintenance/rebuild-memory-projection.js +3 -3
- package/dist/namespaces/migrate.js +3 -3
- package/dist/namespaces/storage.js +2 -2
- package/dist/operator-toolkit.js +5 -5
- package/dist/orchestrator.js +11 -11
- package/dist/retrieval-agents.js +2 -2
- package/dist/schemas.d.ts +22 -22
- package/dist/semantic-consolidation.js +3 -3
- package/dist/semantic-rule-promotion.js +2 -2
- package/dist/semantic-rule-verifier.js +2 -2
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +1 -1
- package/dist/temporal-index.js +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/verified-recall.js +2 -2
- package/package.json +1 -1
- package/src/entity-retrieval.ts +64 -3
- package/src/storage.ts +40 -0
- package/src/temporal-index.test.ts +191 -0
- package/src/temporal-index.ts +291 -100
- package/dist/chunk-FHBEL473.js.map +0 -1
- package/dist/chunk-IRFF6LSF.js.map +0 -1
- package/dist/chunk-V3RXWQIE.js.map +0 -1
- /package/dist/{chunk-ARY5OOLG.js.map → chunk-557IAFPD.js.map} +0 -0
- /package/dist/{chunk-VFB2G5YL.js.map → chunk-5BUGGPBR.js.map} +0 -0
- /package/dist/{chunk-USYGGIJZ.js.map → chunk-D2MMMTDV.js.map} +0 -0
- /package/dist/{chunk-XM7BYXT7.js.map → chunk-D65TSG24.js.map} +0 -0
- /package/dist/{chunk-DARLGSFX.js.map → chunk-ELKI4BB6.js.map} +0 -0
- /package/dist/{chunk-QRWZOCJN.js.map → chunk-F4LM4ULA.js.map} +0 -0
- /package/dist/{chunk-JIBCUYIP.js.map → chunk-IEFHBIU2.js.map} +0 -0
- /package/dist/{chunk-KDUFBSBF.js.map → chunk-IK34DVAC.js.map} +0 -0
- /package/dist/{chunk-OPYFD6PD.js.map → chunk-IK7DCC5H.js.map} +0 -0
- /package/dist/{chunk-574MU2Y3.js.map → chunk-JTDRJQ3K.js.map} +0 -0
- /package/dist/{chunk-LAL7WBLY.js.map → chunk-LYPDMKUT.js.map} +0 -0
- /package/dist/{chunk-GBXGCFRH.js.map → chunk-MA5MWGKP.js.map} +0 -0
- /package/dist/{chunk-HQO5EBUC.js.map → chunk-MLT75J5S.js.map} +0 -0
- /package/dist/{chunk-7X7TBJRX.js.map → chunk-NOMEVTUD.js.map} +0 -0
- /package/dist/{chunk-SUTSSOYU.js.map → chunk-OD5LFAPZ.js.map} +0 -0
- /package/dist/{chunk-XT7XVA53.js.map → chunk-OI27U2HT.js.map} +0 -0
- /package/dist/{chunk-MQEIWDYW.js.map → chunk-QDDHYAKV.js.map} +0 -0
- /package/dist/{chunk-ZY6UPHNY.js.map → chunk-TYICDVQW.js.map} +0 -0
- /package/dist/{chunk-XRWTAEZM.js.map → chunk-W5O2FQTZ.js.map} +0 -0
package/src/temporal-index.ts
CHANGED
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
* - Both indexes are plain JSON; no external dependencies
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import
|
|
19
|
-
import * as
|
|
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.
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
370
|
-
|
|
540
|
+
updateTemporalIndex(memoryDir, (index) => {
|
|
541
|
+
addPathToSet(index.dates, dateKey, memoryPath);
|
|
542
|
+
});
|
|
371
543
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
402
|
-
|
|
564
|
+
updateTemporalIndex(memoryDir, (index) => {
|
|
565
|
+
removePathFromSet(index.dates, dateKey, memoryPath);
|
|
566
|
+
});
|
|
403
567
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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 = [
|
|
673
|
-
"
|
|
674
|
-
|
|
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 = (
|
|
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 = [
|
|
798
|
-
"
|
|
799
|
-
|
|
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 =
|
|
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 = (
|
|
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
|
}
|