@remnic/core 9.3.594 → 9.3.596

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/access-cli.js +14 -14
  2. package/dist/access-http.js +6 -6
  3. package/dist/access-mcp.js +5 -5
  4. package/dist/access-schema.d.ts +7 -7
  5. package/dist/access-service.js +4 -4
  6. package/dist/briefing.js +2 -2
  7. package/dist/causal-consolidation.js +3 -3
  8. package/dist/{chunk-ARY5OOLG.js → chunk-557IAFPD.js} +2 -2
  9. package/dist/{chunk-VFB2G5YL.js → chunk-5BUGGPBR.js} +4 -4
  10. package/dist/{chunk-USYGGIJZ.js → chunk-D2MMMTDV.js} +2 -2
  11. package/dist/{chunk-XM7BYXT7.js → chunk-D65TSG24.js} +2 -2
  12. package/dist/{chunk-FHBEL473.js → chunk-DOX2CG6Y.js} +54 -5
  13. package/dist/chunk-DOX2CG6Y.js.map +1 -0
  14. package/dist/{chunk-DARLGSFX.js → chunk-ELKI4BB6.js} +4 -4
  15. package/dist/{chunk-QRWZOCJN.js → chunk-F4LM4ULA.js} +12 -12
  16. package/dist/{chunk-7WU3FML2.js → chunk-IEFHBIU2.js} +11 -11
  17. package/dist/{chunk-7WU3FML2.js.map → chunk-IEFHBIU2.js.map} +1 -1
  18. package/dist/{chunk-KDUFBSBF.js → chunk-IK34DVAC.js} +2 -2
  19. package/dist/{chunk-OPYFD6PD.js → chunk-IK7DCC5H.js} +2 -2
  20. package/dist/{chunk-574MU2Y3.js → chunk-JTDRJQ3K.js} +2 -2
  21. package/dist/{chunk-LAL7WBLY.js → chunk-LYPDMKUT.js} +3 -3
  22. package/dist/{chunk-GBXGCFRH.js → chunk-MA5MWGKP.js} +2 -2
  23. package/dist/{chunk-HQO5EBUC.js → chunk-MLT75J5S.js} +3 -3
  24. package/dist/{chunk-7X7TBJRX.js → chunk-NOMEVTUD.js} +2 -2
  25. package/dist/{chunk-SUTSSOYU.js → chunk-OD5LFAPZ.js} +2 -2
  26. package/dist/{chunk-XT7XVA53.js → chunk-OI27U2HT.js} +2 -2
  27. package/dist/{chunk-MQEIWDYW.js → chunk-QDDHYAKV.js} +2 -2
  28. package/dist/{chunk-ZY6UPHNY.js → chunk-TYICDVQW.js} +3 -3
  29. package/dist/{chunk-XRWTAEZM.js → chunk-W5O2FQTZ.js} +2 -2
  30. package/dist/{chunk-V3RXWQIE.js → chunk-WXACKLKP.js} +209 -59
  31. package/dist/chunk-WXACKLKP.js.map +1 -0
  32. package/dist/{chunk-IRFF6LSF.js → chunk-YFS5OEKO.js} +36 -1
  33. package/dist/chunk-YFS5OEKO.js.map +1 -0
  34. package/dist/cli.js +15 -15
  35. package/dist/compounding/engine.js +2 -2
  36. package/dist/connectors/codex-materialize-runner.js +2 -2
  37. package/dist/connectors/index.js +2 -2
  38. package/dist/entity-retrieval.js +2 -2
  39. package/dist/index.js +22 -22
  40. package/dist/maintenance/memory-governance.js +2 -2
  41. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +2 -2
  42. package/dist/maintenance/rebuild-memory-projection.js +3 -3
  43. package/dist/namespaces/migrate.js +3 -3
  44. package/dist/namespaces/storage.js +2 -2
  45. package/dist/operator-toolkit.js +5 -5
  46. package/dist/orchestrator.js +11 -11
  47. package/dist/retrieval-agents.js +2 -2
  48. package/dist/semantic-consolidation.js +3 -3
  49. package/dist/semantic-rule-promotion.js +2 -2
  50. package/dist/semantic-rule-verifier.js +2 -2
  51. package/dist/storage.d.ts +2 -0
  52. package/dist/storage.js +1 -1
  53. package/dist/temporal-index.js +1 -1
  54. package/dist/verified-recall.js +2 -2
  55. package/package.json +1 -1
  56. package/src/dedup/semantic.test.ts +33 -0
  57. package/src/dedup/semantic.ts +7 -1
  58. package/src/entity-retrieval.ts +64 -3
  59. package/src/storage.ts +40 -0
  60. package/src/temporal-index.test.ts +191 -0
  61. package/src/temporal-index.ts +291 -100
  62. package/dist/chunk-FHBEL473.js.map +0 -1
  63. package/dist/chunk-IRFF6LSF.js.map +0 -1
  64. package/dist/chunk-V3RXWQIE.js.map +0 -1
  65. /package/dist/{chunk-ARY5OOLG.js.map → chunk-557IAFPD.js.map} +0 -0
  66. /package/dist/{chunk-VFB2G5YL.js.map → chunk-5BUGGPBR.js.map} +0 -0
  67. /package/dist/{chunk-USYGGIJZ.js.map → chunk-D2MMMTDV.js.map} +0 -0
  68. /package/dist/{chunk-XM7BYXT7.js.map → chunk-D65TSG24.js.map} +0 -0
  69. /package/dist/{chunk-DARLGSFX.js.map → chunk-ELKI4BB6.js.map} +0 -0
  70. /package/dist/{chunk-QRWZOCJN.js.map → chunk-F4LM4ULA.js.map} +0 -0
  71. /package/dist/{chunk-KDUFBSBF.js.map → chunk-IK34DVAC.js.map} +0 -0
  72. /package/dist/{chunk-OPYFD6PD.js.map → chunk-IK7DCC5H.js.map} +0 -0
  73. /package/dist/{chunk-574MU2Y3.js.map → chunk-JTDRJQ3K.js.map} +0 -0
  74. /package/dist/{chunk-LAL7WBLY.js.map → chunk-LYPDMKUT.js.map} +0 -0
  75. /package/dist/{chunk-GBXGCFRH.js.map → chunk-MA5MWGKP.js.map} +0 -0
  76. /package/dist/{chunk-HQO5EBUC.js.map → chunk-MLT75J5S.js.map} +0 -0
  77. /package/dist/{chunk-7X7TBJRX.js.map → chunk-NOMEVTUD.js.map} +0 -0
  78. /package/dist/{chunk-SUTSSOYU.js.map → chunk-OD5LFAPZ.js.map} +0 -0
  79. /package/dist/{chunk-XT7XVA53.js.map → chunk-OI27U2HT.js.map} +0 -0
  80. /package/dist/{chunk-MQEIWDYW.js.map → chunk-QDDHYAKV.js.map} +0 -0
  81. /package/dist/{chunk-ZY6UPHNY.js.map → chunk-TYICDVQW.js.map} +0 -0
  82. /package/dist/{chunk-XRWTAEZM.js.map → chunk-W5O2FQTZ.js.map} +0 -0
@@ -138,7 +138,13 @@ export async function decideSemanticDedup(
138
138
  // Defensive: callers ought to return sorted, but don't trust it.
139
139
  let top: SemanticDedupHit | undefined;
140
140
  for (const hit of hits) {
141
- if (!hit || typeof hit.score !== "number" || !Number.isFinite(hit.score)) {
141
+ if (
142
+ !hit ||
143
+ typeof hit.id !== "string" ||
144
+ hit.id.trim().length === 0 ||
145
+ typeof hit.score !== "number" ||
146
+ !Number.isFinite(hit.score)
147
+ ) {
142
148
  continue;
143
149
  }
144
150
  if (!top || hit.score > top.score) {
@@ -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 sanitizedFacts = entity.facts.map((fact) => sanitizeEntityFact(fact)).filter(Boolean).map((fact) => compactLine(fact, 180));
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: (entity.structuredSections ?? []).map((section) => ({
485
+ structuredSections: rawStructuredSections.map((section) => ({
425
486
  key: section.key,
426
487
  title: section.title,
427
- facts: section.facts
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
+ });