@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.
- package/dist/access-admin-ops-surface.js +5 -5
- package/dist/access-boundary.js +6 -6
- package/dist/access-cli.js +36 -36
- 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.js +8 -8
- package/dist/access-recall-surface.js +5 -5
- package/dist/access-service.js +5 -5
- 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-QKVX4Y6D.js → chunk-673NPXE3.js} +2 -2
- package/dist/{chunk-KKASNHGF.js → chunk-7CKS4AXY.js} +19 -19
- package/dist/{chunk-YGAOLHKN.js → chunk-7EX4YOKH.js} +4 -4
- package/dist/{chunk-5CFHYQU3.js → chunk-DLWYAN6A.js} +2 -2
- package/dist/{chunk-TY43GBX2.js → chunk-EQF5763Z.js} +2 -2
- package/dist/{chunk-VCFADTFS.js → chunk-FNJX2O6G.js} +3 -3
- 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-DLPXS7BE.js → chunk-N3LWQJ6Y.js} +55 -48
- package/dist/chunk-N3LWQJ6Y.js.map +1 -0
- package/dist/{chunk-EIKBKQ6L.js → chunk-QVALQE4P.js} +4 -4
- 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-POUD5BLV.js → chunk-UZRIKUFS.js} +2 -2
- package/dist/{chunk-CK6RI52P.js → chunk-VCEH5UYS.js} +2 -2
- package/dist/{chunk-SKXBOZDF.js → chunk-VDLMAJXS.js} +3 -3
- package/dist/{chunk-UXWGPODH.js → chunk-VRAD2FGO.js} +7 -7
- 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.js +43 -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 +22 -22
- 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/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 +12 -12
- package/dist/verified-recall.js +3 -3
- package/package.json +2 -2
- 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/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-HHAA35A3.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-KKASNHGF.js.map → chunk-7CKS4AXY.js.map} +0 -0
- /package/dist/{chunk-YGAOLHKN.js.map → chunk-7EX4YOKH.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-VCFADTFS.js.map → chunk-FNJX2O6G.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-EIKBKQ6L.js.map → chunk-QVALQE4P.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-POUD5BLV.js.map → chunk-UZRIKUFS.js.map} +0 -0
- /package/dist/{chunk-CK6RI52P.js.map → chunk-VCEH5UYS.js.map} +0 -0
- /package/dist/{chunk-SKXBOZDF.js.map → chunk-VDLMAJXS.js.map} +0 -0
- /package/dist/{chunk-UXWGPODH.js.map → chunk-VRAD2FGO.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
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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 = !
|
|
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
|
|
452
|
+
// This marks bootstrap completion so indexesExistAsync() returns true and
|
|
453
453
|
// subsequent extractions skip the full-corpus scan.
|
|
454
|
-
|
|
454
|
+
await clearIndexesAsync(this.deps.config.memoryDir);
|
|
455
455
|
if (entries.length > 0) {
|
|
456
|
-
|
|
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
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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 {
|
|
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
|
-
|
|
345
|
+
await deindexMemoryAsync(
|
|
346
346
|
targetStorage.dir,
|
|
347
347
|
m.path,
|
|
348
348
|
m.frontmatter.created,
|
package/src/orchestrator.ts
CHANGED
|
@@ -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
|
+
}
|