@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 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
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
4
- [![Version: 1.5.3](https://img.shields.io/badge/version-1.5.8-blue.svg)](#install)
4
+ [![Version: 1.5.3](https://img.shields.io/badge/version-1.5.9-blue.svg)](#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
 
@@ -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.8";
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
- proc.stdin.end();
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", "--cache=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: ["-nodisp", "-autoexit", "-loglevel", "error", "-i", "pipe:0"],
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.notify?.("[TTS] Text response will continue, but live audio is unavailable. Install mpv or ffplay.");
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.enqueueChunks(this.chunker?.push(event.content) ?? []);
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.enqueueChunks(this.chunker?.flushAll() ?? []);
967486
+ this.queueTexts(this.chunker?.flushAll() ?? []);
967406
967487
  this.stopTimer();
967407
- const chain4 = this.playbackChain;
967408
- chain4.finally(() => {
967409
- if (this.activeTurnId !== turnId)
967410
- return;
967411
- this.activeTurnId = null;
967412
- this.chunker = null;
967413
- this.abortControllers.clear();
967414
- }).catch(() => {});
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.enqueueChunks(this.chunker.flushDue());
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
- enqueueChunks(chunks) {
967511
+ queueTexts(chunks) {
967431
967512
  for (const chunk of chunks) {
967432
- this.enqueueChunk(chunk);
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
- enqueueChunk(text) {
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.synthesize(text, turnId, sequence, abortController.signal).then((result2) => ({ ok: true, result: result2 })).catch((error73) => ({ ok: false, error: error73 }));
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
- if (abortController.signal.aborted)
967445
- return;
967446
- const result2 = await resultPromise;
967447
- this.abortControllers.delete(abortController);
967448
- if (!result2.ok) {
967449
- this.reportError(result2.error);
967450
- return;
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: new LocalStreamingAudioPlayer,
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
- let _unhandledRejectionCount = 0;
968047
- let _unhandledRejectionWindowStart = Date.now();
968048
- const unhandledRejectionHandler = (reason) => {
968049
- const now5 = Date.now();
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
- stopSpokenOutputForExit?.();
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
- process.exit(0);
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.stop();
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.8",
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",