@seanhogg/builderforce-memory 2026.9.34 → 2026.10.2

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 (97) hide show
  1. package/dist/errors/SSMError.d.ts +1 -1
  2. package/dist/errors/SSMError.d.ts.map +1 -1
  3. package/dist/errors/SSMError.js.map +1 -1
  4. package/dist/handoff/index.d.ts +5 -5
  5. package/dist/handoff/index.js +5 -5
  6. package/dist/handoff/note.d.ts +1 -1
  7. package/dist/handoff/note.js +1 -1
  8. package/dist/handoff/pack.d.ts +13 -4
  9. package/dist/handoff/pack.d.ts.map +1 -1
  10. package/dist/handoff/pack.js +31 -10
  11. package/dist/handoff/pack.js.map +1 -1
  12. package/dist/memory/MemoryStore.d.ts +120 -181
  13. package/dist/memory/MemoryStore.d.ts.map +1 -1
  14. package/dist/memory/MemoryStore.js +201 -389
  15. package/dist/memory/MemoryStore.js.map +1 -1
  16. package/dist/memory/SemanticRecall.d.ts +64 -0
  17. package/dist/memory/SemanticRecall.d.ts.map +1 -0
  18. package/dist/memory/SemanticRecall.js +188 -0
  19. package/dist/memory/SemanticRecall.js.map +1 -0
  20. package/dist/memory/records.d.ts +26 -0
  21. package/dist/memory/records.d.ts.map +1 -0
  22. package/dist/memory/records.js +71 -0
  23. package/dist/memory/records.js.map +1 -0
  24. package/dist/memory/ttlSweeper.d.ts +12 -0
  25. package/dist/memory/ttlSweeper.d.ts.map +1 -0
  26. package/dist/memory/ttlSweeper.js +28 -0
  27. package/dist/memory/ttlSweeper.js.map +1 -0
  28. package/dist/memory/types.d.ts +142 -0
  29. package/dist/memory/types.d.ts.map +1 -0
  30. package/dist/memory/types.js +6 -0
  31. package/dist/memory/types.js.map +1 -0
  32. package/dist/storage/ScopedFactRepository.d.ts +156 -0
  33. package/dist/storage/ScopedFactRepository.d.ts.map +1 -0
  34. package/dist/storage/ScopedFactRepository.js +278 -0
  35. package/dist/storage/ScopedFactRepository.js.map +1 -0
  36. package/dist/storage/factImport.d.ts +21 -0
  37. package/dist/storage/factImport.d.ts.map +1 -0
  38. package/dist/storage/factImport.js +61 -0
  39. package/dist/storage/factImport.js.map +1 -0
  40. package/dist/storage/factMutations.d.ts +46 -0
  41. package/dist/storage/factMutations.d.ts.map +1 -0
  42. package/dist/storage/factMutations.js +73 -0
  43. package/dist/storage/factMutations.js.map +1 -0
  44. package/dist/storage/factQueries.d.ts +16 -0
  45. package/dist/storage/factQueries.d.ts.map +1 -0
  46. package/dist/storage/factQueries.js +36 -0
  47. package/dist/storage/factQueries.js.map +1 -0
  48. package/dist/storage/factRetention.d.ts +30 -0
  49. package/dist/storage/factRetention.d.ts.map +1 -0
  50. package/dist/storage/factRetention.js +65 -0
  51. package/dist/storage/factRetention.js.map +1 -0
  52. package/dist/storage/fileLock.d.ts +56 -0
  53. package/dist/storage/fileLock.d.ts.map +1 -0
  54. package/dist/storage/fileLock.js +138 -0
  55. package/dist/storage/fileLock.js.map +1 -0
  56. package/dist/storage/fileTransaction.d.ts +33 -0
  57. package/dist/storage/fileTransaction.d.ts.map +1 -0
  58. package/dist/storage/fileTransaction.js +108 -0
  59. package/dist/storage/fileTransaction.js.map +1 -0
  60. package/dist/storage/idbAdapter.d.ts +95 -0
  61. package/dist/storage/idbAdapter.d.ts.map +1 -0
  62. package/dist/storage/idbAdapter.js +311 -0
  63. package/dist/storage/idbAdapter.js.map +1 -0
  64. package/dist/storage/nodeFileAdapter.d.ts +127 -0
  65. package/dist/storage/nodeFileAdapter.d.ts.map +1 -0
  66. package/dist/storage/nodeFileAdapter.js +275 -0
  67. package/dist/storage/nodeFileAdapter.js.map +1 -0
  68. package/dist/storage/syncJournal.d.ts +19 -0
  69. package/dist/storage/syncJournal.d.ts.map +1 -0
  70. package/dist/storage/syncJournal.js +46 -0
  71. package/dist/storage/syncJournal.js.map +1 -0
  72. package/dist/storage/types.d.ts +207 -0
  73. package/dist/storage/types.d.ts.map +1 -0
  74. package/dist/storage/types.js +90 -0
  75. package/dist/storage/types.js.map +1 -0
  76. package/package.json +3 -3
  77. package/src/errors/SSMError.ts +1 -0
  78. package/src/handoff/handoff.test.ts +52 -2
  79. package/src/handoff/index.ts +5 -5
  80. package/src/handoff/note.ts +1 -1
  81. package/src/handoff/pack.ts +32 -9
  82. package/src/memory/MemoryStore.ts +245 -525
  83. package/src/memory/SemanticRecall.ts +204 -0
  84. package/src/memory/records.ts +79 -0
  85. package/src/memory/ttlSweeper.ts +29 -0
  86. package/src/memory/types.ts +149 -0
  87. package/src/storage/ScopedFactRepository.ts +373 -0
  88. package/src/storage/factImport.ts +73 -0
  89. package/src/storage/factMutations.ts +106 -0
  90. package/src/storage/factQueries.ts +41 -0
  91. package/src/storage/factRetention.ts +82 -0
  92. package/src/storage/fileLock.ts +162 -0
  93. package/src/storage/fileTransaction.ts +116 -0
  94. package/src/storage/idbAdapter.ts +373 -0
  95. package/src/storage/nodeFileAdapter.ts +331 -0
  96. package/src/storage/syncJournal.ts +47 -0
  97. package/src/storage/types.ts +262 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * storage/factMutations.ts – The write primitives every repository mutation
3
+ * shares, so the counter bump / tombstone / journal rules live in ONE place:
4
+ *
5
+ * - {@link bumpOp}: next durable revision + operation id.
6
+ * - {@link journal}: append a sync-journal entry — ONLY when sync is enabled.
7
+ * Without a sync target there is nothing to acknowledge, so writing rows
8
+ * already marked synced just grew the journal forever.
9
+ * - {@link deleteWithTombstone}: delete a fact, tombstone it when sync is on,
10
+ * journal the delete — one call for forget, clear and TTL purge.
11
+ * - {@link clearTombstone}: a write resurrects a key (remember, import).
12
+ */
13
+ export const COUNTERS_ID = 'counters';
14
+ /** The stores a mutating unit of work spans. */
15
+ export const WRITE_STORES = ['facts', 'tombstones', 'journal', 'meta'];
16
+ export async function readCounters(tx) {
17
+ const existing = await tx.get('meta', COUNTERS_ID);
18
+ return existing ?? { key: COUNTERS_ID, seq: 0, revision: 0, opSeq: 0 };
19
+ }
20
+ export async function writeCounters(tx, counters) {
21
+ await tx.put('meta', { ...counters, key: COUNTERS_ID });
22
+ }
23
+ /** The next revision + operation identity after `counters`. */
24
+ export function bumpOp(counters, ctx) {
25
+ const next = { ...counters, revision: counters.revision + 1, opSeq: counters.opSeq + 1 };
26
+ return { counters: next, opId: `${ctx.contextId}:${next.opSeq}` };
27
+ }
28
+ /** Appends an unsynced journal entry when sync is enabled; a no-op otherwise. */
29
+ export async function journal(tx, ctx, entry) {
30
+ if (!ctx.syncEnabled)
31
+ return;
32
+ await tx.put('journal', { ...entry, synced: false });
33
+ }
34
+ /** Clears any tombstone for `id`; true when one existed (the write resurrected the key). */
35
+ export async function clearTombstone(tx, id) {
36
+ const tombstone = await tx.get('tombstones', id);
37
+ if (tombstone)
38
+ await tx.delete('tombstones', id);
39
+ return !!tombstone;
40
+ }
41
+ /**
42
+ * Deletes one fact as a durable, propagating deletion: bump the counters,
43
+ * delete, tombstone (sync on), journal (sync on). Returns the bumped counters;
44
+ * the caller persists them once per transaction with {@link writeCounters}.
45
+ */
46
+ export async function deleteWithTombstone(tx, ctx, record, counters, baseRevision) {
47
+ const { counters: next, opId } = bumpOp(counters, ctx);
48
+ const at = ctx.now();
49
+ await tx.delete('facts', record.id);
50
+ if (ctx.syncEnabled) {
51
+ await tx.put('tombstones', {
52
+ id: record.id,
53
+ key: record.key,
54
+ scope: record.scope,
55
+ deletedAt: at,
56
+ revision: next.revision,
57
+ opId,
58
+ pendingSync: true,
59
+ });
60
+ }
61
+ await journal(tx, ctx, {
62
+ opId,
63
+ kind: 'forget',
64
+ id: record.id,
65
+ key: record.key,
66
+ scope: record.scope,
67
+ revision: next.revision,
68
+ baseRevision,
69
+ timestamp: at,
70
+ });
71
+ return next;
72
+ }
73
+ //# sourceMappingURL=factMutations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factMutations.js","sourceRoot":"","sources":["../../src/storage/factMutations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * storage/factQueries.ts – Read helpers shared by the repository and its
3
+ * retention policy: partition filtering and index lookups with the
4
+ * "missing index ⇒ fall back, empty hit ⇒ real miss" contract.
5
+ */
6
+ import { type MemoryScope, type StorageTransaction, type StoredFactRecord } from './types.js';
7
+ /** Every fact in exactly `scope`'s partition (never wider, never narrower). */
8
+ export declare function factsInScope(tx: StorageTransaction, scope: MemoryScope | undefined): Promise<StoredFactRecord[]>;
9
+ /**
10
+ * Index lookup. `undefined` means the named index is absent and the caller
11
+ * must fall back. An empty array is a real miss — do not scan after it.
12
+ * Multi-entry tag indexes can return the same record once per matching tag
13
+ * occurrence, so hits are de-duplicated by id.
14
+ */
15
+ export declare function indexedFacts(tx: StorageTransaction, indexName: string, query: string): Promise<StoredFactRecord[] | undefined>;
16
+ //# sourceMappingURL=factQueries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factQueries.d.ts","sourceRoot":"","sources":["../../src/storage/factQueries.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAqB,KAAK,WAAW,EAAE,KAAK,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEjH,+EAA+E;AAC/E,wBAAsB,YAAY,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAItH;AAaD;;;;;GAKG;AACH,wBAAsB,YAAY,CAC9B,EAAE,EAAE,kBAAkB,EACtB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,EAAE,GAAG,SAAS,CAAC,CAIzC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * storage/factQueries.ts – Read helpers shared by the repository and its
3
+ * retention policy: partition filtering and index lookups with the
4
+ * "missing index ⇒ fall back, empty hit ⇒ real miss" contract.
5
+ */
6
+ import { scopePartitionKey } from './types.js';
7
+ /** Every fact in exactly `scope`'s partition (never wider, never narrower). */
8
+ export async function factsInScope(tx, scope) {
9
+ const partition = scopePartitionKey(scope);
10
+ const all = await tx.getAll('facts');
11
+ return all.filter((r) => scopePartitionKey(r.scope) === partition);
12
+ }
13
+ function dedupeById(records) {
14
+ const seen = new Set();
15
+ const out = [];
16
+ for (const record of records) {
17
+ if (seen.has(record.id))
18
+ continue;
19
+ seen.add(record.id);
20
+ out.push(record);
21
+ }
22
+ return out;
23
+ }
24
+ /**
25
+ * Index lookup. `undefined` means the named index is absent and the caller
26
+ * must fall back. An empty array is a real miss — do not scan after it.
27
+ * Multi-entry tag indexes can return the same record once per matching tag
28
+ * occurrence, so hits are de-duplicated by id.
29
+ */
30
+ export async function indexedFacts(tx, indexName, query) {
31
+ if (!tx.getByIndex)
32
+ return undefined;
33
+ const hits = await tx.getByIndex('facts', indexName, query);
34
+ return hits === undefined ? undefined : dedupeById(hits);
35
+ }
36
+ //# sourceMappingURL=factQueries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factQueries.js","sourceRoot":"","sources":["../../src/storage/factQueries.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,iBAAiB,EAAoE,MAAM,YAAY,CAAC;AAEjH,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,EAAsB,EAAE,KAA8B;IACrF,MAAM,SAAS,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,MAAM,CAAmB,OAAO,CAAC,CAAC;IACvD,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,SAAS,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,UAAU,CAAC,OAA2B;IAC3C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAuB,EAAE,CAAC;IACnC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC3B,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAAE,SAAS;QAClC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACpB,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAC9B,EAAsB,EACtB,SAAiB,EACjB,KAAa;IAEb,IAAI,CAAC,EAAE,CAAC,UAAU;QAAE,OAAO,SAAS,CAAC;IACrC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,UAAU,CAAmB,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IAC9E,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;AAC7D,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * storage/factRetention.ts – Retention policy over the fact store (M06):
3
+ * local cap eviction and intentional TTL purge.
4
+ *
5
+ * - Cap eviction is cache management: synced records only, no tombstone, no
6
+ * journal. It counts in a READONLY transaction first and opens a read-write
7
+ * one only when the scope is actually over the cap — on the file adapter
8
+ * every read-write transaction rewrites and fsyncs the whole document, so
9
+ * an under-cap `remember` must not pay for a second one.
10
+ * - TTL purge is a deletion: each expiry goes through
11
+ * {@link deleteWithTombstone} so it propagates when sync is enabled.
12
+ */
13
+ import { type MutationContext } from './factMutations.js';
14
+ import type { FactStoragePort, MemoryScope, StoredFactRecord } from './types.js';
15
+ type IsExpired = (record: StoredFactRecord) => boolean;
16
+ /**
17
+ * Removes the lowest-value SYNCED records in `scope` past `maxEntries`.
18
+ * Returns the evicted keys (empty when under the cap — no write happens).
19
+ */
20
+ export declare function evictForCap(storage: FactStoragePort, scope: MemoryScope | undefined, maxEntries: number, isExpired: IsExpired): Promise<string[]>;
21
+ /**
22
+ * Deletes every expired record in `scope` as a propagating deletion.
23
+ * Returns how many were purged and the store revision after the purge.
24
+ */
25
+ export declare function purgeExpired(storage: FactStoragePort, ctx: MutationContext, scope: MemoryScope | undefined, isExpired: IsExpired): Promise<{
26
+ purged: number;
27
+ revision: number;
28
+ }>;
29
+ export {};
30
+ //# sourceMappingURL=factRetention.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factRetention.d.ts","sourceRoot":"","sources":["../../src/storage/factRetention.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAKH,KAAK,eAAe,EACvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEjF,KAAK,SAAS,GAAG,CAAC,MAAM,EAAE,gBAAgB,KAAK,OAAO,CAAC;AAevD;;;GAGG;AACH,wBAAsB,WAAW,CAC7B,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,SAAS,GACrB,OAAO,CAAC,MAAM,EAAE,CAAC,CAcnB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAC9B,OAAO,EAAE,eAAe,EACxB,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,SAAS,EAAE,SAAS,GACrB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAS/C"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * storage/factRetention.ts – Retention policy over the fact store (M06):
3
+ * local cap eviction and intentional TTL purge.
4
+ *
5
+ * - Cap eviction is cache management: synced records only, no tombstone, no
6
+ * journal. It counts in a READONLY transaction first and opens a read-write
7
+ * one only when the scope is actually over the cap — on the file adapter
8
+ * every read-write transaction rewrites and fsyncs the whole document, so
9
+ * an under-cap `remember` must not pay for a second one.
10
+ * - TTL purge is a deletion: each expiry goes through
11
+ * {@link deleteWithTombstone} so it propagates when sync is enabled.
12
+ */
13
+ import { factsInScope } from './factQueries.js';
14
+ import { WRITE_STORES, deleteWithTombstone, readCounters, writeCounters, } from './factMutations.js';
15
+ /** Lowest value first: expired, then least important, then oldest. */
16
+ function evictionOrder(isExpired) {
17
+ const rank = (e) => [isExpired(e) ? 0 : 1, e.importance ?? 0.5, e.timestamp, e.seq];
18
+ return (a, b) => {
19
+ const ra = rank(a), rb = rank(b);
20
+ for (let i = 0; i < ra.length; i++) {
21
+ if (ra[i] !== rb[i])
22
+ return ra[i] - rb[i];
23
+ }
24
+ return 0;
25
+ };
26
+ }
27
+ /**
28
+ * Removes the lowest-value SYNCED records in `scope` past `maxEntries`.
29
+ * Returns the evicted keys (empty when under the cap — no write happens).
30
+ */
31
+ export async function evictForCap(storage, scope, maxEntries, isExpired) {
32
+ const count = await storage.transact(['facts'], 'readonly', async (tx) => (await factsInScope(tx, scope)).length);
33
+ if (count <= maxEntries)
34
+ return [];
35
+ return storage.transact(['facts'], 'readwrite', async (tx) => {
36
+ // Re-read inside the write: another writer may have changed the set.
37
+ const mine = await factsInScope(tx, scope);
38
+ if (mine.length <= maxEntries)
39
+ return [];
40
+ const victims = mine
41
+ .filter((r) => !r.pendingSync)
42
+ .sort(evictionOrder(isExpired))
43
+ .slice(0, Math.max(0, mine.length - maxEntries));
44
+ for (const v of victims)
45
+ await tx.delete('facts', v.id);
46
+ return victims.map((v) => v.key);
47
+ });
48
+ }
49
+ /**
50
+ * Deletes every expired record in `scope` as a propagating deletion.
51
+ * Returns how many were purged and the store revision after the purge.
52
+ */
53
+ export async function purgeExpired(storage, ctx, scope, isExpired) {
54
+ return storage.transact(WRITE_STORES, 'readwrite', async (tx) => {
55
+ const expired = (await factsInScope(tx, scope)).filter(isExpired);
56
+ if (expired.length === 0)
57
+ return { purged: 0, revision: 0 };
58
+ let counters = await readCounters(tx);
59
+ for (const record of expired)
60
+ counters = await deleteWithTombstone(tx, ctx, record, counters);
61
+ await writeCounters(tx, counters);
62
+ return { purged: expired.length, revision: counters.revision };
63
+ });
64
+ }
65
+ //# sourceMappingURL=factRetention.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factRetention.js","sourceRoot":"","sources":["../../src/storage/factRetention.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * storage/fileLock.ts – Cross-process exclusive lock for the node-file adapter.
3
+ *
4
+ * Two MCP / extension-host processes sharing one memory file each ran
5
+ * refresh → mutate → tmp+rename with nothing between them, so the later rename
6
+ * silently dropped the other's commit. A read-write transaction now holds an
7
+ * exclusive lock file for its whole refresh + mutate + persist:
8
+ *
9
+ * - Acquire = `open(<file>.lock, 'wx')` (atomic create-if-absent), then write
10
+ * our PID into it.
11
+ * - A lock whose recorded PID is no longer alive is STALE and is reclaimed.
12
+ * Staleness is decided by PID liveness, never by age: a slow but live
13
+ * writer (a large fsync) must not have its lock stolen.
14
+ * - Waiting is bounded: past `timeoutMs` the transaction rejects with
15
+ * STORAGE_UNAVAILABLE naming the holder, instead of hanging forever.
16
+ */
17
+ /** The subset of node:fs the lock needs. */
18
+ export interface LockFs {
19
+ openSync(path: string, flags: string): number;
20
+ writeFileSync(fd: number, data: string): void;
21
+ closeSync(fd: number): void;
22
+ readFileSync(path: string, encoding: 'utf8'): string;
23
+ unlinkSync(path: string): void;
24
+ }
25
+ export interface FileLockOptions {
26
+ /** Give up waiting after this long (ms). Default 10 000. */
27
+ timeoutMs?: number;
28
+ /** Poll interval while the lock is held by a live process (ms). Default 20. */
29
+ retryMs?: number;
30
+ /** This process's PID. Default: `process.pid`. */
31
+ pid?: number;
32
+ /** PID liveness probe. Default: `process.kill(pid, 0)`. */
33
+ isAlive?: (pid: number) => boolean;
34
+ /** Injectable sleep (tests). */
35
+ sleep?: (ms: number) => Promise<void>;
36
+ }
37
+ /** `kill(pid, 0)`: ESRCH = gone; EPERM = alive under another user. */
38
+ export declare function defaultIsAlive(pid: number): boolean;
39
+ export declare class FileLock {
40
+ private readonly _fs;
41
+ readonly lockPath: string;
42
+ private readonly _timeoutMs;
43
+ private readonly _retryMs;
44
+ private readonly _pid;
45
+ private readonly _isAlive;
46
+ private readonly _sleep;
47
+ constructor(_fs: LockFs, lockPath: string, opts?: FileLockOptions);
48
+ /** Runs `fn` while holding the lock; always releases it. */
49
+ run<T>(fn: () => Promise<T> | T): Promise<T>;
50
+ private _tryCreate;
51
+ private _acquire;
52
+ /** Removes a dead holder's lock — only if it still names that holder. */
53
+ private _reclaim;
54
+ private _release;
55
+ }
56
+ //# sourceMappingURL=fileLock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileLock.d.ts","sourceRoot":"","sources":["../../src/storage/fileLock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,4CAA4C;AAC5C,MAAM,WAAW,MAAM;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9C,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IACrD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC5B,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACnC,gCAAgC;IAChC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC;AAeD,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASnD;AAeD,qBAAa,QAAQ;IAOL,OAAO,CAAC,QAAQ,CAAC,GAAG;IAAU,QAAQ,CAAC,QAAQ,EAAE,MAAM;IANnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2B;IACpD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;gBAE1B,GAAG,EAAE,MAAM,EAAW,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,eAAoB;IAQ/F,4DAA4D;IACtD,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IASlD,OAAO,CAAC,UAAU;YAgBJ,QAAQ;IAqBtB,yEAAyE;IACzE,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,QAAQ;CASnB"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * storage/fileLock.ts – Cross-process exclusive lock for the node-file adapter.
3
+ *
4
+ * Two MCP / extension-host processes sharing one memory file each ran
5
+ * refresh → mutate → tmp+rename with nothing between them, so the later rename
6
+ * silently dropped the other's commit. A read-write transaction now holds an
7
+ * exclusive lock file for its whole refresh + mutate + persist:
8
+ *
9
+ * - Acquire = `open(<file>.lock, 'wx')` (atomic create-if-absent), then write
10
+ * our PID into it.
11
+ * - A lock whose recorded PID is no longer alive is STALE and is reclaimed.
12
+ * Staleness is decided by PID liveness, never by age: a slow but live
13
+ * writer (a large fsync) must not have its lock stolen.
14
+ * - Waiting is bounded: past `timeoutMs` the transaction rejects with
15
+ * STORAGE_UNAVAILABLE naming the holder, instead of hanging forever.
16
+ */
17
+ import { MemoryStorageError } from './types.js';
18
+ function nodeProcess() {
19
+ return globalThis.process;
20
+ }
21
+ function errorCode(err) {
22
+ return err?.code;
23
+ }
24
+ /** `kill(pid, 0)`: ESRCH = gone; EPERM = alive under another user. */
25
+ export function defaultIsAlive(pid) {
26
+ const proc = nodeProcess();
27
+ if (!proc)
28
+ return true;
29
+ try {
30
+ proc.kill(pid, 0);
31
+ return true;
32
+ }
33
+ catch (err) {
34
+ return errorCode(err) === 'EPERM';
35
+ }
36
+ }
37
+ function readHolder(fs, lockPath) {
38
+ let text;
39
+ try {
40
+ text = fs.readFileSync(lockPath, 'utf8');
41
+ }
42
+ catch (err) {
43
+ // Released between our failed create and this read: retry the create.
44
+ if (errorCode(err) === 'ENOENT')
45
+ return undefined;
46
+ throw err;
47
+ }
48
+ const pid = Number.parseInt(text.trim(), 10);
49
+ return Number.isInteger(pid) && pid > 0 ? pid : undefined;
50
+ }
51
+ export class FileLock {
52
+ _fs;
53
+ lockPath;
54
+ _timeoutMs;
55
+ _retryMs;
56
+ _pid;
57
+ _isAlive;
58
+ _sleep;
59
+ constructor(_fs, lockPath, opts = {}) {
60
+ this._fs = _fs;
61
+ this.lockPath = lockPath;
62
+ this._timeoutMs = opts.timeoutMs ?? 10_000;
63
+ this._retryMs = opts.retryMs ?? 20;
64
+ this._pid = opts.pid ?? nodeProcess()?.pid ?? 0;
65
+ this._isAlive = opts.isAlive ?? defaultIsAlive;
66
+ this._sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
67
+ }
68
+ /** Runs `fn` while holding the lock; always releases it. */
69
+ async run(fn) {
70
+ await this._acquire();
71
+ try {
72
+ return await fn();
73
+ }
74
+ finally {
75
+ this._release();
76
+ }
77
+ }
78
+ _tryCreate() {
79
+ let fd;
80
+ try {
81
+ fd = this._fs.openSync(this.lockPath, 'wx');
82
+ }
83
+ catch (err) {
84
+ if (errorCode(err) === 'EEXIST')
85
+ return false;
86
+ throw new MemoryStorageError('STORAGE_UNAVAILABLE', `Cannot create lock "${this.lockPath}": ${String(err)}`, err);
87
+ }
88
+ try {
89
+ this._fs.writeFileSync(fd, String(this._pid));
90
+ }
91
+ finally {
92
+ this._fs.closeSync(fd);
93
+ }
94
+ return true;
95
+ }
96
+ async _acquire() {
97
+ const deadline = Date.now() + this._timeoutMs;
98
+ let holder;
99
+ for (;;) {
100
+ if (this._tryCreate())
101
+ return;
102
+ holder = readHolder(this._fs, this.lockPath);
103
+ if (holder !== undefined && holder !== this._pid && !this._isAlive(holder)) {
104
+ this._reclaim(holder);
105
+ continue;
106
+ }
107
+ // A live holder, or a lock whose PID is not written yet: wait.
108
+ if (Date.now() >= deadline) {
109
+ throw new MemoryStorageError('STORAGE_UNAVAILABLE', `Timed out after ${this._timeoutMs}ms waiting for "${this.lockPath}" (held by pid ${holder ?? 'unknown'}).`);
110
+ }
111
+ await this._sleep(this._retryMs);
112
+ }
113
+ }
114
+ /** Removes a dead holder's lock — only if it still names that holder. */
115
+ _reclaim(deadPid) {
116
+ if (readHolder(this._fs, this.lockPath) !== deadPid)
117
+ return;
118
+ try {
119
+ this._fs.unlinkSync(this.lockPath);
120
+ }
121
+ catch (err) {
122
+ // Another waiter reclaimed it first; the next create decides.
123
+ if (errorCode(err) !== 'ENOENT')
124
+ throw err;
125
+ }
126
+ }
127
+ _release() {
128
+ try {
129
+ this._fs.unlinkSync(this.lockPath);
130
+ }
131
+ catch (err) {
132
+ if (errorCode(err) !== 'ENOENT') {
133
+ throw new MemoryStorageError('STORAGE_UNAVAILABLE', `Cannot release lock "${this.lockPath}": ${String(err)}`, err);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ //# sourceMappingURL=fileLock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileLock.js","sourceRoot":"","sources":["../../src/storage/fileLock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * storage/fileTransaction.ts – The {@link StorageTransaction} the node-file
3
+ * adapter hands to a unit of work: direct reads/writes on the in-memory
4
+ * document, plus lazily rebuilt key/tag indexes over its facts.
5
+ */
6
+ import { type JournalEntry, type MetaCounters, type StorageStoreName, type StorageTransaction, type StoredFactRecord, type TombstoneRecord } from './types.js';
7
+ /** The on-disk document shape. Versioned so future migrations are explicit. */
8
+ export interface NodeFileDocument {
9
+ formatVersion: 1;
10
+ facts: StoredFactRecord[];
11
+ tombstones: TombstoneRecord[];
12
+ journal: JournalEntry[];
13
+ meta: MetaCounters;
14
+ }
15
+ export declare function emptyDocument(): NodeFileDocument;
16
+ export declare class FileTransaction implements StorageTransaction {
17
+ private readonly _doc;
18
+ /** Lazy key/tag indexes over the in-memory document. Rebuilt after mutations. */
19
+ private _indexDirty;
20
+ private readonly _byKey;
21
+ private readonly _byTag;
22
+ constructor(_doc: NodeFileDocument);
23
+ private _table;
24
+ /** The id field of a table store (`meta` is the singleton and never reaches here). */
25
+ private _idField;
26
+ get<T>(store: StorageStoreName, id: string): Promise<T | undefined>;
27
+ getAll<T>(store: StorageStoreName): Promise<T[]>;
28
+ put(store: StorageStoreName, value: unknown): Promise<void>;
29
+ delete(store: StorageStoreName, id: string): Promise<void>;
30
+ getByIndex<T>(store: StorageStoreName, indexName: string, query: string): Promise<T[] | undefined>;
31
+ private _ensureFactIndexes;
32
+ }
33
+ //# sourceMappingURL=fileTransaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileTransaction.d.ts","sourceRoot":"","sources":["../../src/storage/fileTransaction.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAGH,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACvB,MAAM,YAAY,CAAC;AAEpB,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAC7B,aAAa,EAAE,CAAC,CAAC;IACjB,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,IAAI,EAAE,YAAY,CAAC;CACtB;AAED,wBAAgB,aAAa,IAAI,gBAAgB,CAQhD;AAED,qBAAa,eAAgB,YAAW,kBAAkB;IAM1C,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,iFAAiF;IACjF,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyC;IAChE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyC;gBAEnC,IAAI,EAAE,gBAAgB;IAEnD,OAAO,CAAC,MAAM;IAId,sFAAsF;IACtF,OAAO,CAAC,QAAQ;IAMV,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAKnE,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAIhD,GAAG,CAAC,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAa3D,MAAM,CAAC,KAAK,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1D,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,EAAE,GAAG,SAAS,CAAC;IAUxG,OAAO,CAAC,kBAAkB;CAgB7B"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * storage/fileTransaction.ts – The {@link StorageTransaction} the node-file
3
+ * adapter hands to a unit of work: direct reads/writes on the in-memory
4
+ * document, plus lazily rebuilt key/tag indexes over its facts.
5
+ */
6
+ import { FACT_KEY_INDEX, FACT_TAG_INDEX, } from './types.js';
7
+ export function emptyDocument() {
8
+ return {
9
+ formatVersion: 1,
10
+ facts: [],
11
+ tombstones: [],
12
+ journal: [],
13
+ meta: { key: 'counters', seq: 0, revision: 0, opSeq: 0 },
14
+ };
15
+ }
16
+ export class FileTransaction {
17
+ _doc;
18
+ /** Lazy key/tag indexes over the in-memory document. Rebuilt after mutations. */
19
+ _indexDirty = true;
20
+ _byKey = new Map();
21
+ _byTag = new Map();
22
+ constructor(_doc) {
23
+ this._doc = _doc;
24
+ }
25
+ _table(store) {
26
+ return this._doc[store];
27
+ }
28
+ /** The id field of a table store (`meta` is the singleton and never reaches here). */
29
+ _idField(store) {
30
+ return store === 'journal' ? 'opId' : 'id';
31
+ }
32
+ // `meta` is ONE counters record on disk (`doc.meta`), not an array: the other stores
33
+ // are tables keyed by their id field, `meta` is a singleton keyed 'counters'.
34
+ async get(store, id) {
35
+ if (store === 'meta')
36
+ return (this._doc.meta?.key === id ? this._doc.meta : undefined);
37
+ const f = this._idField(store);
38
+ return this._table(store).find((r) => r[f] === id);
39
+ }
40
+ async getAll(store) {
41
+ if (store === 'meta')
42
+ return (this._doc.meta ? [this._doc.meta] : []);
43
+ return this._table(store).slice();
44
+ }
45
+ async put(store, value) {
46
+ if (store === 'meta') {
47
+ this._doc.meta = value;
48
+ return;
49
+ }
50
+ const f = this._idField(store);
51
+ const table = this._table(store);
52
+ const id = value[f];
53
+ const idx = table.findIndex((r) => r[f] === id);
54
+ if (idx >= 0)
55
+ table[idx] = value;
56
+ else
57
+ table.push(value);
58
+ if (store === 'facts')
59
+ this._indexDirty = true;
60
+ }
61
+ async delete(store, id) {
62
+ if (store === 'meta') {
63
+ if (this._doc.meta?.key === id)
64
+ this._doc.meta = emptyDocument().meta;
65
+ return;
66
+ }
67
+ const f = this._idField(store);
68
+ const table = this._table(store);
69
+ const idx = table.findIndex((r) => r[f] === id);
70
+ if (idx >= 0)
71
+ table.splice(idx, 1);
72
+ if (store === 'facts')
73
+ this._indexDirty = true;
74
+ }
75
+ async getByIndex(store, indexName, query) {
76
+ if (store !== 'facts')
77
+ return undefined;
78
+ if (indexName !== FACT_KEY_INDEX && indexName !== FACT_TAG_INDEX)
79
+ return undefined;
80
+ this._ensureFactIndexes();
81
+ const hits = indexName === FACT_KEY_INDEX
82
+ ? this._byKey.get(query)
83
+ : this._byTag.get(query);
84
+ return (hits ?? []).slice();
85
+ }
86
+ _ensureFactIndexes() {
87
+ if (!this._indexDirty)
88
+ return;
89
+ this._byKey.clear();
90
+ this._byTag.clear();
91
+ for (const fact of this._doc.facts) {
92
+ const keyHits = this._byKey.get(fact.key);
93
+ if (keyHits)
94
+ keyHits.push(fact);
95
+ else
96
+ this._byKey.set(fact.key, [fact]);
97
+ for (const tag of fact.tags ?? []) {
98
+ const tagHits = this._byTag.get(tag);
99
+ if (tagHits)
100
+ tagHits.push(fact);
101
+ else
102
+ this._byTag.set(tag, [fact]);
103
+ }
104
+ }
105
+ this._indexDirty = false;
106
+ }
107
+ }
108
+ //# sourceMappingURL=fileTransaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileTransaction.js","sourceRoot":"","sources":["../../src/storage/fileTransaction.ts"],"names":[],"mappings":"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"}