@livx.cc/agentx 0.96.16 → 0.96.18

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/dist/cli.js CHANGED
@@ -5500,6 +5500,11 @@ var VoiceEngineOptions = class {
5500
5500
  * as a barge and abort the fresh turn (live: mid-sentence self-interruption + steps=1→steps=0 double
5501
5501
  * abort). Short enough that a genuine immediate barge ("no wait—") still lands right after. */
5502
5502
  bargeGraceMs = 600;
5503
+ /** Barge-in (talk over the assistant to interrupt). true = full-duplex (needs echo cancellation, or
5504
+ * the assistant's own TTS bleeds back and self-interrupts). false = HALF-DUPLEX: the engine is deaf
5505
+ * while audible (speaking + drain tail), so echo can never become a phantom turn — the right mode
5506
+ * when there's no AEC (e.g. the non-VPIO mic fallback) and no headphones. Cost: can't interrupt. */
5507
+ bargeIn = true;
5503
5508
  /** Filler phrase spoken when holding for an incomplete utterance ('' disables). */
5504
5509
  holdFiller = "";
5505
5510
  /** Called when the engine holds an incomplete utterance (host can render a visual cue). */
@@ -5593,6 +5598,10 @@ var VoiceEngine = class _VoiceEngine {
5593
5598
  get usingAec() {
5594
5599
  return this.stt.usingAec;
5595
5600
  }
5601
+ /** Flip barge-in at runtime (e.g. the mic fell back to non-VPIO → go half-duplex so echo can't leak). */
5602
+ setBargeIn(on) {
5603
+ this.options.bargeIn = on;
5604
+ }
5596
5605
  idleWaiters = [];
5597
5606
  setState(s) {
5598
5607
  if (this.state === s) return;
@@ -5744,6 +5753,7 @@ var VoiceEngine = class _VoiceEngine {
5744
5753
  }
5745
5754
  handlePartial(text) {
5746
5755
  if (this.speaking) {
5756
+ if (!this.options.bargeIn) return;
5747
5757
  if (now() < this.bargeGraceUntil) {
5748
5758
  if (!this.echoActive() || (this.usingAec ? this.genuine(text) : this.novelWords(text).length >= 1)) this.options.onPartial(text);
5749
5759
  return;
@@ -5819,7 +5829,7 @@ var VoiceEngine = class _VoiceEngine {
5819
5829
  this.stt.reset();
5820
5830
  return;
5821
5831
  }
5822
- if (this.echoActive() && (this.usingAec ? !this.genuine(text) : this.novelWords(text).length < 2)) {
5832
+ if (this.echoActive() && (!this.options.bargeIn || (this.usingAec ? !this.genuine(text) : this.novelWords(text).length < 2))) {
5823
5833
  this.stt.reset();
5824
5834
  return;
5825
5835
  }
@@ -7267,22 +7277,55 @@ var AecDuplexAudio = class {
7267
7277
  this.bin = bin;
7268
7278
  }
7269
7279
  bin;
7270
- aec = true;
7280
+ /** Mutable: starts true (VPIO/AEC). Flips false if we fall back to non-VPIO capture (heuristic tier). */
7281
+ _aec = true;
7282
+ get aec() {
7283
+ return this._aec;
7284
+ }
7271
7285
  onFatal;
7286
+ /** Fired once when capture degrades to the non-VPIO (no-AEC) fallback — the host switches to
7287
+ * half-duplex so the assistant's own TTS can't bleed back as a phantom turn. */
7288
+ onDegrade;
7272
7289
  proc = null;
7273
7290
  stopped = false;
7274
7291
  micDenied = false;
7292
+ noVpio = false;
7293
+ // currently running the non-VPIO fallback
7294
+ triedFallback = false;
7295
+ // one-shot guard
7296
+ gotChunk = false;
7297
+ // any mic audio since (re)spawn?
7298
+ onChunk = () => {
7299
+ };
7300
+ fallbackTimer = null;
7275
7301
  bytesWritten = 0;
7276
7302
  startedAt = 0;
7277
7303
  // --- AudioSource ---
7278
7304
  start(onChunk) {
7279
- this.proc = spawn2(this.bin, [], { stdio: ["pipe", "pipe", "pipe"] });
7305
+ this.onChunk = onChunk;
7306
+ if (process.env.MIC_NO_VPIO === "1") {
7307
+ this.noVpio = true;
7308
+ this.triedFallback = true;
7309
+ this._aec = false;
7310
+ this.onDegrade?.();
7311
+ }
7312
+ this.spawnHelper();
7313
+ }
7314
+ /** (Re)spawn the helper. On the first spawn, arm a fast watchdog: if VPIO delivers NO audio within
7315
+ * ~2.5s, the VP input tap is dead on this machine (seen on macOS 26.5.x) — respawn once with
7316
+ * MIC_NO_VPIO=1 (plain capture, heuristic echo) so the mic actually works instead of starving STT. */
7317
+ spawnHelper() {
7318
+ const env = this.noVpio ? { ...process.env, MIC_NO_VPIO: "1" } : process.env;
7319
+ this.proc = spawn2(this.bin, [], { stdio: ["pipe", "pipe", "pipe"], env });
7280
7320
  this.proc.stdin.on("error", () => {
7281
7321
  });
7282
7322
  this.proc.on("exit", (c) => {
7283
7323
  if (c && !this.stopped) log16.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
7284
7324
  });
7285
- this.proc.stdout.on("data", (chunk) => onChunk(chunk));
7325
+ this.proc.stdout.on("data", (chunk) => {
7326
+ this.gotChunk = true;
7327
+ this.onChunk(chunk);
7328
+ });
7286
7329
  this.proc.stderr.on("data", (d) => {
7287
7330
  for (const ln of String(d).split("\n")) {
7288
7331
  const s = ln.trim();
@@ -7296,9 +7339,22 @@ var AecDuplexAudio = class {
7296
7339
  } else log16.debug(`mic-aec: ${s}`);
7297
7340
  }
7298
7341
  });
7299
- }
7300
- stop() {
7301
- this.stopped = true;
7342
+ if (!this.noVpio && !this.triedFallback) {
7343
+ this.fallbackTimer = setTimeout(() => {
7344
+ if (this.stopped || this.gotChunk) return;
7345
+ this.triedFallback = true;
7346
+ this.noVpio = true;
7347
+ this._aec = false;
7348
+ log16.warn("mic-aec: VPIO delivered no audio in 2.5s \u2014 falling back to non-VPIO capture (no AEC \u2192 half-duplex, no barge-in)");
7349
+ this.onDegrade?.();
7350
+ this.killProc();
7351
+ this.spawnHelper();
7352
+ }, 2500);
7353
+ this.fallbackTimer.unref?.();
7354
+ }
7355
+ }
7356
+ /** Kill the current child WITHOUT marking the whole source stopped (used for the fallback respawn). */
7357
+ killProc() {
7302
7358
  const p = this.proc;
7303
7359
  this.proc = null;
7304
7360
  if (!p) return;
@@ -7310,6 +7366,11 @@ var AecDuplexAudio = class {
7310
7366
  }
7311
7367
  }, 500).unref?.();
7312
7368
  }
7369
+ stop() {
7370
+ this.stopped = true;
7371
+ if (this.fallbackTimer) clearTimeout(this.fallbackTimer);
7372
+ this.killProc();
7373
+ }
7313
7374
  // --- AudioSink (frame writer; same played/drain byte-math as the ffplay Player) ---
7314
7375
  frame(payload) {
7315
7376
  const stdin = this.proc?.stdin;
@@ -7410,6 +7471,7 @@ var VoiceIO = class extends VoiceEngine {
7410
7471
  // textless residue pre-pause: opt-in (hiccup source)
7411
7472
  });
7412
7473
  this.duplexSource = duplex;
7474
+ if (duplex) duplex.onDegrade = () => this.setBargeIn(false);
7413
7475
  }
7414
7476
  /** Host hook for an unrecoverable audio failure — mic permission denied (duplex source) or no mic
7415
7477
  * audio at all (STT watchdog). Routed to whichever can detect it. */