@remnic/core 9.7.9 → 9.7.11
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-admin-ops-surface.js +5 -5
- package/dist/access-boundary.d.ts +1 -1
- package/dist/access-boundary.js +6 -6
- package/dist/access-cli.js +36 -36
- package/dist/access-http.d.ts +2 -1
- package/dist/access-http.js +10 -10
- package/dist/access-identity-continuity-surface.js +5 -5
- package/dist/access-lcm-surface.js +5 -5
- package/dist/access-mcp.js +9 -9
- package/dist/access-observe-write-surface.js +5 -5
- package/dist/access-operations-batch.js +7 -7
- package/dist/access-operations.d.ts +4 -4
- package/dist/access-operations.js +8 -8
- package/dist/access-recall-surface.js +5 -5
- package/dist/access-schema.d.ts +64 -64
- package/dist/access-service.js +5 -5
- package/dist/access-surface-catalog.js +2 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/briefing.js +3 -3
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-HHAA35A3.js → chunk-2VN4JAX7.js} +548 -161
- package/dist/chunk-2VN4JAX7.js.map +1 -0
- package/dist/{chunk-MLT5EG6K.js → chunk-3OQ4IZJN.js} +4 -4
- package/dist/{chunk-DLPXS7BE.js → chunk-5U65RIVI.js} +348 -49
- package/dist/chunk-5U65RIVI.js.map +1 -0
- package/dist/{chunk-QKVX4Y6D.js → chunk-673NPXE3.js} +2 -2
- package/dist/{chunk-POUD5BLV.js → chunk-6LM6PPDX.js} +4 -2
- package/dist/chunk-6LM6PPDX.js.map +1 -0
- package/dist/chunk-7XRUYC6G.js +1794 -0
- package/dist/chunk-7XRUYC6G.js.map +1 -0
- package/dist/{chunk-5CFHYQU3.js → chunk-DLWYAN6A.js} +2 -2
- package/dist/{chunk-TY43GBX2.js → chunk-EQF5763Z.js} +2 -2
- package/dist/{chunk-SKXBOZDF.js → chunk-GHVJASQ4.js} +3 -3
- package/dist/{chunk-YGAOLHKN.js → chunk-JYRFO4EH.js} +4 -4
- package/dist/{chunk-F3FC7SMO.js → chunk-JZM5QUTE.js} +2 -2
- package/dist/{chunk-PV7E2FGY.js → chunk-KXLRMM35.js} +6 -6
- package/dist/{chunk-PHXCPVSX.js → chunk-L4H6MQ7I.js} +2 -2
- package/dist/{chunk-FKCJH7KP.js → chunk-LQKITFGB.js} +2 -2
- package/dist/{chunk-27VEKQQA.js → chunk-MXRMNKSY.js} +3 -3
- package/dist/{chunk-EIKBKQ6L.js → chunk-QHT4W3RU.js} +127 -9
- package/dist/chunk-QHT4W3RU.js.map +1 -0
- package/dist/{chunk-CDGGQGUU.js → chunk-SBFDL2EQ.js} +2 -2
- package/dist/{chunk-I2THZUAF.js → chunk-SECMZACR.js} +2 -2
- package/dist/{chunk-JU3GHO6J.js → chunk-TGBJ2OMK.js} +2 -2
- package/dist/{chunk-DE6ENYLG.js → chunk-TXLGHDHS.js} +2 -2
- package/dist/{chunk-KGNFFCZJ.js → chunk-U4KHQTJ6.js} +2 -2
- package/dist/{chunk-Y5U35ZVS.js → chunk-UTDA43TB.js} +2 -2
- package/dist/{chunk-CK6RI52P.js → chunk-VCEH5UYS.js} +2 -2
- package/dist/{chunk-UXWGPODH.js → chunk-VRAD2FGO.js} +7 -7
- package/dist/{chunk-KKASNHGF.js → chunk-W6R43NLA.js} +19 -19
- package/dist/{chunk-UP25ZZCN.js → chunk-WUDBMLC6.js} +2 -2
- package/dist/{chunk-XG5CGR3K.js → chunk-XF4KOVDK.js} +5 -5
- package/dist/{chunk-KPXAJ77O.js → chunk-YLTG6BO4.js} +2 -2
- package/dist/cli.js +23 -23
- package/dist/compounding/engine.js +3 -3
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/index.js +3 -3
- package/dist/entity-retrieval.js +3 -3
- package/dist/index.d.ts +3990 -4
- package/dist/index.js +90 -43
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +8 -8
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/storage.js +3 -3
- package/dist/operator-toolkit.js +7 -7
- package/dist/orchestration/maintenance.js +5 -5
- package/dist/orchestrator.js +36 -36
- package/dist/retrieval-agents.js +2 -2
- package/dist/schemas.d.ts +74 -74
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/storage.js +2 -2
- package/dist/temporal-index-compat.d.ts +1 -0
- package/dist/temporal-index-compat.js +16 -0
- package/dist/temporal-index-compat.js.map +1 -0
- package/dist/temporal-index.d.ts +58 -7
- package/dist/temporal-index.js +23 -3
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +3 -3
- package/package.json +2 -2
- package/src/access-boundary.ts +2 -0
- package/src/access-http.ts +147 -6
- package/src/access-mcp-cancellation.test.ts +2 -2
- package/src/access-operations.ts +2 -0
- package/src/access-surface-catalog.ts +2 -0
- package/src/index.ts +53 -0
- package/src/orchestration/consolidation-run.ts +5 -5
- package/src/orchestration/contradiction-linking-coordinator.ts +2 -2
- package/src/orchestration/lifecycle-policy-coordinator.ts +2 -2
- package/src/orchestration/persistence-index.ts +6 -6
- package/src/orchestration/recall-search-pipeline.ts +36 -21
- package/src/orchestration/semantic-consolidation-coordinator.ts +2 -2
- package/src/orchestrator.ts +0 -4
- package/src/relay/mission-access.test.ts +379 -0
- package/src/relay/mission-access.ts +125 -0
- package/src/relay/mission-fixture.ts +301 -0
- package/src/relay/mission.test.ts +964 -0
- package/src/relay/mission.ts +1725 -0
- package/src/temporal-index-compat.ts +311 -0
- package/src/temporal-index.test.ts +273 -22
- package/src/temporal-index.ts +427 -178
- package/dist/chunk-DLPXS7BE.js.map +0 -1
- package/dist/chunk-EIKBKQ6L.js.map +0 -1
- package/dist/chunk-HHAA35A3.js.map +0 -1
- package/dist/chunk-POUD5BLV.js.map +0 -1
- package/dist/chunk-VCFADTFS.js +0 -467
- package/dist/chunk-VCFADTFS.js.map +0 -1
- /package/dist/{chunk-MLT5EG6K.js.map → chunk-3OQ4IZJN.js.map} +0 -0
- /package/dist/{chunk-QKVX4Y6D.js.map → chunk-673NPXE3.js.map} +0 -0
- /package/dist/{chunk-5CFHYQU3.js.map → chunk-DLWYAN6A.js.map} +0 -0
- /package/dist/{chunk-TY43GBX2.js.map → chunk-EQF5763Z.js.map} +0 -0
- /package/dist/{chunk-SKXBOZDF.js.map → chunk-GHVJASQ4.js.map} +0 -0
- /package/dist/{chunk-YGAOLHKN.js.map → chunk-JYRFO4EH.js.map} +0 -0
- /package/dist/{chunk-F3FC7SMO.js.map → chunk-JZM5QUTE.js.map} +0 -0
- /package/dist/{chunk-PV7E2FGY.js.map → chunk-KXLRMM35.js.map} +0 -0
- /package/dist/{chunk-PHXCPVSX.js.map → chunk-L4H6MQ7I.js.map} +0 -0
- /package/dist/{chunk-FKCJH7KP.js.map → chunk-LQKITFGB.js.map} +0 -0
- /package/dist/{chunk-27VEKQQA.js.map → chunk-MXRMNKSY.js.map} +0 -0
- /package/dist/{chunk-CDGGQGUU.js.map → chunk-SBFDL2EQ.js.map} +0 -0
- /package/dist/{chunk-I2THZUAF.js.map → chunk-SECMZACR.js.map} +0 -0
- /package/dist/{chunk-JU3GHO6J.js.map → chunk-TGBJ2OMK.js.map} +0 -0
- /package/dist/{chunk-DE6ENYLG.js.map → chunk-TXLGHDHS.js.map} +0 -0
- /package/dist/{chunk-KGNFFCZJ.js.map → chunk-U4KHQTJ6.js.map} +0 -0
- /package/dist/{chunk-Y5U35ZVS.js.map → chunk-UTDA43TB.js.map} +0 -0
- /package/dist/{chunk-CK6RI52P.js.map → chunk-VCEH5UYS.js.map} +0 -0
- /package/dist/{chunk-UXWGPODH.js.map → chunk-VRAD2FGO.js.map} +0 -0
- /package/dist/{chunk-KKASNHGF.js.map → chunk-W6R43NLA.js.map} +0 -0
- /package/dist/{chunk-UP25ZZCN.js.map → chunk-WUDBMLC6.js.map} +0 -0
- /package/dist/{chunk-XG5CGR3K.js.map → chunk-XF4KOVDK.js.map} +0 -0
- /package/dist/{chunk-KPXAJ77O.js.map → chunk-YLTG6BO4.js.map} +0 -0
package/src/temporal-index.ts
CHANGED
|
@@ -15,10 +15,22 @@
|
|
|
15
15
|
* - Both indexes are plain JSON; no external dependencies
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { execFile } from "node:child_process";
|
|
19
19
|
import * as crypto from "node:crypto";
|
|
20
20
|
import * as fs from "node:fs";
|
|
21
21
|
import * as path from "node:path";
|
|
22
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
23
|
+
import { promisify } from "node:util";
|
|
24
|
+
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
clearIndexes,
|
|
29
|
+
deindexMemory,
|
|
30
|
+
indexMemoriesBatch,
|
|
31
|
+
indexMemory,
|
|
32
|
+
indexesExist,
|
|
33
|
+
} from "./temporal-index-compat.js";
|
|
22
34
|
|
|
23
35
|
export interface TemporalIndex {
|
|
24
36
|
/** version bumped when schema changes */
|
|
@@ -82,9 +94,42 @@ const TAG_INDEX_VERSION = 2;
|
|
|
82
94
|
const INDEX_LOCK_STALE_MS = 60_000;
|
|
83
95
|
const INDEX_LOCK_POLL_MS = 10;
|
|
84
96
|
const INDEX_PROCESS_START_TOLERANCE_MS = 2_000;
|
|
85
|
-
const INDEX_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
|
86
97
|
const INDEX_PROCESS_STARTED_AT_MS = Date.now() - process.uptime() * 1000;
|
|
87
98
|
|
|
99
|
+
interface IndexFileIdentity {
|
|
100
|
+
size: number;
|
|
101
|
+
mtimeMs: number;
|
|
102
|
+
ctimeMs: number;
|
|
103
|
+
ino: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface CachedIndex<T> {
|
|
107
|
+
identity: IndexFileIdentity;
|
|
108
|
+
index: T;
|
|
109
|
+
currentSchema: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type IndexLoadResult<T> =
|
|
113
|
+
| { kind: "ok"; index: T; currentSchema: boolean }
|
|
114
|
+
| { kind: "missing" }
|
|
115
|
+
| { kind: "invalid" };
|
|
116
|
+
|
|
117
|
+
const parsedIndexCache = new Map<string, CachedIndex<unknown>>();
|
|
118
|
+
const writeChains = new Map<string, Promise<void>>();
|
|
119
|
+
// Operation-level serialization keyed by memoryDir (issue #1911, Codex Medium):
|
|
120
|
+
// a single logical index mutation touches TWO files (temporal + tag), each with
|
|
121
|
+
// its own per-file writeChain. Without an outer lock, two overlapping
|
|
122
|
+
// indexMemoryAsync calls could interleave so the temporal half of one commits
|
|
123
|
+
// with the tag half of the other. This chain makes each public mutator's paired
|
|
124
|
+
// writes atomic relative to other mutators on the same memoryDir.
|
|
125
|
+
const opChains = new Map<string, Promise<void>>();
|
|
126
|
+
let indexReadObserverForTest: (() => void) | undefined;
|
|
127
|
+
|
|
128
|
+
/** Test-only observer for asserting cache read/parse behavior. */
|
|
129
|
+
export function setIndexReadObserverForTest(observer?: () => void): void {
|
|
130
|
+
indexReadObserverForTest = observer;
|
|
131
|
+
}
|
|
132
|
+
|
|
88
133
|
interface IndexLockOwner {
|
|
89
134
|
pid: number;
|
|
90
135
|
createdAt?: string;
|
|
@@ -105,30 +150,104 @@ function tagIndexPath(memoryDir: string): string {
|
|
|
105
150
|
return path.join(stateDir(memoryDir), TAG_INDEX_FILE);
|
|
106
151
|
}
|
|
107
152
|
|
|
108
|
-
function ensureStateDir(memoryDir: string): void {
|
|
109
|
-
|
|
110
|
-
|
|
153
|
+
async function ensureStateDir(memoryDir: string): Promise<void> {
|
|
154
|
+
await fs.promises.mkdir(stateDir(memoryDir), { recursive: true });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function identityFromStat(stat: fs.Stats): IndexFileIdentity {
|
|
158
|
+
return {
|
|
159
|
+
size: stat.size,
|
|
160
|
+
mtimeMs: stat.mtimeMs,
|
|
161
|
+
ctimeMs: stat.ctimeMs,
|
|
162
|
+
ino: stat.ino,
|
|
163
|
+
};
|
|
111
164
|
}
|
|
112
165
|
|
|
113
|
-
function
|
|
166
|
+
function identityEquals(left: IndexFileIdentity, right: IndexFileIdentity): boolean {
|
|
167
|
+
return left.size === right.size &&
|
|
168
|
+
left.mtimeMs === right.mtimeMs &&
|
|
169
|
+
left.ctimeMs === right.ctimeMs &&
|
|
170
|
+
left.ino === right.ino;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function loadIndexCached<T>(
|
|
174
|
+
filePath: string,
|
|
175
|
+
normalize: (raw: unknown) => T,
|
|
176
|
+
hasCurrentSchema: (raw: unknown) => boolean,
|
|
177
|
+
): Promise<IndexLoadResult<T>> {
|
|
178
|
+
let handle: fs.promises.FileHandle;
|
|
114
179
|
try {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
180
|
+
handle = await fs.promises.open(filePath, "r");
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return (error as NodeJS.ErrnoException)?.code === "ENOENT"
|
|
183
|
+
? { kind: "missing" }
|
|
184
|
+
: { kind: "invalid" };
|
|
119
185
|
}
|
|
120
|
-
}
|
|
121
186
|
|
|
122
|
-
function writeJsonSafe(filePath: string, data: unknown): void {
|
|
123
187
|
try {
|
|
124
|
-
|
|
188
|
+
const identity = identityFromStat(await handle.stat());
|
|
189
|
+
const cached = parsedIndexCache.get(filePath) as CachedIndex<T> | undefined;
|
|
190
|
+
if (cached && identityEquals(cached.identity, identity)) {
|
|
191
|
+
return { kind: "ok", index: cached.index, currentSchema: cached.currentSchema };
|
|
192
|
+
}
|
|
193
|
+
indexReadObserverForTest?.();
|
|
194
|
+
const parsed = JSON.parse(await handle.readFile("utf8")) as unknown;
|
|
195
|
+
const index = normalize(parsed);
|
|
196
|
+
const currentSchema = hasCurrentSchema(parsed);
|
|
197
|
+
parsedIndexCache.set(filePath, { identity, index, currentSchema });
|
|
198
|
+
return { kind: "ok", index, currentSchema };
|
|
125
199
|
} catch {
|
|
126
|
-
|
|
200
|
+
parsedIndexCache.delete(filePath);
|
|
201
|
+
return { kind: "invalid" };
|
|
202
|
+
} finally {
|
|
203
|
+
await handle.close().catch(() => undefined);
|
|
127
204
|
}
|
|
128
205
|
}
|
|
129
206
|
|
|
130
|
-
function
|
|
131
|
-
|
|
207
|
+
function withPathMutex<T>(filePath: string, update: () => Promise<T>): Promise<T> {
|
|
208
|
+
const previous = writeChains.get(filePath) ?? Promise.resolve();
|
|
209
|
+
const run = previous.then(update, update);
|
|
210
|
+
const tail = run.then(() => undefined, () => undefined);
|
|
211
|
+
writeChains.set(filePath, tail);
|
|
212
|
+
void tail.then(() => {
|
|
213
|
+
if (writeChains.get(filePath) === tail) writeChains.delete(filePath);
|
|
214
|
+
});
|
|
215
|
+
return run;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Operation-level mutex keyed by memoryDir (issue #1911, Codex Medium). Wraps a
|
|
220
|
+
* whole logical index mutation (which writes BOTH the temporal and tag index
|
|
221
|
+
* files) so overlapping mutators on the same memoryDir cannot interleave their
|
|
222
|
+
* per-file writes and commit mismatched halves. The inner per-file writeChains
|
|
223
|
+
* still protect each file; this outer chain just orders complete operations.
|
|
224
|
+
*/
|
|
225
|
+
function withMemoryDirMutex<T>(memoryDir: string, op: () => Promise<T>): Promise<T> {
|
|
226
|
+
const previous = opChains.get(memoryDir) ?? Promise.resolve();
|
|
227
|
+
const run = previous.then(op, op);
|
|
228
|
+
const tail = run.then(() => undefined, () => undefined);
|
|
229
|
+
opChains.set(memoryDir, tail);
|
|
230
|
+
void tail.then(() => {
|
|
231
|
+
if (opChains.get(memoryDir) === tail) opChains.delete(memoryDir);
|
|
232
|
+
});
|
|
233
|
+
return run;
|
|
234
|
+
}
|
|
235
|
+
async function primeCacheAfterWrite(
|
|
236
|
+
filePath: string,
|
|
237
|
+
index: unknown,
|
|
238
|
+
isCurrentSchema: () => boolean,
|
|
239
|
+
): Promise<void> {
|
|
240
|
+
try {
|
|
241
|
+
const identity = identityFromStat(await fs.promises.stat(filePath));
|
|
242
|
+
// Derive currentSchema from the ACTUAL written index (issue #1911, Codex):
|
|
243
|
+
// hardcoding true let deindexMemoryAsync (which does not bump version) cache
|
|
244
|
+
// an old-schema index as current, so indexesExistAsync then skipped a
|
|
245
|
+
// needed rebuild. The tag index is always current (no version gate); the
|
|
246
|
+
// temporal index is current only when hasCurrentTemporalIndexSchema holds.
|
|
247
|
+
parsedIndexCache.set(filePath, { identity, index, currentSchema: isCurrentSchema() });
|
|
248
|
+
} catch {
|
|
249
|
+
parsedIndexCache.delete(filePath);
|
|
250
|
+
}
|
|
132
251
|
}
|
|
133
252
|
|
|
134
253
|
function uniqueTempPath(filePath: string): string {
|
|
@@ -142,9 +261,9 @@ function lockOwnerPath(lockDir: string): string {
|
|
|
142
261
|
return path.join(lockDir, "owner.json");
|
|
143
262
|
}
|
|
144
263
|
|
|
145
|
-
function writeIndexLockOwner(lockDir: string): void {
|
|
264
|
+
async function writeIndexLockOwner(lockDir: string): Promise<void> {
|
|
146
265
|
try {
|
|
147
|
-
fs.
|
|
266
|
+
await fs.promises.writeFile(
|
|
148
267
|
lockOwnerPath(lockDir),
|
|
149
268
|
JSON.stringify({
|
|
150
269
|
pid: process.pid,
|
|
@@ -161,9 +280,9 @@ function writeIndexLockOwner(lockDir: string): void {
|
|
|
161
280
|
}
|
|
162
281
|
}
|
|
163
282
|
|
|
164
|
-
function readIndexLockOwner(lockDir: string): IndexLockOwner | null {
|
|
283
|
+
async function readIndexLockOwner(lockDir: string): Promise<IndexLockOwner | null> {
|
|
165
284
|
try {
|
|
166
|
-
const parsed = JSON.parse(fs.
|
|
285
|
+
const parsed = JSON.parse(await fs.promises.readFile(lockOwnerPath(lockDir), "utf8")) as { pid?: unknown };
|
|
167
286
|
if (!(typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0)) return null;
|
|
168
287
|
const owner: IndexLockOwner = { pid: parsed.pid };
|
|
169
288
|
if (
|
|
@@ -193,13 +312,13 @@ function processIsAlive(pid: number): boolean {
|
|
|
193
312
|
}
|
|
194
313
|
}
|
|
195
314
|
|
|
196
|
-
function readProcessStartedAtMs(pid: number): number | null {
|
|
315
|
+
async function readProcessStartedAtMs(pid: number): Promise<number | null> {
|
|
197
316
|
try {
|
|
198
|
-
const
|
|
317
|
+
const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
199
318
|
encoding: "utf8",
|
|
200
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
201
319
|
timeout: 1_000,
|
|
202
|
-
})
|
|
320
|
+
});
|
|
321
|
+
const output = stdout.trim();
|
|
203
322
|
if (!output) return null;
|
|
204
323
|
const startedAtMs = Date.parse(output);
|
|
205
324
|
return Number.isFinite(startedAtMs) ? startedAtMs : null;
|
|
@@ -208,10 +327,10 @@ function readProcessStartedAtMs(pid: number): number | null {
|
|
|
208
327
|
}
|
|
209
328
|
}
|
|
210
329
|
|
|
211
|
-
function lockOwnerIsRunning(owner: IndexLockOwner): boolean {
|
|
330
|
+
async function lockOwnerIsRunning(owner: IndexLockOwner): Promise<boolean> {
|
|
212
331
|
if (!processIsAlive(owner.pid)) return false;
|
|
213
332
|
if (owner.processStartedAtMs === undefined) return true;
|
|
214
|
-
const runningStartedAtMs = readProcessStartedAtMs(owner.pid);
|
|
333
|
+
const runningStartedAtMs = await readProcessStartedAtMs(owner.pid);
|
|
215
334
|
if (runningStartedAtMs === null) return true;
|
|
216
335
|
return runningStartedAtMs <= owner.processStartedAtMs + INDEX_PROCESS_START_TOLERANCE_MS;
|
|
217
336
|
}
|
|
@@ -223,20 +342,20 @@ function lockIsFresh(lockInfo: fs.Stats, owner: IndexLockOwner | null): boolean
|
|
|
223
342
|
return Date.now() - referenceMs < INDEX_LOCK_STALE_MS;
|
|
224
343
|
}
|
|
225
344
|
|
|
226
|
-
function removeAbandonedIndexLock(lockDir: string): IndexLockCleanupResult {
|
|
345
|
+
async function removeAbandonedIndexLock(lockDir: string): Promise<IndexLockCleanupResult> {
|
|
227
346
|
try {
|
|
228
|
-
const info = fs.
|
|
347
|
+
const info = await fs.promises.lstat(lockDir);
|
|
229
348
|
if (info.isSymbolicLink()) return "blocked";
|
|
230
349
|
if (!info.isDirectory()) {
|
|
231
|
-
fs.
|
|
350
|
+
await fs.promises.rm(lockDir, { force: true });
|
|
232
351
|
return "removed";
|
|
233
352
|
}
|
|
234
|
-
const owner = readIndexLockOwner(lockDir);
|
|
353
|
+
const owner = await readIndexLockOwner(lockDir);
|
|
235
354
|
if (owner !== null) {
|
|
236
|
-
if (lockOwnerIsRunning(owner)) return "wait";
|
|
355
|
+
if (await lockOwnerIsRunning(owner)) return "wait";
|
|
237
356
|
}
|
|
238
357
|
if (owner === null && lockIsFresh(info, null)) return "wait";
|
|
239
|
-
fs.
|
|
358
|
+
await fs.promises.rm(lockDir, { recursive: true, force: true });
|
|
240
359
|
return "removed";
|
|
241
360
|
} catch (error) {
|
|
242
361
|
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return "removed";
|
|
@@ -245,65 +364,68 @@ function removeAbandonedIndexLock(lockDir: string): IndexLockCleanupResult {
|
|
|
245
364
|
}
|
|
246
365
|
}
|
|
247
366
|
|
|
248
|
-
function withIndexFileLock(filePath: string, update: () =>
|
|
367
|
+
async function withIndexFileLock<T>(filePath: string, update: () => Promise<T>): Promise<T | undefined> {
|
|
249
368
|
const lockDir = `${filePath}.lock.d`;
|
|
250
369
|
let acquired = false;
|
|
251
370
|
|
|
252
371
|
while (!acquired) {
|
|
253
372
|
try {
|
|
254
|
-
fs.
|
|
255
|
-
writeIndexLockOwner(lockDir);
|
|
373
|
+
await fs.promises.mkdir(lockDir);
|
|
374
|
+
await writeIndexLockOwner(lockDir);
|
|
256
375
|
acquired = true;
|
|
257
376
|
} catch (error) {
|
|
258
377
|
const code = (error as NodeJS.ErrnoException)?.code;
|
|
259
378
|
if (code === "ENOENT") {
|
|
260
379
|
try {
|
|
261
|
-
fs.
|
|
380
|
+
await fs.promises.mkdir(path.dirname(lockDir), { recursive: true });
|
|
262
381
|
} catch {
|
|
263
|
-
return;
|
|
382
|
+
return undefined;
|
|
264
383
|
}
|
|
265
|
-
|
|
384
|
+
await delay(INDEX_LOCK_POLL_MS);
|
|
266
385
|
continue;
|
|
267
386
|
}
|
|
268
|
-
if (code !== "EEXIST") return;
|
|
269
|
-
const cleanupResult = removeAbandonedIndexLock(lockDir);
|
|
270
|
-
if (cleanupResult === "blocked") return;
|
|
271
|
-
|
|
387
|
+
if (code !== "EEXIST") return undefined;
|
|
388
|
+
const cleanupResult = await removeAbandonedIndexLock(lockDir);
|
|
389
|
+
if (cleanupResult === "blocked") return undefined;
|
|
390
|
+
await delay(INDEX_LOCK_POLL_MS);
|
|
272
391
|
}
|
|
273
392
|
}
|
|
274
393
|
|
|
394
|
+
let result: T;
|
|
275
395
|
try {
|
|
276
|
-
update();
|
|
396
|
+
result = await update();
|
|
277
397
|
} finally {
|
|
278
398
|
try {
|
|
279
|
-
fs.
|
|
399
|
+
await fs.promises.rm(lockDir, { recursive: true, force: true });
|
|
280
400
|
} catch {
|
|
281
401
|
// Fail silently — indexes are advisory only
|
|
282
402
|
}
|
|
283
403
|
}
|
|
404
|
+
return result;
|
|
284
405
|
}
|
|
285
406
|
|
|
286
407
|
/**
|
|
287
408
|
* Atomic write: write to a unique `.tmp` sibling then rename so readers never
|
|
288
409
|
* observe a partially-written file.
|
|
289
410
|
*/
|
|
290
|
-
function writeJsonAtomic(filePath: string, data: unknown):
|
|
291
|
-
const payload = JSON.stringify(data
|
|
411
|
+
async function writeJsonAtomic(filePath: string, data: unknown): Promise<boolean> {
|
|
412
|
+
const payload = JSON.stringify(data);
|
|
292
413
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
293
414
|
const tmp = uniqueTempPath(filePath);
|
|
294
415
|
try {
|
|
295
|
-
fs.
|
|
296
|
-
fs.
|
|
297
|
-
return;
|
|
416
|
+
await fs.promises.writeFile(tmp, payload, "utf8");
|
|
417
|
+
await fs.promises.rename(tmp, filePath);
|
|
418
|
+
return true;
|
|
298
419
|
} catch {
|
|
299
420
|
try {
|
|
300
|
-
fs.
|
|
421
|
+
await fs.promises.unlink(tmp);
|
|
301
422
|
} catch {
|
|
302
423
|
// Fail silently — indexes are advisory only
|
|
303
424
|
}
|
|
304
|
-
|
|
425
|
+
await delay(INDEX_LOCK_POLL_MS);
|
|
305
426
|
}
|
|
306
427
|
}
|
|
428
|
+
return false;
|
|
307
429
|
}
|
|
308
430
|
|
|
309
431
|
function emptyTemporalIndex(): TemporalIndex {
|
|
@@ -324,23 +446,56 @@ function normalizeTemporalIndex(raw: TemporalIndex | null | undefined): Temporal
|
|
|
324
446
|
};
|
|
325
447
|
}
|
|
326
448
|
|
|
327
|
-
function
|
|
449
|
+
function hasCurrentTemporalIndexSchema(raw: unknown): boolean {
|
|
450
|
+
return isRecord(raw) &&
|
|
451
|
+
raw.version === INDEX_VERSION &&
|
|
452
|
+
isRecord(raw.dates) &&
|
|
453
|
+
isRecord(raw.events);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function updateTemporalIndex(memoryDir: string, update: (index: TemporalIndex) => void): Promise<boolean> {
|
|
328
457
|
const indexPath = temporalIndexPath(memoryDir);
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
458
|
+
return withPathMutex(indexPath, async () => {
|
|
459
|
+
const ok = await withIndexFileLock(indexPath, async () => {
|
|
460
|
+
const loaded = await loadIndexCached(
|
|
461
|
+
indexPath,
|
|
462
|
+
(raw) => normalizeTemporalIndex(raw as TemporalIndex),
|
|
463
|
+
hasCurrentTemporalIndexSchema,
|
|
464
|
+
);
|
|
465
|
+
const index = structuredClone(loaded.kind === "ok" ? loaded.index : emptyTemporalIndex());
|
|
466
|
+
update(index);
|
|
467
|
+
if (await writeJsonAtomic(indexPath, index)) {
|
|
468
|
+
await primeCacheAfterWrite(indexPath, index, () => hasCurrentTemporalIndexSchema(index));
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
parsedIndexCache.delete(indexPath);
|
|
472
|
+
return false;
|
|
473
|
+
});
|
|
474
|
+
return ok === true;
|
|
333
475
|
});
|
|
334
476
|
}
|
|
335
477
|
|
|
336
|
-
function updateTagIndex(memoryDir: string, update: (index: TagIndex) => void): void {
|
|
478
|
+
async function updateTagIndex(memoryDir: string, update: (index: TagIndex) => void): Promise<void> {
|
|
337
479
|
const indexPath = tagIndexPath(memoryDir);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
480
|
+
await withPathMutex(indexPath, async () => {
|
|
481
|
+
await withIndexFileLock(indexPath, async () => {
|
|
482
|
+
const loaded = await loadIndexCached(
|
|
483
|
+
indexPath,
|
|
484
|
+
(raw) => normalizeTagIndex(raw as TagIndex),
|
|
485
|
+
() => true,
|
|
486
|
+
);
|
|
487
|
+
const index = structuredClone(
|
|
488
|
+
loaded.kind === "ok"
|
|
489
|
+
? loaded.index
|
|
490
|
+
: { version: TAG_INDEX_VERSION, tags: {}, aliases: {} },
|
|
491
|
+
);
|
|
492
|
+
update(index);
|
|
493
|
+
if (await writeJsonAtomic(indexPath, index)) {
|
|
494
|
+
await primeCacheAfterWrite(indexPath, index, () => true);
|
|
495
|
+
} else {
|
|
496
|
+
parsedIndexCache.delete(indexPath);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
344
499
|
});
|
|
345
500
|
}
|
|
346
501
|
|
|
@@ -678,11 +833,62 @@ function aliasPhrase(alias: string): string {
|
|
|
678
833
|
return alias.replace(/\//g, " ").replace(/-/g, " ").replace(/\s+/g, " ").trim();
|
|
679
834
|
}
|
|
680
835
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
836
|
+
export type TemporalIndexCompatibilityMutation =
|
|
837
|
+
| { kind: "index"; entries: TemporalIndexEntry[] }
|
|
838
|
+
| { kind: "deindex"; entries: TemporalIndexEntry[] }
|
|
839
|
+
| { kind: "clear" };
|
|
840
|
+
|
|
841
|
+
/** @internal Pure model shared by the deprecated synchronous compatibility surface. */
|
|
842
|
+
export function applyTemporalIndexCompatibilityMutation(
|
|
843
|
+
raw: unknown,
|
|
844
|
+
mutation: TemporalIndexCompatibilityMutation,
|
|
845
|
+
): TemporalIndex {
|
|
846
|
+
const index = structuredClone(normalizeTemporalIndex(raw as TemporalIndex));
|
|
847
|
+
if (mutation.kind === "clear") {
|
|
848
|
+
return emptyTemporalIndex();
|
|
849
|
+
}
|
|
850
|
+
if (mutation.kind === "index") index.version = INDEX_VERSION;
|
|
851
|
+
for (const entry of mutation.entries) {
|
|
852
|
+
const dateKey = isoDateFromTimestamp(entry.createdAt);
|
|
853
|
+
if (mutation.kind === "index") {
|
|
854
|
+
removePathFromAllSets(index.dates, entry.path);
|
|
855
|
+
addPathToSet(index.dates, dateKey, entry.path);
|
|
856
|
+
index.events[entry.path] = temporalEventFromEntry(entry);
|
|
857
|
+
} else {
|
|
858
|
+
removePathFromSet(index.dates, dateKey, entry.path);
|
|
859
|
+
delete index.events[entry.path];
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return index;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** @internal Pure model shared by the deprecated synchronous compatibility surface. */
|
|
866
|
+
export function applyTagIndexCompatibilityMutation(
|
|
867
|
+
raw: unknown,
|
|
868
|
+
mutation: TemporalIndexCompatibilityMutation,
|
|
869
|
+
): TagIndex {
|
|
870
|
+
const index = structuredClone(normalizeTagIndex(raw as TagIndex));
|
|
871
|
+
if (mutation.kind === "clear") {
|
|
872
|
+
return { version: TAG_INDEX_VERSION, tags: {}, aliases: {} };
|
|
873
|
+
}
|
|
874
|
+
for (const entry of mutation.entries) {
|
|
875
|
+
if (mutation.kind === "index") {
|
|
876
|
+
removePathFromAllTagEntries(index, entry.path);
|
|
877
|
+
for (const tag of entry.tags) {
|
|
878
|
+
if (tag && typeof tag === "string") addTagGraphEntry(index, tag, entry.path);
|
|
879
|
+
}
|
|
880
|
+
} else {
|
|
881
|
+
for (const tag of entry.tags) {
|
|
882
|
+
if (tag && typeof tag === "string") removeTagGraphEntry(index, tag, entry.path);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return index;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** @internal Schema check shared by the deprecated synchronous compatibility surface. */
|
|
890
|
+
export function hasCurrentTemporalIndexSchemaForCompatibility(raw: unknown): boolean {
|
|
891
|
+
return hasCurrentTemporalIndexSchema(raw);
|
|
686
892
|
}
|
|
687
893
|
|
|
688
894
|
// ─── Public API ────────────────────────────────────────────────────────────────
|
|
@@ -695,35 +901,41 @@ function promptContainsAlias(prompt: string, alias: string): boolean {
|
|
|
695
901
|
* @param createdAt ISO timestamp of the memory's creation date
|
|
696
902
|
* @param tags Array of tag strings from the memory's frontmatter
|
|
697
903
|
*/
|
|
698
|
-
export function
|
|
904
|
+
export async function indexMemoryAsync(
|
|
699
905
|
memoryDir: string,
|
|
700
906
|
memoryPath: string,
|
|
701
907
|
createdAt: string,
|
|
702
908
|
tags: string[],
|
|
703
909
|
temporal: Omit<Partial<TemporalIndexEntry>, "path" | "createdAt" | "tags"> = {},
|
|
704
|
-
): void {
|
|
910
|
+
): Promise<void> {
|
|
705
911
|
try {
|
|
706
|
-
ensureStateDir(memoryDir);
|
|
912
|
+
await ensureStateDir(memoryDir);
|
|
707
913
|
|
|
708
914
|
const dateKey = isoDateFromTimestamp(createdAt);
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
915
|
+
await withMemoryDirMutex(memoryDir, async () => {
|
|
916
|
+
// Write the temporal half first; if its atomic write FAILS, do NOT commit
|
|
917
|
+
// the tag half (issue #1911, Cursor Medium) — committing tags while
|
|
918
|
+
// temporal stays unchanged leaves a durable mismatch until a later index.
|
|
919
|
+
const temporalOk = await updateTemporalIndex(memoryDir, (index) => {
|
|
920
|
+
index.version = INDEX_VERSION;
|
|
921
|
+
removePathFromAllSets(index.dates, memoryPath);
|
|
922
|
+
addPathToSet(index.dates, dateKey, memoryPath);
|
|
923
|
+
index.events[memoryPath] = temporalEventFromEntry({
|
|
924
|
+
path: memoryPath,
|
|
925
|
+
createdAt,
|
|
926
|
+
tags,
|
|
927
|
+
...temporal,
|
|
928
|
+
});
|
|
718
929
|
});
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
930
|
+
if (temporalOk) {
|
|
931
|
+
await updateTagIndex(memoryDir, (index) => {
|
|
932
|
+
removePathFromAllTagEntries(index, memoryPath);
|
|
933
|
+
for (const tag of tags) {
|
|
934
|
+
if (tag && typeof tag === "string") {
|
|
935
|
+
addTagGraphEntry(index, tag, memoryPath);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
});
|
|
727
939
|
}
|
|
728
940
|
});
|
|
729
941
|
} catch {
|
|
@@ -734,21 +946,29 @@ export function indexMemory(
|
|
|
734
946
|
/**
|
|
735
947
|
* Remove a memory file from both indexes (called on deletion/archival).
|
|
736
948
|
*/
|
|
737
|
-
export function
|
|
949
|
+
export async function deindexMemoryAsync(
|
|
950
|
+
memoryDir: string,
|
|
951
|
+
memoryPath: string,
|
|
952
|
+
createdAt: string,
|
|
953
|
+
tags: string[],
|
|
954
|
+
): Promise<void> {
|
|
738
955
|
try {
|
|
739
|
-
ensureStateDir(memoryDir);
|
|
956
|
+
await ensureStateDir(memoryDir);
|
|
740
957
|
|
|
741
958
|
const dateKey = isoDateFromTimestamp(createdAt);
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
959
|
+
await withMemoryDirMutex(memoryDir, async () => {
|
|
960
|
+
const temporalOk = await updateTemporalIndex(memoryDir, (index) => {
|
|
961
|
+
removePathFromSet(index.dates, dateKey, memoryPath);
|
|
962
|
+
delete index.events[memoryPath];
|
|
963
|
+
});
|
|
964
|
+
if (temporalOk) {
|
|
965
|
+
await updateTagIndex(memoryDir, (index) => {
|
|
966
|
+
for (const tag of tags) {
|
|
967
|
+
if (tag && typeof tag === "string") {
|
|
968
|
+
removeTagGraphEntry(index, tag, memoryPath);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
});
|
|
752
972
|
}
|
|
753
973
|
});
|
|
754
974
|
} catch {
|
|
@@ -761,20 +981,24 @@ export function deindexMemory(memoryDir: string, memoryPath: string, createdAt:
|
|
|
761
981
|
* Called before a full-corpus rebuild so stale paths in any surviving index
|
|
762
982
|
* file do not persist after the rebuild completes.
|
|
763
983
|
*/
|
|
764
|
-
export function
|
|
984
|
+
export async function clearIndexesAsync(memoryDir: string): Promise<void> {
|
|
765
985
|
try {
|
|
766
|
-
ensureStateDir(memoryDir);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
986
|
+
await ensureStateDir(memoryDir);
|
|
987
|
+
await withMemoryDirMutex(memoryDir, async () => {
|
|
988
|
+
const temporalOk = await updateTemporalIndex(memoryDir, (index) => {
|
|
989
|
+
index.version = INDEX_VERSION;
|
|
990
|
+
index.lastRebuildAt = undefined;
|
|
991
|
+
index.dates = {};
|
|
992
|
+
index.events = {};
|
|
993
|
+
});
|
|
994
|
+
if (temporalOk) {
|
|
995
|
+
await updateTagIndex(memoryDir, (index) => {
|
|
996
|
+
index.version = TAG_INDEX_VERSION;
|
|
997
|
+
index.lastRebuildAt = undefined;
|
|
998
|
+
index.tags = {};
|
|
999
|
+
index.aliases = {};
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
778
1002
|
});
|
|
779
1003
|
} catch {
|
|
780
1004
|
// Fail silently — indexes are advisory only
|
|
@@ -785,11 +1009,15 @@ export function clearIndexes(memoryDir: string): void {
|
|
|
785
1009
|
* Returns true when both index files exist on disk.
|
|
786
1010
|
* Used to detect first-time enablement so callers can trigger a full rebuild.
|
|
787
1011
|
*/
|
|
788
|
-
export function
|
|
1012
|
+
export async function indexesExistAsync(memoryDir: string): Promise<boolean> {
|
|
789
1013
|
try {
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
1014
|
+
await fs.promises.access(tagIndexPath(memoryDir));
|
|
1015
|
+
const loaded = await loadIndexCached(
|
|
1016
|
+
temporalIndexPath(memoryDir),
|
|
1017
|
+
(raw) => normalizeTemporalIndex(raw as TemporalIndex),
|
|
1018
|
+
hasCurrentTemporalIndexSchema,
|
|
1019
|
+
);
|
|
1020
|
+
return loaded.kind === "ok" && loaded.currentSchema;
|
|
793
1021
|
} catch {
|
|
794
1022
|
return false;
|
|
795
1023
|
}
|
|
@@ -797,34 +1025,37 @@ export function indexesExist(memoryDir: string): boolean {
|
|
|
797
1025
|
|
|
798
1026
|
/**
|
|
799
1027
|
* Batch-add multiple memories to both indexes in a single read-modify-write cycle.
|
|
800
|
-
* More efficient than calling
|
|
1028
|
+
* More efficient than calling indexMemoryAsync() per file when adding many at once.
|
|
801
1029
|
*/
|
|
802
|
-
export function
|
|
1030
|
+
export async function indexMemoriesBatchAsync(
|
|
803
1031
|
memoryDir: string,
|
|
804
1032
|
entries: TemporalIndexEntry[]
|
|
805
|
-
): void {
|
|
1033
|
+
): Promise<void> {
|
|
806
1034
|
if (entries.length === 0) return;
|
|
807
1035
|
try {
|
|
808
|
-
ensureStateDir(memoryDir);
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
updateTagIndex(memoryDir, (index) => {
|
|
821
|
-
for (const entry of entries) {
|
|
822
|
-
removePathFromAllTagEntries(index, entry.path);
|
|
823
|
-
for (const tag of entry.tags) {
|
|
824
|
-
if (tag && typeof tag === "string") {
|
|
825
|
-
addTagGraphEntry(index, tag, entry.path);
|
|
826
|
-
}
|
|
1036
|
+
await ensureStateDir(memoryDir);
|
|
1037
|
+
|
|
1038
|
+
await withMemoryDirMutex(memoryDir, async () => {
|
|
1039
|
+
const temporalOk = await updateTemporalIndex(memoryDir, (index) => {
|
|
1040
|
+
index.version = INDEX_VERSION;
|
|
1041
|
+
for (const entry of entries) {
|
|
1042
|
+
const dateKey = isoDateFromTimestamp(entry.createdAt);
|
|
1043
|
+
removePathFromAllSets(index.dates, entry.path);
|
|
1044
|
+
addPathToSet(index.dates, dateKey, entry.path);
|
|
1045
|
+
index.events[entry.path] = temporalEventFromEntry(entry);
|
|
827
1046
|
}
|
|
1047
|
+
});
|
|
1048
|
+
if (temporalOk) {
|
|
1049
|
+
await updateTagIndex(memoryDir, (index) => {
|
|
1050
|
+
for (const entry of entries) {
|
|
1051
|
+
removePathFromAllTagEntries(index, entry.path);
|
|
1052
|
+
for (const tag of entry.tags) {
|
|
1053
|
+
if (tag && typeof tag === "string") {
|
|
1054
|
+
addTagGraphEntry(index, tag, entry.path);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
828
1059
|
}
|
|
829
1060
|
});
|
|
830
1061
|
} catch {
|
|
@@ -854,18 +1085,13 @@ export async function queryByDateRangeAsync(
|
|
|
854
1085
|
): Promise<Set<string> | null> {
|
|
855
1086
|
try {
|
|
856
1087
|
const tPath = temporalIndexPath(memoryDir);
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
raw
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
try {
|
|
865
|
-
tIndex = normalizeTemporalIndex(JSON.parse(raw) as TemporalIndex);
|
|
866
|
-
} catch {
|
|
867
|
-
tIndex = emptyTemporalIndex();
|
|
868
|
-
}
|
|
1088
|
+
const loaded = await loadIndexCached(
|
|
1089
|
+
tPath,
|
|
1090
|
+
(raw) => normalizeTemporalIndex(raw as TemporalIndex),
|
|
1091
|
+
hasCurrentTemporalIndexSchema,
|
|
1092
|
+
);
|
|
1093
|
+
if (loaded.kind === "missing") return null;
|
|
1094
|
+
const tIndex = loaded.kind === "ok" ? loaded.index : emptyTemporalIndex();
|
|
869
1095
|
// toDate is exclusive (first day NOT included). Default: tomorrow so "all of today" is included.
|
|
870
1096
|
const end = toDate ?? new Date(Date.now() + 86_400_000).toISOString().slice(0, 10);
|
|
871
1097
|
|
|
@@ -894,15 +1120,13 @@ export async function queryTemporalTimelineAsync(
|
|
|
894
1120
|
options: { query?: string; limit?: number } = {},
|
|
895
1121
|
): Promise<TemporalIndexEvent[] | null> {
|
|
896
1122
|
try {
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
) return null;
|
|
905
|
-
const index = normalizeTemporalIndex(parsed as unknown as TemporalIndex);
|
|
1123
|
+
const loaded = await loadIndexCached(
|
|
1124
|
+
temporalIndexPath(memoryDir),
|
|
1125
|
+
(raw) => normalizeTemporalIndex(raw as TemporalIndex),
|
|
1126
|
+
hasCurrentTemporalIndexSchema,
|
|
1127
|
+
);
|
|
1128
|
+
if (loaded.kind !== "ok" || !loaded.currentSchema) return null;
|
|
1129
|
+
const index = loaded.index;
|
|
906
1130
|
const chronology = Object.values(index.events)
|
|
907
1131
|
.filter((event) =>
|
|
908
1132
|
typeof event?.path === "string" &&
|
|
@@ -971,20 +1195,13 @@ export async function queryByTagsAsync(memoryDir: string, tags: string[]): Promi
|
|
|
971
1195
|
if (tags.length === 0) return null;
|
|
972
1196
|
try {
|
|
973
1197
|
const gPath = tagIndexPath(memoryDir);
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
raw
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
try {
|
|
982
|
-
gIndex = normalizeTagIndex(JSON.parse(raw) as TagIndex);
|
|
983
|
-
} catch {
|
|
984
|
-
return null;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
return queryByTagsFromIndex(gIndex, tags);
|
|
1198
|
+
const loaded = await loadIndexCached(
|
|
1199
|
+
gPath,
|
|
1200
|
+
(raw) => normalizeTagIndex(raw as TagIndex),
|
|
1201
|
+
() => true,
|
|
1202
|
+
);
|
|
1203
|
+
if (loaded.kind !== "ok") return null;
|
|
1204
|
+
return queryByTagsFromIndex(loaded.index, tags);
|
|
988
1205
|
} catch {
|
|
989
1206
|
return null;
|
|
990
1207
|
}
|
|
@@ -1031,17 +1248,29 @@ export async function resolvePromptTagPrefilterAsync(
|
|
|
1031
1248
|
}> {
|
|
1032
1249
|
const explicitTags = extractTagsFromPrompt(prompt);
|
|
1033
1250
|
try {
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1251
|
+
const loaded = await loadIndexCached(
|
|
1252
|
+
tagIndexPath(memoryDir),
|
|
1253
|
+
(raw) => normalizeTagIndex(raw as TagIndex),
|
|
1254
|
+
() => true,
|
|
1255
|
+
);
|
|
1256
|
+
if (loaded.kind !== "ok") {
|
|
1257
|
+
return { matchedTags: explicitTags, expandedTags: explicitTags, paths: null };
|
|
1258
|
+
}
|
|
1259
|
+
const tagIndex = loaded.index;
|
|
1036
1260
|
const matched = new Set<string>(explicitTags);
|
|
1261
|
+
const normalizedPrompt = ` ${normalizeAliasKey(prompt)} `;
|
|
1262
|
+
const containsAlias = (alias: string): boolean => {
|
|
1263
|
+
const phrase = aliasPhrase(alias);
|
|
1264
|
+
return phrase.length > 0 && normalizedPrompt.includes(` ${phrase} `);
|
|
1265
|
+
};
|
|
1037
1266
|
|
|
1038
1267
|
for (const canonical of Object.keys(tagIndex.tags)) {
|
|
1039
|
-
if (
|
|
1268
|
+
if (containsAlias(canonical)) {
|
|
1040
1269
|
matched.add(canonical);
|
|
1041
1270
|
}
|
|
1042
1271
|
}
|
|
1043
1272
|
for (const [alias, canonicals] of Object.entries(tagIndex.aliases ?? {})) {
|
|
1044
|
-
if (!
|
|
1273
|
+
if (!containsAlias(alias)) continue;
|
|
1045
1274
|
for (const canonical of canonicals) {
|
|
1046
1275
|
matched.add(canonical);
|
|
1047
1276
|
}
|
|
@@ -1066,6 +1295,26 @@ export async function resolvePromptTagPrefilterAsync(
|
|
|
1066
1295
|
}
|
|
1067
1296
|
}
|
|
1068
1297
|
|
|
1298
|
+
/**
|
|
1299
|
+
* Run a group of index reads as ONE consistent snapshot (issue #1911, Codex
|
|
1300
|
+
* Medium). The `read` callback executes inside a single withMemoryDirMutex
|
|
1301
|
+
* acquisition — the same in-memory op-lock every async mutator holds across its
|
|
1302
|
+
* paired temporal+tag writes — so no mutation can land between the reads inside
|
|
1303
|
+
* it. A recall that reads the temporal and tag indexes together therefore never
|
|
1304
|
+
* observes a half-updated state (e.g. a new temporal row with a not-yet-written
|
|
1305
|
+
* tag membership) that would let prefiltering drop the current memory. The
|
|
1306
|
+
* genuine-tag-miss contract is preserved: a truly absent tag still yields an
|
|
1307
|
+
* empty set, only now never as a transient artifact of an in-flight write.
|
|
1308
|
+
* Index reads are cache-backed (microseconds), so ordering them on the op-chain
|
|
1309
|
+
* is negligible beside the multi-second QMD phase.
|
|
1310
|
+
*/
|
|
1311
|
+
export function readIndexSnapshotAsync<T>(
|
|
1312
|
+
memoryDir: string,
|
|
1313
|
+
read: () => Promise<T>,
|
|
1314
|
+
): Promise<T> {
|
|
1315
|
+
return withMemoryDirMutex(memoryDir, read);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1069
1318
|
/**
|
|
1070
1319
|
* Detect if a prompt is time-sensitive (mentions specific time references).
|
|
1071
1320
|
* Used to decide whether to activate the temporal prefilter.
|