@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.
- 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/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/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/entity-retrieval.ts
CHANGED
|
@@ -12,6 +12,8 @@ const RECENT_TRANSCRIPT_LOOKBACK_HOURS = 24;
|
|
|
12
12
|
const INSTRUCTION_LIKE_RE = /\b(always|never|must|should|remember to|do not|don't|process|workflow|template|checklist|instruction)\b/i;
|
|
13
13
|
const METADATA_WRAPPER_RE = /^(source|context|metadata|notes?):/i;
|
|
14
14
|
const ENTITY_PRONOUN_RE = /\b(he|him|his|she|her|they|them|their|it|its)\b/i;
|
|
15
|
+
const BELIEF_LEDGER_SECTION_KEY = "belief_ledger";
|
|
16
|
+
const BELIEF_LEDGER_FACT_RE = /^claim=([^;]+);\s*status=([^;]+);\s*updatedAt=([^;]+);\s*(.+)$/;
|
|
15
17
|
|
|
16
18
|
type EntityQueryMode = "direct" | "timeline" | "follow_up";
|
|
17
19
|
|
|
@@ -110,6 +112,59 @@ function dedupeHintSnippetsByText(snippets: EntityHintSnippet[]): EntityHintSnip
|
|
|
110
112
|
return result;
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
function isBeliefLedgerSection(section: Pick<EntityStructuredSection, "key">): boolean {
|
|
116
|
+
return normalizeEntityText(section.key).replace(/\s+/g, "_") === BELIEF_LEDGER_SECTION_KEY;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function beliefLedgerFactKeys(sections: EntityStructuredSection[]): Set<string> {
|
|
120
|
+
const keys = new Set<string>();
|
|
121
|
+
for (const section of sections) {
|
|
122
|
+
if (!isBeliefLedgerSection(section)) continue;
|
|
123
|
+
for (const fact of section.facts) {
|
|
124
|
+
keys.add(normalizeEntityText(fact));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return keys;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function recallFactsForStructuredSection(section: EntityStructuredSection): string[] {
|
|
131
|
+
if (!isBeliefLedgerSection(section)) return section.facts;
|
|
132
|
+
return currentActiveBeliefLedgerFactTexts(section.facts);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function currentActiveBeliefLedgerFactTexts(facts: string[]): string[] {
|
|
136
|
+
const byClaim = new Map<string, { status: string; updatedAtMs: number; texts: string[] }>();
|
|
137
|
+
for (const fact of facts) {
|
|
138
|
+
const match = BELIEF_LEDGER_FACT_RE.exec(fact.trim());
|
|
139
|
+
if (!match) continue;
|
|
140
|
+
const [, claimId, status, updatedAt, text] = match;
|
|
141
|
+
const updatedAtMs = Date.parse(updatedAt);
|
|
142
|
+
if (!claimId?.trim() || !status?.trim() || !Number.isFinite(updatedAtMs) || !text?.trim()) continue;
|
|
143
|
+
const normalizedStatus = status.trim().toLowerCase();
|
|
144
|
+
const normalizedClaimId = claimId.trim();
|
|
145
|
+
const current = byClaim.get(normalizedClaimId);
|
|
146
|
+
const inactiveTieWins =
|
|
147
|
+
current &&
|
|
148
|
+
updatedAtMs === current.updatedAtMs &&
|
|
149
|
+
current.status === "active" &&
|
|
150
|
+
normalizedStatus !== "active";
|
|
151
|
+
if (!current || updatedAtMs > current.updatedAtMs || inactiveTieWins) {
|
|
152
|
+
byClaim.set(normalizedClaimId, { status: normalizedStatus, updatedAtMs, texts: [text.trim()] });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (updatedAtMs === current.updatedAtMs && current.status === normalizedStatus) {
|
|
156
|
+
current.texts.push(text.trim());
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const result: string[] = [];
|
|
160
|
+
for (const current of byClaim.values()) {
|
|
161
|
+
if (current.status === "active") {
|
|
162
|
+
result.push(...current.texts);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
113
168
|
function relationLine(entry: EntityMentionIndexEntry, relationship: { target: string; label: string }): string {
|
|
114
169
|
const normalizedLabel = relationship.label.replace(/\s+/g, " ").trim();
|
|
115
170
|
if (normalizedLabel.length === 0) return `${entry.name} is connected to ${relationship.target}`;
|
|
@@ -408,7 +463,13 @@ async function buildEntityMentionIndex(
|
|
|
408
463
|
const entities = new Map<string, EntityMentionIndexEntry>();
|
|
409
464
|
for (const entity of entityFiles) {
|
|
410
465
|
const canonicalId = normalizeEntityName(entity.name, entity.type);
|
|
411
|
-
const
|
|
466
|
+
const rawStructuredSections = entity.structuredSections ?? [];
|
|
467
|
+
const rawBeliefLedgerFactKeys = beliefLedgerFactKeys(rawStructuredSections);
|
|
468
|
+
const sanitizedFacts = entity.facts
|
|
469
|
+
.map((fact) => sanitizeEntityFact(fact))
|
|
470
|
+
.filter(Boolean)
|
|
471
|
+
.filter((fact) => !rawBeliefLedgerFactKeys.has(normalizeEntityText(fact)))
|
|
472
|
+
.map((fact) => compactLine(fact, 180));
|
|
412
473
|
const sanitizedTimelineFacts = entity.timeline
|
|
413
474
|
.map((entry) => sanitizeEntityFact(entry.text))
|
|
414
475
|
.filter(Boolean)
|
|
@@ -421,10 +482,10 @@ async function buildEntityMentionIndex(
|
|
|
421
482
|
summary: entity.synthesis?.trim() || entity.summary?.trim() || undefined,
|
|
422
483
|
facts: sanitizedFacts,
|
|
423
484
|
timelineFacts: uniqueStrings(sanitizedTimelineFacts),
|
|
424
|
-
structuredSections:
|
|
485
|
+
structuredSections: rawStructuredSections.map((section) => ({
|
|
425
486
|
key: section.key,
|
|
426
487
|
title: section.title,
|
|
427
|
-
facts: section
|
|
488
|
+
facts: recallFactsForStructuredSection(section)
|
|
428
489
|
.map((fact) => sanitizeEntityFact(fact))
|
|
429
490
|
.filter(Boolean)
|
|
430
491
|
.map((fact) => compactLine(fact, 180)),
|
package/src/storage.ts
CHANGED
|
@@ -3226,6 +3226,35 @@ export class StorageManager {
|
|
|
3226
3226
|
return ContentHashIndex.computeHash(sanitizeMemoryContent(hashSource).text);
|
|
3227
3227
|
}
|
|
3228
3228
|
|
|
3229
|
+
private async addActiveFactContentHash(memory: MemoryFile): Promise<void> {
|
|
3230
|
+
if (memory.frontmatter.category !== "fact") return;
|
|
3231
|
+
if (inferMemoryStatus(memory.frontmatter, memory.path) !== "active") return;
|
|
3232
|
+
const hash = this.factContentHashForRemoval(memory);
|
|
3233
|
+
if (!hash) return;
|
|
3234
|
+
|
|
3235
|
+
await this.ensureFactHashIndexAuthoritative();
|
|
3236
|
+
const factHashIndex = await this.getFactHashIndex();
|
|
3237
|
+
factHashIndex.addByHash(hash);
|
|
3238
|
+
await factHashIndex.save();
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
private async syncFactHashIndexAfterRewrite(before: MemoryFile, after: MemoryFile): Promise<void> {
|
|
3242
|
+
if (before.frontmatter.category !== "fact" && after.frontmatter.category !== "fact") return;
|
|
3243
|
+
|
|
3244
|
+
const beforeHash = this.factContentHashForRemoval(before);
|
|
3245
|
+
const afterHash = this.factContentHashForRemoval(after);
|
|
3246
|
+
const beforeStatus = inferMemoryStatus(before.frontmatter, before.path);
|
|
3247
|
+
const afterStatus = inferMemoryStatus(after.frontmatter, after.path);
|
|
3248
|
+
if (beforeHash === afterHash && beforeStatus === afterStatus) return;
|
|
3249
|
+
|
|
3250
|
+
if (beforeStatus === "active" && beforeHash && (beforeHash !== afterHash || afterStatus !== "active")) {
|
|
3251
|
+
await this.removeFactContentHashesForMemories([before]);
|
|
3252
|
+
}
|
|
3253
|
+
if (afterStatus === "active" && afterHash && (beforeHash !== afterHash || beforeStatus !== "active")) {
|
|
3254
|
+
await this.addActiveFactContentHash(after);
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3229
3258
|
async removeFactContentHashesForMemories(memories: MemoryFile[]): Promise<void> {
|
|
3230
3259
|
await this.ensureFactHashIndexAuthoritative();
|
|
3231
3260
|
const factHashIndex = await this.getFactHashIndex();
|
|
@@ -4492,6 +4521,17 @@ export class StorageManager {
|
|
|
4492
4521
|
if (memory.path.includes(`${path.sep}cold${path.sep}`)) {
|
|
4493
4522
|
this.invalidateColdMemoriesCache();
|
|
4494
4523
|
}
|
|
4524
|
+
try {
|
|
4525
|
+
await this.syncFactHashIndexAfterRewrite(
|
|
4526
|
+
memory,
|
|
4527
|
+
{
|
|
4528
|
+
...memory,
|
|
4529
|
+
frontmatter: updated,
|
|
4530
|
+
},
|
|
4531
|
+
);
|
|
4532
|
+
} catch (err) {
|
|
4533
|
+
log.warn(`storage.writeMemoryFrontmatter completed but failed to update fact hash index: ${err}`);
|
|
4534
|
+
}
|
|
4495
4535
|
await this.appendGeneratedMemoryLifecycleEventFailOpen(
|
|
4496
4536
|
"storage.writeMemoryFrontmatter",
|
|
4497
4537
|
{
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, symlink, utimes, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
8
|
+
|
|
9
|
+
import { queryByDateRangeAsync, queryByTagsAsync } from "./temporal-index.js";
|
|
10
|
+
|
|
11
|
+
async function runIndexWorker(moduleUrl: string, memoryDir: string, workerId: number, count: number): Promise<void> {
|
|
12
|
+
const workerSource = `
|
|
13
|
+
const { indexMemory } = await import(process.argv[1]);
|
|
14
|
+
const memoryDir = process.argv[2];
|
|
15
|
+
const workerId = Number(process.argv[3]);
|
|
16
|
+
const count = Number(process.argv[4]);
|
|
17
|
+
for (let i = 0; i < count; i += 1) {
|
|
18
|
+
indexMemory(
|
|
19
|
+
memoryDir,
|
|
20
|
+
\`/tmp/remnic-temporal-worker-\${workerId}-memory-\${i}.md\`,
|
|
21
|
+
"2026-03-09T12:00:00.000Z",
|
|
22
|
+
["concurrency/shared", \`concurrency/worker-\${workerId}\`],
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
await new Promise<void>((resolve, reject) => {
|
|
28
|
+
const child = spawn(
|
|
29
|
+
process.execPath,
|
|
30
|
+
["--import", "tsx", "-e", workerSource, moduleUrl, memoryDir, String(workerId), String(count)],
|
|
31
|
+
{
|
|
32
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
let stderr = "";
|
|
36
|
+
child.stderr.setEncoding("utf8");
|
|
37
|
+
child.stderr.on("data", (chunk) => {
|
|
38
|
+
stderr += chunk;
|
|
39
|
+
});
|
|
40
|
+
child.on("error", reject);
|
|
41
|
+
child.on("close", (code) => {
|
|
42
|
+
if (code === 0) {
|
|
43
|
+
resolve();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
reject(new Error(`index worker ${workerId} exited ${code}: ${stderr}`));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test("temporal index concurrent writers retain every date and tag path", async () => {
|
|
52
|
+
const memoryDir = await mkdtemp(join(tmpdir(), "remnic-temporal-index-concurrent-"));
|
|
53
|
+
const moduleUrl = new URL("./temporal-index.ts", import.meta.url).href;
|
|
54
|
+
const workerCount = 4;
|
|
55
|
+
const entriesPerWorker = 12;
|
|
56
|
+
const expectedPaths = new Set<string>();
|
|
57
|
+
|
|
58
|
+
for (let workerId = 0; workerId < workerCount; workerId += 1) {
|
|
59
|
+
for (let i = 0; i < entriesPerWorker; i += 1) {
|
|
60
|
+
expectedPaths.add(`/tmp/remnic-temporal-worker-${workerId}-memory-${i}.md`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await Promise.all(
|
|
65
|
+
Array.from({ length: workerCount }, (_, workerId) =>
|
|
66
|
+
runIndexWorker(moduleUrl, memoryDir, workerId, entriesPerWorker)
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const dateMatches = await queryByDateRangeAsync(memoryDir, "2026-03-09", "2026-03-10");
|
|
71
|
+
const tagMatches = await queryByTagsAsync(memoryDir, ["concurrency/shared"]);
|
|
72
|
+
|
|
73
|
+
assert.deepEqual(dateMatches, expectedPaths);
|
|
74
|
+
assert.deepEqual(tagMatches, expectedPaths);
|
|
75
|
+
|
|
76
|
+
const timeIndex = JSON.parse(await readFile(join(memoryDir, "state", "index_time.json"), "utf8"));
|
|
77
|
+
const tagIndex = JSON.parse(await readFile(join(memoryDir, "state", "index_tags.json"), "utf8"));
|
|
78
|
+
assert.equal(timeIndex.dates["2026-03-09"].length, expectedPaths.size);
|
|
79
|
+
assert.equal(tagIndex.tags["concurrency/shared"].paths.length, expectedPaths.size);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("temporal index writers wait for old locks owned by live processes", async () => {
|
|
83
|
+
const memoryDir = await mkdtemp(join(tmpdir(), "remnic-temporal-index-live-lock-"));
|
|
84
|
+
const stateDir = join(memoryDir, "state");
|
|
85
|
+
const lockDir = join(stateDir, "index_time.json.lock.d");
|
|
86
|
+
const moduleUrl = new URL("./temporal-index.ts", import.meta.url).href;
|
|
87
|
+
await mkdir(lockDir, { recursive: true });
|
|
88
|
+
await writeFile(join(lockDir, "owner.json"), JSON.stringify({ pid: process.pid }), "utf8");
|
|
89
|
+
const oldLockTime = new Date(Date.now() - 120_000);
|
|
90
|
+
await utimes(lockDir, oldLockTime, oldLockTime);
|
|
91
|
+
|
|
92
|
+
const workerSource = `
|
|
93
|
+
const { indexMemory } = await import(process.argv[1]);
|
|
94
|
+
indexMemory(
|
|
95
|
+
process.argv[2],
|
|
96
|
+
"/tmp/remnic-temporal-live-lock-memory.md",
|
|
97
|
+
"2026-03-10T12:00:00.000Z",
|
|
98
|
+
["concurrency/live-lock"],
|
|
99
|
+
);
|
|
100
|
+
`;
|
|
101
|
+
let closed = false;
|
|
102
|
+
const workerDone = new Promise<void>((resolve, reject) => {
|
|
103
|
+
const child = spawn(process.execPath, ["--import", "tsx", "-e", workerSource, moduleUrl, memoryDir], {
|
|
104
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
105
|
+
});
|
|
106
|
+
let stderr = "";
|
|
107
|
+
child.stderr.setEncoding("utf8");
|
|
108
|
+
child.stderr.on("data", (chunk) => {
|
|
109
|
+
stderr += chunk;
|
|
110
|
+
});
|
|
111
|
+
child.on("error", reject);
|
|
112
|
+
child.on("close", (code) => {
|
|
113
|
+
closed = true;
|
|
114
|
+
if (code === 0) {
|
|
115
|
+
resolve();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
reject(new Error(`live-lock worker exited ${code}: ${stderr}`));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await delay(150);
|
|
123
|
+
assert.equal(closed, false);
|
|
124
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
125
|
+
await workerDone;
|
|
126
|
+
|
|
127
|
+
const dateMatches = await queryByDateRangeAsync(memoryDir, "2026-03-10", "2026-03-11");
|
|
128
|
+
const tagMatches = await queryByTagsAsync(memoryDir, ["concurrency/live-lock"]);
|
|
129
|
+
assert.deepEqual(dateMatches, new Set(["/tmp/remnic-temporal-live-lock-memory.md"]));
|
|
130
|
+
assert.deepEqual(tagMatches, new Set(["/tmp/remnic-temporal-live-lock-memory.md"]));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("temporal index writers clear stale locks whose owner pid was recycled", async () => {
|
|
134
|
+
const memoryDir = await mkdtemp(join(tmpdir(), "remnic-temporal-index-recycled-pid-"));
|
|
135
|
+
const stateDir = join(memoryDir, "state");
|
|
136
|
+
const lockDir = join(stateDir, "index_time.json.lock.d");
|
|
137
|
+
const moduleUrl = new URL("./temporal-index.ts", import.meta.url).href;
|
|
138
|
+
await mkdir(lockDir, { recursive: true });
|
|
139
|
+
await writeFile(
|
|
140
|
+
join(lockDir, "owner.json"),
|
|
141
|
+
JSON.stringify({
|
|
142
|
+
pid: process.pid,
|
|
143
|
+
processStartedAtMs: Date.now() - 7 * 86_400_000,
|
|
144
|
+
createdAt: new Date(Date.now() - 120_000).toISOString(),
|
|
145
|
+
}),
|
|
146
|
+
"utf8"
|
|
147
|
+
);
|
|
148
|
+
const oldLockTime = new Date(Date.now() - 120_000);
|
|
149
|
+
await utimes(lockDir, oldLockTime, oldLockTime);
|
|
150
|
+
|
|
151
|
+
await runIndexWorker(moduleUrl, memoryDir, 99, 1);
|
|
152
|
+
|
|
153
|
+
const dateMatches = await queryByDateRangeAsync(memoryDir, "2026-03-09", "2026-03-10");
|
|
154
|
+
const tagMatches = await queryByTagsAsync(memoryDir, ["concurrency/shared"]);
|
|
155
|
+
assert.deepEqual(dateMatches, new Set(["/tmp/remnic-temporal-worker-99-memory-0.md"]));
|
|
156
|
+
assert.deepEqual(tagMatches, new Set(["/tmp/remnic-temporal-worker-99-memory-0.md"]));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("temporal index writers remove regular file lock blockers", async () => {
|
|
160
|
+
const memoryDir = await mkdtemp(join(tmpdir(), "remnic-temporal-index-file-lock-"));
|
|
161
|
+
const stateDir = join(memoryDir, "state");
|
|
162
|
+
const lockPath = join(stateDir, "index_time.json.lock.d");
|
|
163
|
+
const moduleUrl = new URL("./temporal-index.ts", import.meta.url).href;
|
|
164
|
+
await mkdir(stateDir, { recursive: true });
|
|
165
|
+
await writeFile(lockPath, "not a lock directory", "utf8");
|
|
166
|
+
|
|
167
|
+
await runIndexWorker(moduleUrl, memoryDir, 100, 1);
|
|
168
|
+
|
|
169
|
+
const dateMatches = await queryByDateRangeAsync(memoryDir, "2026-03-09", "2026-03-10");
|
|
170
|
+
const tagMatches = await queryByTagsAsync(memoryDir, ["concurrency/shared"]);
|
|
171
|
+
assert.deepEqual(dateMatches, new Set(["/tmp/remnic-temporal-worker-100-memory-0.md"]));
|
|
172
|
+
assert.deepEqual(tagMatches, new Set(["/tmp/remnic-temporal-worker-100-memory-0.md"]));
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("temporal index writers fail open on symlink lock blockers", async () => {
|
|
176
|
+
const memoryDir = await mkdtemp(join(tmpdir(), "remnic-temporal-index-symlink-lock-"));
|
|
177
|
+
const stateDir = join(memoryDir, "state");
|
|
178
|
+
const lockPath = join(stateDir, "index_time.json.lock.d");
|
|
179
|
+
const symlinkTarget = join(memoryDir, "outside-lock-target");
|
|
180
|
+
const moduleUrl = new URL("./temporal-index.ts", import.meta.url).href;
|
|
181
|
+
await mkdir(stateDir, { recursive: true });
|
|
182
|
+
await writeFile(symlinkTarget, "do not follow", "utf8");
|
|
183
|
+
await symlink(symlinkTarget, lockPath);
|
|
184
|
+
|
|
185
|
+
await runIndexWorker(moduleUrl, memoryDir, 101, 1);
|
|
186
|
+
|
|
187
|
+
const dateMatches = await queryByDateRangeAsync(memoryDir, "2026-03-09", "2026-03-10");
|
|
188
|
+
const tagMatches = await queryByTagsAsync(memoryDir, ["concurrency/shared"]);
|
|
189
|
+
assert.equal(dateMatches, null);
|
|
190
|
+
assert.deepEqual(tagMatches, new Set(["/tmp/remnic-temporal-worker-101-memory-0.md"]));
|
|
191
|
+
});
|