ai-or-die 0.1.72 → 0.1.73
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/bin/ai-or-die.js +10 -12
- package/package.json +1 -1
- package/src/server.js +33 -1
- package/src/sticky-note-engine.js +75 -32
- package/src/sticky-note-jsonl.js +9 -2
- package/src/sticky-note-worker.js +7 -0
- package/src/stt-engine.js +97 -9
- package/src/stt-worker.js +14 -0
- package/src/usage-reader.js +5 -2
package/bin/ai-or-die.js
CHANGED
|
@@ -142,7 +142,7 @@ async function main() {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
const app = new ClaudeCodeWebServer(serverOptions);
|
|
145
|
-
|
|
145
|
+
await app.start();
|
|
146
146
|
|
|
147
147
|
const protocol = options.https ? 'https' : 'http';
|
|
148
148
|
const baseUrl = `${protocol}://localhost:${port}`;
|
|
@@ -187,17 +187,15 @@ async function main() {
|
|
|
187
187
|
|
|
188
188
|
console.log('\nPress Ctrl+C to stop the server\n');
|
|
189
189
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
process.on('SIGINT', () => { shutdown(); });
|
|
200
|
-
process.on('SIGTERM', () => { shutdown(); });
|
|
190
|
+
// Shutdown is owned by the server's single SIGINT/SIGTERM handler
|
|
191
|
+
// (ClaudeCodeWebServer.handleShutdown), which performs the ordered graceful
|
|
192
|
+
// teardown: cooperative disposal of the local-LLM (sticky-note) and STT
|
|
193
|
+
// native worker threads, tunnel stop, session save, then server close.
|
|
194
|
+
// A second handler here used to race it — its httpServer.close() callback
|
|
195
|
+
// fires immediately when there are no open connections and called
|
|
196
|
+
// process.exit(0) before the worker threads could dispose their ggml-based
|
|
197
|
+
// native models, which aborted the process (SIGABRT / exit 134) on Ctrl+C.
|
|
198
|
+
// So we deliberately do NOT register a SIGINT/SIGTERM handler here.
|
|
201
199
|
|
|
202
200
|
} catch (error) {
|
|
203
201
|
console.error('Error starting server:', error.message);
|
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -475,13 +475,31 @@ class ClaudeCodeWebServer {
|
|
|
475
475
|
forceExitTimer.unref();
|
|
476
476
|
|
|
477
477
|
console.log(`\nGracefully shutting down (exit code: ${exitCode})...`);
|
|
478
|
+
// Persist sessions FIRST, before the (bounded but potentially multi-second)
|
|
479
|
+
// native-engine teardown below. If a pathological native teardown ever blew
|
|
480
|
+
// the 15s force-exit budget, sessions would already be safe on disk. close()
|
|
481
|
+
// saves again at the end of a normal shutdown.
|
|
482
|
+
try { await this.saveSessionsToDisk(true); } catch (_) { /* ignore */ }
|
|
478
483
|
// Tear down the local-LLM summariser + worker so the model/worker thread
|
|
479
484
|
// don't keep the process alive (and don't hold a GGUF file lock on Windows).
|
|
480
485
|
if (this._stickyInitTimer) { clearTimeout(this._stickyInitTimer); this._stickyInitTimer = null; }
|
|
481
486
|
if (this._stickyJsonlPoll) { clearInterval(this._stickyJsonlPoll); this._stickyJsonlPoll = null; }
|
|
482
487
|
this._stickyJsonl.clear();
|
|
483
488
|
try { this.stickyNoteSummarizer.shutdown(); } catch (_) { /* ignore */ }
|
|
484
|
-
|
|
489
|
+
// Tear down both local native worker engines (sticky-note = node-llama-cpp,
|
|
490
|
+
// STT = sherpa-onnx) concurrently. Each disposes its loaded model/recognizer
|
|
491
|
+
// on its worker thread before exiting; force-tearing them down via
|
|
492
|
+
// process.exit() while a model is loaded/loading aborts the process (SIGABRT)
|
|
493
|
+
// during native cleanup. Running them in parallel keeps total shutdown well
|
|
494
|
+
// inside the 15s force-exit budget above. STT shutdown was previously missing
|
|
495
|
+
// entirely. The CLI dev tunnel (only set in --tunnel mode) used to be stopped
|
|
496
|
+
// by a second SIGINT handler in bin/ai-or-die.js, now removed to avoid a
|
|
497
|
+
// shutdown race; its stop moves here onto the single graceful path.
|
|
498
|
+
await Promise.allSettled([
|
|
499
|
+
Promise.resolve().then(() => this.stickyNoteEngine.shutdown()),
|
|
500
|
+
Promise.resolve().then(() => this.sttEngine.shutdown()),
|
|
501
|
+
Promise.resolve().then(() => (this.tunnelManager ? this.tunnelManager.stop() : undefined)),
|
|
502
|
+
]);
|
|
485
503
|
await this.close();
|
|
486
504
|
clearTimeout(forceExitTimer);
|
|
487
505
|
process.exit(exitCode);
|
|
@@ -5286,6 +5304,20 @@ class ClaudeCodeWebServer {
|
|
|
5286
5304
|
// Save sessions before closing
|
|
5287
5305
|
await this.saveSessionsToDisk(true);
|
|
5288
5306
|
|
|
5307
|
+
// Tear down the STT (sherpa-onnx) native worker. close() is the cleanup path
|
|
5308
|
+
// shared by the signal handler (handleShutdown -> close) AND direct close()
|
|
5309
|
+
// callers (e.g. the e2e test servers, which construct a ClaudeCodeWebServer
|
|
5310
|
+
// and call server.close()). Without this, a server that has loaded the STT
|
|
5311
|
+
// model leaks its worker thread past close() and keeps the process alive —
|
|
5312
|
+
// this hung the Windows e2e jobs once the model was cached/present. The
|
|
5313
|
+
// shutdown is cooperative (graceful message, no terminate()) so native
|
|
5314
|
+
// teardown can't abort the process, and idempotent (handleShutdown already
|
|
5315
|
+
// ran it on the signal path, so this is then a no-op). The sticky-note engine
|
|
5316
|
+
// is torn down by handleShutdown only: it is disabled in the e2e test
|
|
5317
|
+
// servers, and its teardown must precede close()'s session-output flush to
|
|
5318
|
+
// avoid re-triggering a summary, so it stays out of this shared path.
|
|
5319
|
+
try { await this.sttEngine.shutdown(); } catch (_) { /* ignore */ }
|
|
5320
|
+
|
|
5289
5321
|
// Clear all intervals
|
|
5290
5322
|
if (this.autoSaveInterval) {
|
|
5291
5323
|
clearInterval(this.autoSaveInterval);
|
|
@@ -30,6 +30,7 @@ class StickyNoteEngine {
|
|
|
30
30
|
|
|
31
31
|
this._status = 'unavailable';
|
|
32
32
|
this._worker = null;
|
|
33
|
+
this._spawningWorker = null;
|
|
33
34
|
this._queue = [];
|
|
34
35
|
this._currentRequest = null;
|
|
35
36
|
this._requestIdCounter = 0;
|
|
@@ -88,6 +89,11 @@ class StickyNoteEngine {
|
|
|
88
89
|
});
|
|
89
90
|
}
|
|
90
91
|
this._status = 'loading';
|
|
92
|
+
// If shutdown began while we were checking/downloading the model, do NOT
|
|
93
|
+
// spawn a worker we'd immediately have to kill mid-native-load (which aborts
|
|
94
|
+
// the process). shutdown() awaits this in-flight init, so bailing here lets
|
|
95
|
+
// it complete cleanly with no worker.
|
|
96
|
+
if (this._stopping) return;
|
|
91
97
|
await this._spawnWorker();
|
|
92
98
|
}
|
|
93
99
|
|
|
@@ -198,6 +204,11 @@ class StickyNoteEngine {
|
|
|
198
204
|
_spawnWorker() {
|
|
199
205
|
return new Promise((resolve, reject) => {
|
|
200
206
|
const worker = this._createWorker();
|
|
207
|
+
// Track the worker from the moment it's created (not just after 'ready')
|
|
208
|
+
// so shutdown() can stop it cooperatively even while it's still loading the
|
|
209
|
+
// model. Cleared once it becomes this._worker or fails to boot.
|
|
210
|
+
this._spawningWorker = worker;
|
|
211
|
+
const clearPending = () => { if (this._spawningWorker === worker) this._spawningWorker = null; };
|
|
201
212
|
|
|
202
213
|
const onReady = (msg) => {
|
|
203
214
|
if (!msg) return;
|
|
@@ -205,6 +216,16 @@ class StickyNoteEngine {
|
|
|
205
216
|
worker.off('message', onReady);
|
|
206
217
|
worker.off('error', onError);
|
|
207
218
|
worker.off('exit', onBootExit);
|
|
219
|
+
clearPending();
|
|
220
|
+
// If shutdown started while this worker was still loading, do NOT
|
|
221
|
+
// promote it to the active worker — that would resurrect a torn-down
|
|
222
|
+
// engine. Ask it to dispose + exit and resolve init as cancelled.
|
|
223
|
+
if (this._stopping) {
|
|
224
|
+
this._status = 'unavailable';
|
|
225
|
+
try { worker.postMessage({ type: 'shutdown' }); } catch { /* ignore */ }
|
|
226
|
+
resolve();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
208
229
|
this._worker = worker;
|
|
209
230
|
this._status = 'ready';
|
|
210
231
|
this._restartAttempts = 0;
|
|
@@ -217,6 +238,7 @@ class StickyNoteEngine {
|
|
|
217
238
|
worker.off('message', onReady);
|
|
218
239
|
worker.off('error', onError);
|
|
219
240
|
worker.off('exit', onBootExit);
|
|
241
|
+
clearPending();
|
|
220
242
|
if (msg.code === 'MODULE_NOT_FOUND') this._lastSpawnError = 'MODULE_NOT_FOUND';
|
|
221
243
|
this._status = 'unavailable';
|
|
222
244
|
reject(new Error(msg.message || 'worker error'));
|
|
@@ -226,6 +248,7 @@ class StickyNoteEngine {
|
|
|
226
248
|
worker.off('message', onReady);
|
|
227
249
|
worker.off('error', onError);
|
|
228
250
|
worker.off('exit', onBootExit);
|
|
251
|
+
clearPending();
|
|
229
252
|
if (err && (err.code === 'MODULE_NOT_FOUND' || (err.message && err.message.includes('node-llama-cpp')))) {
|
|
230
253
|
this._lastSpawnError = 'MODULE_NOT_FOUND';
|
|
231
254
|
}
|
|
@@ -238,6 +261,7 @@ class StickyNoteEngine {
|
|
|
238
261
|
worker.off('message', onReady);
|
|
239
262
|
worker.off('error', onError);
|
|
240
263
|
worker.off('exit', onBootExit);
|
|
264
|
+
clearPending();
|
|
241
265
|
this._status = 'unavailable';
|
|
242
266
|
reject(new Error(`sticky-note worker exited during init (code ${code})`));
|
|
243
267
|
};
|
|
@@ -263,6 +287,32 @@ class StickyNoteEngine {
|
|
|
263
287
|
|
|
264
288
|
async shutdown() {
|
|
265
289
|
this._stopping = true;
|
|
290
|
+
|
|
291
|
+
// Shared time budget: the init-wait + cooperative-exit waits below together
|
|
292
|
+
// stay within this window so the whole engine teardown (run concurrently with
|
|
293
|
+
// the STT engine by handleShutdown) finishes inside handleShutdown's 15s
|
|
294
|
+
// force-exit budget, leaving room for close(). Realistic teardown is a few
|
|
295
|
+
// hundred ms; this only caps pathological hangs.
|
|
296
|
+
const deadline = Date.now() + 10000;
|
|
297
|
+
const remaining = () => Math.max(0, deadline - Date.now());
|
|
298
|
+
|
|
299
|
+
// If a worker is mid model-LOAD (status 'loading' = the native model is being
|
|
300
|
+
// constructed in the worker thread), wait — bounded — for it to settle so we
|
|
301
|
+
// can dispose it cooperatively; a worker killed mid-native-load aborts the
|
|
302
|
+
// process (SIGABRT / exit 134). We do NOT wait during 'downloading' (no native
|
|
303
|
+
// worker is loaded yet, so process.exit can't abort, and the download can take
|
|
304
|
+
// minutes — _doInitialize bails before spawning once _stopping is set, so a
|
|
305
|
+
// Ctrl+C during a first-run download still exits promptly).
|
|
306
|
+
if (this._initPromise && this._status === 'loading') {
|
|
307
|
+
await Promise.race([
|
|
308
|
+
Promise.resolve(this._initPromise).catch(() => {}),
|
|
309
|
+
new Promise((resolve) => {
|
|
310
|
+
const t = setTimeout(resolve, remaining());
|
|
311
|
+
if (t.unref) t.unref();
|
|
312
|
+
}),
|
|
313
|
+
]);
|
|
314
|
+
}
|
|
315
|
+
|
|
266
316
|
if (this._restartTimer) {
|
|
267
317
|
clearTimeout(this._restartTimer);
|
|
268
318
|
this._restartTimer = null;
|
|
@@ -273,38 +323,31 @@ class StickyNoteEngine {
|
|
|
273
323
|
}
|
|
274
324
|
this._queue = [];
|
|
275
325
|
this._currentRequest = null;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const t = setTimeout(finish, 3000);
|
|
302
|
-
if (t.unref) t.unref();
|
|
303
|
-
});
|
|
304
|
-
if (!exited) await w.terminate();
|
|
305
|
-
} catch {
|
|
306
|
-
/* ignore */
|
|
307
|
-
}
|
|
326
|
+
// Cooperatively stop the worker — the live one, or one still booting (tracked
|
|
327
|
+
// from creation in _spawnWorker). Ask it to dispose its native model/context
|
|
328
|
+
// and exit on its own. We deliberately do NOT call worker.terminate():
|
|
329
|
+
// force-killing a thread that is inside native ggml code (mid model-load or
|
|
330
|
+
// mid-inference) throws an uncaught Napi error during worker-env teardown and
|
|
331
|
+
// ggml's set_terminate aborts the whole process (SIGABRT / exit 134) — the
|
|
332
|
+
// bug this fixes. The wait is bounded (shared deadline) so handleShutdown can
|
|
333
|
+
// still save sessions + close(); a worker that never exits is reaped by
|
|
334
|
+
// handleShutdown's 15s force-exit backstop.
|
|
335
|
+
const w = this._worker || this._spawningWorker;
|
|
336
|
+
this._worker = null;
|
|
337
|
+
this._spawningWorker = null;
|
|
338
|
+
if (w) {
|
|
339
|
+
await new Promise((resolve) => {
|
|
340
|
+
let done = false;
|
|
341
|
+
const finish = () => { if (!done) { done = true; resolve(); } };
|
|
342
|
+
w.once('exit', finish);
|
|
343
|
+
try {
|
|
344
|
+
w.postMessage({ type: 'shutdown' });
|
|
345
|
+
} catch {
|
|
346
|
+
finish();
|
|
347
|
+
}
|
|
348
|
+
const t = setTimeout(finish, Math.max(1000, remaining()));
|
|
349
|
+
if (t.unref) t.unref();
|
|
350
|
+
});
|
|
308
351
|
}
|
|
309
352
|
this._status = 'unavailable';
|
|
310
353
|
}
|
package/src/sticky-note-jsonl.js
CHANGED
|
@@ -39,9 +39,16 @@ function cleanProse(s) {
|
|
|
39
39
|
.trim();
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Claude's project-dir slug for a cwd: every non-alphanumeric char → '-'
|
|
44
|
+
* (drive-letter colon, path separators, spaces, dots, etc.). Matches the folder
|
|
45
|
+
* claude writes under ~/.claude/projects/. A separator-only replace leaves the
|
|
46
|
+
* Windows drive-letter colon in place (`C:-Users-...`) and never matches claude's
|
|
47
|
+
* `C--Users-...`, so the transcript binding — and with it the tab title and the
|
|
48
|
+
* sticky note — silently fail on Windows.
|
|
49
|
+
*/
|
|
43
50
|
function slugForCwd(cwd) {
|
|
44
|
-
return String(cwd || '').replace(/[
|
|
51
|
+
return String(cwd || '').replace(/[^a-zA-Z0-9]/g, '-');
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
/** The claude session id is the JSONL basename (the --resume key). */
|
|
@@ -90,8 +90,15 @@ parentPort.on('message', (msg) => {
|
|
|
90
90
|
_inferChain
|
|
91
91
|
.catch(() => {})
|
|
92
92
|
.then(async () => {
|
|
93
|
+
// Dispose in dependency order: context + model, then the top-level
|
|
94
|
+
// llama backend. Disposing the backend (await llama.dispose()) is what
|
|
95
|
+
// actually drains node-llama-cpp's native async work; without it the
|
|
96
|
+
// worker-thread env teardown that follows process.exit() can hit a
|
|
97
|
+
// pending Napi completion and ggml's set_terminate aborts the whole
|
|
98
|
+
// process (SIGABRT / exit 134) on Ctrl+C.
|
|
93
99
|
try { if (context) await context.dispose(); } catch { /* ignore */ }
|
|
94
100
|
try { if (model) await model.dispose(); } catch { /* ignore */ }
|
|
101
|
+
try { if (llama) await llama.dispose(); } catch { /* ignore */ }
|
|
95
102
|
})
|
|
96
103
|
.finally(() => process.exit(0));
|
|
97
104
|
}
|
package/src/stt-engine.js
CHANGED
|
@@ -17,11 +17,13 @@ class SttEngine {
|
|
|
17
17
|
this._numThreads = options.numThreads || Math.min(4, os.cpus().length);
|
|
18
18
|
this._status = 'unavailable';
|
|
19
19
|
this._worker = null;
|
|
20
|
+
this._spawningWorker = null;
|
|
20
21
|
this._queue = [];
|
|
21
22
|
this._currentRequest = null;
|
|
22
23
|
this._requestIdCounter = 0;
|
|
23
24
|
this._restartAttempts = 0;
|
|
24
25
|
this._lastSpawnError = null;
|
|
26
|
+
this._stopping = false;
|
|
25
27
|
this._initPromise = null;
|
|
26
28
|
this._modelManager = new ModelManager({
|
|
27
29
|
modelsDir: options.modelsDir
|
|
@@ -62,6 +64,11 @@ class SttEngine {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
this._status = 'loading';
|
|
67
|
+
// If shutdown began while we were checking/downloading the model, do NOT
|
|
68
|
+
// spawn a worker we'd immediately have to kill mid-native-load (which aborts
|
|
69
|
+
// the process). shutdown() awaits this in-flight init, so bailing here lets
|
|
70
|
+
// it complete cleanly with no worker.
|
|
71
|
+
if (this._stopping) return;
|
|
65
72
|
await this._spawnWorker();
|
|
66
73
|
}
|
|
67
74
|
|
|
@@ -275,6 +282,11 @@ class SttEngine {
|
|
|
275
282
|
|
|
276
283
|
_restartWorker(delay) {
|
|
277
284
|
setTimeout(async () => {
|
|
285
|
+
// Don't respawn if shutdown started after this restart was scheduled.
|
|
286
|
+
if (this._stopping) {
|
|
287
|
+
this._status = 'unavailable';
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
278
290
|
try {
|
|
279
291
|
await this._spawnWorker();
|
|
280
292
|
} catch (err) {
|
|
@@ -294,11 +306,29 @@ class SttEngine {
|
|
|
294
306
|
nodeModulesDir: path.resolve(__dirname, '..', 'node_modules')
|
|
295
307
|
}
|
|
296
308
|
});
|
|
309
|
+
// Track the worker from creation (not just after 'ready') so shutdown() can
|
|
310
|
+
// stop it cooperatively even while it is still loading the recognizer.
|
|
311
|
+
this._spawningWorker = worker;
|
|
312
|
+
const clearPending = () => { if (this._spawningWorker === worker) this._spawningWorker = null; };
|
|
313
|
+
const detach = () => {
|
|
314
|
+
worker.off('message', onReady);
|
|
315
|
+
worker.off('error', onError);
|
|
316
|
+
worker.off('exit', onBootExit);
|
|
317
|
+
};
|
|
297
318
|
|
|
298
319
|
const onReady = (msg) => {
|
|
299
320
|
if (msg.type === 'ready') {
|
|
300
|
-
|
|
301
|
-
|
|
321
|
+
detach();
|
|
322
|
+
clearPending();
|
|
323
|
+
// If shutdown started while this worker was still loading, do NOT
|
|
324
|
+
// promote it to the active worker — that would resurrect a torn-down
|
|
325
|
+
// engine. Ask it to exit and resolve init as cancelled.
|
|
326
|
+
if (this._stopping) {
|
|
327
|
+
this._status = 'unavailable';
|
|
328
|
+
try { worker.postMessage({ type: 'shutdown' }); } catch { /* ignore */ }
|
|
329
|
+
resolve();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
302
332
|
this._worker = worker;
|
|
303
333
|
this._status = 'ready';
|
|
304
334
|
this._restartAttempts = 0;
|
|
@@ -311,15 +341,15 @@ class SttEngine {
|
|
|
311
341
|
this._processQueue();
|
|
312
342
|
resolve();
|
|
313
343
|
} else if (msg.type === 'error') {
|
|
314
|
-
|
|
315
|
-
|
|
344
|
+
detach();
|
|
345
|
+
clearPending();
|
|
316
346
|
reject(new Error(msg.message));
|
|
317
347
|
}
|
|
318
348
|
};
|
|
319
349
|
|
|
320
350
|
const onError = (err) => {
|
|
321
|
-
|
|
322
|
-
|
|
351
|
+
detach();
|
|
352
|
+
clearPending();
|
|
323
353
|
// Tag dependency errors so _onWorkerExit can skip futile retries
|
|
324
354
|
if (err.code === 'MODULE_NOT_FOUND' || (err.message && err.message.includes('sherpa-onnx-node'))) {
|
|
325
355
|
this._lastSpawnError = 'MODULE_NOT_FOUND';
|
|
@@ -327,8 +357,19 @@ class SttEngine {
|
|
|
327
357
|
reject(err);
|
|
328
358
|
};
|
|
329
359
|
|
|
360
|
+
// If the worker dies before emitting ready/error, neither listener above
|
|
361
|
+
// fires — without this the init Promise would hang forever (and shutdown
|
|
362
|
+
// would burn its full bounded wait on it).
|
|
363
|
+
const onBootExit = (code) => {
|
|
364
|
+
detach();
|
|
365
|
+
clearPending();
|
|
366
|
+
this._status = 'unavailable';
|
|
367
|
+
reject(new Error(`STT worker exited during init (code ${code})`));
|
|
368
|
+
};
|
|
369
|
+
|
|
330
370
|
worker.on('message', onReady);
|
|
331
371
|
worker.on('error', onError);
|
|
372
|
+
worker.on('exit', onBootExit);
|
|
332
373
|
});
|
|
333
374
|
}
|
|
334
375
|
|
|
@@ -421,6 +462,31 @@ class SttEngine {
|
|
|
421
462
|
// See docs/audits/proc-child-processes.md gap 1.
|
|
422
463
|
this._stopping = true;
|
|
423
464
|
|
|
465
|
+
// Shared time budget for the init-wait + cooperative-exit waits below, so the
|
|
466
|
+
// whole engine teardown (run concurrently with the sticky-note engine by
|
|
467
|
+
// handleShutdown) finishes inside handleShutdown's 15s force-exit budget,
|
|
468
|
+
// leaving room for close(). Realistic teardown is sub-second; this only caps
|
|
469
|
+
// pathological hangs.
|
|
470
|
+
const deadline = Date.now() + 10000;
|
|
471
|
+
const remaining = () => Math.max(0, deadline - Date.now());
|
|
472
|
+
|
|
473
|
+
// If a worker is mid model-LOAD (status 'loading' = the recognizer is being
|
|
474
|
+
// constructed in the worker thread), wait — bounded — for it to settle so we
|
|
475
|
+
// can tear it down cooperatively; a worker killed mid-native-load aborts the
|
|
476
|
+
// process (SIGABRT / exit 134). We do NOT wait during 'downloading' (no native
|
|
477
|
+
// worker is loaded yet, and the download can take minutes — _doInitialize
|
|
478
|
+
// bails before spawning once _stopping is set, so a Ctrl+C during a first-run
|
|
479
|
+
// download still exits promptly).
|
|
480
|
+
if (this._initPromise && this._status === 'loading') {
|
|
481
|
+
await Promise.race([
|
|
482
|
+
Promise.resolve(this._initPromise).catch(() => {}),
|
|
483
|
+
new Promise((resolve) => {
|
|
484
|
+
const t = setTimeout(resolve, remaining());
|
|
485
|
+
if (t.unref) t.unref();
|
|
486
|
+
}),
|
|
487
|
+
]);
|
|
488
|
+
}
|
|
489
|
+
|
|
424
490
|
// Reject all queued requests
|
|
425
491
|
for (const req of this._queue) {
|
|
426
492
|
clearTimeout(req.timer);
|
|
@@ -429,9 +495,31 @@ class SttEngine {
|
|
|
429
495
|
this._queue = [];
|
|
430
496
|
this._currentRequest = null;
|
|
431
497
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
498
|
+
// Cooperatively stop the worker — the live one, or one still booting (tracked
|
|
499
|
+
// from creation in _spawnWorker). Ask it to exit on its own. We deliberately
|
|
500
|
+
// do NOT call worker.terminate(): force-killing a thread inside native
|
|
501
|
+
// sherpa-onnx code (mid-load or mid-transcribe) throws an uncaught Napi error
|
|
502
|
+
// during worker-env teardown and ggml's set_terminate aborts the whole
|
|
503
|
+
// process (SIGABRT / exit 134) — the bug this fixes. The wait is bounded
|
|
504
|
+
// (shared deadline) so handleShutdown can still save sessions + close(); a
|
|
505
|
+
// worker that never exits is reaped by handleShutdown's 15s force-exit
|
|
506
|
+
// backstop.
|
|
507
|
+
const w = this._worker || this._spawningWorker;
|
|
508
|
+
this._worker = null;
|
|
509
|
+
this._spawningWorker = null;
|
|
510
|
+
if (w) {
|
|
511
|
+
await new Promise((resolve) => {
|
|
512
|
+
let done = false;
|
|
513
|
+
const finish = () => { if (!done) { done = true; resolve(); } };
|
|
514
|
+
w.once('exit', finish);
|
|
515
|
+
try {
|
|
516
|
+
w.postMessage({ type: 'shutdown' });
|
|
517
|
+
} catch {
|
|
518
|
+
finish();
|
|
519
|
+
}
|
|
520
|
+
const t = setTimeout(finish, Math.max(1000, remaining()));
|
|
521
|
+
if (t.unref) t.unref();
|
|
522
|
+
});
|
|
435
523
|
}
|
|
436
524
|
|
|
437
525
|
this._status = 'unavailable';
|
package/src/stt-worker.js
CHANGED
|
@@ -72,8 +72,22 @@ try {
|
|
|
72
72
|
process.exit(1);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
let _shuttingDown = false;
|
|
75
76
|
parentPort.on('message', (msg) => {
|
|
77
|
+
if (!msg) return;
|
|
78
|
+
if (msg.type === 'shutdown') {
|
|
79
|
+
// Graceful teardown. sherpa-onnx-node exposes no dispose API (the recognizer
|
|
80
|
+
// is GC/finalizer-managed), and transcribe runs synchronously here, so when
|
|
81
|
+
// this message is processed nothing is in flight. Exit cleanly while idle so
|
|
82
|
+
// the worker-env teardown doesn't race a pending native op — a bare
|
|
83
|
+
// terminate() with the recognizer loaded can abort the process during native
|
|
84
|
+
// cleanup (SIGABRT / exit 134) on Ctrl+C.
|
|
85
|
+
_shuttingDown = true;
|
|
86
|
+
process.exit(0);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
76
89
|
if (msg.type === 'transcribe') {
|
|
90
|
+
if (_shuttingDown) return;
|
|
77
91
|
try {
|
|
78
92
|
// Two input shapes:
|
|
79
93
|
// - msg.pcm16: raw 16-bit PCM (Int16Array). Conversion to Float32 runs
|
package/src/usage-reader.js
CHANGED
|
@@ -321,8 +321,11 @@ class UsageReader {
|
|
|
321
321
|
try {
|
|
322
322
|
// Get the current working directory to find the right project folder
|
|
323
323
|
const cwd = process.cwd();
|
|
324
|
-
// Claude uses format: -home-user-Development-project
|
|
325
|
-
|
|
324
|
+
// Claude uses format: -home-user-Development-project. It replaces EVERY
|
|
325
|
+
// non-alphanumeric char with '-', so on Windows the drive-letter colon
|
|
326
|
+
// becomes a dash too (`C:\Users\me` -> `C--Users-me`). A separator-only
|
|
327
|
+
// replace leaves the colon and never matches the real folder.
|
|
328
|
+
const projectDirName = cwd.replace(/[^a-zA-Z0-9]/g, '-');
|
|
326
329
|
let projectPath = path.join(this.claudeProjectsPath, projectDirName);
|
|
327
330
|
|
|
328
331
|
// Check if the project directory exists
|