@pellux/goodvibes-agent 1.5.8 → 1.5.9
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/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/dist/package/main.js +282 -62
- package/package.json +1 -1
- package/src/audio/player.ts +91 -5
- package/src/audio/spoken-turn-controller.ts +281 -29
- package/src/audio/spoken-turn-wiring.ts +13 -2
- package/src/main.ts +24 -29
- package/src/runtime/unhandled-rejection-guard.ts +41 -0
- package/src/version.ts +1 -1
package/src/main.ts
CHANGED
|
@@ -53,6 +53,7 @@ import { FOCUS_ENABLE, FOCUS_DISABLE, installFocusModeExitGuard, markFocusModeEn
|
|
|
53
53
|
import { prepareShellCliRuntime } from './cli/entrypoint.ts';
|
|
54
54
|
import { applyInitialTuiCliState, formatFatalStartupErrorForLog, formatFatalStartupErrorForUser, getInteractiveTerminalLaunchError } from './cli/tui-startup.ts';
|
|
55
55
|
import { wireSpokenTurnRuntime } from './audio/spoken-turn-wiring.ts';
|
|
56
|
+
import { createUnhandledRejectionHandler } from './runtime/unhandled-rejection-guard.ts';
|
|
56
57
|
import { attachSpokenTurnModelRouting, createSpokenTurnInputOptions } from './audio/spoken-turn-model-routing.ts';
|
|
57
58
|
import { allowTerminalWrite, installTuiTerminalOutputGuard } from './runtime/terminal-output-guard.ts';
|
|
58
59
|
import { buildCommandArgsHint } from './input/command-args-hint.ts';
|
|
@@ -221,43 +222,34 @@ async function main() {
|
|
|
221
222
|
|
|
222
223
|
const unsubs: Array<() => void> = [];
|
|
223
224
|
let recoveryInterval: ReturnType<typeof setInterval> | null = null;
|
|
224
|
-
let stopSpokenOutputForExit: (() => void) | null = null;
|
|
225
|
+
let stopSpokenOutputForExit: (() => Promise<void>) | null = null;
|
|
225
226
|
let recoveryPending = false;
|
|
226
227
|
|
|
227
228
|
const sigintHandler = (): void => input.feed('\x03');
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (now - _unhandledRejectionWindowStart > 10000) {
|
|
233
|
-
_unhandledRejectionCount = 0;
|
|
234
|
-
_unhandledRejectionWindowStart = now;
|
|
235
|
-
}
|
|
236
|
-
_unhandledRejectionCount++;
|
|
237
|
-
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
238
|
-
if (_unhandledRejectionCount > 3) {
|
|
239
|
-
logger.error('CRITICAL: cascading unhandled rejections — consider restarting', {
|
|
240
|
-
count: _unhandledRejectionCount,
|
|
241
|
-
windowMs: now - _unhandledRejectionWindowStart,
|
|
242
|
-
error: String(reason),
|
|
243
|
-
});
|
|
244
|
-
systemMessageRouter.high(
|
|
245
|
-
`[Critical] Multiple errors detected (${_unhandledRejectionCount} in 10s). If the issue persists, please restart. Latest: ${msg}`
|
|
246
|
-
);
|
|
247
|
-
} else {
|
|
248
|
-
systemMessageRouter.high(`[Error] ${msg}`);
|
|
249
|
-
logger.error('unhandledRejection', { error: String(reason) });
|
|
250
|
-
}
|
|
251
|
-
render();
|
|
252
|
-
};
|
|
229
|
+
const unhandledRejectionHandler = createUnhandledRejectionHandler({
|
|
230
|
+
notifyHigh: (message) => systemMessageRouter.high(message),
|
|
231
|
+
render: () => render(),
|
|
232
|
+
});
|
|
253
233
|
const resizeHandler = (): void => {
|
|
254
234
|
input.setContentWidth(getPromptContentWidth());
|
|
255
235
|
compositor.resetDiff();
|
|
256
236
|
render();
|
|
257
237
|
};
|
|
258
238
|
|
|
239
|
+
let exiting = false;
|
|
259
240
|
const exitApp = (): void => {
|
|
260
|
-
|
|
241
|
+
// Reentrancy guard: a second /exit or keypress during the bounded
|
|
242
|
+
// spoken-audio drain below must not re-run teardown.
|
|
243
|
+
if (exiting) return;
|
|
244
|
+
exiting = true;
|
|
245
|
+
// Exit lets the spoken audio the user is already hearing finish inside a
|
|
246
|
+
// short bounded window (capped inside stopForExit) instead of killing the
|
|
247
|
+
// player mid-drain; queued-but-unplayed speech is dropped. Deliberate
|
|
248
|
+
// interrupts (Ctrl+C, /tts stop) still cut instantly via spokenTurns.stop().
|
|
249
|
+
let spokenOutputDrain: Promise<void> = Promise.resolve();
|
|
250
|
+
try {
|
|
251
|
+
spokenOutputDrain = Promise.resolve(stopSpokenOutputForExit?.()).then(() => undefined);
|
|
252
|
+
} catch { /* non-fatal to exit */ }
|
|
261
253
|
unsubs.forEach(fn => fn());
|
|
262
254
|
// Persist last-seen before shutdown so the next launch can compute the digest.
|
|
263
255
|
autonomy.stop();
|
|
@@ -275,7 +267,9 @@ async function main() {
|
|
|
275
267
|
allowTerminalWrite(() => stdout.write(PASTE_DISABLE + KEYBOARD_EXT_DISABLE + MOUSE_DISABLE + FOCUS_DISABLE + CURSOR_SHOW + exitScreen));
|
|
276
268
|
terminalOutputGuard.dispose();
|
|
277
269
|
stdin.setRawMode(false);
|
|
278
|
-
process
|
|
270
|
+
// The terminal is already restored above; only the process exit waits for
|
|
271
|
+
// the (internally capped, ~2s max) audio drain.
|
|
272
|
+
void spokenOutputDrain.catch(() => undefined).then(() => process.exit(0));
|
|
279
273
|
};
|
|
280
274
|
|
|
281
275
|
commandContext.exit = exitApp;
|
|
@@ -286,7 +280,8 @@ async function main() {
|
|
|
286
280
|
events: uiServices.events,
|
|
287
281
|
notify: (message) => { systemMessageRouter.high(message); render(); },
|
|
288
282
|
});
|
|
289
|
-
|
|
283
|
+
// Exit-path stop: bounded drain of the audio already playing (see stopForExit).
|
|
284
|
+
stopSpokenOutputForExit = () => spokenTurns.stopForExit();
|
|
290
285
|
unsubs.push(...spokenTurns.unsubs);
|
|
291
286
|
unsubs.push(attachSpokenTurnModelRouting({
|
|
292
287
|
orchestrator,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds the process-level unhandledRejection handler for the shell.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from main() so the entrypoint stays under the architecture
|
|
7
|
+
* line-count cap. Behavior is unchanged: isolated rejections surface as a
|
|
8
|
+
* single "[Error]" line, while more than three within a rolling 10s window
|
|
9
|
+
* escalate to a "[Critical]" restart suggestion so a cascading failure is
|
|
10
|
+
* named instead of scrolling by as noise.
|
|
11
|
+
*/
|
|
12
|
+
export function createUnhandledRejectionHandler(deps: {
|
|
13
|
+
readonly notifyHigh: (message: string) => void;
|
|
14
|
+
readonly render: () => void;
|
|
15
|
+
}): (reason: unknown) => void {
|
|
16
|
+
let rejectionCount = 0;
|
|
17
|
+
let windowStart = Date.now();
|
|
18
|
+
return (reason: unknown): void => {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
if (now - windowStart > 10000) {
|
|
21
|
+
rejectionCount = 0;
|
|
22
|
+
windowStart = now;
|
|
23
|
+
}
|
|
24
|
+
rejectionCount++;
|
|
25
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
26
|
+
if (rejectionCount > 3) {
|
|
27
|
+
logger.error('CRITICAL: cascading unhandled rejections — consider restarting', {
|
|
28
|
+
count: rejectionCount,
|
|
29
|
+
windowMs: now - windowStart,
|
|
30
|
+
error: String(reason),
|
|
31
|
+
});
|
|
32
|
+
deps.notifyHigh(
|
|
33
|
+
`[Critical] Multiple errors detected (${rejectionCount} in 10s). If the issue persists, please restart. Latest: ${msg}`
|
|
34
|
+
);
|
|
35
|
+
} else {
|
|
36
|
+
deps.notifyHigh(`[Error] ${msg}`);
|
|
37
|
+
logger.error('unhandledRejection', { error: String(reason) });
|
|
38
|
+
}
|
|
39
|
+
deps.render();
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '1.5.
|
|
9
|
+
let _version = '1.5.9';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {
|
|
12
12
|
readonly version?: unknown;
|