sensemaking 0.24.3 → 0.24.5

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 (131) hide show
  1. package/README.md +16 -12
  2. package/dist/cjs/cli/status.js +14 -24
  3. package/dist/cjs/cli/status.js.map +1 -1
  4. package/dist/cjs/commands/search.js +22 -0
  5. package/dist/cjs/commands/search.js.map +1 -1
  6. package/dist/cjs/config/types.js.map +1 -1
  7. package/dist/cjs/errors.d.cts +1 -1
  8. package/dist/cjs/errors.d.ts +1 -1
  9. package/dist/cjs/errors.js.map +1 -1
  10. package/dist/cjs/lib/worker-file.d.cts +1 -0
  11. package/dist/cjs/lib/worker-file.d.ts +1 -0
  12. package/dist/cjs/lib/worker-file.js +34 -0
  13. package/dist/cjs/lib/worker-file.js.map +1 -0
  14. package/dist/cjs/output/search-error.js +38 -0
  15. package/dist/cjs/output/search-error.js.map +1 -1
  16. package/dist/cjs/scan/pool.js +2 -23
  17. package/dist/cjs/scan/pool.js.map +1 -1
  18. package/dist/cjs/store/builder.d.cts +2 -1
  19. package/dist/cjs/store/builder.d.ts +2 -1
  20. package/dist/cjs/store/builder.js +4 -4
  21. package/dist/cjs/store/builder.js.map +1 -1
  22. package/dist/cjs/store/duckdb/lexical.d.cts +1 -0
  23. package/dist/cjs/store/duckdb/lexical.d.ts +1 -0
  24. package/dist/cjs/store/duckdb/lexical.js +28 -9
  25. package/dist/cjs/store/duckdb/lexical.js.map +1 -1
  26. package/dist/cjs/store/duckdb/native.d.cts +2 -0
  27. package/dist/cjs/store/duckdb/native.d.ts +2 -0
  28. package/dist/cjs/store/duckdb/native.js +11 -1
  29. package/dist/cjs/store/duckdb/native.js.map +1 -1
  30. package/dist/cjs/store/duckdb/ordered-bm25.d.cts +17 -0
  31. package/dist/cjs/store/duckdb/ordered-bm25.d.ts +17 -0
  32. package/dist/cjs/store/duckdb/ordered-bm25.js +238 -0
  33. package/dist/cjs/store/duckdb/ordered-bm25.js.map +1 -0
  34. package/dist/cjs/store/lock-wait.d.cts +2 -2
  35. package/dist/cjs/store/lock-wait.d.ts +2 -2
  36. package/dist/cjs/store/lock-wait.js +14 -11
  37. package/dist/cjs/store/lock-wait.js.map +1 -1
  38. package/dist/cjs/store/native.d.cts +4 -0
  39. package/dist/cjs/store/native.d.ts +4 -0
  40. package/dist/cjs/store/native.js +64 -9
  41. package/dist/cjs/store/native.js.map +1 -1
  42. package/dist/cjs/store/open.js +11 -7
  43. package/dist/cjs/store/open.js.map +1 -1
  44. package/dist/cjs/store/reconcile.d.cts +5 -1
  45. package/dist/cjs/store/reconcile.d.ts +5 -1
  46. package/dist/cjs/store/reconcile.js +5 -4
  47. package/dist/cjs/store/reconcile.js.map +1 -1
  48. package/dist/cjs/store/turso/connection.js +2 -2
  49. package/dist/cjs/store/turso/connection.js.map +1 -1
  50. package/dist/cjs/store/turso/lexical.js +5 -1
  51. package/dist/cjs/store/turso/lexical.js.map +1 -1
  52. package/dist/cjs/store/turso/reconcile.js +7 -6
  53. package/dist/cjs/store/turso/reconcile.js.map +1 -1
  54. package/dist/cjs/text/segment.js +168 -3
  55. package/dist/cjs/text/segment.js.map +1 -1
  56. package/dist/cjs/watch-claim.d.cts +39 -0
  57. package/dist/cjs/watch-claim.d.ts +39 -0
  58. package/dist/cjs/watch-claim.js +446 -0
  59. package/dist/cjs/watch-claim.js.map +1 -0
  60. package/dist/cjs/watch.js +240 -141
  61. package/dist/cjs/watch.js.map +1 -1
  62. package/dist/cjs/workers/watch-heartbeat.d.cts +1 -0
  63. package/dist/cjs/workers/watch-heartbeat.d.ts +1 -0
  64. package/dist/cjs/workers/watch-heartbeat.js +308 -0
  65. package/dist/cjs/workers/watch-heartbeat.js.map +1 -0
  66. package/dist/esm/cli/status.js +6 -5
  67. package/dist/esm/cli/status.js.map +1 -1
  68. package/dist/esm/commands/search.js +21 -0
  69. package/dist/esm/commands/search.js.map +1 -1
  70. package/dist/esm/config/types.js.map +1 -1
  71. package/dist/esm/errors.d.ts +1 -1
  72. package/dist/esm/errors.js.map +1 -1
  73. package/dist/esm/lib/worker-file.d.ts +1 -0
  74. package/dist/esm/lib/worker-file.js +24 -0
  75. package/dist/esm/lib/worker-file.js.map +1 -0
  76. package/dist/esm/output/search-error.js +21 -0
  77. package/dist/esm/output/search-error.js.map +1 -1
  78. package/dist/esm/scan/pool.js +2 -22
  79. package/dist/esm/scan/pool.js.map +1 -1
  80. package/dist/esm/store/builder.d.ts +2 -1
  81. package/dist/esm/store/builder.js +4 -4
  82. package/dist/esm/store/builder.js.map +1 -1
  83. package/dist/esm/store/duckdb/lexical.d.ts +1 -0
  84. package/dist/esm/store/duckdb/lexical.js +16 -7
  85. package/dist/esm/store/duckdb/lexical.js.map +1 -1
  86. package/dist/esm/store/duckdb/native.d.ts +2 -0
  87. package/dist/esm/store/duckdb/native.js +5 -1
  88. package/dist/esm/store/duckdb/native.js.map +1 -1
  89. package/dist/esm/store/duckdb/ordered-bm25.d.ts +17 -0
  90. package/dist/esm/store/duckdb/ordered-bm25.js +56 -0
  91. package/dist/esm/store/duckdb/ordered-bm25.js.map +1 -0
  92. package/dist/esm/store/lock-wait.d.ts +2 -2
  93. package/dist/esm/store/lock-wait.js +17 -14
  94. package/dist/esm/store/lock-wait.js.map +1 -1
  95. package/dist/esm/store/native.d.ts +4 -0
  96. package/dist/esm/store/native.js +60 -6
  97. package/dist/esm/store/native.js.map +1 -1
  98. package/dist/esm/store/open.js +9 -6
  99. package/dist/esm/store/open.js.map +1 -1
  100. package/dist/esm/store/reconcile.d.ts +5 -1
  101. package/dist/esm/store/reconcile.js +4 -3
  102. package/dist/esm/store/reconcile.js.map +1 -1
  103. package/dist/esm/store/turso/connection.js +1 -1
  104. package/dist/esm/store/turso/connection.js.map +1 -1
  105. package/dist/esm/store/turso/lexical.js +5 -1
  106. package/dist/esm/store/turso/lexical.js.map +1 -1
  107. package/dist/esm/store/turso/reconcile.js +8 -5
  108. package/dist/esm/store/turso/reconcile.js.map +1 -1
  109. package/dist/esm/text/segment.js +130 -4
  110. package/dist/esm/text/segment.js.map +1 -1
  111. package/dist/esm/watch-claim.d.ts +39 -0
  112. package/dist/esm/watch-claim.js +176 -0
  113. package/dist/esm/watch-claim.js.map +1 -0
  114. package/dist/esm/watch.js +168 -117
  115. package/dist/esm/watch.js.map +1 -1
  116. package/dist/esm/workers/watch-heartbeat.d.ts +1 -0
  117. package/dist/esm/workers/watch-heartbeat.js +106 -0
  118. package/dist/esm/workers/watch-heartbeat.js.map +1 -0
  119. package/package.json +2 -2
  120. package/skills/sense/SKILL.md +105 -104
  121. package/skills/sense/references/search.md +47 -0
  122. package/skills/sense/references/sql.md +120 -0
  123. package/skills/sense/references/stores/duckdb.md +35 -0
  124. package/skills/sense/references/stores/sqlite.md +66 -0
  125. package/skills/sense/references/stores/turso.md +36 -0
  126. package/skills/sense-setup/SKILL.md +93 -34
  127. package/skills/sense-setup/references/embeddings.md +51 -0
  128. package/skills/sense-setup/references/store-benchmarks.md +16 -0
  129. package/skills/sense-setup/references/stores/duckdb.md +28 -0
  130. package/skills/sense-setup/references/stores/sqlite.md +21 -0
  131. package/skills/sense-setup/references/stores/turso.md +25 -0
package/dist/esm/watch.js CHANGED
@@ -2,137 +2,188 @@ import { watch as fsWatch } from 'node:fs';
2
2
  import { STATE_DIR } from './config/index.js';
3
3
  import { SenseError } from './errors.js';
4
4
  import { guardedTick } from './lib/guarded-tick.js';
5
- import { docCount, getMeta, openStore, setMeta } from './store/index.js';
5
+ import { docCount, openStore } from './store/index.js';
6
+ import { startWatchClaim, WATCH_HEARTBEAT_INTERVAL_MS } from './watch-claim.js';
6
7
  // Watch is a cache pre-warmer, not a correctness mechanism: open() always reconciles anyway, so any fs event just triggers a debounced full reconcile.
7
8
  const DEBOUNCE_MS = 200;
8
- const HEARTBEAT_INTERVAL_MS = 5000;
9
- const STALE_HEARTBEAT_MS = 15000;
10
- // Runs in the foreground until SIGINT/SIGTERM/signal abort. duckdb/turso hand the cache file to
11
- // one process at a time, so every cycle opens, reconciles, writes the heartbeat/pid rows, and closes.
9
+ // Runs in the foreground until SIGINT/SIGTERM/signal abort. DuckDB and Turso hand the cache file to
10
+ // one process at a time, so every reconcile cycle opens and closes instead of holding an idle lock.
12
11
  export async function runWatch(cfg, opts = {}) {
13
- var _opts_onEvent, _opts_debounceMs, _opts_heartbeatIntervalMs, _cfg_rootDir;
12
+ var _opts_onEvent, _opts_debounceMs, _opts_heartbeatIntervalMs, _cfg_rootDir, _cfg_configDir, _opts_force;
14
13
  const onEvent = (_opts_onEvent = opts.onEvent) !== null && _opts_onEvent !== void 0 ? _opts_onEvent : ()=>{};
15
14
  const debounceMs = (_opts_debounceMs = opts.debounceMs) !== null && _opts_debounceMs !== void 0 ? _opts_debounceMs : DEBOUNCE_MS;
16
- const heartbeatIntervalMs = (_opts_heartbeatIntervalMs = opts.heartbeatIntervalMs) !== null && _opts_heartbeatIntervalMs !== void 0 ? _opts_heartbeatIntervalMs : HEARTBEAT_INTERVAL_MS;
17
- const { store: initialStore, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);
18
- // openStore normalizes the legacy direct-API baseDir before a watcher touches the filesystem.
15
+ const reconcileIntervalMs = (_opts_heartbeatIntervalMs = opts.heartbeatIntervalMs) !== null && _opts_heartbeatIntervalMs !== void 0 ? _opts_heartbeatIntervalMs : WATCH_HEARTBEAT_INTERVAL_MS;
16
+ if (!Number.isFinite(reconcileIntervalMs) || reconcileIntervalMs <= 0) throw new SenseError('CONFIG_INVALID', 'watch heartbeatIntervalMs must be a finite positive number');
17
+ // Reconciliation may be parked for a caller, but live ownership must renew before fixed expiry.
18
+ const claimIntervalMs = Math.min(reconcileIntervalMs, WATCH_HEARTBEAT_INTERVAL_MS);
19
19
  const rootDir = (_cfg_rootDir = cfg.rootDir) !== null && _cfg_rootDir !== void 0 ? _cfg_rootDir : cfg.baseDir;
20
- const existingHeartbeat = await getMeta(initialStore, 'watch_heartbeat');
21
- if (existingHeartbeat && !opts.force) {
22
- const age = Date.now() - Date.parse(existingHeartbeat);
23
- if (age >= 0 && age < STALE_HEARTBEAT_MS) {
24
- await initialStore.close();
25
- throw new SenseError('WATCH_ACTIVE', `another watcher appears active (heartbeat ${Math.round(age / 1000)}s ago); use --force to override`);
26
- }
27
- }
28
- // total is read before close: an event fires only once nothing is held open again.
29
- const initialTotal = initialWarnings.length > 0 || initialParsed > 0 ? await docCount(initialStore) : 0;
30
- await setMeta(initialStore, 'watch_heartbeat', new Date().toISOString());
31
- await setMeta(initialStore, 'watch_pid', String(process.pid));
32
- await initialStore.close();
33
- onEvent({
34
- type: 'started',
35
- rootDir,
36
- dbPath
20
+ const configDir = (_cfg_configDir = cfg.configDir) !== null && _cfg_configDir !== void 0 ? _cfg_configDir : rootDir;
21
+ let claimFailure = null;
22
+ let requestShutdown;
23
+ const claim = await startWatchClaim(configDir, (_opts_force = opts.force) !== null && _opts_force !== void 0 ? _opts_force : false, claimIntervalMs, (err)=>{
24
+ claimFailure !== null && claimFailure !== void 0 ? claimFailure : claimFailure = err;
25
+ requestShutdown === null || requestShutdown === void 0 ? void 0 : requestShutdown(err);
37
26
  });
38
- if (initialWarnings.length > 0 || initialParsed > 0) {
39
- onEvent({
40
- type: 'reconciled',
41
- parsed: initialParsed,
42
- total: initialTotal,
43
- warnings: initialWarnings
44
- });
45
- }
46
- let stopping = false;
47
- // At most one open+reconcile+close cycle runs at a time; a trigger that arrives mid-cycle is
48
- // coalesced into a single rerun instead of opening a second, overlapping connection.
49
- let running = null;
50
- let queuedAlways = null;
51
- const tick = async (alwaysEmit)=>{
27
+ let claimCloseStarted = false;
28
+ const closeClaim = ()=>{
29
+ claimCloseStarted = true;
30
+ return claim.close();
31
+ };
32
+ try {
33
+ const { store: initialStore, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);
34
+ let initialTotal = 0;
52
35
  try {
53
- const { store, parsed, warnings } = await openStore(cfg);
36
+ // total is read before close: an event fires only once nothing is held open again.
37
+ initialTotal = initialWarnings.length > 0 || initialParsed > 0 ? await docCount(initialStore) : 0;
38
+ } finally{
39
+ await initialStore.close();
40
+ }
41
+ if (claimFailure) throw claimFailure;
42
+ let stopping = false;
43
+ // At most one open+reconcile+close cycle runs at a time; a trigger that arrives mid-cycle is
44
+ // coalesced into a single rerun instead of opening a second, overlapping connection.
45
+ let running = null;
46
+ let queuedAlways = null;
47
+ const tick = async (alwaysEmit)=>{
54
48
  try {
55
- await setMeta(store, 'watch_heartbeat', new Date().toISOString());
56
- await setMeta(store, 'watch_pid', String(process.pid));
57
- if (alwaysEmit || parsed > 0 || warnings.length > 0) {
58
- onEvent({
59
- type: 'reconciled',
60
- parsed,
61
- total: await docCount(store),
62
- warnings
63
- });
49
+ const { store, parsed, warnings } = await openStore(cfg);
50
+ try {
51
+ if (alwaysEmit || parsed > 0 || warnings.length > 0) {
52
+ onEvent({
53
+ type: 'reconciled',
54
+ parsed,
55
+ total: await docCount(store),
56
+ warnings
57
+ });
58
+ }
59
+ } finally{
60
+ await store.close();
64
61
  }
65
- } finally{
66
- await store.close();
62
+ } catch (err) {
63
+ onEvent({
64
+ type: 'reconcile-error',
65
+ message: err.message
66
+ });
67
67
  }
68
- } catch (err) {
69
- onEvent({
70
- type: 'reconcile-error',
71
- message: err.message
68
+ };
69
+ const startCycle = (alwaysEmit)=>{
70
+ running = tick(alwaysEmit).then(()=>{
71
+ running = null;
72
+ if (queuedAlways !== null && !stopping) {
73
+ const again = queuedAlways;
74
+ queuedAlways = null;
75
+ startCycle(again);
76
+ }
72
77
  });
73
- }
74
- };
75
- const startCycle = (alwaysEmit)=>{
76
- running = tick(alwaysEmit).then(()=>{
77
- running = null;
78
- if (queuedAlways !== null && !stopping) {
79
- const again = queuedAlways;
80
- queuedAlways = null;
81
- startCycle(again);
82
- }
83
- });
84
- };
85
- const requestCycle = (alwaysEmit)=>{
86
- if (stopping) return;
87
- if (running) {
88
- queuedAlways = queuedAlways || alwaysEmit;
89
- return;
90
- }
91
- startCycle(alwaysEmit);
92
- };
93
- let debounceTimer = null;
94
- const scheduleReconcile = ()=>{
95
- if (debounceTimer) clearTimeout(debounceTimer);
96
- debounceTimer = setTimeout(()=>{
97
- debounceTimer = null;
98
- requestCycle(true);
99
- }, debounceMs);
100
- };
101
- // Ignore our own state dir, or the heartbeat write would retrigger itself forever. An event with
102
- // an unresolvable filename (null, which fs.watch delivers under load) reconciles: parsing nothing costs less than missing a real edit.
103
- const watcher = fsWatch(rootDir, {
104
- recursive: true
105
- }, (_event, filename)=>{
106
- if (typeof filename === 'string' && filename.startsWith(STATE_DIR)) return;
107
- scheduleReconcile();
108
- });
109
- const heartbeatTimer = setInterval(guardedTick(async ()=>requestCycle(false), ()=>stopping), heartbeatIntervalMs);
110
- return new Promise((resolveShutdown)=>{
111
- var _opts_signal, _opts_signal1;
112
- // SIGINT/SIGTERM and an aborted signal all run this same path exactly once; each is
113
- // unregistered here too so a second runWatch call in the same process starts clean.
114
- const shutdown = async ()=>{
115
- var _opts_signal;
78
+ };
79
+ const requestCycle = (alwaysEmit)=>{
116
80
  if (stopping) return;
117
- stopping = true;
118
- process.off('SIGINT', shutdown);
119
- process.off('SIGTERM', shutdown);
120
- (_opts_signal = opts.signal) === null || _opts_signal === void 0 ? void 0 : _opts_signal.removeEventListener('abort', shutdown);
121
- clearInterval(heartbeatTimer);
81
+ if (running) {
82
+ queuedAlways = queuedAlways || alwaysEmit;
83
+ return;
84
+ }
85
+ startCycle(alwaysEmit);
86
+ };
87
+ let debounceTimer = null;
88
+ const scheduleReconcile = ()=>{
122
89
  if (debounceTimer) clearTimeout(debounceTimer);
123
- watcher.close();
124
- await running;
125
- const { store: finalStore } = await openStore(cfg);
126
- await setMeta(finalStore, 'watch_heartbeat', null);
127
- await setMeta(finalStore, 'watch_pid', null);
128
- await finalStore.close();
129
- resolveShutdown();
90
+ debounceTimer = setTimeout(()=>{
91
+ debounceTimer = null;
92
+ requestCycle(true);
93
+ }, debounceMs);
130
94
  };
131
- process.once('SIGINT', shutdown);
132
- process.once('SIGTERM', shutdown);
133
- if ((_opts_signal = opts.signal) === null || _opts_signal === void 0 ? void 0 : _opts_signal.aborted) shutdown();
134
- else (_opts_signal1 = opts.signal) === null || _opts_signal1 === void 0 ? void 0 : _opts_signal1.addEventListener('abort', shutdown, {
135
- once: true
95
+ // Ignore our own state files, or cache and claim writes would retrigger reconciliation forever.
96
+ // An unresolvable filename reconciles because parsing nothing costs less than missing a real edit.
97
+ const watcher = fsWatch(rootDir, {
98
+ recursive: true
99
+ }, (_event, filename)=>{
100
+ if (typeof filename === 'string' && filename.startsWith(STATE_DIR)) return;
101
+ scheduleReconcile();
136
102
  });
137
- });
103
+ const reconcileTimer = setInterval(guardedTick(async ()=>requestCycle(false), ()=>stopping), reconcileIntervalMs);
104
+ return await new Promise((resolveShutdown, rejectShutdown)=>{
105
+ var _opts_signal;
106
+ let shutdownReason = null;
107
+ const shutdown = (reason)=>{
108
+ shutdownReason !== null && shutdownReason !== void 0 ? shutdownReason : shutdownReason = reason !== null && reason !== void 0 ? reason : null;
109
+ if (stopping) return;
110
+ stopping = true;
111
+ const shutdownTask = (async ()=>{
112
+ var _opts_signal;
113
+ process.off('SIGINT', stopNormally);
114
+ process.off('SIGTERM', stopNormally);
115
+ (_opts_signal = opts.signal) === null || _opts_signal === void 0 ? void 0 : _opts_signal.removeEventListener('abort', stopNormally);
116
+ clearInterval(reconcileTimer);
117
+ if (debounceTimer) clearTimeout(debounceTimer);
118
+ watcher.close();
119
+ await running;
120
+ let finalError;
121
+ try {
122
+ // A normal stop performs one final freshness pass. Ownership loss drains existing work
123
+ // but starts nothing new because the replacement watcher now owns that responsibility.
124
+ if (!shutdownReason) {
125
+ const { store: finalStore } = await openStore(cfg);
126
+ await finalStore.close();
127
+ }
128
+ } catch (err) {
129
+ finalError = err;
130
+ }
131
+ try {
132
+ await closeClaim();
133
+ } catch (err) {
134
+ if (finalError) throw new AggregateError([
135
+ finalError,
136
+ err
137
+ ], 'watch shutdown and claim cleanup both failed');
138
+ throw err;
139
+ }
140
+ if (finalError) throw finalError;
141
+ if (shutdownReason) throw shutdownReason;
142
+ })();
143
+ shutdownTask.then(resolveShutdown, rejectShutdown);
144
+ };
145
+ const stopNormally = ()=>shutdown();
146
+ requestShutdown = shutdown;
147
+ process.once('SIGINT', stopNormally);
148
+ process.once('SIGTERM', stopNormally);
149
+ if (claimFailure) shutdown(claimFailure);
150
+ else if ((_opts_signal = opts.signal) === null || _opts_signal === void 0 ? void 0 : _opts_signal.aborted) shutdown();
151
+ else {
152
+ var _opts_signal1;
153
+ (_opts_signal1 = opts.signal) === null || _opts_signal1 === void 0 ? void 0 : _opts_signal1.addEventListener('abort', stopNormally, {
154
+ once: true
155
+ });
156
+ try {
157
+ onEvent({
158
+ type: 'started',
159
+ rootDir,
160
+ dbPath
161
+ });
162
+ if (!stopping && (initialWarnings.length > 0 || initialParsed > 0)) {
163
+ onEvent({
164
+ type: 'reconciled',
165
+ parsed: initialParsed,
166
+ total: initialTotal,
167
+ warnings: initialWarnings
168
+ });
169
+ }
170
+ } catch (err) {
171
+ shutdown(err instanceof Error ? err : new Error(String(err)));
172
+ }
173
+ }
174
+ });
175
+ } catch (err) {
176
+ if (claimCloseStarted) throw err;
177
+ try {
178
+ await closeClaim();
179
+ } catch (closeError) {
180
+ throw new AggregateError([
181
+ err,
182
+ closeError
183
+ ], 'watch setup and claim cleanup both failed');
184
+ }
185
+ throw err;
186
+ } finally{
187
+ requestShutdown = undefined;
188
+ }
138
189
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/watch.ts"],"sourcesContent":["import { watch as fsWatch } from 'node:fs';\nimport type { ResolvedConfig } from './config/index.ts';\nimport { STATE_DIR } from './config/index.ts';\nimport { SenseError } from './errors.ts';\nimport { guardedTick } from './lib/guarded-tick.ts';\nimport { docCount, getMeta, openStore, setMeta } from './store/index.ts';\n\n// Watch is a cache pre-warmer, not a correctness mechanism: open() always reconciles anyway, so any fs event just triggers a debounced full reconcile.\nconst DEBOUNCE_MS = 200;\nconst HEARTBEAT_INTERVAL_MS = 5000;\nconst STALE_HEARTBEAT_MS = 15000;\n\nexport type WatchEvent = { type: 'started'; rootDir: string; dbPath: string } | { type: 'reconciled'; parsed: number; total: number; warnings: string[] } | { type: 'reconcile-error'; message: string };\n\nexport interface WatchOptions {\n force?: boolean;\n onEvent?: (event: WatchEvent) => void;\n // Aborting runs the same shutdown path as SIGINT/SIGTERM.\n signal?: AbortSignal;\n debounceMs?: number;\n heartbeatIntervalMs?: number;\n}\n\n// Runs in the foreground until SIGINT/SIGTERM/signal abort. duckdb/turso hand the cache file to\n// one process at a time, so every cycle opens, reconciles, writes the heartbeat/pid rows, and closes.\nexport async function runWatch(cfg: ResolvedConfig, opts: WatchOptions = {}): Promise<void> {\n const onEvent = opts.onEvent ?? (() => {});\n const debounceMs = opts.debounceMs ?? DEBOUNCE_MS;\n const heartbeatIntervalMs = opts.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS;\n const { store: initialStore, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);\n // openStore normalizes the legacy direct-API baseDir before a watcher touches the filesystem.\n const rootDir = cfg.rootDir ?? cfg.baseDir;\n const existingHeartbeat = await getMeta(initialStore, 'watch_heartbeat');\n if (existingHeartbeat && !opts.force) {\n const age = Date.now() - Date.parse(existingHeartbeat);\n if (age >= 0 && age < STALE_HEARTBEAT_MS) {\n await initialStore.close();\n throw new SenseError('WATCH_ACTIVE', `another watcher appears active (heartbeat ${Math.round(age / 1000)}s ago); use --force to override`);\n }\n }\n\n // total is read before close: an event fires only once nothing is held open again.\n const initialTotal = initialWarnings.length > 0 || initialParsed > 0 ? await docCount(initialStore) : 0;\n await setMeta(initialStore, 'watch_heartbeat', new Date().toISOString());\n await setMeta(initialStore, 'watch_pid', String(process.pid));\n await initialStore.close();\n\n onEvent({ type: 'started', rootDir, dbPath });\n if (initialWarnings.length > 0 || initialParsed > 0) {\n onEvent({ type: 'reconciled', parsed: initialParsed, total: initialTotal, warnings: initialWarnings });\n }\n\n let stopping = false;\n // At most one open+reconcile+close cycle runs at a time; a trigger that arrives mid-cycle is\n // coalesced into a single rerun instead of opening a second, overlapping connection.\n let running: Promise<void> | null = null;\n let queuedAlways: boolean | null = null;\n\n const tick = async (alwaysEmit: boolean): Promise<void> => {\n try {\n const { store, parsed, warnings } = await openStore(cfg);\n try {\n await setMeta(store, 'watch_heartbeat', new Date().toISOString());\n await setMeta(store, 'watch_pid', String(process.pid));\n if (alwaysEmit || parsed > 0 || warnings.length > 0) {\n onEvent({ type: 'reconciled', parsed, total: await docCount(store), warnings });\n }\n } finally {\n await store.close();\n }\n } catch (err) {\n onEvent({ type: 'reconcile-error', message: (err as Error).message });\n }\n };\n\n const startCycle = (alwaysEmit: boolean) => {\n running = tick(alwaysEmit).then(() => {\n running = null;\n if (queuedAlways !== null && !stopping) {\n const again = queuedAlways;\n queuedAlways = null;\n startCycle(again);\n }\n });\n };\n\n const requestCycle = (alwaysEmit: boolean) => {\n if (stopping) return;\n if (running) {\n queuedAlways = queuedAlways || alwaysEmit;\n return;\n }\n startCycle(alwaysEmit);\n };\n\n let debounceTimer: NodeJS.Timeout | null = null;\n const scheduleReconcile = () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n requestCycle(true);\n }, debounceMs);\n };\n\n // Ignore our own state dir, or the heartbeat write would retrigger itself forever. An event with\n // an unresolvable filename (null, which fs.watch delivers under load) reconciles: parsing nothing costs less than missing a real edit.\n const watcher = fsWatch(rootDir, { recursive: true }, (_event, filename) => {\n if (typeof filename === 'string' && filename.startsWith(STATE_DIR)) return;\n scheduleReconcile();\n });\n const heartbeatTimer = setInterval(\n guardedTick(\n async () => requestCycle(false),\n () => stopping\n ),\n heartbeatIntervalMs\n );\n\n return new Promise<void>((resolveShutdown) => {\n // SIGINT/SIGTERM and an aborted signal all run this same path exactly once; each is\n // unregistered here too so a second runWatch call in the same process starts clean.\n const shutdown = async () => {\n if (stopping) return;\n stopping = true;\n process.off('SIGINT', shutdown);\n process.off('SIGTERM', shutdown);\n opts.signal?.removeEventListener('abort', shutdown);\n clearInterval(heartbeatTimer);\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n await running;\n const { store: finalStore } = await openStore(cfg);\n await setMeta(finalStore, 'watch_heartbeat', null);\n await setMeta(finalStore, 'watch_pid', null);\n await finalStore.close();\n resolveShutdown();\n };\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n if (opts.signal?.aborted) shutdown();\n else opts.signal?.addEventListener('abort', shutdown, { once: true });\n });\n}\n"],"names":["watch","fsWatch","STATE_DIR","SenseError","guardedTick","docCount","getMeta","openStore","setMeta","DEBOUNCE_MS","HEARTBEAT_INTERVAL_MS","STALE_HEARTBEAT_MS","runWatch","cfg","opts","onEvent","debounceMs","heartbeatIntervalMs","store","initialStore","dbPath","warnings","initialWarnings","parsed","initialParsed","rootDir","baseDir","existingHeartbeat","force","age","Date","now","parse","close","Math","round","initialTotal","length","toISOString","String","process","pid","type","total","stopping","running","queuedAlways","tick","alwaysEmit","err","message","startCycle","then","again","requestCycle","debounceTimer","scheduleReconcile","clearTimeout","setTimeout","watcher","recursive","_event","filename","startsWith","heartbeatTimer","setInterval","Promise","resolveShutdown","shutdown","off","signal","removeEventListener","clearInterval","finalStore","once","aborted","addEventListener"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/watch.ts"],"sourcesContent":["import { watch as fsWatch } from 'node:fs';\nimport type { ResolvedConfig } from './config/index.ts';\nimport { STATE_DIR } from './config/index.ts';\nimport { SenseError } from './errors.ts';\nimport { guardedTick } from './lib/guarded-tick.ts';\nimport { docCount, openStore } from './store/index.ts';\nimport { startWatchClaim, WATCH_HEARTBEAT_INTERVAL_MS } from './watch-claim.ts';\n\n// Watch is a cache pre-warmer, not a correctness mechanism: open() always reconciles anyway, so any fs event just triggers a debounced full reconcile.\nconst DEBOUNCE_MS = 200;\n\nexport type WatchEvent = { type: 'started'; rootDir: string; dbPath: string } | { type: 'reconciled'; parsed: number; total: number; warnings: string[] } | { type: 'reconcile-error'; message: string };\n\nexport interface WatchOptions {\n force?: boolean;\n onEvent?: (event: WatchEvent) => void;\n // Aborting runs the same shutdown path as SIGINT/SIGTERM.\n signal?: AbortSignal;\n debounceMs?: number;\n heartbeatIntervalMs?: number;\n}\n\n// Runs in the foreground until SIGINT/SIGTERM/signal abort. DuckDB and Turso hand the cache file to\n// one process at a time, so every reconcile cycle opens and closes instead of holding an idle lock.\nexport async function runWatch(cfg: ResolvedConfig, opts: WatchOptions = {}): Promise<void> {\n const onEvent = opts.onEvent ?? (() => {});\n const debounceMs = opts.debounceMs ?? DEBOUNCE_MS;\n const reconcileIntervalMs = opts.heartbeatIntervalMs ?? WATCH_HEARTBEAT_INTERVAL_MS;\n if (!Number.isFinite(reconcileIntervalMs) || reconcileIntervalMs <= 0) throw new SenseError('CONFIG_INVALID', 'watch heartbeatIntervalMs must be a finite positive number');\n // Reconciliation may be parked for a caller, but live ownership must renew before fixed expiry.\n const claimIntervalMs = Math.min(reconcileIntervalMs, WATCH_HEARTBEAT_INTERVAL_MS);\n const rootDir = cfg.rootDir ?? cfg.baseDir;\n const configDir = cfg.configDir ?? rootDir;\n let claimFailure: Error | null = null;\n let requestShutdown: ((reason: Error) => void) | undefined;\n const claim = await startWatchClaim(configDir, opts.force ?? false, claimIntervalMs, (err) => {\n claimFailure ??= err;\n requestShutdown?.(err);\n });\n let claimCloseStarted = false;\n const closeClaim = (): Promise<void> => {\n claimCloseStarted = true;\n return claim.close();\n };\n try {\n const { store: initialStore, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);\n let initialTotal = 0;\n try {\n // total is read before close: an event fires only once nothing is held open again.\n initialTotal = initialWarnings.length > 0 || initialParsed > 0 ? await docCount(initialStore) : 0;\n } finally {\n await initialStore.close();\n }\n if (claimFailure) throw claimFailure;\n\n let stopping = false;\n // At most one open+reconcile+close cycle runs at a time; a trigger that arrives mid-cycle is\n // coalesced into a single rerun instead of opening a second, overlapping connection.\n let running: Promise<void> | null = null;\n let queuedAlways: boolean | null = null;\n\n const tick = async (alwaysEmit: boolean): Promise<void> => {\n try {\n const { store, parsed, warnings } = await openStore(cfg);\n try {\n if (alwaysEmit || parsed > 0 || warnings.length > 0) {\n onEvent({ type: 'reconciled', parsed, total: await docCount(store), warnings });\n }\n } finally {\n await store.close();\n }\n } catch (err) {\n onEvent({ type: 'reconcile-error', message: (err as Error).message });\n }\n };\n\n const startCycle = (alwaysEmit: boolean) => {\n running = tick(alwaysEmit).then(() => {\n running = null;\n if (queuedAlways !== null && !stopping) {\n const again = queuedAlways;\n queuedAlways = null;\n startCycle(again);\n }\n });\n };\n\n const requestCycle = (alwaysEmit: boolean) => {\n if (stopping) return;\n if (running) {\n queuedAlways = queuedAlways || alwaysEmit;\n return;\n }\n startCycle(alwaysEmit);\n };\n\n let debounceTimer: NodeJS.Timeout | null = null;\n const scheduleReconcile = () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n requestCycle(true);\n }, debounceMs);\n };\n\n // Ignore our own state files, or cache and claim writes would retrigger reconciliation forever.\n // An unresolvable filename reconciles because parsing nothing costs less than missing a real edit.\n const watcher = fsWatch(rootDir, { recursive: true }, (_event, filename) => {\n if (typeof filename === 'string' && filename.startsWith(STATE_DIR)) return;\n scheduleReconcile();\n });\n const reconcileTimer = setInterval(\n guardedTick(\n async () => requestCycle(false),\n () => stopping\n ),\n reconcileIntervalMs\n );\n\n return await new Promise<void>((resolveShutdown, rejectShutdown) => {\n let shutdownReason: Error | null = null;\n const shutdown = (reason?: Error) => {\n shutdownReason ??= reason ?? null;\n if (stopping) return;\n stopping = true;\n const shutdownTask = (async () => {\n process.off('SIGINT', stopNormally);\n process.off('SIGTERM', stopNormally);\n opts.signal?.removeEventListener('abort', stopNormally);\n clearInterval(reconcileTimer);\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n await running;\n let finalError: unknown;\n try {\n // A normal stop performs one final freshness pass. Ownership loss drains existing work\n // but starts nothing new because the replacement watcher now owns that responsibility.\n if (!shutdownReason) {\n const { store: finalStore } = await openStore(cfg);\n await finalStore.close();\n }\n } catch (err) {\n finalError = err;\n }\n try {\n await closeClaim();\n } catch (err) {\n if (finalError) throw new AggregateError([finalError, err], 'watch shutdown and claim cleanup both failed');\n throw err;\n }\n if (finalError) throw finalError;\n if (shutdownReason) throw shutdownReason;\n })();\n shutdownTask.then(resolveShutdown, rejectShutdown);\n };\n const stopNormally = () => shutdown();\n requestShutdown = shutdown;\n process.once('SIGINT', stopNormally);\n process.once('SIGTERM', stopNormally);\n if (claimFailure) shutdown(claimFailure);\n else if (opts.signal?.aborted) shutdown();\n else {\n opts.signal?.addEventListener('abort', stopNormally, { once: true });\n try {\n onEvent({ type: 'started', rootDir, dbPath });\n if (!stopping && (initialWarnings.length > 0 || initialParsed > 0)) {\n onEvent({ type: 'reconciled', parsed: initialParsed, total: initialTotal, warnings: initialWarnings });\n }\n } catch (err) {\n shutdown(err instanceof Error ? err : new Error(String(err)));\n }\n }\n });\n } catch (err) {\n if (claimCloseStarted) throw err;\n try {\n await closeClaim();\n } catch (closeError) {\n throw new AggregateError([err, closeError], 'watch setup and claim cleanup both failed');\n }\n throw err;\n } finally {\n requestShutdown = undefined;\n }\n}\n"],"names":["watch","fsWatch","STATE_DIR","SenseError","guardedTick","docCount","openStore","startWatchClaim","WATCH_HEARTBEAT_INTERVAL_MS","DEBOUNCE_MS","runWatch","cfg","opts","onEvent","debounceMs","reconcileIntervalMs","heartbeatIntervalMs","Number","isFinite","claimIntervalMs","Math","min","rootDir","baseDir","configDir","claimFailure","requestShutdown","claim","force","err","claimCloseStarted","closeClaim","close","store","initialStore","dbPath","warnings","initialWarnings","parsed","initialParsed","initialTotal","length","stopping","running","queuedAlways","tick","alwaysEmit","type","total","message","startCycle","then","again","requestCycle","debounceTimer","scheduleReconcile","clearTimeout","setTimeout","watcher","recursive","_event","filename","startsWith","reconcileTimer","setInterval","Promise","resolveShutdown","rejectShutdown","shutdownReason","shutdown","reason","shutdownTask","process","off","stopNormally","signal","removeEventListener","clearInterval","finalError","finalStore","AggregateError","once","aborted","addEventListener","Error","String","closeError","undefined"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,106 @@
1
+ import { parentPort, workerData } from 'node:worker_threads';
2
+ import { SenseError } from '../errors.js';
3
+ import { serializeError } from '../scan/worker-error.js';
4
+ import { WatchClaimDatabase } from '../watch-claim.js';
5
+ function requireParentPort() {
6
+ const port = parentPort;
7
+ if (!port) throw new Error('watch heartbeat worker requires a parent port');
8
+ return port;
9
+ }
10
+ const port = requireParentPort();
11
+ const data = workerData;
12
+ let database;
13
+ let timer;
14
+ let stopping = false;
15
+ let renewing = Promise.resolve();
16
+ function send(message) {
17
+ port.postMessage(message);
18
+ }
19
+ function asError(value) {
20
+ return value instanceof Error ? value : new Error(String(value));
21
+ }
22
+ function closeDatabase() {
23
+ if (timer) clearInterval(timer);
24
+ timer = undefined;
25
+ try {
26
+ database === null || database === void 0 ? void 0 : database.close();
27
+ database = undefined;
28
+ return undefined;
29
+ } catch (err) {
30
+ return asError(err);
31
+ }
32
+ }
33
+ function addFailure(primary, secondary, message) {
34
+ return secondary ? new AggregateError([
35
+ primary,
36
+ secondary
37
+ ], message) : primary;
38
+ }
39
+ function fail(err) {
40
+ if (stopping) return;
41
+ stopping = true;
42
+ let failure = asError(err);
43
+ try {
44
+ database === null || database === void 0 ? void 0 : database.release(data.token);
45
+ } catch (releaseError) {
46
+ failure = addFailure(failure, asError(releaseError), 'watch heartbeat and claim release both failed');
47
+ }
48
+ const closeError = closeDatabase();
49
+ failure = addFailure(failure, closeError, 'watch heartbeat and database cleanup both failed');
50
+ try {
51
+ send({
52
+ type: 'failure',
53
+ error: serializeError(failure)
54
+ });
55
+ } finally{
56
+ port.close();
57
+ }
58
+ }
59
+ async function stop() {
60
+ if (stopping) return;
61
+ stopping = true;
62
+ if (timer) clearInterval(timer);
63
+ let failure;
64
+ try {
65
+ await renewing;
66
+ if (!(database === null || database === void 0 ? void 0 : database.release(data.token))) throw new SenseError('WATCH_ACTIVE', 'watch ownership was replaced by another watcher');
67
+ } catch (err) {
68
+ failure = asError(err);
69
+ } finally{
70
+ const closeError = closeDatabase();
71
+ if (closeError) failure = failure ? addFailure(failure, closeError, 'watch heartbeat shutdown and database cleanup both failed') : closeError;
72
+ try {
73
+ if (failure) send({
74
+ type: 'failure',
75
+ error: serializeError(failure)
76
+ });
77
+ else send({
78
+ type: 'stopped'
79
+ });
80
+ } finally{
81
+ port.close();
82
+ }
83
+ }
84
+ }
85
+ async function main() {
86
+ try {
87
+ database = new WatchClaimDatabase(data.configDir);
88
+ await database.acquire(data.token, data.pid, data.force);
89
+ send({
90
+ type: 'acquired'
91
+ });
92
+ timer = setInterval(()=>{
93
+ if (stopping) return;
94
+ renewing = renewing.then(()=>{
95
+ if (!(database === null || database === void 0 ? void 0 : database.renew(data.token))) throw new SenseError('WATCH_ACTIVE', 'watch ownership was replaced by another watcher');
96
+ });
97
+ renewing.catch(fail);
98
+ }, data.heartbeatIntervalMs);
99
+ port.on('message', (message)=>{
100
+ if (message.type === 'stop') void stop();
101
+ });
102
+ } catch (err) {
103
+ fail(err);
104
+ }
105
+ }
106
+ void main();
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/workers/watch-heartbeat.ts"],"sourcesContent":["import { parentPort, workerData } from 'node:worker_threads';\nimport { SenseError } from '../errors.ts';\nimport { serializeError } from '../scan/worker-error.ts';\nimport { WatchClaimDatabase, type WatchClaimWorkerData, type WatchClaimWorkerMessage } from '../watch-claim.ts';\n\nfunction requireParentPort() {\n const port = parentPort;\n if (!port) throw new Error('watch heartbeat worker requires a parent port');\n return port;\n}\n\nconst port = requireParentPort();\nconst data = workerData as WatchClaimWorkerData;\nlet database: WatchClaimDatabase | undefined;\nlet timer: NodeJS.Timeout | undefined;\nlet stopping = false;\nlet renewing = Promise.resolve();\n\nfunction send(message: WatchClaimWorkerMessage): void {\n port.postMessage(message);\n}\n\nfunction asError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\nfunction closeDatabase(): Error | undefined {\n if (timer) clearInterval(timer);\n timer = undefined;\n try {\n database?.close();\n database = undefined;\n return undefined;\n } catch (err) {\n return asError(err);\n }\n}\n\nfunction addFailure(primary: Error, secondary: Error | undefined, message: string): Error {\n return secondary ? new AggregateError([primary, secondary], message) : primary;\n}\n\nfunction fail(err: unknown): void {\n if (stopping) return;\n stopping = true;\n let failure = asError(err);\n try {\n database?.release(data.token);\n } catch (releaseError) {\n failure = addFailure(failure, asError(releaseError), 'watch heartbeat and claim release both failed');\n }\n const closeError = closeDatabase();\n failure = addFailure(failure, closeError, 'watch heartbeat and database cleanup both failed');\n try {\n send({ type: 'failure', error: serializeError(failure) });\n } finally {\n port.close();\n }\n}\n\nasync function stop(): Promise<void> {\n if (stopping) return;\n stopping = true;\n if (timer) clearInterval(timer);\n let failure: Error | undefined;\n try {\n await renewing;\n if (!database?.release(data.token)) throw new SenseError('WATCH_ACTIVE', 'watch ownership was replaced by another watcher');\n } catch (err) {\n failure = asError(err);\n } finally {\n const closeError = closeDatabase();\n if (closeError) failure = failure ? addFailure(failure, closeError, 'watch heartbeat shutdown and database cleanup both failed') : closeError;\n try {\n if (failure) send({ type: 'failure', error: serializeError(failure) });\n else send({ type: 'stopped' });\n } finally {\n port.close();\n }\n }\n}\n\nasync function main(): Promise<void> {\n try {\n database = new WatchClaimDatabase(data.configDir);\n await database.acquire(data.token, data.pid, data.force);\n send({ type: 'acquired' });\n timer = setInterval(() => {\n if (stopping) return;\n renewing = renewing.then(() => {\n if (!database?.renew(data.token)) throw new SenseError('WATCH_ACTIVE', 'watch ownership was replaced by another watcher');\n });\n renewing.catch(fail);\n }, data.heartbeatIntervalMs);\n port.on('message', (message: { type?: string }) => {\n if (message.type === 'stop') void stop();\n });\n } catch (err) {\n fail(err);\n }\n}\n\nvoid main();\n"],"names":["parentPort","workerData","SenseError","serializeError","WatchClaimDatabase","requireParentPort","port","Error","data","database","timer","stopping","renewing","Promise","resolve","send","message","postMessage","asError","value","String","closeDatabase","clearInterval","undefined","close","err","addFailure","primary","secondary","AggregateError","fail","failure","release","token","releaseError","closeError","type","error","stop","main","configDir","acquire","pid","force","setInterval","then","renew","catch","heartbeatIntervalMs","on"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sensemaking",
3
- "version": "0.24.3",
3
+ "version": "0.24.5",
4
4
  "description": "Query and search your markdown notes with context-aware progressive disclosure: SQL over frontmatter, links, and text, plus semantic search and link-graph ranking. No server, no build step.",
5
5
  "keywords": [
6
6
  "markdown",
@@ -93,7 +93,7 @@
93
93
  "yaml": "^2.9.0"
94
94
  },
95
95
  "devDependencies": {
96
- "@duckdb/node-api": "*",
96
+ "@duckdb/node-api": "1.5.5-r.4",
97
97
  "@tursodatabase/database": "*",
98
98
  "@types/markdown-it-footnote": "^3.0.4",
99
99
  "@types/mocha": "*",