@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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Product-facing release notes for GoodVibes Agent.
|
|
4
4
|
|
|
5
|
+
## 1.5.9 - 2026-07-06
|
|
6
|
+
|
|
7
|
+
- Fixed spoken output (text-to-speech) so playback no longer clips at the start: the Agent now waits until the audio player has actually started before sending it audio, instead of writing into a player that hasn't opened yet.
|
|
8
|
+
- Fixed responses getting cut off at the end: playback now waits for the audio to fully drain before counting a response as finished, and quitting while the last bit of audio is still playing waits briefly for it to finish instead of killing it mid-sentence. A deliberate interrupt (stopping playback on purpose) still stops instantly.
|
|
9
|
+
- Synthesis requests to the voice provider are now batched into as few requests as possible and capped at two running at the same time, so the Agent stays within the provider's concurrent-request limits (for example, an ElevenLabs plan's allowance) instead of firing one request per sentence.
|
|
10
|
+
- Transient rate-limit errors from the voice provider now retry quietly in the background instead of stopping playback. If a piece of speech still fails after retrying, that piece is skipped with a single notice and the rest of the response keeps playing normally.
|
|
11
|
+
|
|
5
12
|
## 1.5.8 - 2026-07-06
|
|
6
13
|
|
|
7
14
|
- The `memory` CLI command (`goodvibes-agent memory ...`) now writes to the same shared cross-surface memory store that the runtime, the terminal UI, and the SDK already read and write. Previously the CLI opened its own private database, so a memory added from the CLI was invisible everywhere else; a memory added from the CLI is now visible in the Agent runtime, the TUI, and the SDK, and vice versa.
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# GoodVibes Agent
|
|
2
2
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
|
-
[](#install)
|
|
5
5
|
|
|
6
6
|
GoodVibes Agent is the installable autonomous operator assistant for GoodVibes. It keeps the existing terminal renderer and workspace bones, but the product goal is different from a vibecoding harness: the user should experience one assistant that can chat, plan, remember, research, schedule, send, generate, run visible agents, and operate the GoodVibes daemon contract with clear confirmation gates.
|
|
7
7
|
|
package/dist/package/main.js
CHANGED
|
@@ -830727,7 +830727,7 @@ var createStyledCell = (char, overrides = {}) => ({
|
|
|
830727
830727
|
// src/version.ts
|
|
830728
830728
|
import { readFileSync } from "fs";
|
|
830729
830729
|
import { join } from "path";
|
|
830730
|
-
var _version = "1.5.
|
|
830730
|
+
var _version = "1.5.9";
|
|
830731
830731
|
try {
|
|
830732
830732
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, "..", "package.json"), "utf-8"));
|
|
830733
830733
|
_version = typeof pkg.version === "string" ? pkg.version : _version;
|
|
@@ -967093,7 +967093,7 @@ class LocalStreamingAudioPlayer {
|
|
|
967093
967093
|
activeProcess = null;
|
|
967094
967094
|
spawnProcess;
|
|
967095
967095
|
constructor(options = {}) {
|
|
967096
|
-
this.command = resolveStreamingAudioPlayerCommand(options.env ?? process.env);
|
|
967096
|
+
this.command = options.command !== undefined ? options.command : resolveStreamingAudioPlayerCommand(options.env ?? process.env);
|
|
967097
967097
|
this.spawnProcess = options.spawnProcess ?? defaultSpawnProcess;
|
|
967098
967098
|
}
|
|
967099
967099
|
get label() {
|
|
@@ -967120,6 +967120,9 @@ class LocalStreamingAudioPlayer {
|
|
|
967120
967120
|
};
|
|
967121
967121
|
options.signal?.addEventListener("abort", abort6, { once: true });
|
|
967122
967122
|
try {
|
|
967123
|
+
await awaitReady(proc, options.signal);
|
|
967124
|
+
if (options.signal?.aborted)
|
|
967125
|
+
return;
|
|
967123
967126
|
for await (const chunk of chunks) {
|
|
967124
967127
|
if (options.signal?.aborted)
|
|
967125
967128
|
break;
|
|
@@ -967127,7 +967130,11 @@ class LocalStreamingAudioPlayer {
|
|
|
967127
967130
|
continue;
|
|
967128
967131
|
await writeStdin(proc, chunk.data);
|
|
967129
967132
|
}
|
|
967130
|
-
|
|
967133
|
+
if (options.signal?.aborted)
|
|
967134
|
+
return;
|
|
967135
|
+
try {
|
|
967136
|
+
proc.stdin.end();
|
|
967137
|
+
} catch {}
|
|
967131
967138
|
await waitForExit(proc);
|
|
967132
967139
|
} finally {
|
|
967133
967140
|
options.signal?.removeEventListener("abort", abort6);
|
|
@@ -967147,13 +967154,30 @@ class LocalStreamingAudioPlayer {
|
|
|
967147
967154
|
proc.kill("SIGTERM");
|
|
967148
967155
|
} catch {}
|
|
967149
967156
|
}
|
|
967157
|
+
waitForDrain(timeoutMs) {
|
|
967158
|
+
const proc = this.activeProcess;
|
|
967159
|
+
if (!proc)
|
|
967160
|
+
return Promise.resolve();
|
|
967161
|
+
return new Promise((resolve45) => {
|
|
967162
|
+
let settled = false;
|
|
967163
|
+
const settle = () => {
|
|
967164
|
+
if (settled)
|
|
967165
|
+
return;
|
|
967166
|
+
settled = true;
|
|
967167
|
+
clearTimeout(timer);
|
|
967168
|
+
resolve45();
|
|
967169
|
+
};
|
|
967170
|
+
const timer = setTimeout(settle, timeoutMs);
|
|
967171
|
+
proc.once("close", settle);
|
|
967172
|
+
});
|
|
967173
|
+
}
|
|
967150
967174
|
}
|
|
967151
967175
|
function resolveStreamingAudioPlayerCommand(env2 = process.env) {
|
|
967152
967176
|
const mpv = findExecutable("mpv", env2);
|
|
967153
967177
|
if (mpv) {
|
|
967154
967178
|
return {
|
|
967155
967179
|
command: mpv,
|
|
967156
|
-
args: ["--no-terminal", "--really-quiet", "--force-window=no", "
|
|
967180
|
+
args: ["--no-terminal", "--really-quiet", "--force-window=no", "-"],
|
|
967157
967181
|
label: "mpv"
|
|
967158
967182
|
};
|
|
967159
967183
|
}
|
|
@@ -967161,19 +967185,41 @@ function resolveStreamingAudioPlayerCommand(env2 = process.env) {
|
|
|
967161
967185
|
if (ffplay) {
|
|
967162
967186
|
return {
|
|
967163
967187
|
command: ffplay,
|
|
967164
|
-
args:
|
|
967188
|
+
args: FFPLAY_BASE_ARGS,
|
|
967165
967189
|
label: "ffplay"
|
|
967166
967190
|
};
|
|
967167
967191
|
}
|
|
967168
967192
|
return null;
|
|
967169
967193
|
}
|
|
967194
|
+
var FFPLAY_APAD = ["-af", "apad=pad_dur=0.3"];
|
|
967195
|
+
var FFPLAY_BASE_ARGS = ["-nodisp", "-autoexit", "-loglevel", "error", ...FFPLAY_APAD, "-i", "pipe:0"];
|
|
967170
967196
|
function buildPlayerArgs(command8, format3) {
|
|
967171
967197
|
if (command8.label !== "ffplay" || !format3)
|
|
967172
967198
|
return command8.args;
|
|
967173
967199
|
const normalized3 = format3.trim().toLowerCase();
|
|
967174
967200
|
if (!normalized3 || normalized3.includes("/"))
|
|
967175
967201
|
return command8.args;
|
|
967176
|
-
return ["-nodisp", "-autoexit", "-loglevel", "error", "-f", normalized3, "-i", "pipe:0"];
|
|
967202
|
+
return ["-nodisp", "-autoexit", "-loglevel", "error", ...FFPLAY_APAD, "-f", normalized3, "-i", "pipe:0"];
|
|
967203
|
+
}
|
|
967204
|
+
function awaitReady(proc, signal) {
|
|
967205
|
+
if (signal?.aborted)
|
|
967206
|
+
return Promise.resolve();
|
|
967207
|
+
return new Promise((resolve45, reject) => {
|
|
967208
|
+
let settled = false;
|
|
967209
|
+
const settle = (action2) => {
|
|
967210
|
+
if (settled)
|
|
967211
|
+
return;
|
|
967212
|
+
settled = true;
|
|
967213
|
+
signal?.removeEventListener("abort", onAbort);
|
|
967214
|
+
action2();
|
|
967215
|
+
};
|
|
967216
|
+
const onSpawn = () => settle(resolve45);
|
|
967217
|
+
const onError2 = (error73) => settle(() => reject(error73 instanceof Error ? error73 : new Error(String(error73))));
|
|
967218
|
+
const onAbort = () => settle(resolve45);
|
|
967219
|
+
proc.once("spawn", onSpawn);
|
|
967220
|
+
proc.once("error", onError2);
|
|
967221
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
967222
|
+
});
|
|
967177
967223
|
}
|
|
967178
967224
|
function findExecutable(name51, env2) {
|
|
967179
967225
|
const pathValue = env2.PATH ?? "";
|
|
@@ -967311,6 +967357,10 @@ function normalizeSpeechText(text) {
|
|
|
967311
967357
|
}
|
|
967312
967358
|
|
|
967313
967359
|
// src/audio/spoken-turn-controller.ts
|
|
967360
|
+
var SYNTHESIS_PIPELINE_WINDOW = 2;
|
|
967361
|
+
var SYNTHESIS_MERGE_MAX_CHARS = 1500;
|
|
967362
|
+
var SYNTHESIS_RETRY_DELAYS_MS = [1000, 2500];
|
|
967363
|
+
|
|
967314
967364
|
class SpokenTurnController {
|
|
967315
967365
|
pendingPrompt = null;
|
|
967316
967366
|
activeTurnId = null;
|
|
@@ -967320,6 +967370,12 @@ class SpokenTurnController {
|
|
|
967320
967370
|
abortControllers = new Set;
|
|
967321
967371
|
timer = null;
|
|
967322
967372
|
errorReportedForTurn = false;
|
|
967373
|
+
noPlayerNoticed = false;
|
|
967374
|
+
pendingTexts = [];
|
|
967375
|
+
pipelineDepth = 0;
|
|
967376
|
+
pipelineGeneration = 0;
|
|
967377
|
+
pumpScheduled = false;
|
|
967378
|
+
completedTurnId = null;
|
|
967323
967379
|
voiceService;
|
|
967324
967380
|
configManager;
|
|
967325
967381
|
player;
|
|
@@ -967327,6 +967383,8 @@ class SpokenTurnController {
|
|
|
967327
967383
|
now;
|
|
967328
967384
|
setIntervalImpl;
|
|
967329
967385
|
clearIntervalImpl;
|
|
967386
|
+
setTimeoutImpl;
|
|
967387
|
+
clearTimeoutImpl;
|
|
967330
967388
|
constructor(options) {
|
|
967331
967389
|
this.voiceService = options.voiceService;
|
|
967332
967390
|
this.configManager = options.configManager;
|
|
@@ -967335,6 +967393,8 @@ class SpokenTurnController {
|
|
|
967335
967393
|
this.now = options.now ?? (() => Date.now());
|
|
967336
967394
|
this.setIntervalImpl = options.setInterval ?? setInterval;
|
|
967337
967395
|
this.clearIntervalImpl = options.clearInterval ?? clearInterval;
|
|
967396
|
+
this.setTimeoutImpl = options.setTimeout ?? setTimeout;
|
|
967397
|
+
this.clearTimeoutImpl = options.clearTimeout ?? clearTimeout;
|
|
967338
967398
|
}
|
|
967339
967399
|
submitNextTurn(prompt) {
|
|
967340
967400
|
const normalized3 = prompt.trim();
|
|
@@ -967342,26 +967402,46 @@ class SpokenTurnController {
|
|
|
967342
967402
|
return false;
|
|
967343
967403
|
this.stop();
|
|
967344
967404
|
if (!this.player.available) {
|
|
967345
|
-
this.
|
|
967405
|
+
if (!this.noPlayerNoticed) {
|
|
967406
|
+
this.noPlayerNoticed = true;
|
|
967407
|
+
this.notify?.("[TTS] Text response will continue, but live audio is unavailable. Install mpv or ffplay.");
|
|
967408
|
+
}
|
|
967346
967409
|
return false;
|
|
967347
967410
|
}
|
|
967411
|
+
this.noPlayerNoticed = false;
|
|
967348
967412
|
this.pendingPrompt = normalized3;
|
|
967349
967413
|
return true;
|
|
967350
967414
|
}
|
|
967351
967415
|
stop(message) {
|
|
967416
|
+
const wasActive = this.pendingPrompt !== null || this.activeTurnId !== null || this.chunker !== null || this.abortControllers.size > 0;
|
|
967352
967417
|
this.pendingPrompt = null;
|
|
967353
967418
|
this.activeTurnId = null;
|
|
967354
967419
|
this.chunker?.reset();
|
|
967355
967420
|
this.chunker = null;
|
|
967356
967421
|
this.stopTimer();
|
|
967422
|
+
this.resetPipeline();
|
|
967357
967423
|
for (const controller of this.abortControllers)
|
|
967358
967424
|
controller.abort();
|
|
967359
967425
|
this.abortControllers.clear();
|
|
967360
967426
|
this.player.stop();
|
|
967361
967427
|
this.playbackChain = Promise.resolve();
|
|
967362
967428
|
this.errorReportedForTurn = false;
|
|
967363
|
-
if (message)
|
|
967429
|
+
if (message && wasActive)
|
|
967364
967430
|
this.notify?.(`[TTS] ${message}`);
|
|
967431
|
+
return wasActive;
|
|
967432
|
+
}
|
|
967433
|
+
async stopForExit(drainTimeoutMs = 2000) {
|
|
967434
|
+
this.pendingPrompt = null;
|
|
967435
|
+
this.activeTurnId = null;
|
|
967436
|
+
this.chunker?.reset();
|
|
967437
|
+
this.chunker = null;
|
|
967438
|
+
this.stopTimer();
|
|
967439
|
+
this.resetPipeline();
|
|
967440
|
+
for (const controller of this.abortControllers)
|
|
967441
|
+
controller.abort();
|
|
967442
|
+
this.abortControllers.clear();
|
|
967443
|
+
await this.player.waitForDrain(drainTimeoutMs);
|
|
967444
|
+
this.stop();
|
|
967365
967445
|
}
|
|
967366
967446
|
handleTurnEvent(event) {
|
|
967367
967447
|
if (event.type === "TURN_SUBMITTED") {
|
|
@@ -967371,7 +967451,7 @@ class SpokenTurnController {
|
|
|
967371
967451
|
if (!this.activeTurnId || event.turnId !== this.activeTurnId)
|
|
967372
967452
|
return;
|
|
967373
967453
|
if (event.type === "STREAM_DELTA") {
|
|
967374
|
-
this.
|
|
967454
|
+
this.queueTexts(this.chunker?.push(event.content) ?? []);
|
|
967375
967455
|
return;
|
|
967376
967456
|
}
|
|
967377
967457
|
if (event.type === "STREAM_END") {
|
|
@@ -967396,29 +967476,30 @@ class SpokenTurnController {
|
|
|
967396
967476
|
this.errorReportedForTurn = false;
|
|
967397
967477
|
this.chunker = new TtsTextChunker({ now: this.now });
|
|
967398
967478
|
this.playbackChain = Promise.resolve();
|
|
967479
|
+
this.resetPipeline();
|
|
967399
967480
|
this.startTimer();
|
|
967400
967481
|
this.notify?.(`[TTS] Live playback queued through ${this.player.label}.`);
|
|
967401
967482
|
}
|
|
967402
967483
|
finishTurn(turnId) {
|
|
967403
967484
|
if (turnId !== this.activeTurnId)
|
|
967404
967485
|
return;
|
|
967405
|
-
this.
|
|
967486
|
+
this.queueTexts(this.chunker?.flushAll() ?? []);
|
|
967406
967487
|
this.stopTimer();
|
|
967407
|
-
|
|
967408
|
-
|
|
967409
|
-
|
|
967410
|
-
|
|
967411
|
-
|
|
967412
|
-
|
|
967413
|
-
|
|
967414
|
-
|
|
967488
|
+
this.completedTurnId = turnId;
|
|
967489
|
+
this.maybeReleaseTurn();
|
|
967490
|
+
}
|
|
967491
|
+
resetPipeline() {
|
|
967492
|
+
this.pendingTexts = [];
|
|
967493
|
+
this.pipelineDepth = 0;
|
|
967494
|
+
this.pipelineGeneration++;
|
|
967495
|
+
this.completedTurnId = null;
|
|
967415
967496
|
}
|
|
967416
967497
|
startTimer() {
|
|
967417
967498
|
this.stopTimer();
|
|
967418
967499
|
this.timer = this.setIntervalImpl(() => {
|
|
967419
967500
|
if (!this.activeTurnId || !this.chunker)
|
|
967420
967501
|
return;
|
|
967421
|
-
this.
|
|
967502
|
+
this.queueTexts(this.chunker.flushDue());
|
|
967422
967503
|
}, 250);
|
|
967423
967504
|
}
|
|
967424
967505
|
stopTimer() {
|
|
@@ -967427,42 +967508,136 @@ class SpokenTurnController {
|
|
|
967427
967508
|
this.clearIntervalImpl(this.timer);
|
|
967428
967509
|
this.timer = null;
|
|
967429
967510
|
}
|
|
967430
|
-
|
|
967511
|
+
queueTexts(chunks) {
|
|
967431
967512
|
for (const chunk of chunks) {
|
|
967432
|
-
|
|
967513
|
+
if (chunk.trim())
|
|
967514
|
+
this.pendingTexts.push(chunk);
|
|
967515
|
+
}
|
|
967516
|
+
if (this.pendingTexts.length > 0)
|
|
967517
|
+
this.schedulePump();
|
|
967518
|
+
}
|
|
967519
|
+
schedulePump() {
|
|
967520
|
+
if (this.pumpScheduled)
|
|
967521
|
+
return;
|
|
967522
|
+
this.pumpScheduled = true;
|
|
967523
|
+
queueMicrotask(() => {
|
|
967524
|
+
this.pumpScheduled = false;
|
|
967525
|
+
this.pump();
|
|
967526
|
+
});
|
|
967527
|
+
}
|
|
967528
|
+
pump() {
|
|
967529
|
+
while (this.activeTurnId && this.pendingTexts.length > 0 && this.pipelineDepth < SYNTHESIS_PIPELINE_WINDOW) {
|
|
967530
|
+
this.dispatchChunk(this.takeMergedText());
|
|
967531
|
+
}
|
|
967532
|
+
this.maybeReleaseTurn();
|
|
967533
|
+
}
|
|
967534
|
+
takeMergedText() {
|
|
967535
|
+
let merged = "";
|
|
967536
|
+
while (this.pendingTexts.length > 0) {
|
|
967537
|
+
const next = this.pendingTexts[0];
|
|
967538
|
+
if (!merged && next.length > SYNTHESIS_MERGE_MAX_CHARS) {
|
|
967539
|
+
const cut = findSplitIndex(next, SYNTHESIS_MERGE_MAX_CHARS);
|
|
967540
|
+
this.pendingTexts[0] = next.slice(cut).trim();
|
|
967541
|
+
return next.slice(0, cut).trim();
|
|
967542
|
+
}
|
|
967543
|
+
if (merged && merged.length + 1 + next.length > SYNTHESIS_MERGE_MAX_CHARS)
|
|
967544
|
+
break;
|
|
967545
|
+
merged = merged ? `${merged} ${next}` : next;
|
|
967546
|
+
this.pendingTexts.shift();
|
|
967433
967547
|
}
|
|
967548
|
+
return merged;
|
|
967434
967549
|
}
|
|
967435
|
-
|
|
967550
|
+
dispatchChunk(text) {
|
|
967436
967551
|
const turnId = this.activeTurnId;
|
|
967437
967552
|
if (!turnId || !text.trim())
|
|
967438
967553
|
return;
|
|
967439
967554
|
const sequence = ++this.chunkSequence;
|
|
967555
|
+
const generation = this.pipelineGeneration;
|
|
967556
|
+
this.pipelineDepth++;
|
|
967440
967557
|
const abortController = new AbortController;
|
|
967441
967558
|
this.abortControllers.add(abortController);
|
|
967442
|
-
const resultPromise = this.
|
|
967559
|
+
const resultPromise = this.synthesizeWithRetry(text, turnId, sequence, abortController.signal).then((result2) => ({ ok: true, result: result2 })).catch((error73) => ({ ok: false, error: error73 }));
|
|
967443
967560
|
this.playbackChain = this.playbackChain.then(async () => {
|
|
967444
|
-
|
|
967445
|
-
|
|
967446
|
-
|
|
967447
|
-
|
|
967448
|
-
|
|
967449
|
-
|
|
967450
|
-
|
|
967561
|
+
try {
|
|
967562
|
+
if (abortController.signal.aborted) {
|
|
967563
|
+
this.abortControllers.delete(abortController);
|
|
967564
|
+
return;
|
|
967565
|
+
}
|
|
967566
|
+
const result2 = await resultPromise;
|
|
967567
|
+
this.abortControllers.delete(abortController);
|
|
967568
|
+
if (abortController.signal.aborted)
|
|
967569
|
+
return;
|
|
967570
|
+
if (!result2.ok) {
|
|
967571
|
+
this.reportSkippedChunk(result2.error);
|
|
967572
|
+
return;
|
|
967573
|
+
}
|
|
967574
|
+
await this.player.play(result2.result.chunks, {
|
|
967575
|
+
format: String(result2.result.format ?? "mp3"),
|
|
967576
|
+
signal: abortController.signal
|
|
967577
|
+
});
|
|
967578
|
+
} finally {
|
|
967579
|
+
this.releasePipelineSlot(generation);
|
|
967451
967580
|
}
|
|
967452
|
-
await this.player.play(result2.result.chunks, {
|
|
967453
|
-
format: String(result2.result.format ?? "mp3"),
|
|
967454
|
-
signal: abortController.signal
|
|
967455
|
-
});
|
|
967456
967581
|
}).catch((error73) => {
|
|
967457
967582
|
this.abortControllers.delete(abortController);
|
|
967458
967583
|
this.reportError(error73);
|
|
967459
967584
|
});
|
|
967460
967585
|
}
|
|
967586
|
+
releasePipelineSlot(generation) {
|
|
967587
|
+
if (generation !== this.pipelineGeneration)
|
|
967588
|
+
return;
|
|
967589
|
+
this.pipelineDepth = Math.max(0, this.pipelineDepth - 1);
|
|
967590
|
+
if (this.pendingTexts.length > 0) {
|
|
967591
|
+
this.schedulePump();
|
|
967592
|
+
return;
|
|
967593
|
+
}
|
|
967594
|
+
this.maybeReleaseTurn();
|
|
967595
|
+
}
|
|
967596
|
+
maybeReleaseTurn() {
|
|
967597
|
+
if (!this.completedTurnId || this.completedTurnId !== this.activeTurnId)
|
|
967598
|
+
return;
|
|
967599
|
+
if (this.pendingTexts.length > 0 || this.pipelineDepth > 0 || this.pumpScheduled)
|
|
967600
|
+
return;
|
|
967601
|
+
this.activeTurnId = null;
|
|
967602
|
+
this.completedTurnId = null;
|
|
967603
|
+
this.chunker = null;
|
|
967604
|
+
this.abortControllers.clear();
|
|
967605
|
+
}
|
|
967606
|
+
async synthesizeWithRetry(text, turnId, sequence, signal) {
|
|
967607
|
+
for (let attempt = 0;; attempt++) {
|
|
967608
|
+
try {
|
|
967609
|
+
return await this.synthesize(text, turnId, sequence, signal);
|
|
967610
|
+
} catch (error73) {
|
|
967611
|
+
const retryable = attempt < SYNTHESIS_RETRY_DELAYS_MS.length && !signal.aborted && isTransientSynthesisError(error73);
|
|
967612
|
+
if (!retryable)
|
|
967613
|
+
throw error73;
|
|
967614
|
+
await this.delay(SYNTHESIS_RETRY_DELAYS_MS[attempt], signal);
|
|
967615
|
+
}
|
|
967616
|
+
}
|
|
967617
|
+
}
|
|
967618
|
+
delay(ms, signal) {
|
|
967619
|
+
return new Promise((resolve45, reject) => {
|
|
967620
|
+
if (signal.aborted) {
|
|
967621
|
+
reject(new Error("Synthesis retry cancelled"));
|
|
967622
|
+
return;
|
|
967623
|
+
}
|
|
967624
|
+
const timer = this.setTimeoutImpl(() => {
|
|
967625
|
+
signal.removeEventListener("abort", onAbort);
|
|
967626
|
+
resolve45();
|
|
967627
|
+
}, ms);
|
|
967628
|
+
const onAbort = () => {
|
|
967629
|
+
this.clearTimeoutImpl(timer);
|
|
967630
|
+
reject(new Error("Synthesis retry cancelled"));
|
|
967631
|
+
};
|
|
967632
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
967633
|
+
});
|
|
967634
|
+
}
|
|
967461
967635
|
synthesize(text, turnId, sequence, signal) {
|
|
967462
967636
|
return this.voiceService.synthesizeStream(readOptionalConfigString2(this.configManager, "tts.provider"), {
|
|
967463
967637
|
text,
|
|
967464
967638
|
voiceId: readOptionalConfigString2(this.configManager, "tts.voice"),
|
|
967465
967639
|
format: "mp3",
|
|
967640
|
+
speed: readOptionalConfigNumber(this.configManager, "tts.speed"),
|
|
967466
967641
|
signal,
|
|
967467
967642
|
metadata: {
|
|
967468
967643
|
source: "goodvibes-agent",
|
|
@@ -967472,6 +967647,12 @@ class SpokenTurnController {
|
|
|
967472
967647
|
}
|
|
967473
967648
|
});
|
|
967474
967649
|
}
|
|
967650
|
+
reportSkippedChunk(error73) {
|
|
967651
|
+
if (this.errorReportedForTurn)
|
|
967652
|
+
return;
|
|
967653
|
+
this.errorReportedForTurn = true;
|
|
967654
|
+
this.notify?.(`[TTS] Skipping part of the spoken response \u2014 synthesis kept failing (${summarizeError(error73)}). Playback continues with the rest.`);
|
|
967655
|
+
}
|
|
967475
967656
|
reportError(error73) {
|
|
967476
967657
|
if (this.errorReportedForTurn)
|
|
967477
967658
|
return;
|
|
@@ -967479,6 +967660,7 @@ class SpokenTurnController {
|
|
|
967479
967660
|
this.activeTurnId = null;
|
|
967480
967661
|
this.chunker = null;
|
|
967481
967662
|
this.stopTimer();
|
|
967663
|
+
this.resetPipeline();
|
|
967482
967664
|
for (const controller of this.abortControllers)
|
|
967483
967665
|
controller.abort();
|
|
967484
967666
|
this.abortControllers.clear();
|
|
@@ -967487,17 +967669,35 @@ class SpokenTurnController {
|
|
|
967487
967669
|
this.notify?.(`[TTS] Live playback stopped: ${summarizeError(error73)}`);
|
|
967488
967670
|
}
|
|
967489
967671
|
}
|
|
967672
|
+
function isTransientSynthesisError(error73) {
|
|
967673
|
+
const message = (error73 instanceof Error ? error73.message : String(error73)).toLowerCase();
|
|
967674
|
+
if (message.includes("429") || message.includes("rate limit") || message.includes("rate_limit") || message.includes("too many requests") || message.includes("concurrent"))
|
|
967675
|
+
return true;
|
|
967676
|
+
if (/http 5\d\d/.test(message))
|
|
967677
|
+
return true;
|
|
967678
|
+
return message.includes("fetch failed") || message.includes("network") || message.includes("timed out") || message.includes("timeout") || message.includes("econnreset") || message.includes("socket");
|
|
967679
|
+
}
|
|
967680
|
+
function findSplitIndex(text, limit3) {
|
|
967681
|
+
const space = text.lastIndexOf(" ", limit3);
|
|
967682
|
+
return space > 0 ? space : limit3;
|
|
967683
|
+
}
|
|
967490
967684
|
function readOptionalConfigString2(configManager, key) {
|
|
967491
967685
|
const value = String(configManager.get(key) ?? "").trim();
|
|
967492
967686
|
return value || undefined;
|
|
967493
967687
|
}
|
|
967688
|
+
function readOptionalConfigNumber(configManager, key) {
|
|
967689
|
+
const raw = configManager.get(key);
|
|
967690
|
+
const value = typeof raw === "number" ? raw : parseFloat(String(raw ?? ""));
|
|
967691
|
+
return isFinite(value) && value > 0 ? value : undefined;
|
|
967692
|
+
}
|
|
967494
967693
|
|
|
967495
967694
|
// src/audio/spoken-turn-wiring.ts
|
|
967496
967695
|
function wireSpokenTurnRuntime(options) {
|
|
967696
|
+
const player = options.playerFactory ? options.playerFactory() : new LocalStreamingAudioPlayer;
|
|
967497
967697
|
const controller = new SpokenTurnController({
|
|
967498
967698
|
voiceService: options.voiceService,
|
|
967499
967699
|
configManager: options.configManager,
|
|
967500
|
-
player
|
|
967700
|
+
player,
|
|
967501
967701
|
notify: options.notify
|
|
967502
967702
|
});
|
|
967503
967703
|
const turns = options.events.turns;
|
|
@@ -967513,7 +967713,35 @@ function wireSpokenTurnRuntime(options) {
|
|
|
967513
967713
|
return {
|
|
967514
967714
|
unsubs,
|
|
967515
967715
|
submitNextTurn: (prompt) => controller.submitNextTurn(prompt),
|
|
967516
|
-
stop: (message) => controller.stop(message)
|
|
967716
|
+
stop: (message) => controller.stop(message),
|
|
967717
|
+
stopForExit: (drainTimeoutMs) => controller.stopForExit(drainTimeoutMs)
|
|
967718
|
+
};
|
|
967719
|
+
}
|
|
967720
|
+
|
|
967721
|
+
// src/runtime/unhandled-rejection-guard.ts
|
|
967722
|
+
function createUnhandledRejectionHandler(deps) {
|
|
967723
|
+
let rejectionCount = 0;
|
|
967724
|
+
let windowStart = Date.now();
|
|
967725
|
+
return (reason) => {
|
|
967726
|
+
const now5 = Date.now();
|
|
967727
|
+
if (now5 - windowStart > 1e4) {
|
|
967728
|
+
rejectionCount = 0;
|
|
967729
|
+
windowStart = now5;
|
|
967730
|
+
}
|
|
967731
|
+
rejectionCount++;
|
|
967732
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
967733
|
+
if (rejectionCount > 3) {
|
|
967734
|
+
logger.error("CRITICAL: cascading unhandled rejections \u2014 consider restarting", {
|
|
967735
|
+
count: rejectionCount,
|
|
967736
|
+
windowMs: now5 - windowStart,
|
|
967737
|
+
error: String(reason)
|
|
967738
|
+
});
|
|
967739
|
+
deps.notifyHigh(`[Critical] Multiple errors detected (${rejectionCount} in 10s). If the issue persists, please restart. Latest: ${msg}`);
|
|
967740
|
+
} else {
|
|
967741
|
+
deps.notifyHigh(`[Error] ${msg}`);
|
|
967742
|
+
logger.error("unhandledRejection", { error: String(reason) });
|
|
967743
|
+
}
|
|
967744
|
+
deps.render();
|
|
967517
967745
|
};
|
|
967518
967746
|
}
|
|
967519
967747
|
|
|
@@ -968043,36 +968271,26 @@ async function main() {
|
|
|
968043
968271
|
let stopSpokenOutputForExit = null;
|
|
968044
968272
|
let recoveryPending = false;
|
|
968045
968273
|
const sigintHandler = () => input.feed("\x03");
|
|
968046
|
-
|
|
968047
|
-
|
|
968048
|
-
|
|
968049
|
-
|
|
968050
|
-
if (now5 - _unhandledRejectionWindowStart > 1e4) {
|
|
968051
|
-
_unhandledRejectionCount = 0;
|
|
968052
|
-
_unhandledRejectionWindowStart = now5;
|
|
968053
|
-
}
|
|
968054
|
-
_unhandledRejectionCount++;
|
|
968055
|
-
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
968056
|
-
if (_unhandledRejectionCount > 3) {
|
|
968057
|
-
logger.error("CRITICAL: cascading unhandled rejections \u2014 consider restarting", {
|
|
968058
|
-
count: _unhandledRejectionCount,
|
|
968059
|
-
windowMs: now5 - _unhandledRejectionWindowStart,
|
|
968060
|
-
error: String(reason)
|
|
968061
|
-
});
|
|
968062
|
-
systemMessageRouter.high(`[Critical] Multiple errors detected (${_unhandledRejectionCount} in 10s). If the issue persists, please restart. Latest: ${msg}`);
|
|
968063
|
-
} else {
|
|
968064
|
-
systemMessageRouter.high(`[Error] ${msg}`);
|
|
968065
|
-
logger.error("unhandledRejection", { error: String(reason) });
|
|
968066
|
-
}
|
|
968067
|
-
render();
|
|
968068
|
-
};
|
|
968274
|
+
const unhandledRejectionHandler = createUnhandledRejectionHandler({
|
|
968275
|
+
notifyHigh: (message) => systemMessageRouter.high(message),
|
|
968276
|
+
render: () => render()
|
|
968277
|
+
});
|
|
968069
968278
|
const resizeHandler = () => {
|
|
968070
968279
|
input.setContentWidth(getPromptContentWidth());
|
|
968071
968280
|
compositor.resetDiff();
|
|
968072
968281
|
render();
|
|
968073
968282
|
};
|
|
968283
|
+
let exiting = false;
|
|
968074
968284
|
const exitApp = () => {
|
|
968075
|
-
|
|
968285
|
+
if (exiting)
|
|
968286
|
+
return;
|
|
968287
|
+
exiting = true;
|
|
968288
|
+
let spokenOutputDrain = Promise.resolve();
|
|
968289
|
+
try {
|
|
968290
|
+
spokenOutputDrain = Promise.resolve(stopSpokenOutputForExit?.()).then(() => {
|
|
968291
|
+
return;
|
|
968292
|
+
});
|
|
968293
|
+
} catch {}
|
|
968076
968294
|
unsubs.forEach((fn) => fn());
|
|
968077
968295
|
autonomy.stop();
|
|
968078
968296
|
const snapshot2 = buildCurrentSessionSnapshot();
|
|
@@ -968092,7 +968310,9 @@ async function main() {
|
|
|
968092
968310
|
allowTerminalWrite(() => stdout.write(PASTE_DISABLE + KEYBOARD_EXT_DISABLE2 + MOUSE_DISABLE + FOCUS_DISABLE + CURSOR_SHOW + exitScreen));
|
|
968093
968311
|
terminalOutputGuard.dispose();
|
|
968094
968312
|
stdin.setRawMode(false);
|
|
968095
|
-
|
|
968313
|
+
spokenOutputDrain.catch(() => {
|
|
968314
|
+
return;
|
|
968315
|
+
}).then(() => process.exit(0));
|
|
968096
968316
|
};
|
|
968097
968317
|
commandContext.exit = exitApp;
|
|
968098
968318
|
const spokenTurns = wireSpokenTurnRuntime({
|
|
@@ -968104,7 +968324,7 @@ async function main() {
|
|
|
968104
968324
|
render();
|
|
968105
968325
|
}
|
|
968106
968326
|
});
|
|
968107
|
-
stopSpokenOutputForExit = () => spokenTurns.
|
|
968327
|
+
stopSpokenOutputForExit = () => spokenTurns.stopForExit();
|
|
968108
968328
|
unsubs.push(...spokenTurns.unsubs);
|
|
968109
968329
|
unsubs.push(attachSpokenTurnModelRouting({
|
|
968110
968330
|
orchestrator,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-agent",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "GoodVibes personal operator assistant TUI with a proactive Agent product brain, isolated Agent Knowledge, local profiles, routines, skills, personas, and explicit build delegation.",
|
|
6
6
|
"type": "module",
|