@mmnto/totem 0.44.0 → 1.1.0

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 (46) hide show
  1. package/dist/ast-classifier.d.ts +8 -0
  2. package/dist/ast-classifier.d.ts.map +1 -1
  3. package/dist/ast-classifier.js +2 -2
  4. package/dist/ast-classifier.js.map +1 -1
  5. package/dist/ast-query.d.ts +18 -0
  6. package/dist/ast-query.d.ts.map +1 -0
  7. package/dist/ast-query.js +177 -0
  8. package/dist/ast-query.js.map +1 -0
  9. package/dist/ast-query.test.d.ts +2 -0
  10. package/dist/ast-query.test.d.ts.map +1 -0
  11. package/dist/ast-query.test.js +79 -0
  12. package/dist/ast-query.test.js.map +1 -0
  13. package/dist/compiler.d.ts +27 -11
  14. package/dist/compiler.d.ts.map +1 -1
  15. package/dist/compiler.js +94 -5
  16. package/dist/compiler.js.map +1 -1
  17. package/dist/compiler.test.js +2 -2
  18. package/dist/compiler.test.js.map +1 -1
  19. package/dist/config-schema.d.ts +8 -0
  20. package/dist/config-schema.d.ts.map +1 -1
  21. package/dist/config-schema.js +4 -0
  22. package/dist/config-schema.js.map +1 -1
  23. package/dist/embedders/ollama-embedder.d.ts.map +1 -1
  24. package/dist/embedders/ollama-embedder.js +5 -1
  25. package/dist/embedders/ollama-embedder.js.map +1 -1
  26. package/dist/index.d.ts +6 -3
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +6 -3
  29. package/dist/index.js.map +1 -1
  30. package/dist/ingest/pipeline.d.ts +11 -0
  31. package/dist/ingest/pipeline.d.ts.map +1 -1
  32. package/dist/ingest/pipeline.js +64 -0
  33. package/dist/ingest/pipeline.js.map +1 -1
  34. package/dist/ingest/sync.d.ts +1 -1
  35. package/dist/ingest/sync.d.ts.map +1 -1
  36. package/dist/ingest/sync.js +1 -1
  37. package/dist/ingest/sync.js.map +1 -1
  38. package/dist/lock.d.ts +18 -0
  39. package/dist/lock.d.ts.map +1 -0
  40. package/dist/lock.js +192 -0
  41. package/dist/lock.js.map +1 -0
  42. package/dist/lock.test.d.ts +2 -0
  43. package/dist/lock.test.d.ts.map +1 -0
  44. package/dist/lock.test.js +94 -0
  45. package/dist/lock.test.js.map +1 -0
  46. package/package.json +1 -1
package/dist/lock.js ADDED
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Lightweight filesystem lock for preventing concurrent mutations
3
+ * to .totem/lessons/ and .lancedb/ directories.
4
+ *
5
+ * Uses a lockfile with PID + timestamp. Stale locks (>30s) are
6
+ * automatically cleaned up to handle crashed processes.
7
+ */
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ const LOCK_FILE = 'sync.lock';
11
+ const STALE_THRESHOLD_MS = 120_000; // 2 minutes — sync can take 30-60s on large repos
12
+ const MAX_RETRIES = 20;
13
+ const BASE_DELAY_MS = 500;
14
+ /**
15
+ * Check if a process is still running via signal 0 (no-op probe).
16
+ * Returns true if alive, false if dead (ESRCH), true on permission errors (assume alive).
17
+ */
18
+ function isProcessAlive(pid) {
19
+ try {
20
+ // Signal 0 does not terminate — it only checks if the process exists
21
+ process.kill(pid, 0); // totem-ignore: signal-0 probe, not a kill
22
+ return true;
23
+ }
24
+ catch (err) {
25
+ return err.code !== 'ESRCH';
26
+ }
27
+ }
28
+ function lockPath(totemDir) {
29
+ return path.join(totemDir, LOCK_FILE);
30
+ }
31
+ function isStale(data) {
32
+ return Date.now() - data.timestamp > STALE_THRESHOLD_MS;
33
+ }
34
+ const MAX_BACKOFF_EXPONENT = 5;
35
+ function backoffDelay(attempt) {
36
+ return BASE_DELAY_MS * Math.pow(2, Math.min(attempt, MAX_BACKOFF_EXPONENT));
37
+ }
38
+ function readLock(filePath) {
39
+ try {
40
+ const raw = fs.readFileSync(filePath, 'utf-8');
41
+ const data = JSON.parse(raw);
42
+ if (data !== null &&
43
+ typeof data === 'object' &&
44
+ typeof data.pid === 'number' &&
45
+ typeof data.timestamp === 'number') {
46
+ return data;
47
+ }
48
+ return null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ /**
55
+ * Attempt to acquire the sync lock.
56
+ * Returns a release function on success, or throws after retries are exhausted.
57
+ */
58
+ export async function acquireLock(totemDir, onWarn) {
59
+ const file = lockPath(totemDir);
60
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
61
+ const existing = readLock(file);
62
+ // Corrupted lockfile (empty, bad JSON) — remove if not mid-write
63
+ if (!existing && fs.existsSync(file)) {
64
+ try {
65
+ const stat = fs.statSync(file);
66
+ const ageMs = Date.now() - stat.mtimeMs;
67
+ if (ageMs > 1000) {
68
+ // File is old enough to be genuinely corrupted, not mid-write
69
+ fs.unlinkSync(file);
70
+ }
71
+ else {
72
+ // File may be mid-write by another process — wait and retry
73
+ const delay = backoffDelay(attempt);
74
+ await new Promise((resolve) => setTimeout(resolve, delay));
75
+ }
76
+ }
77
+ catch {
78
+ // Another process may have cleaned it up
79
+ }
80
+ continue;
81
+ }
82
+ if (existing) {
83
+ if (isStale(existing)) {
84
+ // Stale timestamp is the final authority — delete regardless of PID liveness.
85
+ // Per lesson-5b1929d1: PID checks fail across container namespaces.
86
+ onWarn?.(`Removing stale lock from PID ${existing.pid} (${Math.round((Date.now() - existing.timestamp) / 1000)}s old)`);
87
+ try {
88
+ fs.unlinkSync(file);
89
+ }
90
+ catch {
91
+ // Another process may have cleaned it up
92
+ }
93
+ }
94
+ else if (!isProcessAlive(existing.pid)) {
95
+ // Lock is fresh but owning process is dead (e.g., Ctrl+C during sync)
96
+ onWarn?.(`Removing orphaned lock from dead PID ${existing.pid} (${Math.round((Date.now() - existing.timestamp) / 1000)}s old)`);
97
+ try {
98
+ fs.unlinkSync(file);
99
+ }
100
+ catch {
101
+ // Another process may have cleaned it up
102
+ }
103
+ }
104
+ else {
105
+ // Lock is held by a live process — wait and retry
106
+ if (attempt === 0) {
107
+ onWarn?.(`Waiting for sync lock (held by PID ${existing.pid})...`);
108
+ }
109
+ const delay = backoffDelay(attempt);
110
+ await new Promise((resolve) => setTimeout(resolve, delay));
111
+ continue;
112
+ }
113
+ }
114
+ // Try to acquire (ensure directory exists)
115
+ const lockData = { pid: process.pid, timestamp: Date.now() };
116
+ try {
117
+ fs.mkdirSync(path.dirname(file), { recursive: true });
118
+ fs.writeFileSync(file, JSON.stringify(lockData), { flag: 'wx' });
119
+ // Acquired — return release function
120
+ return () => {
121
+ try {
122
+ fs.unlinkSync(file);
123
+ }
124
+ catch {
125
+ // Already cleaned up
126
+ }
127
+ };
128
+ }
129
+ catch (err) {
130
+ // EEXIST = another process grabbed it between our read and write
131
+ const code = err.code;
132
+ if (code === 'EEXIST') {
133
+ const delay = backoffDelay(attempt);
134
+ await new Promise((resolve) => setTimeout(resolve, delay));
135
+ continue;
136
+ }
137
+ throw err;
138
+ }
139
+ }
140
+ throw new Error(`[Totem Error] Could not acquire sync lock after ${MAX_RETRIES} attempts. ` +
141
+ `Another totem process may be running. Check ${file} or delete it manually.`);
142
+ }
143
+ /**
144
+ * Execute a function while holding the sync lock.
145
+ * Automatically releases the lock when done (even on error).
146
+ */
147
+ export async function withLock(totemDir, fn, onWarn) {
148
+ const release = await acquireLock(totemDir, onWarn);
149
+ // Best-effort cleanup on process termination (Ctrl+C, kill)
150
+ const removeHandlers = () => {
151
+ process.removeListener('SIGINT', onSigint);
152
+ process.removeListener('SIGTERM', onSigterm);
153
+ if (process.platform !== 'win32') {
154
+ process.removeListener('SIGHUP', onSighup);
155
+ process.removeListener('SIGQUIT', onSigquit);
156
+ }
157
+ };
158
+ const onSigint = () => {
159
+ release();
160
+ removeHandlers();
161
+ process.kill(process.pid, 'SIGINT'); // totem-ignore: re-raising caught signal
162
+ };
163
+ const onSigterm = () => {
164
+ release();
165
+ removeHandlers();
166
+ process.kill(process.pid, 'SIGTERM'); // totem-ignore: re-raising caught signal
167
+ };
168
+ const onSighup = () => {
169
+ release();
170
+ removeHandlers();
171
+ process.kill(process.pid, 'SIGHUP'); // totem-ignore: re-raising caught signal
172
+ };
173
+ const onSigquit = () => {
174
+ release();
175
+ removeHandlers();
176
+ process.kill(process.pid, 'SIGQUIT'); // totem-ignore: re-raising caught signal
177
+ };
178
+ process.on('SIGINT', onSigint);
179
+ process.on('SIGTERM', onSigterm);
180
+ if (process.platform !== 'win32') {
181
+ process.on('SIGHUP', onSighup);
182
+ process.on('SIGQUIT', onSigquit);
183
+ }
184
+ try {
185
+ return await fn();
186
+ }
187
+ finally {
188
+ removeHandlers();
189
+ release();
190
+ }
191
+ }
192
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../src/lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lock.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.test.d.ts","sourceRoot":"","sources":["../src/lock.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,94 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { acquireLock, withLock } from './lock.js';
6
+ describe('acquireLock', () => {
7
+ let tmpDir;
8
+ beforeEach(() => {
9
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-lock-'));
10
+ });
11
+ afterEach(() => {
12
+ fs.rmSync(tmpDir, { recursive: true, force: true });
13
+ });
14
+ it('acquires and releases a lock', async () => {
15
+ const release = await acquireLock(tmpDir);
16
+ expect(fs.existsSync(path.join(tmpDir, 'sync.lock'))).toBe(true);
17
+ release();
18
+ expect(fs.existsSync(path.join(tmpDir, 'sync.lock'))).toBe(false);
19
+ });
20
+ it('second caller waits and succeeds after first releases', async () => {
21
+ const release1 = await acquireLock(tmpDir);
22
+ // Start second acquisition (will wait)
23
+ let acquired2 = false;
24
+ const promise2 = acquireLock(tmpDir).then((release) => {
25
+ acquired2 = true;
26
+ return release;
27
+ });
28
+ // Second should still be waiting
29
+ await new Promise((r) => setTimeout(r, 50));
30
+ expect(acquired2).toBe(false);
31
+ // Release first — second should acquire
32
+ release1();
33
+ const release2 = await promise2;
34
+ expect(acquired2).toBe(true);
35
+ release2();
36
+ });
37
+ it('cleans up stale locks from dead processes', async () => {
38
+ // Write a stale lock with a PID guaranteed not to exist (PID 1 is init/system, use a very high PID)
39
+ const lockPath = path.join(tmpDir, 'sync.lock');
40
+ const deadPid = 2_147_483_647; // max 32-bit PID — virtually guaranteed to be unused
41
+ fs.writeFileSync(lockPath, JSON.stringify({ pid: deadPid, timestamp: Date.now() - 130_000 }));
42
+ const warnings = [];
43
+ const release = await acquireLock(tmpDir, (msg) => warnings.push(msg));
44
+ expect(warnings.some((w) => w.includes('stale') || w.includes('dead'))).toBe(true);
45
+ release();
46
+ });
47
+ it('withLock releases on success', async () => {
48
+ const result = await withLock(tmpDir, async () => 42);
49
+ expect(result).toBe(42);
50
+ expect(fs.existsSync(path.join(tmpDir, 'sync.lock'))).toBe(false);
51
+ });
52
+ it('withLock releases on error', async () => {
53
+ await expect(withLock(tmpDir, async () => {
54
+ throw new Error('boom');
55
+ })).rejects.toThrow('boom');
56
+ expect(fs.existsSync(path.join(tmpDir, 'sync.lock'))).toBe(false);
57
+ });
58
+ it('withLock serializes concurrent operations', async () => {
59
+ const order = [];
60
+ const op = (id, ms) => withLock(tmpDir, async () => {
61
+ order.push(id);
62
+ await new Promise((r) => setTimeout(r, ms));
63
+ order.push(id * 10);
64
+ });
65
+ // Launch two operations concurrently — second must wait for first
66
+ await Promise.all([op(1, 100), op(2, 50)]);
67
+ // First operation should fully complete before second starts
68
+ expect(order[0]).toBe(1);
69
+ expect(order[1]).toBe(10);
70
+ expect(order[2]).toBe(2);
71
+ expect(order[3]).toBe(20);
72
+ });
73
+ it('recovers from corrupted lockfile', async () => {
74
+ const lockFile = path.join(tmpDir, 'sync.lock');
75
+ fs.writeFileSync(lockFile, 'not valid json');
76
+ const release = await acquireLock(tmpDir);
77
+ expect(fs.existsSync(lockFile)).toBe(true);
78
+ release();
79
+ });
80
+ it('recovers from empty lockfile', async () => {
81
+ const lockFile = path.join(tmpDir, 'sync.lock');
82
+ fs.writeFileSync(lockFile, '');
83
+ const release = await acquireLock(tmpDir);
84
+ expect(fs.existsSync(lockFile)).toBe(true);
85
+ release();
86
+ });
87
+ it('creates totemDir if it does not exist', async () => {
88
+ const nested = path.join(tmpDir, 'deep', 'nested', '.totem');
89
+ const release = await acquireLock(nested);
90
+ expect(fs.existsSync(path.join(nested, 'sync.lock'))).toBe(true);
91
+ release();
92
+ });
93
+ });
94
+ //# sourceMappingURL=lock.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.test.js","sourceRoot":"","sources":["../src/lock.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmnto/totem",
3
- "version": "0.44.0",
3
+ "version": "1.1.0",
4
4
  "description": "Persistent memory and context layer for AI agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",