@remnic/core 9.7.9 → 9.7.10

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 (112) hide show
  1. package/dist/access-admin-ops-surface.js +5 -5
  2. package/dist/access-boundary.js +6 -6
  3. package/dist/access-cli.js +36 -36
  4. package/dist/access-http.js +10 -10
  5. package/dist/access-identity-continuity-surface.js +5 -5
  6. package/dist/access-lcm-surface.js +5 -5
  7. package/dist/access-mcp.js +9 -9
  8. package/dist/access-observe-write-surface.js +5 -5
  9. package/dist/access-operations-batch.js +7 -7
  10. package/dist/access-operations.js +8 -8
  11. package/dist/access-recall-surface.js +5 -5
  12. package/dist/access-service.js +5 -5
  13. package/dist/briefing.js +3 -3
  14. package/dist/causal-consolidation.js +4 -4
  15. package/dist/{chunk-HHAA35A3.js → chunk-2VN4JAX7.js} +548 -161
  16. package/dist/chunk-2VN4JAX7.js.map +1 -0
  17. package/dist/{chunk-MLT5EG6K.js → chunk-3OQ4IZJN.js} +4 -4
  18. package/dist/{chunk-QKVX4Y6D.js → chunk-673NPXE3.js} +2 -2
  19. package/dist/{chunk-KKASNHGF.js → chunk-7CKS4AXY.js} +19 -19
  20. package/dist/{chunk-YGAOLHKN.js → chunk-7EX4YOKH.js} +4 -4
  21. package/dist/{chunk-5CFHYQU3.js → chunk-DLWYAN6A.js} +2 -2
  22. package/dist/{chunk-TY43GBX2.js → chunk-EQF5763Z.js} +2 -2
  23. package/dist/{chunk-VCFADTFS.js → chunk-FNJX2O6G.js} +3 -3
  24. package/dist/{chunk-F3FC7SMO.js → chunk-JZM5QUTE.js} +2 -2
  25. package/dist/{chunk-PV7E2FGY.js → chunk-KXLRMM35.js} +6 -6
  26. package/dist/{chunk-PHXCPVSX.js → chunk-L4H6MQ7I.js} +2 -2
  27. package/dist/{chunk-FKCJH7KP.js → chunk-LQKITFGB.js} +2 -2
  28. package/dist/{chunk-27VEKQQA.js → chunk-MXRMNKSY.js} +3 -3
  29. package/dist/{chunk-DLPXS7BE.js → chunk-N3LWQJ6Y.js} +55 -48
  30. package/dist/chunk-N3LWQJ6Y.js.map +1 -0
  31. package/dist/{chunk-EIKBKQ6L.js → chunk-QVALQE4P.js} +4 -4
  32. package/dist/{chunk-CDGGQGUU.js → chunk-SBFDL2EQ.js} +2 -2
  33. package/dist/{chunk-I2THZUAF.js → chunk-SECMZACR.js} +2 -2
  34. package/dist/{chunk-JU3GHO6J.js → chunk-TGBJ2OMK.js} +2 -2
  35. package/dist/{chunk-DE6ENYLG.js → chunk-TXLGHDHS.js} +2 -2
  36. package/dist/{chunk-KGNFFCZJ.js → chunk-U4KHQTJ6.js} +2 -2
  37. package/dist/{chunk-Y5U35ZVS.js → chunk-UTDA43TB.js} +2 -2
  38. package/dist/{chunk-POUD5BLV.js → chunk-UZRIKUFS.js} +2 -2
  39. package/dist/{chunk-CK6RI52P.js → chunk-VCEH5UYS.js} +2 -2
  40. package/dist/{chunk-SKXBOZDF.js → chunk-VDLMAJXS.js} +3 -3
  41. package/dist/{chunk-UXWGPODH.js → chunk-VRAD2FGO.js} +7 -7
  42. package/dist/{chunk-UP25ZZCN.js → chunk-WUDBMLC6.js} +2 -2
  43. package/dist/{chunk-XG5CGR3K.js → chunk-XF4KOVDK.js} +5 -5
  44. package/dist/{chunk-KPXAJ77O.js → chunk-YLTG6BO4.js} +2 -2
  45. package/dist/cli.js +23 -23
  46. package/dist/compounding/engine.js +3 -3
  47. package/dist/connectors/codex-materialize-runner.js +3 -3
  48. package/dist/connectors/index.js +3 -3
  49. package/dist/entity-retrieval.js +3 -3
  50. package/dist/index.js +43 -43
  51. package/dist/lcm/engine.js +3 -3
  52. package/dist/lcm/index.js +8 -8
  53. package/dist/maintenance/memory-governance.js +3 -3
  54. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  55. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  56. package/dist/namespaces/migrate.js +4 -4
  57. package/dist/namespaces/storage.js +3 -3
  58. package/dist/operator-toolkit.js +7 -7
  59. package/dist/orchestration/maintenance.js +5 -5
  60. package/dist/orchestrator.js +36 -36
  61. package/dist/retrieval-agents.js +2 -2
  62. package/dist/schemas.d.ts +22 -22
  63. package/dist/semantic-consolidation.js +4 -4
  64. package/dist/semantic-rule-promotion.js +3 -3
  65. package/dist/semantic-rule-verifier.js +3 -3
  66. package/dist/storage.js +2 -2
  67. package/dist/temporal-index-compat.d.ts +1 -0
  68. package/dist/temporal-index-compat.js +16 -0
  69. package/dist/temporal-index-compat.js.map +1 -0
  70. package/dist/temporal-index.d.ts +58 -7
  71. package/dist/temporal-index.js +23 -3
  72. package/dist/transfer/types.d.ts +12 -12
  73. package/dist/verified-recall.js +3 -3
  74. package/package.json +2 -2
  75. package/src/orchestration/consolidation-run.ts +5 -5
  76. package/src/orchestration/contradiction-linking-coordinator.ts +2 -2
  77. package/src/orchestration/lifecycle-policy-coordinator.ts +2 -2
  78. package/src/orchestration/persistence-index.ts +6 -6
  79. package/src/orchestration/recall-search-pipeline.ts +36 -21
  80. package/src/orchestration/semantic-consolidation-coordinator.ts +2 -2
  81. package/src/orchestrator.ts +0 -4
  82. package/src/temporal-index-compat.ts +311 -0
  83. package/src/temporal-index.test.ts +273 -22
  84. package/src/temporal-index.ts +427 -178
  85. package/dist/chunk-DLPXS7BE.js.map +0 -1
  86. package/dist/chunk-HHAA35A3.js.map +0 -1
  87. /package/dist/{chunk-MLT5EG6K.js.map → chunk-3OQ4IZJN.js.map} +0 -0
  88. /package/dist/{chunk-QKVX4Y6D.js.map → chunk-673NPXE3.js.map} +0 -0
  89. /package/dist/{chunk-KKASNHGF.js.map → chunk-7CKS4AXY.js.map} +0 -0
  90. /package/dist/{chunk-YGAOLHKN.js.map → chunk-7EX4YOKH.js.map} +0 -0
  91. /package/dist/{chunk-5CFHYQU3.js.map → chunk-DLWYAN6A.js.map} +0 -0
  92. /package/dist/{chunk-TY43GBX2.js.map → chunk-EQF5763Z.js.map} +0 -0
  93. /package/dist/{chunk-VCFADTFS.js.map → chunk-FNJX2O6G.js.map} +0 -0
  94. /package/dist/{chunk-F3FC7SMO.js.map → chunk-JZM5QUTE.js.map} +0 -0
  95. /package/dist/{chunk-PV7E2FGY.js.map → chunk-KXLRMM35.js.map} +0 -0
  96. /package/dist/{chunk-PHXCPVSX.js.map → chunk-L4H6MQ7I.js.map} +0 -0
  97. /package/dist/{chunk-FKCJH7KP.js.map → chunk-LQKITFGB.js.map} +0 -0
  98. /package/dist/{chunk-27VEKQQA.js.map → chunk-MXRMNKSY.js.map} +0 -0
  99. /package/dist/{chunk-EIKBKQ6L.js.map → chunk-QVALQE4P.js.map} +0 -0
  100. /package/dist/{chunk-CDGGQGUU.js.map → chunk-SBFDL2EQ.js.map} +0 -0
  101. /package/dist/{chunk-I2THZUAF.js.map → chunk-SECMZACR.js.map} +0 -0
  102. /package/dist/{chunk-JU3GHO6J.js.map → chunk-TGBJ2OMK.js.map} +0 -0
  103. /package/dist/{chunk-DE6ENYLG.js.map → chunk-TXLGHDHS.js.map} +0 -0
  104. /package/dist/{chunk-KGNFFCZJ.js.map → chunk-U4KHQTJ6.js.map} +0 -0
  105. /package/dist/{chunk-Y5U35ZVS.js.map → chunk-UTDA43TB.js.map} +0 -0
  106. /package/dist/{chunk-POUD5BLV.js.map → chunk-UZRIKUFS.js.map} +0 -0
  107. /package/dist/{chunk-CK6RI52P.js.map → chunk-VCEH5UYS.js.map} +0 -0
  108. /package/dist/{chunk-SKXBOZDF.js.map → chunk-VDLMAJXS.js.map} +0 -0
  109. /package/dist/{chunk-UXWGPODH.js.map → chunk-VRAD2FGO.js.map} +0 -0
  110. /package/dist/{chunk-UP25ZZCN.js.map → chunk-WUDBMLC6.js.map} +0 -0
  111. /package/dist/{chunk-XG5CGR3K.js.map → chunk-XF4KOVDK.js.map} +0 -0
  112. /package/dist/{chunk-KPXAJ77O.js.map → chunk-YLTG6BO4.js.map} +0 -0
@@ -30,7 +30,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
30
30
  import { log } from "../logger.js";
31
31
  import { applyCommitmentLedgerLifecycle } from "../commitment-ledger.js";
32
32
  import { recordDreamsPhaseRun } from "../maintenance/dreams-ledger.js";
33
- import { clearIndexes, deindexMemory, indexMemoriesBatch, indexesExist } from "../temporal-index.js";
33
+ import { deindexMemoryAsync } from "../temporal-index.js";
34
34
  import { isActiveMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
35
35
  import {
36
36
  resolveConsolidationCapabilities,
@@ -165,7 +165,7 @@ export class ConsolidationRunCoordinator {
165
165
  memoryItemMutated = true;
166
166
  await this.deps.embeddingFallback.removeFromIndex(item.existingId);
167
167
  if (toInvalidate?.path && toInvalidate.frontmatter?.created) {
168
- deindexMemory(
168
+ await deindexMemoryAsync(
169
169
  config.memoryDir,
170
170
  toInvalidate.path,
171
171
  toInvalidate.frontmatter.created,
@@ -216,7 +216,7 @@ export class ConsolidationRunCoordinator {
216
216
  toMergeInvalidate?.path &&
217
217
  toMergeInvalidate.frontmatter?.created
218
218
  ) {
219
- deindexMemory(
219
+ await deindexMemoryAsync(
220
220
  config.memoryDir,
221
221
  toMergeInvalidate.path,
222
222
  toMergeInvalidate.frontmatter.created,
@@ -293,7 +293,7 @@ export class ConsolidationRunCoordinator {
293
293
  log.info(`cleaned ${deletedCommitments.length} expired commitments`);
294
294
  if (resolveIndexingCapabilities(config).queryAwareIndexing) {
295
295
  for (const m of deletedCommitments) {
296
- deindexMemory(
296
+ await deindexMemoryAsync(
297
297
  config.memoryDir,
298
298
  m.path,
299
299
  m.frontmatter.created,
@@ -336,7 +336,7 @@ export class ConsolidationRunCoordinator {
336
336
  log.info(`cleaned ${deletedTTL.length} TTL-expired memories`);
337
337
  if (resolveIndexingCapabilities(config).queryAwareIndexing) {
338
338
  for (const m of deletedTTL) {
339
- deindexMemory(
339
+ await deindexMemoryAsync(
340
340
  config.memoryDir,
341
341
  m.path,
342
342
  m.frontmatter.created,
@@ -27,7 +27,7 @@ import type { StorageManager } from "../index.js";
27
27
  import type { ExtractionEngine } from "../extraction.js";
28
28
  import { log } from "../logger.js";
29
29
  import { resolveIndexingCapabilities } from "../capabilities.js";
30
- import { deindexMemory } from "../temporal-index.js";
30
+ import { deindexMemoryAsync } from "../temporal-index.js";
31
31
 
32
32
  /** Result type of {@link ContradictionLinkingCoordinator.checkForContradiction}. */
33
33
  export interface ContradictionResult {
@@ -253,7 +253,7 @@ export class ContradictionLinkingCoordinator {
253
253
  resolveIndexingCapabilities(config).queryAwareIndexing &&
254
254
  contradiction.supersededPath
255
255
  ) {
256
- deindexMemory(
256
+ await deindexMemoryAsync(
257
257
  config.memoryDir,
258
258
  contradiction.supersededPath,
259
259
  contradiction.supersededCreated,
@@ -34,7 +34,7 @@ import {
34
34
  resolveCreationMemoryCapabilities,
35
35
  } from "../capabilities.js";
36
36
  import { isActiveMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
37
- import { deindexMemory } from "../temporal-index.js";
37
+ import { deindexMemoryAsync } from "../temporal-index.js";
38
38
  import { extractTopics } from "../topics.js";
39
39
  import { log } from "../logger.js";
40
40
  import path from "node:path";
@@ -345,7 +345,7 @@ export class LifecyclePolicyCoordinator {
345
345
  memory.path &&
346
346
  memory.frontmatter?.created
347
347
  ) {
348
- deindexMemory(
348
+ await deindexMemoryAsync(
349
349
  this.config.memoryDir,
350
350
  memory.path,
351
351
  memory.frontmatter.created,
@@ -23,7 +23,7 @@ import { ContentHashIndex, StorageManager } from "../index.js";
23
23
  import { log } from "../logger.js";
24
24
  import { isActiveMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
25
25
  import { stripCitationForTemplate } from "../source-attribution.js";
26
- import { clearIndexes, indexMemoriesBatch, indexesExist } from "../temporal-index.js";
26
+ import { clearIndexesAsync, indexMemoriesBatchAsync, indexesExistAsync } from "../temporal-index.js";
27
27
  import { normalizeSupersessionKey } from "../temporal-supersession.js";
28
28
  import type { MemoryFile, MemoryFrontmatter, PluginConfig } from "../types.js";
29
29
  import {
@@ -392,7 +392,7 @@ export class PersistenceIndexCoordinator {
392
392
  return;
393
393
  // Check for missing indexes BEFORE the early-return so first-time enablement
394
394
  // can bootstrap the full corpus even when this extraction turn persisted nothing.
395
- const needsFullRebuild = !indexesExist(this.deps.config.memoryDir);
395
+ const needsFullRebuild = !(await indexesExistAsync(this.deps.config.memoryDir));
396
396
  if (!needsFullRebuild && persistedIds.length === 0) return;
397
397
  try {
398
398
  // Read the corpus once to avoid N separate full-corpus scans.
@@ -449,17 +449,17 @@ export class PersistenceIndexCoordinator {
449
449
  if (needsFullRebuild) {
450
450
  // Always write empty indexes on full rebuild — even when the active pool
451
451
  // is empty (e.g. store contains only archived/superseded entries).
452
- // This marks bootstrap completion so indexesExist() returns true and
452
+ // This marks bootstrap completion so indexesExistAsync() returns true and
453
453
  // subsequent extractions skip the full-corpus scan.
454
- clearIndexes(this.deps.config.memoryDir);
454
+ await clearIndexesAsync(this.deps.config.memoryDir);
455
455
  if (entries.length > 0) {
456
- indexMemoriesBatch(this.deps.config.memoryDir, entries);
456
+ await indexMemoriesBatchAsync(this.deps.config.memoryDir, entries);
457
457
  }
458
458
  log.info(
459
459
  `temporal-index: bootstrapped from ${entries.length} active memories`,
460
460
  );
461
461
  } else if (entries.length > 0) {
462
- indexMemoriesBatch(this.deps.config.memoryDir, entries);
462
+ await indexMemoriesBatchAsync(this.deps.config.memoryDir, entries);
463
463
  }
464
464
  } catch (err) {
465
465
  log.debug(`temporal-index update failed (non-fatal): ${err}`);
@@ -33,7 +33,7 @@ import { RerankCache, rerankLocalOrNoop } from "../rerank.js";
33
33
  import type { SearchBackend, SearchDegradation, SearchExecutionOptions, SearchQueryOptions } from "../search/port.js";
34
34
  import { SecureStoreLockedError } from "../secure-store/index.js";
35
35
  import { isPathInsideStorageRoot } from "../storage-paths.js";
36
- import { extractTagsFromPrompt, isTemporalQuery, queryByDateRangeAsync, queryByTagsAsync, recencyWindowFromPrompt, resolvePromptTagPrefilterAsync } from "../temporal-index.js";
36
+ import { extractTagsFromPrompt, isTemporalQuery, queryByDateRangeAsync, queryByTagsAsync, readIndexSnapshotAsync, recencyWindowFromPrompt, resolvePromptTagPrefilterAsync } from "../temporal-index.js";
37
37
  import { shouldFilterSupersededFromRecall } from "../temporal-supersession.js";
38
38
  import { isValidAsOf, isValidityExpiredNow } from "../temporal-validity.js";
39
39
  import type { TrustStageResultItem } from "../trust-score-stage.js";
@@ -305,18 +305,26 @@ export class RecallSearchPipelineCoordinator {
305
305
  const temporalFromDate = isTemporalQuery(prompt)
306
306
  ? recencyWindowFromPrompt(prompt, Date.now())
307
307
  : null;
308
- const [rawTemporal, tagSignals] = await Promise.all([
309
- temporalFromDate
310
- ? queryByDateRangeAsync(this.deps.config.memoryDir, temporalFromDate)
311
- : Promise.resolve<Set<string> | null>(null),
312
- resolvePromptTagPrefilterAsync(this.deps.config.memoryDir, prompt).catch(
313
- () => ({
314
- matchedTags: extractTagsFromPrompt(prompt),
315
- expandedTags: extractTagsFromPrompt(prompt),
316
- paths: null,
317
- }),
318
- ),
319
- ]);
308
+ // Read the temporal + tag prefilter indexes as ONE consistent snapshot
309
+ // (issue #1911, Codex Medium) so a concurrent async index mutation can't be
310
+ // observed half-applied (new temporal row, tag membership not yet written),
311
+ // which would drop the current memory from the tag prefilter.
312
+ const [rawTemporal, tagSignals] = await readIndexSnapshotAsync(
313
+ this.deps.config.memoryDir,
314
+ () =>
315
+ Promise.all([
316
+ temporalFromDate
317
+ ? queryByDateRangeAsync(this.deps.config.memoryDir, temporalFromDate)
318
+ : Promise.resolve<Set<string> | null>(null),
319
+ resolvePromptTagPrefilterAsync(this.deps.config.memoryDir, prompt).catch(
320
+ () => ({
321
+ matchedTags: extractTagsFromPrompt(prompt),
322
+ expandedTags: extractTagsFromPrompt(prompt),
323
+ paths: null,
324
+ }),
325
+ ),
326
+ ]),
327
+ );
320
328
 
321
329
  const temporalCandidates = this.deps.scopeQueryAwarePaths(
322
330
  rawTemporal,
@@ -1516,14 +1524,21 @@ export class RecallSearchPipelineCoordinator {
1516
1524
  promptTags = extractTagsFromPrompt(prompt);
1517
1525
  }
1518
1526
 
1519
- const [rawTemporal, rawTags] = await Promise.all([
1520
- temporalFromDate !== null
1521
- ? queryByDateRangeAsync(this.deps.config.memoryDir, temporalFromDate)
1522
- : Promise.resolve<Set<string> | null>(null),
1523
- promptTags.length > 0
1524
- ? queryByTagsAsync(this.deps.config.memoryDir, promptTags)
1525
- : Promise.resolve<Set<string> | null>(null),
1526
- ]);
1527
+ // Consistent temporal+tag snapshot (issue #1911, Codex Medium): read both
1528
+ // indexes under one op-lock so an in-flight async mutation is never observed
1529
+ // half-applied.
1530
+ const [rawTemporal, rawTags] = await readIndexSnapshotAsync(
1531
+ this.deps.config.memoryDir,
1532
+ () =>
1533
+ Promise.all([
1534
+ temporalFromDate !== null
1535
+ ? queryByDateRangeAsync(this.deps.config.memoryDir, temporalFromDate)
1536
+ : Promise.resolve<Set<string> | null>(null),
1537
+ promptTags.length > 0
1538
+ ? queryByTagsAsync(this.deps.config.memoryDir, promptTags)
1539
+ : Promise.resolve<Set<string> | null>(null),
1540
+ ]),
1541
+ );
1527
1542
 
1528
1543
  const queryIntent =
1529
1544
  resolveConversationContextCapabilities(this.deps.config).intentRouting && prompt
@@ -42,7 +42,7 @@ import {
42
42
  gatewayTaskChainOptions,
43
43
  } from "../fallback-llm.js";
44
44
  import { resolveIndexingCapabilities, resolveConsolidationCapabilities } from "../capabilities.js";
45
- import { deindexMemory } from "../temporal-index.js";
45
+ import { deindexMemoryAsync } from "../temporal-index.js";
46
46
  import { runPeerProfileReasoner } from "../peers/index.js";
47
47
  import type { StorageManager } from "../index.js";
48
48
  import type { LocalLlmClient } from "../local-llm.js";
@@ -342,7 +342,7 @@ export class SemanticConsolidationCoordinator {
342
342
  m.path &&
343
343
  m.frontmatter?.created
344
344
  ) {
345
- deindexMemory(
345
+ await deindexMemoryAsync(
346
346
  targetStorage.dir,
347
347
  m.path,
348
348
  m.frontmatter.created,
@@ -326,10 +326,6 @@ import {
326
326
  } from "./lifecycle.js";
327
327
  import { isActiveMemoryStatus } from "./memory-lifecycle-ledger-utils.js";
328
328
  import {
329
- indexMemoriesBatch,
330
- clearIndexes,
331
- indexesExist,
332
- deindexMemory,
333
329
  queryByDateRangeAsync,
334
330
  queryByTagsAsync,
335
331
  isTemporalQuery,
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Synchronous compatibility surface for the historical temporal-index API.
3
+ *
4
+ * New runtime code must use the explicit `*Async` exports from temporal-index.
5
+ * These wrappers preserve return timing and boolean truthiness for published
6
+ * JavaScript consumers that predate the async index implementation.
7
+ */
8
+
9
+ import { execFileSync } from "node:child_process";
10
+ import * as crypto from "node:crypto";
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+
14
+ import {
15
+ applyTagIndexCompatibilityMutation,
16
+ applyTemporalIndexCompatibilityMutation,
17
+ hasCurrentTemporalIndexSchemaForCompatibility,
18
+ type TemporalIndexCompatibilityMutation,
19
+ type TemporalIndexEntry,
20
+ } from "./temporal-index.js";
21
+
22
+ const TEMPORAL_INDEX_FILE = "index_time.json";
23
+ const TAG_INDEX_FILE = "index_tags.json";
24
+ const INDEX_LOCK_STALE_MS = 60_000;
25
+ // Bounded blocking-wait cap for the sync per-file lock (issue #1911, Cursor
26
+ // Medium): the sync waiter blocks the Node event loop with sleepSync, so if a
27
+ // lock is held by an in-flight async write (awaited I/O) it can't be released
28
+ // until the loop runs again. Cap the stall at this bound and fail open rather
29
+ // than deadlock or stall for INDEX_LOCK_STALE_MS. 1.5s tolerates a slow disk
30
+ // write while bounding the worst-case event-loop freeze.
31
+ const SYNC_INDEX_LOCK_BOUND_MS = 1_500;
32
+ const INDEX_LOCK_POLL_MS = 10;
33
+ const INDEX_PROCESS_START_TOLERANCE_MS = 2_000;
34
+ const INDEX_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(4));
35
+ const INDEX_PROCESS_STARTED_AT_MS = Date.now() - process.uptime() * 1000;
36
+
37
+ interface IndexLockOwner {
38
+ pid: number;
39
+ createdAt?: string;
40
+ processStartedAtMs?: number;
41
+ }
42
+
43
+ type IndexLockCleanupResult = "removed" | "wait" | "blocked";
44
+
45
+ function stateDir(memoryDir: string): string {
46
+ return path.join(memoryDir, "state");
47
+ }
48
+
49
+ function temporalIndexPath(memoryDir: string): string {
50
+ return path.join(stateDir(memoryDir), TEMPORAL_INDEX_FILE);
51
+ }
52
+
53
+ function tagIndexPath(memoryDir: string): string {
54
+ return path.join(stateDir(memoryDir), TAG_INDEX_FILE);
55
+ }
56
+
57
+ function sleepSync(ms: number): void {
58
+ Atomics.wait(INDEX_LOCK_SLEEP, 0, 0, ms);
59
+ }
60
+
61
+ function uniqueTempPath(filePath: string): string {
62
+ const nonce = crypto.randomBytes(6).toString("hex");
63
+ return path.join(
64
+ path.dirname(filePath),
65
+ `.${path.basename(filePath)}.${process.pid}.${Date.now()}.${nonce}.tmp`,
66
+ );
67
+ }
68
+
69
+ function lockOwnerPath(lockDir: string): string {
70
+ return path.join(lockDir, "owner.json");
71
+ }
72
+
73
+ function writeIndexLockOwner(lockDir: string): void {
74
+ try {
75
+ fs.writeFileSync(
76
+ lockOwnerPath(lockDir),
77
+ JSON.stringify({
78
+ pid: process.pid,
79
+ createdAt: new Date().toISOString(),
80
+ processStartedAtMs: INDEX_PROCESS_STARTED_AT_MS,
81
+ }),
82
+ { encoding: "utf8", flag: "wx" },
83
+ );
84
+ } catch {
85
+ // The directory remains the serialization primitive.
86
+ }
87
+ }
88
+
89
+ function readIndexLockOwner(lockDir: string): IndexLockOwner | null {
90
+ try {
91
+ const parsed = JSON.parse(fs.readFileSync(lockOwnerPath(lockDir), "utf8")) as Record<string, unknown>;
92
+ if (!(typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0)) return null;
93
+ return {
94
+ pid: parsed.pid,
95
+ ...(typeof parsed.createdAt === "string" && parsed.createdAt.length > 0
96
+ ? { createdAt: parsed.createdAt }
97
+ : {}),
98
+ ...(typeof parsed.processStartedAtMs === "number" &&
99
+ Number.isFinite(parsed.processStartedAtMs) &&
100
+ parsed.processStartedAtMs > 0
101
+ ? { processStartedAtMs: parsed.processStartedAtMs }
102
+ : {}),
103
+ };
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ function processIsAlive(pid: number): boolean {
110
+ try {
111
+ process.kill(pid, 0);
112
+ return true;
113
+ } catch (error) {
114
+ return (error as NodeJS.ErrnoException)?.code === "EPERM";
115
+ }
116
+ }
117
+
118
+ function readProcessStartedAtMs(pid: number): number | null {
119
+ try {
120
+ const output = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
121
+ encoding: "utf8",
122
+ stdio: ["ignore", "pipe", "ignore"],
123
+ timeout: 1_000,
124
+ }).trim();
125
+ if (!output) return null;
126
+ const startedAtMs = Date.parse(output);
127
+ return Number.isFinite(startedAtMs) ? startedAtMs : null;
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ function lockOwnerIsRunning(owner: IndexLockOwner): boolean {
134
+ if (!processIsAlive(owner.pid)) return false;
135
+ if (owner.processStartedAtMs === undefined) return true;
136
+ const runningStartedAtMs = readProcessStartedAtMs(owner.pid);
137
+ if (runningStartedAtMs === null) return true;
138
+ return runningStartedAtMs <= owner.processStartedAtMs + INDEX_PROCESS_START_TOLERANCE_MS;
139
+ }
140
+
141
+ function lockIsFresh(lockInfo: fs.Stats, owner: IndexLockOwner | null): boolean {
142
+ const ownerCreatedAtMs = owner?.createdAt ? Date.parse(owner.createdAt) : Number.NaN;
143
+ const referenceMs = Number.isFinite(ownerCreatedAtMs) ? ownerCreatedAtMs : lockInfo.mtimeMs;
144
+ return Date.now() - referenceMs < INDEX_LOCK_STALE_MS;
145
+ }
146
+
147
+ function removeAbandonedIndexLock(lockDir: string): IndexLockCleanupResult {
148
+ try {
149
+ const info = fs.lstatSync(lockDir);
150
+ if (info.isSymbolicLink()) return "blocked";
151
+ if (!info.isDirectory()) {
152
+ fs.rmSync(lockDir, { force: true });
153
+ return "removed";
154
+ }
155
+ const owner = readIndexLockOwner(lockDir);
156
+ if (owner && lockOwnerIsRunning(owner)) return "wait";
157
+ if (!owner && lockIsFresh(info, null)) return "wait";
158
+ fs.rmSync(lockDir, { recursive: true, force: true });
159
+ return "removed";
160
+ } catch (error) {
161
+ return (error as NodeJS.ErrnoException)?.code === "ENOENT" ? "removed" : "blocked";
162
+ }
163
+ }
164
+
165
+ function withIndexFileLock(filePath: string, update: () => void): void {
166
+ const lockDir = `${filePath}.lock.d`;
167
+ // Bound the acquisition (issue #1911, Codex Medium). sleepSync() blocks the
168
+ // Node event loop, so an UNBOUNDED wait here would stall the whole process if
169
+ const maxAttempts = Math.max(1, Math.ceil(SYNC_INDEX_LOCK_BOUND_MS / INDEX_LOCK_POLL_MS));
170
+ // (advisory indexes) rather than risk starving the loop — e.g. a lock held by
171
+ // in-flight async work that needs the very loop this call is blocking.
172
+ let acquired = false;
173
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
174
+ try {
175
+ fs.mkdirSync(lockDir);
176
+ writeIndexLockOwner(lockDir);
177
+ acquired = true;
178
+ break;
179
+ } catch (error) {
180
+ const code = (error as NodeJS.ErrnoException)?.code;
181
+ if (code === "ENOENT") {
182
+ try {
183
+ fs.mkdirSync(path.dirname(lockDir), { recursive: true });
184
+ } catch {
185
+ return;
186
+ }
187
+ sleepSync(INDEX_LOCK_POLL_MS);
188
+ continue;
189
+ }
190
+ if (code !== "EEXIST") return;
191
+ if (removeAbandonedIndexLock(lockDir) === "blocked") return;
192
+ sleepSync(INDEX_LOCK_POLL_MS);
193
+ }
194
+ }
195
+ if (!acquired) return; // fail open — could not acquire within the bound
196
+
197
+ try {
198
+ update();
199
+ } finally {
200
+ try {
201
+ fs.rmSync(lockDir, { recursive: true, force: true });
202
+ } catch {
203
+ // Advisory indexes fail open.
204
+ }
205
+ }
206
+ }
207
+
208
+ function readJson(filePath: string): unknown {
209
+ try {
210
+ return JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+
216
+ function writeJsonAtomic(filePath: string, value: unknown): void {
217
+ const payload = JSON.stringify(value);
218
+ for (let attempt = 0; attempt < 3; attempt += 1) {
219
+ const tempPath = uniqueTempPath(filePath);
220
+ try {
221
+ fs.writeFileSync(tempPath, payload, "utf8");
222
+ fs.renameSync(tempPath, filePath);
223
+ return;
224
+ } catch {
225
+ try {
226
+ fs.unlinkSync(tempPath);
227
+ } catch {
228
+ // Advisory indexes fail open.
229
+ }
230
+ sleepSync(INDEX_LOCK_POLL_MS);
231
+ }
232
+ }
233
+ }
234
+
235
+ function applyMutation(memoryDir: string, mutation: TemporalIndexCompatibilityMutation): void {
236
+ try {
237
+ fs.mkdirSync(stateDir(memoryDir), { recursive: true });
238
+ // Coordinate with the ASYNC surface by acquiring the SAME per-file lockdirs
239
+ // (issue #1911, Cursor Medium) — `<temporal>.lock.d` then `<tag>.lock.d`,
240
+ // nested in a fixed order (no deadlock: the async surface acquires them one
241
+ // at a time, never both). The acquisition is BOUNDED + fail-open
242
+ // (withIndexFileLock), so an event-loop-blocking sync waiter can never stall
243
+ // the loop waiting on a lock held across an async await — if the async write
244
+ // doesn't release in the bound, sync gives up (advisory indexes fail open).
245
+ // Holding both locks together makes the paired write atomic vs. both surfaces.
246
+ const temporalPath = temporalIndexPath(memoryDir);
247
+ const tagPath = tagIndexPath(memoryDir);
248
+ withIndexFileLock(temporalPath, () => {
249
+ withIndexFileLock(tagPath, () => {
250
+ writeJsonAtomic(
251
+ temporalPath,
252
+ applyTemporalIndexCompatibilityMutation(readJson(temporalPath), mutation),
253
+ );
254
+ writeJsonAtomic(
255
+ tagPath,
256
+ applyTagIndexCompatibilityMutation(readJson(tagPath), mutation),
257
+ );
258
+ });
259
+ });
260
+ } catch {
261
+ // Advisory indexes fail open.
262
+ }
263
+ }
264
+
265
+ /** @deprecated Use `indexMemoryAsync` in new code. */
266
+ export function indexMemory(
267
+ memoryDir: string,
268
+ memoryPath: string,
269
+ createdAt: string,
270
+ tags: string[],
271
+ temporal: Omit<Partial<TemporalIndexEntry>, "path" | "createdAt" | "tags"> = {},
272
+ ): void {
273
+ applyMutation(memoryDir, {
274
+ kind: "index",
275
+ entries: [{ path: memoryPath, createdAt, tags, ...temporal }],
276
+ });
277
+ }
278
+
279
+ /** @deprecated Use `deindexMemoryAsync` in new code. */
280
+ export function deindexMemory(
281
+ memoryDir: string,
282
+ memoryPath: string,
283
+ createdAt: string,
284
+ tags: string[],
285
+ ): void {
286
+ applyMutation(memoryDir, {
287
+ kind: "deindex",
288
+ entries: [{ path: memoryPath, createdAt, tags }],
289
+ });
290
+ }
291
+
292
+ /** @deprecated Use `clearIndexesAsync` in new code. */
293
+ export function clearIndexes(memoryDir: string): void {
294
+ applyMutation(memoryDir, { kind: "clear" });
295
+ }
296
+
297
+ /** @deprecated Use `indexesExistAsync` in new code. */
298
+ export function indexesExist(memoryDir: string): boolean {
299
+ try {
300
+ if (!fs.existsSync(tagIndexPath(memoryDir))) return false;
301
+ return hasCurrentTemporalIndexSchemaForCompatibility(readJson(temporalIndexPath(memoryDir)));
302
+ } catch {
303
+ return false;
304
+ }
305
+ }
306
+
307
+ /** @deprecated Use `indexMemoriesBatchAsync` in new code. */
308
+ export function indexMemoriesBatch(memoryDir: string, entries: TemporalIndexEntry[]): void {
309
+ if (entries.length === 0) return;
310
+ applyMutation(memoryDir, { kind: "index", entries });
311
+ }