@livx.cc/agentx 0.99.100 → 0.99.102
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.d.ts +4 -0
- package/dist/cli.js +365 -182
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +53 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -709,16 +709,16 @@ ${entries.map((s) => " " + s).join("\n")}`);
|
|
|
709
709
|
return blocks.length ? blocks.join("\n") : `(no ${label} found)`;
|
|
710
710
|
}
|
|
711
711
|
function fuzzyLineReplace(content, oldStr, newStr) {
|
|
712
|
-
const
|
|
712
|
+
const norm4 = (s) => s.trim();
|
|
713
713
|
const cl = content.split("\n");
|
|
714
|
-
const ol = oldStr.split("\n").map(
|
|
714
|
+
const ol = oldStr.split("\n").map(norm4);
|
|
715
715
|
while (ol.length && ol[ol.length - 1] === "") ol.pop();
|
|
716
716
|
while (ol.length && ol[0] === "") ol.shift();
|
|
717
717
|
if (!ol.length) return null;
|
|
718
718
|
const matches = [];
|
|
719
719
|
for (let i2 = 0; i2 + ol.length <= cl.length; i2++) {
|
|
720
720
|
let ok = true;
|
|
721
|
-
for (let j = 0; j < ol.length; j++) if (
|
|
721
|
+
for (let j = 0; j < ol.length; j++) if (norm4(cl[i2 + j]) !== ol[j]) {
|
|
722
722
|
ok = false;
|
|
723
723
|
break;
|
|
724
724
|
}
|
|
@@ -2954,7 +2954,7 @@ async function psGroupCpuSeconds(pgid) {
|
|
|
2954
2954
|
try {
|
|
2955
2955
|
return await winTreeCpuSeconds(pgid);
|
|
2956
2956
|
} catch (e) {
|
|
2957
|
-
|
|
2957
|
+
log25.debug(`cpu probe failed for ${pgid}: ${e}`);
|
|
2958
2958
|
return void 0;
|
|
2959
2959
|
}
|
|
2960
2960
|
}
|
|
@@ -2963,7 +2963,7 @@ async function psGroupCpuSeconds(pgid) {
|
|
|
2963
2963
|
const out = await new Promise((res, rej) => execFile5("ps", ["-A", "-o", "pid=,ppid=,pgid=,time="], { timeout: 5e3, env: { ...process.env, LC_ALL: "C" } }, (e, so) => e ? rej(e) : res(String(so))));
|
|
2964
2964
|
return sumTreeCpu(out, pgid);
|
|
2965
2965
|
} catch (e) {
|
|
2966
|
-
|
|
2966
|
+
log25.debug(`ps cpu probe failed for group ${pgid}: ${e}`);
|
|
2967
2967
|
return void 0;
|
|
2968
2968
|
}
|
|
2969
2969
|
}
|
|
@@ -3112,7 +3112,7 @@ function makeRealShellTool(options) {
|
|
|
3112
3112
|
proc.stderr?.on("data", collect);
|
|
3113
3113
|
proc.on("error", (err2) => {
|
|
3114
3114
|
if (err2?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean2(out)));
|
|
3115
|
-
|
|
3115
|
+
log25.debug("shell spawn error", err2);
|
|
3116
3116
|
finish(`[exit 1] ${err2?.message ?? err2}${out ? "\n" + clean2(out) : ""}`);
|
|
3117
3117
|
});
|
|
3118
3118
|
proc.on("close", (code) => {
|
|
@@ -3191,7 +3191,7 @@ ${clean2(out) || "(no output yet)"}`;
|
|
|
3191
3191
|
}
|
|
3192
3192
|
];
|
|
3193
3193
|
}
|
|
3194
|
-
var
|
|
3194
|
+
var log25, clean2, DETACHED, shellArgv, SECRET_ENV_RE, _spawn, ShellJobRegistry, shellStatus, NO_JOB2;
|
|
3195
3195
|
var init_tools_shell = __esm({
|
|
3196
3196
|
"src/tools.shell.ts"() {
|
|
3197
3197
|
"use strict";
|
|
@@ -3201,7 +3201,7 @@ var init_tools_shell = __esm({
|
|
|
3201
3201
|
init_shell_sandbox();
|
|
3202
3202
|
init_tools_jobs();
|
|
3203
3203
|
init_platform();
|
|
3204
|
-
|
|
3204
|
+
log25 = forComponent("shell");
|
|
3205
3205
|
clean2 = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
|
|
3206
3206
|
DETACHED = { stdio: ["ignore", "pipe", "pipe"], ...DETACH_OPTIONS };
|
|
3207
3207
|
shellArgv = (sh, command) => ({ bin: sh.bin, args: sh.args(command), ...sh.spawnOptions ? { spawnOptions: sh.spawnOptions } : {} });
|
|
@@ -7697,9 +7697,9 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7697
7697
|
this.ackProbe = null;
|
|
7698
7698
|
this.adaptiveAckFired = false;
|
|
7699
7699
|
const lead2 = m ? m[1] : bare;
|
|
7700
|
-
const
|
|
7700
|
+
const norm4 = lead2.replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
|
|
7701
7701
|
const same = this.ackPickedFired ? bare.match(_VoiceEngine.leadingPhrase(this.lastAck)) : null;
|
|
7702
|
-
if (_VoiceEngine.ACK_LEADING.test(
|
|
7702
|
+
if (_VoiceEngine.ACK_LEADING.test(norm4)) {
|
|
7703
7703
|
const rest = m ? m[2] : "";
|
|
7704
7704
|
this.diag("reflex_ack_suppressed", { lead: lead2.trim() });
|
|
7705
7705
|
if (!rest.trim()) return "";
|
|
@@ -7813,8 +7813,8 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7813
7813
|
this.adaptiveAckFired = false;
|
|
7814
7814
|
this.ackProbe = null;
|
|
7815
7815
|
if (heldProbe) {
|
|
7816
|
-
const
|
|
7817
|
-
if (_VoiceEngine.ACK_LEADING.test(
|
|
7816
|
+
const norm4 = heldProbe.replace(/\[emotion[^\]]*\]/gi, "").replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
|
|
7817
|
+
if (_VoiceEngine.ACK_LEADING.test(norm4)) this.diag("reflex_ack_suppressed", { lead: heldProbe.trim() });
|
|
7818
7818
|
else this.speakDelta(heldProbe);
|
|
7819
7819
|
}
|
|
7820
7820
|
if (!this.speaking) return;
|
|
@@ -8352,14 +8352,14 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
8352
8352
|
if (!prev) return next;
|
|
8353
8353
|
if (!next) return prev;
|
|
8354
8354
|
const pw = prev.split(/\s+/), nw = next.split(/\s+/);
|
|
8355
|
-
const
|
|
8355
|
+
const norm4 = _VoiceEngine.normWord;
|
|
8356
8356
|
if (pw.length >= 2 && pw.length <= nw.length) {
|
|
8357
8357
|
let pfx = true;
|
|
8358
|
-
for (let i = 0; i < pw.length - 1; i++) if (
|
|
8358
|
+
for (let i = 0; i < pw.length - 1; i++) if (norm4(pw[i]) !== norm4(nw[i])) {
|
|
8359
8359
|
pfx = false;
|
|
8360
8360
|
break;
|
|
8361
8361
|
}
|
|
8362
|
-
if (pfx &&
|
|
8362
|
+
if (pfx && norm4(nw[pw.length - 1]).startsWith(norm4(pw[pw.length - 1]))) return next;
|
|
8363
8363
|
}
|
|
8364
8364
|
if (_VoiceEngine.restates(prev, next)) {
|
|
8365
8365
|
this.diag("merge_restated", { dropped: prev.slice(0, 60) });
|
|
@@ -8368,7 +8368,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
8368
8368
|
const max = Math.min(pw.length, nw.length);
|
|
8369
8369
|
for (let k = max; k > 0; k--) {
|
|
8370
8370
|
let match = true;
|
|
8371
|
-
for (let i = 0; i < k; i++) if (
|
|
8371
|
+
for (let i = 0; i < k; i++) if (norm4(pw[pw.length - k + i]) !== norm4(nw[i])) {
|
|
8372
8372
|
match = false;
|
|
8373
8373
|
break;
|
|
8374
8374
|
}
|
|
@@ -8420,11 +8420,11 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
8420
8420
|
* race), never worse. Fillers ("mhm") deliberately don't count as replies — see speakFiller. */
|
|
8421
8421
|
refinalizes(next) {
|
|
8422
8422
|
const pw = this.lastDispatchWords, nw = next.trim().split(/\s+/);
|
|
8423
|
-
const
|
|
8423
|
+
const norm4 = _VoiceEngine.normWord;
|
|
8424
8424
|
if (pw.length < 3 || nw.length < pw.length) return false;
|
|
8425
8425
|
if (this.clock.now() - this.lastDispatchAt >= _VoiceEngine.DUP_FINAL_MS) return false;
|
|
8426
|
-
for (let i = 0; i < pw.length - 1; i++) if (
|
|
8427
|
-
const pLast =
|
|
8426
|
+
for (let i = 0; i < pw.length - 1; i++) if (norm4(pw[i]) !== norm4(nw[i])) return false;
|
|
8427
|
+
const pLast = norm4(pw[pw.length - 1]), nSame = norm4(nw[pw.length - 1]);
|
|
8428
8428
|
if (!nSame.startsWith(pLast)) return false;
|
|
8429
8429
|
return nw.length > pw.length || nSame.length > pLast.length;
|
|
8430
8430
|
}
|
|
@@ -9252,7 +9252,91 @@ configureLogging((name) => {
|
|
|
9252
9252
|
// src/voice/instinct.ts
|
|
9253
9253
|
init_logging();
|
|
9254
9254
|
import { AIClient, DecideClient, JevAdapter, LlmDecideAdapter } from "ai.libx.js";
|
|
9255
|
-
|
|
9255
|
+
|
|
9256
|
+
// src/voice/overlap.ts
|
|
9257
|
+
init_logging();
|
|
9258
|
+
var log19 = forComponent("Overlap");
|
|
9259
|
+
var norm2 = (s) => s.toLowerCase().replace(/[’']/g, "'").replace(/[^\p{L}\p{N}' -]+/gu, " ").replace(/\s+/g, " ").trim();
|
|
9260
|
+
var HARD = /\b(?:stop|wait|no|nope|nah|hold on|hang on|hold up|pause|cancel|quiet|enough|shut up|never ?mind|actually|but|sorry what|what|why|how|who|when|where|which|can you|could you|tell me|i mean|excuse me)\b/;
|
|
9261
|
+
var ACK = new Set("yeah yea yes yep yup ya ok okay k right sure true totally exactly definitely absolutely indeed mm mmm hmm mhm mm-hm uh-huh uh huh aha ah oh ooh wow nice cool great awesome perfect good fine interesting gotcha got it i see makes sense fair agreed correct of course for sure lol ha haha hahaha heh hehe yeah yeah alright all right sweet neat oh nice oh wow oh cool oh okay oh yeah oh right that's right that's true thats right thats true love it i know me too same".split(" "));
|
|
9262
|
+
var PHRASES = ["got it", "i see", "makes sense", "of course", "for sure", "all right", "that's right", "that's true", "thats right", "thats true", "love it", "i know", "me too", "uh huh"];
|
|
9263
|
+
function overlapRule(text) {
|
|
9264
|
+
if (/\?/.test(text)) return "cut";
|
|
9265
|
+
let t = norm2(text);
|
|
9266
|
+
if (!t) return void 0;
|
|
9267
|
+
if (HARD.test(t)) return "cut";
|
|
9268
|
+
for (const p of PHRASES) t = t.split(p).join(" ");
|
|
9269
|
+
const words3 = t.split(/[\s-]+/).filter(Boolean);
|
|
9270
|
+
if (words3.every((w) => ACK.has(w) || /^(?:ha)+h?$|^(?:he)+h?$|^m+(?:hm+)?$|^h?m+$/.test(w))) return "keep";
|
|
9271
|
+
return void 0;
|
|
9272
|
+
}
|
|
9273
|
+
function fastOverlapModel(ai, model = "groq/qwen/qwen3.8-27b") {
|
|
9274
|
+
return async (heard, signal) => {
|
|
9275
|
+
const prompt = 'Live voice call. The assistant is mid-sentence and the user just said this over it: "' + heard + '"\nIs it ONLY a listener cue \u2014 agreement, acknowledgement, a short reaction or laugh \u2014 so the assistant should keep talking? Answer no if they want the floor: a question, objection, correction, request, or new information.\nAnswer yes or no.';
|
|
9276
|
+
const reasoning = /qwen/.test(model) ? { providerOptions: { reasoning_effort: "none" } } : /gpt-oss/.test(model) ? { providerOptions: { reasoning_effort: "low" } } : {};
|
|
9277
|
+
const r = await ai.chat({ model, messages: [{ role: "user", content: prompt }], stream: false, signal, temperature: 0, maxTokens: /gpt-oss/.test(model) ? 200 : 3, ...reasoning });
|
|
9278
|
+
const a = (r.content ?? "").trim().toLowerCase();
|
|
9279
|
+
return /^yes/.test(a) ? "keep" : /^no/.test(a) ? "cut" : null;
|
|
9280
|
+
};
|
|
9281
|
+
}
|
|
9282
|
+
var OverlapJudgeOptions = class {
|
|
9283
|
+
/** The fast model for ambiguous overlaps; absent → ambiguous cuts. */
|
|
9284
|
+
model;
|
|
9285
|
+
/** A model verdict later than this is dropped → cut. */
|
|
9286
|
+
budgetMs = 300;
|
|
9287
|
+
onDecision = () => {
|
|
9288
|
+
};
|
|
9289
|
+
now = () => performance.now();
|
|
9290
|
+
};
|
|
9291
|
+
var OverlapJudge = class {
|
|
9292
|
+
constructor(options) {
|
|
9293
|
+
this.options = options;
|
|
9294
|
+
this.options = { ...new OverlapJudgeOptions(), ...options };
|
|
9295
|
+
}
|
|
9296
|
+
options;
|
|
9297
|
+
get o() {
|
|
9298
|
+
return this.options;
|
|
9299
|
+
}
|
|
9300
|
+
async judge(heard) {
|
|
9301
|
+
const t0 = this.o.now();
|
|
9302
|
+
const done = (verdict, by) => {
|
|
9303
|
+
const d = { verdict, by, ms: Math.round(this.o.now() - t0) };
|
|
9304
|
+
if (by === "late" || by === "error" || by === "none") log19.warn(`overlap "${heard.slice(0, 60)}": no verdict (${by}, ${d.ms}ms) \u2192 cut`);
|
|
9305
|
+
else log19.debug(`overlap "${heard.slice(0, 60)}": ${verdict} by ${by} ${d.ms}ms`);
|
|
9306
|
+
this.o.onDecision(heard, d);
|
|
9307
|
+
return d;
|
|
9308
|
+
};
|
|
9309
|
+
const rule = overlapRule(heard);
|
|
9310
|
+
if (rule) return done(rule, "rule");
|
|
9311
|
+
if (!this.o.model) return done("cut", "none");
|
|
9312
|
+
const ctl = new AbortController();
|
|
9313
|
+
let timer;
|
|
9314
|
+
const late = new Promise((r) => {
|
|
9315
|
+
timer = setTimeout(() => {
|
|
9316
|
+
ctl.abort();
|
|
9317
|
+
r("late");
|
|
9318
|
+
}, this.o.budgetMs);
|
|
9319
|
+
});
|
|
9320
|
+
try {
|
|
9321
|
+
const v = await Promise.race([this.o.model(heard, ctl.signal).catch((e) => {
|
|
9322
|
+
log19.warn(`overlap model failed: ${e.message}`);
|
|
9323
|
+
return "error";
|
|
9324
|
+
}), late]);
|
|
9325
|
+
if (v === "late" || v === "error") return done("cut", v);
|
|
9326
|
+
return v ? done(v, "model") : done("cut", "error");
|
|
9327
|
+
} finally {
|
|
9328
|
+
clearTimeout(timer);
|
|
9329
|
+
}
|
|
9330
|
+
}
|
|
9331
|
+
};
|
|
9332
|
+
function overlapModelFor(ai, env = process.env) {
|
|
9333
|
+
const model = env.AGENTX_CHANNEL_DECIDER_MODEL || void 0;
|
|
9334
|
+
return model || env.GROQ_API_KEY ? fastOverlapModel(ai, model) : void 0;
|
|
9335
|
+
}
|
|
9336
|
+
var softBarge = (mode) => mode !== "turn";
|
|
9337
|
+
|
|
9338
|
+
// src/voice/instinct.ts
|
|
9339
|
+
var log20 = forComponent("Instinct");
|
|
9256
9340
|
function voiceDeciderOptions(env = process.env) {
|
|
9257
9341
|
if (env.TYPESAFE_AI_KEY) return { apiKey: env.TYPESAFE_AI_KEY };
|
|
9258
9342
|
if (env.GROQ_API_KEY) return { provider: "llm", model: "groq/openai/gpt-oss-120b", apiKey: env.GROQ_API_KEY };
|
|
@@ -9285,6 +9369,10 @@ var InstinctOptions = class {
|
|
|
9285
9369
|
* (~300-400ms) and can never act. A hold that survives this window is the undecided case (a 1-word overlap
|
|
9286
9370
|
* the engine would RESUME over at overlapResumeMs=700) — the one where a yield actually changes the outcome. */
|
|
9287
9371
|
overlapSettleMs = 250;
|
|
9372
|
+
/** Soft barge-in's fast yes/no model (overlap.ts fastOverlapModel) for overlaps the 0ms rules leave open, inside
|
|
9373
|
+
* `overlapBudgetMs`; late/failed → yield. Absent → the generic Choice decides them (slower; no verdict → engine default). */
|
|
9374
|
+
overlapModel;
|
|
9375
|
+
overlapBudgetMs = 300;
|
|
9288
9376
|
/** ackFor waits this long for the host's ackOutcome before treating the ack as unspoken. Hosts set it to the engine's
|
|
9289
9377
|
* last chance to fire (bod-voice: 2×adaptiveAckMs — past that a pending pick is dropped as pick_late). */
|
|
9290
9378
|
ackSettleMs = 1500;
|
|
@@ -9457,7 +9545,8 @@ var Instinct = class {
|
|
|
9457
9545
|
const gen = ++this.overlapGen;
|
|
9458
9546
|
if (this.o.overlapSettleMs > 0) await new Promise((r) => setTimeout(r, this.o.overlapSettleMs));
|
|
9459
9547
|
if (gen !== this.overlapGen) return;
|
|
9460
|
-
const
|
|
9548
|
+
const fast = overlapRule(heard) ?? this.o.overlapModel;
|
|
9549
|
+
const d = fast ? await this.judgeOverlap(heard) : await this.ask(
|
|
9461
9550
|
"overlap",
|
|
9462
9551
|
heard,
|
|
9463
9552
|
{ user_said_over_us: heard },
|
|
@@ -9474,6 +9563,12 @@ var Instinct = class {
|
|
|
9474
9563
|
this.o.onDecision({ ...d, acted: acted("keep_talking", this.o.onKeepTalking(heard)) });
|
|
9475
9564
|
} else if (d) this.o.onDecision(d);
|
|
9476
9565
|
}
|
|
9566
|
+
/** Rules + fast model (OverlapJudge) as an overlap decision; undecided → yield (today's hard barge). */
|
|
9567
|
+
async judgeOverlap(heard) {
|
|
9568
|
+
const j = await new OverlapJudge({ model: this.o.overlapModel, budgetMs: this.o.overlapBudgetMs, now: this.o.now }).judge(heard);
|
|
9569
|
+
const choice = j.verdict === "keep" ? "keep_talking" : "yield";
|
|
9570
|
+
return { kind: "overlap", heard, choice, probabilities: { [choice]: 1 }, ms: j.ms };
|
|
9571
|
+
}
|
|
9477
9572
|
/** The engine's hold ended (it ceded the floor, resumed over a stalled overlap, began a new turn, or was
|
|
9478
9573
|
* interrupted) — drop any pending/in-flight overlap consult. Hosts call this on every hold exit. */
|
|
9479
9574
|
overlapEnded() {
|
|
@@ -9512,13 +9607,13 @@ var Instinct = class {
|
|
|
9512
9607
|
* unfinished partial is no turn to react to — the interjection itself is the reply. */
|
|
9513
9608
|
pickAck(ctx) {
|
|
9514
9609
|
if (this.interjecting) {
|
|
9515
|
-
|
|
9610
|
+
log20.debug(`ack: cut-in turn \u2192 silent (engine context "${ctx.text.slice(0, 40)}" is the previous final)`);
|
|
9516
9611
|
return Promise.resolve(null);
|
|
9517
9612
|
}
|
|
9518
9613
|
const text = ctx.text.trim();
|
|
9519
9614
|
const p = (async () => {
|
|
9520
9615
|
const intentP = ctx.barged ? this.bargeIntent(text).catch((e) => {
|
|
9521
|
-
|
|
9616
|
+
log20.warn(`barge intent for the ack failed \u2014 situational ack: ${e.message}`);
|
|
9522
9617
|
return null;
|
|
9523
9618
|
}) : null;
|
|
9524
9619
|
const d = await this.ask(
|
|
@@ -9540,7 +9635,7 @@ var Instinct = class {
|
|
|
9540
9635
|
settle = r;
|
|
9541
9636
|
});
|
|
9542
9637
|
const timer = setTimeout(() => {
|
|
9543
|
-
|
|
9638
|
+
log20.debug(`ack: no engine outcome in ${this.o.ackSettleMs}ms \u2014 not claimed`);
|
|
9544
9639
|
settle(null);
|
|
9545
9640
|
}, this.o.ackSettleMs);
|
|
9546
9641
|
void spoken.then(() => clearTimeout(timer));
|
|
@@ -9589,7 +9684,7 @@ var Instinct = class {
|
|
|
9589
9684
|
});
|
|
9590
9685
|
return { kind, heard, choice: answers.act.choice, probabilities: answers.act.probabilities, ms: Math.round(this.o.now() - t0) };
|
|
9591
9686
|
} catch (e) {
|
|
9592
|
-
|
|
9687
|
+
log20.warn(`${kind} decision failed \u2014 falling back to engine defaults: ${e.message}`);
|
|
9593
9688
|
if (rethrow) throw e;
|
|
9594
9689
|
return null;
|
|
9595
9690
|
}
|
|
@@ -9598,7 +9693,7 @@ var Instinct = class {
|
|
|
9598
9693
|
|
|
9599
9694
|
// src/voice/unsaid.ts
|
|
9600
9695
|
init_logging();
|
|
9601
|
-
var
|
|
9696
|
+
var log21 = forComponent("Unsaid");
|
|
9602
9697
|
var STOP2 = new Set("that this with have from they them their there then than what when were will would your about into just also been some only over more most very which while after before because these those does here".split(" "));
|
|
9603
9698
|
var contentStems = (s) => (s.toLowerCase().match(/[a-z0-9']+/g) ?? []).filter((w) => w.length >= 4 && !STOP2.has(w)).map((w) => w.slice(0, 4));
|
|
9604
9699
|
var SAID_OVERLAP = 0.6;
|
|
@@ -9638,7 +9733,7 @@ async function unsaidClauses(pieces, spoken, decider, timeoutMs = 5e3) {
|
|
|
9638
9733
|
return p < CONVEYED_MIN;
|
|
9639
9734
|
}).map((x) => x.c);
|
|
9640
9735
|
} catch (e) {
|
|
9641
|
-
|
|
9736
|
+
log21.warn(`unsaid judge failed \u2014 only near-zero-overlap clauses count as unsaid: ${e.message}`);
|
|
9642
9737
|
return fallback();
|
|
9643
9738
|
} finally {
|
|
9644
9739
|
clearTimeout(timer);
|
|
@@ -9722,13 +9817,13 @@ function scrubSpokenJunk(text, final = false) {
|
|
|
9722
9817
|
return s;
|
|
9723
9818
|
}
|
|
9724
9819
|
function isRepeatOf(sentence, spoken) {
|
|
9725
|
-
const
|
|
9726
|
-
const s =
|
|
9727
|
-
return s.replace(/ /g, "").length >= 16 && spoken.split(SENTENCE_SPLIT).some((x) =>
|
|
9820
|
+
const norm4 = (x) => x.replace(/\[[a-z_ ]+\]/gi, "").replace(/[^\p{L}\p{N}]+/gu, " ").trim().toLowerCase();
|
|
9821
|
+
const s = norm4(sentence);
|
|
9822
|
+
return s.replace(/ /g, "").length >= 16 && spoken.split(SENTENCE_SPLIT).some((x) => norm4(x) === s);
|
|
9728
9823
|
}
|
|
9729
9824
|
|
|
9730
9825
|
// src/duplex.ts
|
|
9731
|
-
var
|
|
9826
|
+
var log22 = forComponent("DuplexAgent");
|
|
9732
9827
|
function describeCall(call) {
|
|
9733
9828
|
const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
|
|
9734
9829
|
const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
|
|
@@ -9916,7 +10011,7 @@ function isOfferAcceptance(text) {
|
|
|
9916
10011
|
}
|
|
9917
10012
|
var isQuestion = (s) => /\?["'”’)\]]*$/.test(s.trim());
|
|
9918
10013
|
var OFFER_OVERRUN = 150;
|
|
9919
|
-
var VOICE_SYSTEM_PROMPT = 'You are a spoken voice assistant \u2014 the user HEARS everything you say. Use short sentences. One idea per sentence. No markdown, no bullet lists, no code blocks, no headings, no emoji. Never emit stage directions or parenthetical asides about your own process \u2014 nothing like "(waiting for the result...)" or "(checking)"; while work runs, either say it as plain speech or end your turn.\nThis holds even when asked to "print", "list", "show", or "make a table" \u2014 there is no screen for the spoken channel. Speak it as flowing prose ("Tuesday is half a meter, Wednesday a bit less\u2026"), or if they truly need it on screen, route it to Act to render. Never emit dashes or pipes into speech.\nKeep turns SHORT \u2014 one to three sentences, then stop. Never lecture, enumerate cases, or add caveats unprompted. Conversation is a fast exchange: give the one thing asked, and let the user pull more if they want it.\nYou have three cognitive tiers \u2014 like a human brain:\n\u2022 YOU (reflex) \u2014 instant, lightweight. Handle greetings, simple questions, status checks, QuickLook.\n\u2022 `Act` \u2014 your hands. A background worker with its own configured tools and access to the user\'s environment (files and shell{{WORKER_WEB}}). Use for reading, editing, searching, running tasks, building \u2014 any real work.\n{{THINK_SLOT}}\nWhen you are unsure whether you can do or access something, do NOT assume and do NOT claim a capability you have not confirmed. To check what you can do, QuickLook `capabilities` (instant \u2014 it lists your worker\'s real tools) and answer from that. Never promise an ability that is not in your capabilities; if it is not there, tell the user plainly you can\'t. To actually DO real work, call `Act`. When the user mentions their project, folder, files, or environment ("this project", "the current folder", "my code"), call `Act` IMMEDIATELY \u2014 do not ask for paths or details the worker can discover itself. Never pretend to have done the work or invent results \u2014 the worker\'s report is your only source.\nYou cannot mute the microphone or stop voice capture yourself \u2014 no tool does it. If the user asks you to stop listening or turn the voice off, never claim you did: tell them to say exactly "voice off" (handled by the app directly), or type /voice.\nYou are NOT a knowledge base. For any question whose answer needs SPECIFIC verifiable facts you do not already have in hand \u2014 how to build/configure/implement something, exact API, library, entitlement, command or option names, current events, or particular numbers, dates, or names \u2014 do NOT answer from your own memory: you will confidently make things up (a fake API, a wrong entitlement, an event that did not happen). Route it to `Act`, which can search and verify, and speak only what its report says. DELEGATION RULE \u2014 decide for yourself, the user never has to push: if you cannot answer confidently from the conversation plus trivial well-known knowledge, do NOT refuse and do NOT guess \u2014 dispatch `Act` immediately with a clear brief and say you are checking. Anything needing CURRENT data (weather, news, prices, dates, sky/astronomy, "right now"), real computation, or verification is an automatic dispatch \u2014 never a refusal. The user should never need to say "search the web" or "think harder" to make you act; needing fresh or verified information IS the trigger. Refuse only what your worker genuinely cannot do (check `capabilities`), and say why. Answer inline ONLY for general conversation, chit-chat, and trivia you are sure of, or facts you can see via QuickLook. When elaborating on a completed task ("tell me more", "the gist"), stay strictly within what that result actually said \u2014 if the user asks for something the result did not cover, that is NEW information: dispatch `Act`, do not improvise.\nALWAYS react before you work: the FIRST thing in your turn is a brief spoken acknowledgement of what you heard and what you are about to do ("got it \u2014 opening that now", "sure, let me pull it up", "okay, checking"). NEVER call a tool (Act, Think, QuickLook) silently \u2014 the user must hear you react before you go quiet to work. After dispatching Act or Think, that same one short sentence IS your turn \u2014 end it and do not wait for the result. Exactly ONE short line: never stack a second acknowledgement, and never narrate your own presence or status while waiting ("I\'m here", "let me see", "still checking" right after you already acked). One clean ack then silence reads as competent; repeated check-ins read as nervous.\nA completed task speaks its OWN result to the user (the worker voices what matters as it finishes) \u2014 you do NOT re-voice clean task results. A FAILED or INCOMPLETE task still arrives as a "[task t1 failed] \u2026" event for you to handle. The completed result stays in YOUR context \u2014 it is yours to draw on. When the user follows up ("tell me more", "what else", "and?"), answer FROM that result first: you already have the detail, so elaborate on what you have. Do NOT spawn a fresh worker to re-search or re-gather what you were just handed. Re-dispatch ONLY when genuinely new information is needed \u2014 e.g. the user wants the full contents of a SPECIFIC source, which is one WebFetch of that URL, not a brand-new search. "[task t1 progress] \u2026" events are interim status, NOT results \u2014 on a genuinely LONG wait you MAY give one brief half-sentence aside, but only occasionally; silence while working is normal and fine (the user knows you are on it). Do NOT narrate every step or re-announce yourself. Never present progress as a finished result.\nCRITICAL: while a task is still running you have NO answer yet \u2014 never state a specific result of any kind (a number, size, count, name, path, or value). The real answer exists ONLY once the task finishes (the worker speaks it, and `TaskStatus` then returns it); inventing one meanwhile (a made-up disk size, commit count, etc.) is a serious error. Until then, only acknowledge and wait.\nNever read raw file paths, diffs, or code aloud verbatim.\nDo NOT end every turn with the same canned offer ("want a rundown?", "want the steps?"). Offer once at most; if the user pushes back, repeats themselves, or sounds unsatisfied ("you know what I mean?", "think deeper", "are you sure?"), do NOT re-offer the same thing \u2014 change approach: dispatch `Act`/`Think` to actually dig in, or ask one concrete clarifying question. Repeating a non-answer is worse than silence. When the user says you looked in the wrong place, or doubts a "not found", do not defend the search you did: dispatch `Act` with their hint (the place, the id, the tool they name). Only say you checked or searched something you actually checked with a tool this conversation \u2014 name exactly what you looked at.\n"[task t1 asks] \u2026" events are QUESTIONS from a background task \u2014 relay to the user in your own words, short, then end your turn. When the user answers, call `AnswerTask` with that id and their answer. NEVER answer on the user\'s behalf for permissions or risky operations; if their reply is ambiguous, confirm first.\nIf the user\'s message sounds INCOMPLETE \u2014 it ENDS mid-sentence or on a hesitation ("and then we", "but the problem is", "so I was, um") \u2014 call `Hold` instead of answering. A hesitation INSIDE a finished message ("can you, um, open it?") does not make it incomplete, and an unclear or odd but finished message is not a fragment: answer it, or ask one short question \u2014 never Hold on it. This keeps listening for the rest of their thought. Only respond with substance when you have a complete question or request.\nDispatch discipline: send ONE self-contained task per request \u2014 a single worker with the full brief beats several workers with fragments (each worker starts fresh and re-discovers context). NEVER dispatch a worker just to read files or gather information \u2014 workers explore and discover context themselves; pass on what you already know and let one worker do the whole job. Split into parallel tasks only when the user asks for genuinely independent things. When a task completes, report its result and stop \u2014 do NOT dispatch follow-up work (verification, polish, extras) the user did not ask for, unless the report itself signals failure or doubt.\nDo not fire a second Act/Think for work already in flight, and NEVER spawn a second task to re-count, cross-check, or verify a result a worker already gave you \u2014 trust its answer; a single question gets ONE task. Call `TaskStatus` at most ONCE per turn; if a task is still running, give one brief status aside and end the turn \u2014 never poll it again and again in a loop. "Still on it" is ONLY for a repeat check on a task you already acknowledged earlier \u2014 NEVER as the ack when you first dispatch work ("still" is wrong when nothing was running yet; use a fresh "on it"/"got it, checking" instead). Use `CancelTask` when the user asks to stop something \u2014 to stop everything ("stop all", "cancel everything") pass id "all". When the user only ADDS to or CORRECTS a request whose task is still running (a detail, a path, a name, a spelling), call `Act` with `refines` set to that task id \u2014 it updates the running worker in place; never cancel and re-dispatch for a refinement.\nPRIORITY: when the user says goodbye or wants to end/finish/wrap up the session ("ok bye", "that\'s all", "let\'s finish", "let\'s end", "goodnight", "exit", "wrap up"), call `ExitSession` IMMEDIATELY \u2014 do not act, do not check status, just exit.\nFor TRIVIAL instant lookups only \u2014 current time, git branch, listing a folder, peeking at a small file, or checking your own `capabilities`/tools \u2014 use `QuickLook` (instant, no task). Whenever the user asks what you can do or whether you have some ability, QuickLook `capabilities` and answer from that \u2014 never guess. Anything requiring searching, reasoning, running commands, or editing goes through `Act`.\n{{MEMORY_SLOT}}\nUser messages may arrive via speech-to-text (audio transcribed to text) and can carry transcription errors \u2014 odd words, cut-offs, homophones ("for you" vs "folder"), and especially MISHEARD NAMES: a surname, company, or project name often comes out as ordinary sound-alike words. Read for INTENT, not surface text, and do not silently rewrite what they said. If a message seems garbled, surprising, or only half-parses, or a word sounds like a name you already know (from memory or this conversation) but is spelled differently, treat it with suspicion \u2014 do NOT act on the odd word blindly or improvise content from it; briefly confirm what they meant ("did you mean\u2026?") and wait. A one-line confirm beats a confident wrong answer. NEVER save to memory a name, spelling, or term you only HEARD (it may be a transcription error) \u2014 when the user spells something aloud, read the spelling back and get a yes before storing it.';
|
|
10014
|
+
var VOICE_SYSTEM_PROMPT = 'You are a spoken voice assistant \u2014 the user HEARS everything you say. Use short sentences. One idea per sentence. No markdown, no bullet lists, no code blocks, no headings, no emoji. Never emit stage directions or parenthetical asides about your own process \u2014 nothing like "(waiting for the result...)" or "(checking)"; while work runs, either say it as plain speech or end your turn.\nThis holds even when asked to "print", "list", "show", or "make a table" \u2014 there is no screen for the spoken channel. Speak it as flowing prose ("Tuesday is half a meter, Wednesday a bit less\u2026"), or if they truly need it on screen, route it to Act to render. Never emit dashes or pipes into speech.\nKeep turns SHORT \u2014 one to three sentences, then stop. Never lecture, enumerate cases, or add caveats unprompted. Conversation is a fast exchange: give the one thing asked, and let the user pull more if they want it.\nYou have three cognitive tiers \u2014 like a human brain:\n\u2022 YOU (reflex) \u2014 instant, lightweight. Handle greetings, simple questions, status checks, QuickLook.\n\u2022 `Act` \u2014 your hands. A background worker with its own configured tools and access to the user\'s environment (files and shell{{WORKER_WEB}}). Use for reading, editing, searching, running tasks, building \u2014 any real work.\n{{THINK_SLOT}}\nWhen you are unsure whether you can do or access something, do NOT assume and do NOT claim a capability you have not confirmed. To check what you can do, QuickLook `capabilities` (instant \u2014 it lists your worker\'s real tools) and answer from that. Never promise an ability that is not in your capabilities; if it is not there, tell the user plainly you can\'t. To actually DO real work, call `Act`. When the user mentions their project, folder, files, or environment ("this project", "the current folder", "my code"), call `Act` IMMEDIATELY \u2014 do not ask for paths or details the worker can discover itself. Never pretend to have done the work or invent results \u2014 the worker\'s report is your only source.\nYou cannot mute the microphone or stop voice capture yourself \u2014 no tool does it. If the user asks you to stop listening or turn the voice off, never claim you did: tell them to say exactly "voice off" (handled by the app directly), or type /voice.\nYou are NOT a knowledge base. For any question whose answer needs SPECIFIC verifiable facts you do not already have in hand \u2014 how to build/configure/implement something, exact API, library, entitlement, command or option names, current events, or particular numbers, dates, or names \u2014 do NOT answer from your own memory: you will confidently make things up (a fake API, a wrong entitlement, an event that did not happen). Route it to `Act`, which can search and verify, and speak only what its report says. DELEGATION RULE \u2014 decide for yourself, the user never has to push: if you cannot answer confidently from the conversation plus trivial well-known knowledge, do NOT refuse and do NOT guess \u2014 dispatch `Act` immediately with a clear brief and say you are checking. Anything needing CURRENT data (weather, news, prices, dates, sky/astronomy, "right now"), real computation, or verification is an automatic dispatch \u2014 never a refusal. The user should never need to say "search the web" or "think harder" to make you act; needing fresh or verified information IS the trigger. Refuse only what your worker genuinely cannot do (check `capabilities`), and say why. Answer inline ONLY for general conversation, chit-chat, and trivia you are sure of, or facts you can see via QuickLook. When elaborating on a completed task ("tell me more", "the gist"), stay strictly within what that result actually said \u2014 if the user asks for something the result did not cover, that is NEW information: dispatch `Act`, do not improvise.\nALWAYS react before you work: the FIRST thing in your turn is a brief spoken acknowledgement of what you heard and what you are about to do ("got it \u2014 opening that now", "sure, let me pull it up", "okay, checking"). NEVER call a tool (Act, Think, QuickLook) silently \u2014 the user must hear you react before you go quiet to work. After dispatching Act or Think, that same one short sentence IS your turn \u2014 end it and do not wait for the result. Exactly ONE short line: never stack a second acknowledgement, and never narrate your own presence or status while waiting ("I\'m here", "let me see", "still checking" right after you already acked). One clean ack then silence reads as competent; repeated check-ins read as nervous.\nA completed task speaks its OWN result to the user (the worker voices what matters as it finishes) \u2014 you do NOT re-voice clean task results. A FAILED or INCOMPLETE task still arrives as a "[task t1 failed] \u2026" event for you to handle. The completed result stays in YOUR context \u2014 it is yours to draw on. When the user follows up ("tell me more", "what else", "and?"), answer FROM that result first: you already have the detail, so elaborate on what you have. Do NOT spawn a fresh worker to re-search or re-gather what you were just handed. Re-dispatch ONLY when genuinely new information is needed \u2014 e.g. the user wants the full contents of a SPECIFIC source, which is one WebFetch of that URL, not a brand-new search. "[task t1 progress] \u2026" events are interim status, NOT results \u2014 on a genuinely LONG wait you MAY give one brief half-sentence aside, but only occasionally; silence while working is normal and fine (the user knows you are on it). Do NOT narrate every step or re-announce yourself. Never present progress as a finished result.\nCRITICAL: while a task is still running you have NO answer yet \u2014 never state a specific result of any kind (a number, size, count, name, path, or value). The real answer exists ONLY once the task finishes (the worker speaks it, and `TaskStatus` then returns it); inventing one meanwhile (a made-up disk size, commit count, etc.) is a serious error. Until then, only acknowledge and wait.\nNever read raw file paths, diffs, or code aloud verbatim.\nDo NOT end every turn with the same canned offer ("want a rundown?", "want the steps?"). Offer once at most; if the user pushes back, repeats themselves, or sounds unsatisfied ("you know what I mean?", "think deeper", "are you sure?"), do NOT re-offer the same thing \u2014 change approach: dispatch `Act`/`Think` to actually dig in, or ask one concrete clarifying question. Repeating a non-answer is worse than silence. A short yes or agreement right after you asked or offered something answers THAT question: act on it now (dispatch the work it needs) \u2014 never re-ask it, rephrase it, or narrow it into a new question. When the user says you looked in the wrong place, or doubts a "not found", do not defend the search you did: dispatch `Act` with their hint (the place, the id, the tool they name). Only say you checked or searched something you actually checked with a tool this conversation \u2014 name exactly what you looked at.\n"[task t1 asks] \u2026" events are QUESTIONS from a background task \u2014 relay to the user in your own words, short, then end your turn. When the user answers, call `AnswerTask` with that id and their answer. NEVER answer on the user\'s behalf for permissions or risky operations; if their reply is ambiguous, confirm first.\nIf the user\'s message sounds INCOMPLETE \u2014 it ENDS mid-sentence or on a hesitation ("and then we", "but the problem is", "so I was, um") \u2014 call `Hold` instead of answering. A hesitation INSIDE a finished message ("can you, um, open it?") does not make it incomplete, and an unclear or odd but finished message is not a fragment: answer it, or ask one short question \u2014 never Hold on it. This keeps listening for the rest of their thought. Only respond with substance when you have a complete question or request.\nDispatch discipline: send ONE self-contained task per request \u2014 a single worker with the full brief beats several workers with fragments (each worker starts fresh and re-discovers context). NEVER dispatch a worker just to read files or gather information \u2014 workers explore and discover context themselves; pass on what you already know and let one worker do the whole job. Split into parallel tasks only when the user asks for genuinely independent things. When a task completes, report its result and stop \u2014 do NOT dispatch follow-up work (verification, polish, extras) the user did not ask for, unless the report itself signals failure or doubt.\nDo not fire a second Act/Think for work already in flight, and NEVER spawn a second task to re-count, cross-check, or verify a result a worker already gave you \u2014 trust its answer; a single question gets ONE task. Call `TaskStatus` at most ONCE per turn; if a task is still running, give one brief status aside and end the turn \u2014 never poll it again and again in a loop. "Still on it" is ONLY for a repeat check on a task you already acknowledged earlier \u2014 NEVER as the ack when you first dispatch work ("still" is wrong when nothing was running yet; use a fresh "on it"/"got it, checking" instead). Use `CancelTask` when the user asks to stop something \u2014 to stop everything ("stop all", "cancel everything") pass id "all". When the user only ADDS to or CORRECTS a request whose task is still running (a detail, a path, a name, a spelling), call `Act` with `refines` set to that task id \u2014 it updates the running worker in place; never cancel and re-dispatch for a refinement.\nPRIORITY: when the user says goodbye or wants to end/finish/wrap up the session ("ok bye", "that\'s all", "let\'s finish", "let\'s end", "goodnight", "exit", "wrap up"), call `ExitSession` IMMEDIATELY \u2014 do not act, do not check status, just exit.\nFor TRIVIAL instant lookups only \u2014 current time, git branch, listing a folder, peeking at a small file, or checking your own `capabilities`/tools \u2014 use `QuickLook` (instant, no task). Whenever the user asks what you can do or whether you have some ability, QuickLook `capabilities` and answer from that \u2014 never guess. Anything requiring searching, reasoning, running commands, or editing goes through `Act`.\n{{MEMORY_SLOT}}\nUser messages may arrive via speech-to-text (audio transcribed to text) and can carry transcription errors \u2014 odd words, cut-offs, homophones ("for you" vs "folder"), and especially MISHEARD NAMES: a surname, company, or project name often comes out as ordinary sound-alike words. Read for INTENT, not surface text, and do not silently rewrite what they said. If a message seems garbled, surprising, or only half-parses, or a word sounds like a name you already know (from memory or this conversation) but is spelled differently, treat it with suspicion \u2014 do NOT act on the odd word blindly or improvise content from it; briefly confirm what they meant ("did you mean\u2026?") and wait. A one-line confirm beats a confident wrong answer. NEVER save to memory a name, spelling, or term you only HEARD (it may be a transcription error) \u2014 when the user spells something aloud, read the spelling back and get a yes before storing it.';
|
|
9920
10015
|
var THINK_GUIDANCE = "\u2022 `Think` \u2014 your brain. A premium reasoning model, FAR more expensive than Act. Reserve it for open-ended architecture/design questions, or a problem Act already FAILED at. ALL implementation work \u2014 coding, refactoring, debugging, edge cases, tests \u2014 goes to Act; Act is highly capable. Never send the same work to both.";
|
|
9921
10016
|
var THINK_DISABLED_GUIDANCE = "(Think tier is not available \u2014 use Act for all escalations.)";
|
|
9922
10017
|
var VOICE_STYLE_CONVERSATIONAL = `Speak like a person in a live conversation, not an assistant reading a script. React first, then deliver: a quick impulsive beat ("oh nice", "hmm, hold on", "ah, got it") before the substance. Use contractions always. Vary sentence length \u2014 some very short. Light fillers and backchannels are fine ("mm-hm", "right", "let's see") but at most one per reply \u2014 never stack them. When you escalate to Act or Think, say it like a human would ("hang on, let me actually dig into that \u2014 gimme a minute") instead of announcing a task. When a result comes back, react to it like you just found out ("okay so \u2014 turns out\u2026"). Match the user's energy: a quick question gets a quick answer \u2014 a few words is a perfectly good turn. Prefer a short answer plus an offer ("want the details?") over covering everything. Never narrate your own mechanics (no "I will now act", no task ids out loud).`;
|
|
@@ -10083,7 +10178,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10083
10178
|
const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
|
|
10084
10179
|
if (m) {
|
|
10085
10180
|
this.fabricationCut = true;
|
|
10086
|
-
|
|
10181
|
+
log22.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
|
|
10087
10182
|
this.reflexBuf = this.reflexBuf.slice(0, Math.max(m.index, this.reflexForwarded));
|
|
10088
10183
|
this.pumpReflex(true);
|
|
10089
10184
|
return;
|
|
@@ -10207,7 +10302,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10207
10302
|
this.leakHit = true;
|
|
10208
10303
|
this.leakCuts.push(leak.trim().slice(0, 40));
|
|
10209
10304
|
const why = this.cutKind === "claim" ? "stated a result no task has given yet" : this.cutKind === "restart" ? "started a second, restated reply" : "spoke its own analysis";
|
|
10210
|
-
|
|
10305
|
+
log22.warn(`reflex ${why} ("${leak.trim().slice(0, 40)}\u2026") \u2014 cut before TTS`);
|
|
10211
10306
|
this.notify("diag", this.cutKind === "claim" ? "result_claim" : this.cutKind === "restart" ? "reply_restart" : "analysis_leak", { text: leak.trim().slice(0, 80), at: this.turnSpoken.trim() ? "mid_reply" : "before_speech" });
|
|
10212
10307
|
const next = this.stepMarks.find((k) => k > at);
|
|
10213
10308
|
if (next === void 0) {
|
|
@@ -10224,6 +10319,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10224
10319
|
if (!out) return;
|
|
10225
10320
|
if (out.trim()) this.spokeThisTurn = true;
|
|
10226
10321
|
this.turnSpoken += out;
|
|
10322
|
+
this.noteVoiced(this.turnSpoken);
|
|
10227
10323
|
this.emitHost({ kind: "text_delta", message: out });
|
|
10228
10324
|
}
|
|
10229
10325
|
/** Leak verdict for one complete sentence: false = speak, true = cut, undefined = the judge is still out.
|
|
@@ -10247,7 +10343,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10247
10343
|
this.notify("diag", "leak_judge", { text: s.slice(0, 80), p: Math.round(p * 100) / 100, ms: Math.round(performance.now() - t0) });
|
|
10248
10344
|
return p >= LEAK_JUDGE_THRESHOLD;
|
|
10249
10345
|
}, (e) => {
|
|
10250
|
-
|
|
10346
|
+
log22.warn(`leak judge failed \u2014 speaking the sentence: ${e.message}`);
|
|
10251
10347
|
return false;
|
|
10252
10348
|
}).then((leak) => {
|
|
10253
10349
|
clearTimeout(timer);
|
|
@@ -10319,7 +10415,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10319
10415
|
let i = t.length - 1;
|
|
10320
10416
|
while (i >= this.turnBase && !(t[i].role === "assistant" && typeof t[i].content === "string" && t[i].content.includes(key))) i--;
|
|
10321
10417
|
if (i < this.turnBase) {
|
|
10322
|
-
|
|
10418
|
+
log22.warn("analysis leak cut, but its text was not found in the transcript to scrub");
|
|
10323
10419
|
continue;
|
|
10324
10420
|
}
|
|
10325
10421
|
const m = t[i];
|
|
@@ -10374,7 +10470,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10374
10470
|
const res = await this.voice.send(content);
|
|
10375
10471
|
const fb = this.options.reflexFallbackModel, model = this.voice.options.model;
|
|
10376
10472
|
if (res.finishReason !== "error" || !rateLimited(res.error) || !fb || fb === model || fb.includes("/") && model.includes("/") && fb.split("/")[0] !== model.split("/")[0] || this.turnDispatched || this.turnSpoken.trim()) return res;
|
|
10377
|
-
|
|
10473
|
+
log22.warn(`reflex ${model} rate-limited \u2014 re-running the turn on ${fb}`);
|
|
10378
10474
|
this.notify("diag", "reflex_fallback", { from: model, to: fb });
|
|
10379
10475
|
this.voice.transcript.length = base;
|
|
10380
10476
|
this.voice.options.model = fb;
|
|
@@ -10407,7 +10503,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10407
10503
|
try {
|
|
10408
10504
|
await this.voice.send(reminder);
|
|
10409
10505
|
} catch (e) {
|
|
10410
|
-
|
|
10506
|
+
log22.warn(`follow-through nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
10411
10507
|
} finally {
|
|
10412
10508
|
const i = this.voice.transcript.findIndex((m, k) => k >= base && m.role === "user" && m.content === reminder);
|
|
10413
10509
|
if (i >= 0) this.voice.transcript.splice(i, 1);
|
|
@@ -10468,7 +10564,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10468
10564
|
return {
|
|
10469
10565
|
preToolUse: (call) => {
|
|
10470
10566
|
if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
|
|
10471
|
-
|
|
10567
|
+
log22.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
|
|
10472
10568
|
this.abortSpeculation();
|
|
10473
10569
|
return { block: true, reason: "Speculative turn aborted." };
|
|
10474
10570
|
}
|
|
@@ -10542,7 +10638,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10542
10638
|
try {
|
|
10543
10639
|
await this.voice.send(reminder);
|
|
10544
10640
|
} catch (e) {
|
|
10545
|
-
|
|
10641
|
+
log22.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
10546
10642
|
} finally {
|
|
10547
10643
|
this.nudging = false;
|
|
10548
10644
|
const i = this.voice.transcript.findIndex((m, k) => k >= base && m.role === "user" && m.content === reminder);
|
|
@@ -10597,7 +10693,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10597
10693
|
const accepted = this.offerOpen && this.parkedRedeliver.length > 0 && this.parkedRedeliver.every((p) => p.task === this.offer.task) && (isOfferAcceptance(content) || isTrivialBarge(content));
|
|
10598
10694
|
const judge = accepted ? void 0 : this.options.bargeIntent;
|
|
10599
10695
|
return (accepted ? Promise.resolve("resume") : judge ? judge(content).catch((e) => {
|
|
10600
|
-
|
|
10696
|
+
log22.warn(`bargeIntent judge failed \u2014 using the allowlist: ${e.message}`);
|
|
10601
10697
|
return fallback();
|
|
10602
10698
|
}) : Promise.resolve(fallback())).then((intent) => {
|
|
10603
10699
|
this.notify("diag", "barge_intent", { intent, text: content.slice(0, 80) });
|
|
@@ -10622,7 +10718,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10622
10718
|
return this.sendTurn(content, false, rec, ext);
|
|
10623
10719
|
});
|
|
10624
10720
|
}
|
|
10625
|
-
return this.sendTurn(content, false, rec, ext);
|
|
10721
|
+
return this.sendTurn(typeof content === "string" ? content + this.acceptNote(content) : content, false, rec, ext);
|
|
10626
10722
|
}
|
|
10627
10723
|
/** Host-named user turns (send opts.id), so a later send can replace one. Bounded; a replace that arrives
|
|
10628
10724
|
* BEFORE its target (the host's dispatch hops can reorder) leaves a pre-superseded record behind. */
|
|
@@ -10793,7 +10889,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10793
10889
|
try {
|
|
10794
10890
|
res = await this.voice.send(spec.text);
|
|
10795
10891
|
} catch (e) {
|
|
10796
|
-
|
|
10892
|
+
log22.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
|
|
10797
10893
|
} finally {
|
|
10798
10894
|
this.voice.options.signal = prevSignal;
|
|
10799
10895
|
}
|
|
@@ -10807,7 +10903,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10807
10903
|
if (d === "abort") {
|
|
10808
10904
|
if (this.voice.transcript.length > base) this.voice.transcript.length = base;
|
|
10809
10905
|
this.speculativeAbortedCalls++;
|
|
10810
|
-
|
|
10906
|
+
log22.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
|
|
10811
10907
|
if (this.spec === spec) this.spec = void 0;
|
|
10812
10908
|
return res ?? empty;
|
|
10813
10909
|
}
|
|
@@ -10875,6 +10971,7 @@ Update: ${brief}`;
|
|
|
10875
10971
|
this.bargeMark = this.parkedRedeliver.length;
|
|
10876
10972
|
this.reflexCut = false;
|
|
10877
10973
|
this.offerOpen = false;
|
|
10974
|
+
this.asked = "";
|
|
10878
10975
|
}
|
|
10879
10976
|
const parked = [];
|
|
10880
10977
|
for (const rec of this.tasks.values())
|
|
@@ -10918,6 +11015,7 @@ Update: ${brief}`;
|
|
|
10918
11015
|
if (say) {
|
|
10919
11016
|
this.offerOpen = false;
|
|
10920
11017
|
this.options.host?.notify?.({ kind: "speak_utterance", message: say });
|
|
11018
|
+
this.noteVoiced(say);
|
|
10921
11019
|
if (rec) {
|
|
10922
11020
|
this.voicedBy.set(say.trim(), rec.id);
|
|
10923
11021
|
if (this.voicedBy.size > 64) this.voicedBy.delete(this.voicedBy.keys().next().value);
|
|
@@ -10930,6 +11028,7 @@ Update: ${brief}`;
|
|
|
10930
11028
|
this.offer = { text: rec.capOffer = this.options.spokenOffer, task: rec.id };
|
|
10931
11029
|
this.offerOpen = true;
|
|
10932
11030
|
this.options.host?.notify?.({ kind: "speak_utterance", message: this.offer.text });
|
|
11031
|
+
this.noteVoiced(this.offer.text);
|
|
10933
11032
|
}
|
|
10934
11033
|
this.notify("diag", "delivery_budget", { task: rec.id, spoke: rec.voicedChars, parked: rest.length });
|
|
10935
11034
|
} else if (rec.deliveryParked === "barge") this.parkedRedeliver.push({ text: seg, task: rec.id });
|
|
@@ -10956,6 +11055,20 @@ Update: ${brief}`;
|
|
|
10956
11055
|
offer = { text: "", task: "" };
|
|
10957
11056
|
/** The cap offer was the last thing voiced (no delivery or turn has spoken since) — a plain yes accepts it (send). */
|
|
10958
11057
|
offerOpen = false;
|
|
11058
|
+
/** The last thing voiced (reflex reply or delivery), when it ended on a question — a plain yes answers it (acceptNote). */
|
|
11059
|
+
asked = "";
|
|
11060
|
+
noteVoiced(text) {
|
|
11061
|
+
const last = text.trim().split(SENTENCE_SPLIT).at(-1) ?? "";
|
|
11062
|
+
this.asked = isQuestion(last) ? last.trim() : "";
|
|
11063
|
+
}
|
|
11064
|
+
/** A plain yes to the question just voiced, tied to it in the user's line: left to the reflex, a bare "Oh, yeah." after
|
|
11065
|
+
* an offer read as ambiguous and the offer was asked again instead of done (Para phone call 2026-10-13). */
|
|
11066
|
+
acceptNote(content) {
|
|
11067
|
+
const q2 = this.asked;
|
|
11068
|
+
this.asked = "";
|
|
11069
|
+
return q2 && isOfferAcceptance(content) ? `
|
|
11070
|
+
[this answers the question you just asked \u2014 "${q2}" \u2014 with yes: act on it now (do what it offered, or pass the answer on to the task that asked); do not ask it again.]` : "";
|
|
11071
|
+
}
|
|
10959
11072
|
/** The task's outcome now belongs to its integration turn (failed / stopped early): take back its parked pieces and
|
|
10960
11073
|
* keep it from being revived — a "go on" must not replay raw segments of a result the reflex is about to explain. */
|
|
10961
11074
|
dropParked(rec) {
|
|
@@ -10982,6 +11095,7 @@ Update: ${brief}`;
|
|
|
10982
11095
|
if (!text) return;
|
|
10983
11096
|
this.offerOpen = false;
|
|
10984
11097
|
this.options.host?.notify?.({ kind: "speak_utterance", message: text });
|
|
11098
|
+
this.noteVoiced(text);
|
|
10985
11099
|
this.notify("diag", "parked_redelivered", { chars: text.length });
|
|
10986
11100
|
}
|
|
10987
11101
|
/** Resolve when all queued voice turns AND all in-flight worker tasks have settled (tests, graceful shutdown). */
|
|
@@ -11172,7 +11286,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11172
11286
|
this.notify("task_verify", `task ${id}: verifying`, { id });
|
|
11173
11287
|
const cres = await new Agent(checkerOpts).run(checkBrief);
|
|
11174
11288
|
if (cres.finishReason !== "stop") {
|
|
11175
|
-
|
|
11289
|
+
log22.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
|
|
11176
11290
|
this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
|
|
11177
11291
|
}
|
|
11178
11292
|
const sum = (a = 0, b = 0) => a + b;
|
|
@@ -11308,7 +11422,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11308
11422
|
rec.status = "done";
|
|
11309
11423
|
rec.result = res.text;
|
|
11310
11424
|
const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
|
|
11311
|
-
|
|
11425
|
+
log22.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
|
|
11312
11426
|
this.notify("task_done", `task ${id} (${rec.label}) completed`, {
|
|
11313
11427
|
id,
|
|
11314
11428
|
text: res.text,
|
|
@@ -11342,14 +11456,14 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11342
11456
|
foldIfSuperseded(rec, note) {
|
|
11343
11457
|
if (rec.deliveryParked !== "moved_on") return false;
|
|
11344
11458
|
if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
|
|
11345
|
-
|
|
11459
|
+
log22.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
|
|
11346
11460
|
return true;
|
|
11347
11461
|
}
|
|
11348
11462
|
failTask(rec, msg) {
|
|
11349
11463
|
this.dropAsk(rec.id);
|
|
11350
11464
|
rec.status = "error";
|
|
11351
11465
|
rec.result = msg;
|
|
11352
|
-
|
|
11466
|
+
log22.warn(`task ${rec.id} failed: ${msg}`);
|
|
11353
11467
|
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`, { id: rec.id });
|
|
11354
11468
|
if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
|
|
11355
11469
|
this.dropParked(rec);
|
|
@@ -11647,7 +11761,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11647
11761
|
init_llm();
|
|
11648
11762
|
init_logging();
|
|
11649
11763
|
init_models();
|
|
11650
|
-
var
|
|
11764
|
+
var log23 = forComponent("ModelRouter");
|
|
11651
11765
|
var ModelRouterOptions = class {
|
|
11652
11766
|
classifier = jevClassifier();
|
|
11653
11767
|
/** Capability tiers → model id (any provider). basic/standard run the main session; premium is only ever an offload. */
|
|
@@ -11731,10 +11845,10 @@ var ModelRouter = class {
|
|
|
11731
11845
|
if (tier === "premium") d.offload = this.state.offload && (c.sameThread ?? 0) >= o.sameThreadAt ? "continue" : "fresh";
|
|
11732
11846
|
} catch (e) {
|
|
11733
11847
|
const msg = e instanceof Error ? e.message : String(e);
|
|
11734
|
-
|
|
11848
|
+
log23.warn(`classifier failed, falling back to standard: ${msg}`);
|
|
11735
11849
|
d = { tier: "standard", model: o.models.standard, probs: {}, fallback: msg, classifyMs: Math.round(performance.now() - t0) };
|
|
11736
11850
|
}
|
|
11737
|
-
|
|
11851
|
+
log23.info(`route ${d.tier}${d.offload ? `(${d.offload})` : ""} \xB7 argmax=${d.choice ?? "-"} \xB7 p=${JSON.stringify(d.probs)}${d.sameThread != null ? ` \xB7 same=${d.sameThread.toFixed(2)}` : ""} \xB7 ${d.classifyMs}ms`);
|
|
11738
11852
|
return d;
|
|
11739
11853
|
}
|
|
11740
11854
|
/** Run one user turn on `agent` with routing. Returns a RunResult carrying `costUsd` + `routing`. */
|
|
@@ -11786,7 +11900,7 @@ ${text}`;
|
|
|
11786
11900
|
const answer = res.text || `(premium sub-agent finished with no reply; finishReason=${res.finishReason})`;
|
|
11787
11901
|
agent.transcript.push({ role: "user", content }, { role: "assistant", content: answer });
|
|
11788
11902
|
this.state.offload = { messages: child.transcript, mainLen: agent.transcript.length, question: text };
|
|
11789
|
-
|
|
11903
|
+
log23.info(`offload ${cont ? "continue" : "fresh"} done \xB7 ${res.finishReason} \xB7 ${res.steps} steps`);
|
|
11790
11904
|
const costUsd = o.costOf(main2, briefUsage) + o.costOf(o.models.premium, res.usage);
|
|
11791
11905
|
return { ...res, text: answer, messages: agent.transcript, usage: addUsage(briefUsage, res.usage), costUsd, routing: d };
|
|
11792
11906
|
}
|
|
@@ -11802,7 +11916,7 @@ The request:
|
|
|
11802
11916
|
${text}`);
|
|
11803
11917
|
return { text: r.text.trim() || text, usage: r.usage };
|
|
11804
11918
|
} catch (e) {
|
|
11805
|
-
|
|
11919
|
+
log23.warn(`brief failed, sending the raw request: ${e instanceof Error ? e.message : String(e)}`);
|
|
11806
11920
|
return { text };
|
|
11807
11921
|
} finally {
|
|
11808
11922
|
agent.transcript = snap;
|
|
@@ -11847,7 +11961,7 @@ async function jevAvailable(opts = {}) {
|
|
|
11847
11961
|
const { env } = await loadJev(opts);
|
|
11848
11962
|
return JEV_KEYS.some((k) => env[k]?.trim());
|
|
11849
11963
|
} catch (e) {
|
|
11850
|
-
|
|
11964
|
+
log23.debug(`jev unavailable: ${e instanceof Error ? e.message : String(e)}`);
|
|
11851
11965
|
return false;
|
|
11852
11966
|
}
|
|
11853
11967
|
}
|
|
@@ -12076,7 +12190,7 @@ init_logging();
|
|
|
12076
12190
|
init_platform();
|
|
12077
12191
|
import { spawn } from "child_process";
|
|
12078
12192
|
import { createHash } from "crypto";
|
|
12079
|
-
var
|
|
12193
|
+
var log24 = forComponent("mcp");
|
|
12080
12194
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
12081
12195
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
12082
12196
|
var DEFAULT_CALL_TIMEOUT_MS = 30 * 6e4;
|
|
@@ -12108,7 +12222,7 @@ var StdioTransport = class {
|
|
|
12108
12222
|
proc.stdout.setEncoding("utf8");
|
|
12109
12223
|
proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
12110
12224
|
proc.stderr.setEncoding("utf8");
|
|
12111
|
-
proc.stderr.on("data", (chunk) =>
|
|
12225
|
+
proc.stderr.on("data", (chunk) => log24.debug(`[${command}] stderr:`, chunk.trimEnd()));
|
|
12112
12226
|
proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
|
|
12113
12227
|
proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
|
|
12114
12228
|
}
|
|
@@ -12122,7 +12236,7 @@ var StdioTransport = class {
|
|
|
12122
12236
|
try {
|
|
12123
12237
|
this.dispatch(JSON.parse(line));
|
|
12124
12238
|
} catch (e) {
|
|
12125
|
-
|
|
12239
|
+
log24.debug("dropping non-JSON line from MCP server:", line, e);
|
|
12126
12240
|
}
|
|
12127
12241
|
}
|
|
12128
12242
|
}
|
|
@@ -12132,7 +12246,7 @@ var StdioTransport = class {
|
|
|
12132
12246
|
try {
|
|
12133
12247
|
p2?.onProgress?.(msg.params ?? {});
|
|
12134
12248
|
} catch (e) {
|
|
12135
|
-
|
|
12249
|
+
log24.debug("onProgress handler threw", e);
|
|
12136
12250
|
}
|
|
12137
12251
|
return;
|
|
12138
12252
|
}
|
|
@@ -12170,7 +12284,7 @@ var StdioTransport = class {
|
|
|
12170
12284
|
const onAbort = () => {
|
|
12171
12285
|
if (!this.pending.delete(id)) return;
|
|
12172
12286
|
clearTimeout(timer);
|
|
12173
|
-
this.notify("notifications/cancelled", { requestId: id, reason: String(signal?.reason?.message ?? "cancelled") }).catch((e) =>
|
|
12287
|
+
this.notify("notifications/cancelled", { requestId: id, reason: String(signal?.reason?.message ?? "cancelled") }).catch((e) => log24.debug("cancel notify failed", e));
|
|
12174
12288
|
reject(abortError(signal));
|
|
12175
12289
|
};
|
|
12176
12290
|
const done = () => signal?.removeEventListener("abort", onAbort);
|
|
@@ -12194,7 +12308,7 @@ var StdioTransport = class {
|
|
|
12194
12308
|
try {
|
|
12195
12309
|
this.proc?.stdin?.end();
|
|
12196
12310
|
} catch (e) {
|
|
12197
|
-
|
|
12311
|
+
log24.debug("stdin end failed", e);
|
|
12198
12312
|
}
|
|
12199
12313
|
if (!(IS_WIN && killTree(this.proc?.pid, "SIGTERM"))) this.proc?.kill();
|
|
12200
12314
|
}
|
|
@@ -12229,7 +12343,7 @@ var HttpTransport = class {
|
|
|
12229
12343
|
return await this.post({ jsonrpc: "2.0", id, method, params: opts.onProgress ? withProgressToken(params, id) : params }, false, signal, opts.onProgress);
|
|
12230
12344
|
} catch (e) {
|
|
12231
12345
|
if (opts.signal?.aborted) {
|
|
12232
|
-
void this.notify("notifications/cancelled", { requestId: id, reason: String(opts.signal.reason?.message ?? "cancelled") }).catch((err2) =>
|
|
12346
|
+
void this.notify("notifications/cancelled", { requestId: id, reason: String(opts.signal.reason?.message ?? "cancelled") }).catch((err2) => log24.debug("cancel notify failed", err2));
|
|
12233
12347
|
throw abortError(opts.signal);
|
|
12234
12348
|
}
|
|
12235
12349
|
throw e;
|
|
@@ -12272,7 +12386,7 @@ function sseMessage(line) {
|
|
|
12272
12386
|
try {
|
|
12273
12387
|
return JSON.parse(trimmed.slice(5).trim());
|
|
12274
12388
|
} catch (e) {
|
|
12275
|
-
|
|
12389
|
+
log24.debug("skipping unparseable SSE data line", e);
|
|
12276
12390
|
return void 0;
|
|
12277
12391
|
}
|
|
12278
12392
|
}
|
|
@@ -12282,7 +12396,7 @@ async function readSseResponse(resp, id, onProgress) {
|
|
|
12282
12396
|
try {
|
|
12283
12397
|
onProgress?.(obj.params ?? {});
|
|
12284
12398
|
} catch (e) {
|
|
12285
|
-
|
|
12399
|
+
log24.debug("onProgress handler threw", e);
|
|
12286
12400
|
}
|
|
12287
12401
|
return void 0;
|
|
12288
12402
|
}
|
|
@@ -12381,7 +12495,7 @@ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
|
|
|
12381
12495
|
return { name, client, tools, specs, serverInfo: init?.serverInfo, config: cfg };
|
|
12382
12496
|
})(), mountTimeoutMs, name);
|
|
12383
12497
|
} catch (e) {
|
|
12384
|
-
await client.close().catch((err2) =>
|
|
12498
|
+
await client.close().catch((err2) => log24.debug(`close after failed mount of "${name}": ${err2}`));
|
|
12385
12499
|
throw e;
|
|
12386
12500
|
}
|
|
12387
12501
|
}
|
|
@@ -12392,15 +12506,15 @@ function validEntries(servers) {
|
|
|
12392
12506
|
return Object.entries(servers).filter(([name, cfg]) => {
|
|
12393
12507
|
if (!cfg || cfg.disabled) return false;
|
|
12394
12508
|
if (!cfg.command && !cfg.url) {
|
|
12395
|
-
|
|
12509
|
+
log24.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
|
|
12396
12510
|
return false;
|
|
12397
12511
|
}
|
|
12398
12512
|
return true;
|
|
12399
12513
|
});
|
|
12400
12514
|
}
|
|
12401
12515
|
function logMountFailure(name, e) {
|
|
12402
|
-
if (e instanceof McpAuthError)
|
|
12403
|
-
else
|
|
12516
|
+
if (e instanceof McpAuthError) log24.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
|
|
12517
|
+
else log24.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
|
|
12404
12518
|
}
|
|
12405
12519
|
async function mountMcpServers(servers = {}, opts = {}) {
|
|
12406
12520
|
const entries = validEntries(servers);
|
|
@@ -12410,7 +12524,7 @@ async function mountMcpServers(servers = {}, opts = {}) {
|
|
|
12410
12524
|
const name = entries[i][0];
|
|
12411
12525
|
if (r.status === "fulfilled") {
|
|
12412
12526
|
out.push(r.value);
|
|
12413
|
-
|
|
12527
|
+
log24.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
|
|
12414
12528
|
} else logMountFailure(name, r.reason);
|
|
12415
12529
|
});
|
|
12416
12530
|
return out;
|
|
@@ -12450,7 +12564,7 @@ var McpPool = class {
|
|
|
12450
12564
|
const prev = this.warm.get(key);
|
|
12451
12565
|
if (prev) {
|
|
12452
12566
|
clearTimeout(prev.timer);
|
|
12453
|
-
if (prev.client !== client) void prev.client.close().catch((err2) =>
|
|
12567
|
+
if (prev.client !== client) void prev.client.close().catch((err2) => log24.debug(`warm-pool replace close failed: ${err2}`));
|
|
12454
12568
|
}
|
|
12455
12569
|
const e = { client, timer: void 0 };
|
|
12456
12570
|
this.warm.set(key, e);
|
|
@@ -12467,7 +12581,7 @@ var McpPool = class {
|
|
|
12467
12581
|
const e = this.warm.get(key);
|
|
12468
12582
|
if (!e) return;
|
|
12469
12583
|
this.warm.delete(key);
|
|
12470
|
-
await e.client.close().catch((err2) =>
|
|
12584
|
+
await e.client.close().catch((err2) => log24.debug(`warm-pool evict close failed: ${err2}`));
|
|
12471
12585
|
}
|
|
12472
12586
|
async closeAll() {
|
|
12473
12587
|
for (const e of this.warm.values()) {
|
|
@@ -12686,7 +12800,7 @@ init_tools_shell();
|
|
|
12686
12800
|
// src/tools.notify.ts
|
|
12687
12801
|
init_logging();
|
|
12688
12802
|
import { execFile as execFile2 } from "child_process";
|
|
12689
|
-
var
|
|
12803
|
+
var log26 = forComponent("notify");
|
|
12690
12804
|
var WIN_TOAST = [
|
|
12691
12805
|
"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null",
|
|
12692
12806
|
"$t = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)",
|
|
@@ -12717,7 +12831,7 @@ function makeNotifyTool(opts = {}) {
|
|
|
12717
12831
|
return new Promise((resolve6) => {
|
|
12718
12832
|
run(argv[0], argv[1], { timeout: 5e3, ...extra }, (e) => {
|
|
12719
12833
|
if (e) {
|
|
12720
|
-
|
|
12834
|
+
log26.debug("notification failed", e);
|
|
12721
12835
|
resolve6(`Notification failed: ${e.message}`);
|
|
12722
12836
|
} else resolve6("Notification shown.");
|
|
12723
12837
|
});
|
|
@@ -12733,7 +12847,7 @@ import { BodDB as BodDB2 } from "@bod.ee/db";
|
|
|
12733
12847
|
init_logging();
|
|
12734
12848
|
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
12735
12849
|
import { homedir } from "os";
|
|
12736
|
-
var
|
|
12850
|
+
var log27 = forComponent("cli-util");
|
|
12737
12851
|
function dotDirs(base, sub, opts = {}) {
|
|
12738
12852
|
const home = opts.home ?? homedir();
|
|
12739
12853
|
const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
|
|
@@ -12750,7 +12864,7 @@ function parseJson(text, fallback, what = "json") {
|
|
|
12750
12864
|
try {
|
|
12751
12865
|
return JSON.parse(text);
|
|
12752
12866
|
} catch (e) {
|
|
12753
|
-
|
|
12867
|
+
log27.debug(`parseJson(${what}) failed: ${e.message}`);
|
|
12754
12868
|
return fallback;
|
|
12755
12869
|
}
|
|
12756
12870
|
}
|
|
@@ -12760,7 +12874,7 @@ function readJsonFile(path, fallback) {
|
|
|
12760
12874
|
try {
|
|
12761
12875
|
text = readFileSync5(path, "utf8");
|
|
12762
12876
|
} catch (e) {
|
|
12763
|
-
|
|
12877
|
+
log27.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
|
|
12764
12878
|
return fallback;
|
|
12765
12879
|
}
|
|
12766
12880
|
return parseJson(text, fallback, path);
|
|
@@ -13064,7 +13178,7 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as rea
|
|
|
13064
13178
|
import { homedir as homedir3 } from "os";
|
|
13065
13179
|
import { dirname as dirname4, join as join7 } from "path";
|
|
13066
13180
|
import { fileURLToPath } from "url";
|
|
13067
|
-
var
|
|
13181
|
+
var log28 = forComponent("VoiceIO");
|
|
13068
13182
|
var now3 = () => performance.now();
|
|
13069
13183
|
var Player = class {
|
|
13070
13184
|
proc = null;
|
|
@@ -13078,7 +13192,7 @@ var Player = class {
|
|
|
13078
13192
|
["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
|
|
13079
13193
|
{ stdio: ["pipe", "ignore", "ignore"] }
|
|
13080
13194
|
);
|
|
13081
|
-
this.proc.on("error", (e) =>
|
|
13195
|
+
this.proc.on("error", (e) => log28.warn(`ffplay error: ${e.message}`));
|
|
13082
13196
|
this.proc.stdin.on("error", () => {
|
|
13083
13197
|
});
|
|
13084
13198
|
this.bytesWritten = 0;
|
|
@@ -13116,7 +13230,7 @@ function detectFfmpegMic() {
|
|
|
13116
13230
|
const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
|
|
13117
13231
|
const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
|
|
13118
13232
|
if (!mic) throw new Error("no audio input device found");
|
|
13119
|
-
|
|
13233
|
+
log28.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
|
|
13120
13234
|
return `:${mic.idx}`;
|
|
13121
13235
|
}
|
|
13122
13236
|
function detectDshowMic() {
|
|
@@ -13127,7 +13241,7 @@ function detectDshowMic() {
|
|
|
13127
13241
|
const devices = [...(r.stderr ?? "").matchAll(/"([^"]+)"\s*\(audio\)/g)].map((m) => m[1]);
|
|
13128
13242
|
const mic = devices.find((d) => !/stereo mix|loopback|virtual|cable|teams/i.test(d)) ?? devices[0];
|
|
13129
13243
|
if (!mic) throw new Error("no audio input device found (set MIC_DEVICE to the DirectShow device name)");
|
|
13130
|
-
|
|
13244
|
+
log28.debug(`ffmpeg dshow mic: ${mic}`);
|
|
13131
13245
|
return `audio=${mic}`;
|
|
13132
13246
|
}
|
|
13133
13247
|
function detectedInputDevice() {
|
|
@@ -13163,15 +13277,15 @@ function resolveAecBinary() {
|
|
|
13163
13277
|
if (existsSync4(bin) && statSync6(bin).mtimeMs >= statSync6(src).mtimeMs) return bin;
|
|
13164
13278
|
if (spawnSync3("which", ["swiftc"]).status !== 0) return null;
|
|
13165
13279
|
mkdirSync5(cacheDir, { recursive: true });
|
|
13166
|
-
|
|
13280
|
+
log28.info("compiling AEC mic helper (first run)\u2026");
|
|
13167
13281
|
const build = spawnSync3("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
|
|
13168
13282
|
if (build.status !== 0) {
|
|
13169
|
-
|
|
13283
|
+
log28.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
|
|
13170
13284
|
return null;
|
|
13171
13285
|
}
|
|
13172
13286
|
const sign = spawnSync3("codesign", ["-fs", "-", bin], { encoding: "utf8" });
|
|
13173
13287
|
if (sign.status !== 0) {
|
|
13174
|
-
|
|
13288
|
+
log28.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
|
|
13175
13289
|
return null;
|
|
13176
13290
|
}
|
|
13177
13291
|
return bin;
|
|
@@ -13213,17 +13327,17 @@ var NodeMicSource = class {
|
|
|
13213
13327
|
this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
|
|
13214
13328
|
} else {
|
|
13215
13329
|
if (!hasCommand("ffmpeg")) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
|
|
13216
|
-
|
|
13330
|
+
log28.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
|
|
13217
13331
|
const input = process.platform === "win32" ? ["-f", "dshow", "-i", detectDshowMic()] : ["-f", "avfoundation", "-i", detectFfmpegMic()];
|
|
13218
13332
|
this.proc = spawn2(
|
|
13219
13333
|
"ffmpeg",
|
|
13220
13334
|
["-loglevel", "error", ...input, "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
|
|
13221
13335
|
{ stdio: ["ignore", "pipe", "pipe"], ...process.platform === "win32" ? { windowsHide: true } : {} }
|
|
13222
13336
|
);
|
|
13223
|
-
this.proc.stderr.on("data", (d) =>
|
|
13337
|
+
this.proc.stderr.on("data", (d) => log28.warn(`ffmpeg: ${String(d).trim()}`));
|
|
13224
13338
|
}
|
|
13225
13339
|
this.proc.on("exit", (c) => {
|
|
13226
|
-
if (c && !this.stopped)
|
|
13340
|
+
if (c && !this.stopped) log28.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
|
|
13227
13341
|
});
|
|
13228
13342
|
this.proc.stdout.on("data", (chunk) => onChunk(chunk));
|
|
13229
13343
|
}
|
|
@@ -13289,7 +13403,7 @@ var AecDuplexAudio = class {
|
|
|
13289
13403
|
this.proc.stdin.on("error", () => {
|
|
13290
13404
|
});
|
|
13291
13405
|
this.proc.on("exit", (c) => {
|
|
13292
|
-
if (c && !this.stopped)
|
|
13406
|
+
if (c && !this.stopped) log28.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
|
|
13293
13407
|
});
|
|
13294
13408
|
this.proc.stdout.on("data", (chunk) => {
|
|
13295
13409
|
this.gotChunk = true;
|
|
@@ -13305,7 +13419,7 @@ var AecDuplexAudio = class {
|
|
|
13305
13419
|
openMicSettings();
|
|
13306
13420
|
this.onFatal?.("microphone permission denied \u2014 enable it in System Settings \u2192 Privacy & Security \u2192 Microphone for your terminal, then restart it");
|
|
13307
13421
|
}
|
|
13308
|
-
} else
|
|
13422
|
+
} else log28.debug(`mic-aec: ${s}`);
|
|
13309
13423
|
}
|
|
13310
13424
|
});
|
|
13311
13425
|
if (!this.noVpio && !this.triedFallback) {
|
|
@@ -13314,7 +13428,7 @@ var AecDuplexAudio = class {
|
|
|
13314
13428
|
this.triedFallback = true;
|
|
13315
13429
|
this.noVpio = true;
|
|
13316
13430
|
this._aec = false;
|
|
13317
|
-
|
|
13431
|
+
log28.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)");
|
|
13318
13432
|
this.onDegrade?.();
|
|
13319
13433
|
this.killProc();
|
|
13320
13434
|
this.spawnHelper();
|
|
@@ -13485,7 +13599,7 @@ function bindAckInstinct(v, instinct) {
|
|
|
13485
13599
|
if (ev.kind === "ack_fired") instinct.ackOutcome(String(ev.phrase));
|
|
13486
13600
|
else if (ev.kind === "ack_skipped" || ev.kind === "ack_cancelled") instinct.ackOutcome(null);
|
|
13487
13601
|
} catch (e) {
|
|
13488
|
-
|
|
13602
|
+
log28.warn(`ack outcome tap failed: ${e instanceof Error ? e.message : e}`);
|
|
13489
13603
|
}
|
|
13490
13604
|
onDiag(ev);
|
|
13491
13605
|
};
|
|
@@ -13562,7 +13676,7 @@ function channelInput(sc, m) {
|
|
|
13562
13676
|
}
|
|
13563
13677
|
if (m.type === "agent_state") {
|
|
13564
13678
|
if (typeof m.state === "string" && MAIN_STATES.has(m.state)) sc.onAgentState(m.state);
|
|
13565
|
-
else
|
|
13679
|
+
else log28.warn(`agent_state: unknown state ${JSON.stringify(m.state)} \u2014 ignored`);
|
|
13566
13680
|
return true;
|
|
13567
13681
|
}
|
|
13568
13682
|
return false;
|
|
@@ -13686,7 +13800,7 @@ function fakeVoiceParts(uttFile) {
|
|
|
13686
13800
|
init_logging();
|
|
13687
13801
|
import { appendFile, mkdirSync as mkdirSync6 } from "fs";
|
|
13688
13802
|
import { dirname as dirname5 } from "path";
|
|
13689
|
-
var
|
|
13803
|
+
var log29 = forComponent("VoiceDiag");
|
|
13690
13804
|
var JsonlDiagSink = class {
|
|
13691
13805
|
constructor(path) {
|
|
13692
13806
|
this.path = path;
|
|
@@ -13727,7 +13841,7 @@ var JsonlDiagSink = class {
|
|
|
13727
13841
|
disable(e) {
|
|
13728
13842
|
this.disabled = true;
|
|
13729
13843
|
this.queue.length = 0;
|
|
13730
|
-
|
|
13844
|
+
log29.debug(`voice diag sink disabled (${this.path}): ${e instanceof Error ? e.message : e}`);
|
|
13731
13845
|
}
|
|
13732
13846
|
/** True once a failure permanently disabled the sink (tests/inspection). */
|
|
13733
13847
|
get isDisabled() {
|
|
@@ -13749,7 +13863,7 @@ init_logging();
|
|
|
13749
13863
|
init_logging();
|
|
13750
13864
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
13751
13865
|
import { join as join8 } from "path";
|
|
13752
|
-
var
|
|
13866
|
+
var log30 = forComponent("ChannelBanks");
|
|
13753
13867
|
var LINE_KINDS = ["backchannel", "filler", "bridge"];
|
|
13754
13868
|
var BANK_KEYS = [...LINE_KINDS, "phatic"];
|
|
13755
13869
|
var EN_PHATIC = [
|
|
@@ -13782,9 +13896,9 @@ var EN_PHATIC = [
|
|
|
13782
13896
|
var EN_BANK = { ...CHANNEL_BANK, phatic: EN_PHATIC };
|
|
13783
13897
|
var primaryLang = (tag) => tag.trim().toLowerCase().split(/[-_]/)[0];
|
|
13784
13898
|
function isPhatic(text, bank) {
|
|
13785
|
-
const t =
|
|
13899
|
+
const t = norm3(text);
|
|
13786
13900
|
return !!t && !!bank?.phatic?.some((p) => {
|
|
13787
|
-
const q2 =
|
|
13901
|
+
const q2 = norm3(p);
|
|
13788
13902
|
return !!q2 && (t === q2 || t.startsWith(q2 + " "));
|
|
13789
13903
|
});
|
|
13790
13904
|
}
|
|
@@ -13862,16 +13976,16 @@ function fitsLanguage(line, lang) {
|
|
|
13862
13976
|
const ws = line.toLowerCase().match(new RegExp("\\p{L}+", "gu")) ?? [];
|
|
13863
13977
|
return !ws.some((w) => !mine?.has(w) && Object.entries(STOP3).some(([l, s]) => l !== lang && s.has(w)));
|
|
13864
13978
|
}
|
|
13865
|
-
var
|
|
13979
|
+
var norm3 = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}' ]/gu, "").replace(/\s+/g, " ").trim();
|
|
13866
13980
|
function cleanLine(s, lang, maxWords = 6) {
|
|
13867
13981
|
const raw = s.trim().split("\n")[0];
|
|
13868
13982
|
const l = raw.replace(/^[-*•\d.)\s]+/, "").replace(/^["'“”«»]+|["'“”«»]+$/g, "").replace(new RegExp("^\\p{L}+:\\s*", "u"), "").trim();
|
|
13869
13983
|
if (!l || isLeakSuspect(raw) || isLeakSuspect(l)) {
|
|
13870
|
-
if (l)
|
|
13984
|
+
if (l) log30.debug(`line "${raw}" reads as meta \u2014 rejected`);
|
|
13871
13985
|
return "";
|
|
13872
13986
|
}
|
|
13873
13987
|
if (lang && !fitsLanguage(l, lang)) {
|
|
13874
|
-
|
|
13988
|
+
log30.debug(`line "${l}" is not ${lang} \u2014 rejected`);
|
|
13875
13989
|
return "";
|
|
13876
13990
|
}
|
|
13877
13991
|
return l.split(/\s+/).length > maxWords || l.length > 60 ? "" : l;
|
|
@@ -13883,9 +13997,9 @@ function validateBank(raw, lang, minPerKind = 3) {
|
|
|
13883
13997
|
for (const k of BANK_KEYS) {
|
|
13884
13998
|
const v = raw[k];
|
|
13885
13999
|
const seen = /* @__PURE__ */ new Set();
|
|
13886
|
-
out[k] = (Array.isArray(v) ? v : []).filter((x) => typeof x === "string").map((x) => cleanLine(x, lang)).filter((x) => x && !(k === "filler" && SPEAKER_FILLER.test(
|
|
14000
|
+
out[k] = (Array.isArray(v) ? v : []).filter((x) => typeof x === "string").map((x) => cleanLine(x, lang)).filter((x) => x && !(k === "filler" && SPEAKER_FILLER.test(norm3(x))) && !seen.has(norm3(x)) && seen.add(norm3(x)));
|
|
13887
14001
|
if (out[k].length < minPerKind) {
|
|
13888
|
-
|
|
14002
|
+
log30.warn(`bank ${lang}: only ${out[k].length} valid ${k} lines \u2014 rejected`);
|
|
13889
14003
|
return null;
|
|
13890
14004
|
}
|
|
13891
14005
|
}
|
|
@@ -13955,11 +14069,11 @@ var ChannelBanks = class {
|
|
|
13955
14069
|
const f = this.file(lang);
|
|
13956
14070
|
let cached = null;
|
|
13957
14071
|
if (f) cached = await readFile(f, "utf8").then((s) => JSON.parse(s)).catch((e) => {
|
|
13958
|
-
if (e.code !== "ENOENT")
|
|
14072
|
+
if (e.code !== "ENOENT") log30.warn(`bank cache ${f} unreadable \u2014 regenerating: ${e.message}`);
|
|
13959
14073
|
return null;
|
|
13960
14074
|
});
|
|
13961
14075
|
if (cached && cached.v !== BANK_VERSION) {
|
|
13962
|
-
|
|
14076
|
+
log30.debug(`bank ${lang}: cache v${cached.v ?? 1} predates v${BANK_VERSION} \u2014 regenerating`);
|
|
13963
14077
|
cached = null;
|
|
13964
14078
|
}
|
|
13965
14079
|
const valid = cached && validateBank(cached.bank, lang, 0);
|
|
@@ -13967,7 +14081,7 @@ var ChannelBanks = class {
|
|
|
13967
14081
|
if (valid) {
|
|
13968
14082
|
for (const k of BANK_KEYS) if (!valid[k].length) valid[k] = [...this.o.fallback[lang]?.[k] ?? []];
|
|
13969
14083
|
this.banks.set(lang, valid);
|
|
13970
|
-
|
|
14084
|
+
log30.debug(`bank ${lang}: cache hit (${BANK_KEYS.map((k) => valid[k].length).join("/")})${complete ? "" : " \u2014 missing kinds, refreshing"}`);
|
|
13971
14085
|
}
|
|
13972
14086
|
if (complete && this.o.now() - cached.at < this.o.refreshMs) return;
|
|
13973
14087
|
await this.generate(lang, valid);
|
|
@@ -13983,14 +14097,14 @@ var ChannelBanks = class {
|
|
|
13983
14097
|
const bank = {};
|
|
13984
14098
|
for (const k of BANK_KEYS) {
|
|
13985
14099
|
const seen = /* @__PURE__ */ new Set();
|
|
13986
|
-
bank[k] = [...fresh[k], ...have?.[k] ?? []].filter((x) => !seen.has(
|
|
14100
|
+
bank[k] = [...fresh[k], ...have?.[k] ?? []].filter((x) => !seen.has(norm3(x)) && seen.add(norm3(x))).slice(0, this.o.maxPerKind);
|
|
13987
14101
|
}
|
|
13988
14102
|
this.banks.set(lang, bank);
|
|
13989
|
-
|
|
14103
|
+
log30.debug(`bank ${lang}: ${have ? "refreshed" : "generated"} (${BANK_KEYS.map((k) => bank[k].length).join("/")}) in ${Math.round(this.o.now() - t0)}ms`);
|
|
13990
14104
|
const f = this.file(lang);
|
|
13991
|
-
if (f) await mkdir(this.o.cacheDir, { recursive: true }).then(() => writeFile(f, JSON.stringify({ at: this.o.now(), v: BANK_VERSION, bank }))).catch((e) =>
|
|
14105
|
+
if (f) await mkdir(this.o.cacheDir, { recursive: true }).then(() => writeFile(f, JSON.stringify({ at: this.o.now(), v: BANK_VERSION, bank }))).catch((e) => log30.warn(`bank cache write ${f} failed: ${e.message}`));
|
|
13992
14106
|
} catch (e) {
|
|
13993
|
-
|
|
14107
|
+
log30.warn(`bank ${lang}: generation failed \u2014 ${have ? "keeping the cached bank" : lang in this.o.fallback ? "fallback bank" : "silent"}: ${e.message}`);
|
|
13994
14108
|
} finally {
|
|
13995
14109
|
clearTimeout(t);
|
|
13996
14110
|
if (!this.banks.has(lang)) setTimeout(() => this.pending.delete(lang), this.o.retryMs).unref?.();
|
|
@@ -13999,7 +14113,7 @@ var ChannelBanks = class {
|
|
|
13999
14113
|
};
|
|
14000
14114
|
|
|
14001
14115
|
// src/voice/backchannelPlan.ts
|
|
14002
|
-
var
|
|
14116
|
+
var log31 = forComponent("BackchannelPlan");
|
|
14003
14117
|
var TURN_TYPES = ["story", "list", "complaint", "explanation", "short_command", "question", "other"];
|
|
14004
14118
|
var DENSITIES = ["none", "low", "normal", "high"];
|
|
14005
14119
|
var silentTurn = (p) => p.density === "none" || p.turn === "short_command" || p.turn === "question";
|
|
@@ -14064,14 +14178,41 @@ Reply JSON only: {"turn":"...","density":"...","cues":[{"when":["phrase",...],"l
|
|
|
14064
14178
|
const reasoning = /gpt-oss/.test(o.model) ? { providerOptions: { reasoning_effort: "low" } } : {};
|
|
14065
14179
|
const r = await this.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false, signal, maxTokens: o.maxTokens, ...reasoning });
|
|
14066
14180
|
const p = parsePlan(r.content ?? "", c.lang);
|
|
14067
|
-
if (!p)
|
|
14181
|
+
if (!p) log31.debug(`unusable plan: ${(r.content ?? "").slice(0, 120)}`);
|
|
14068
14182
|
return p;
|
|
14069
14183
|
}
|
|
14070
14184
|
};
|
|
14071
14185
|
|
|
14072
14186
|
// src/voice/secondChannel.ts
|
|
14073
|
-
var
|
|
14187
|
+
var log32 = forComponent("SecondChannel");
|
|
14188
|
+
var CHANNEL_MODES = {
|
|
14189
|
+
turn: { kinds: ["bridge"], bridgeDelayMs: 1500 },
|
|
14190
|
+
normal: {},
|
|
14191
|
+
chatty: {
|
|
14192
|
+
anticipate: true,
|
|
14193
|
+
cueOnly: false,
|
|
14194
|
+
generateFor: ["backchannel", "bridge"],
|
|
14195
|
+
reactionGapMs: 6e3,
|
|
14196
|
+
generateBudgetMs: 400,
|
|
14197
|
+
backchannelMinWords: 5,
|
|
14198
|
+
backchannelGapMs: 1200,
|
|
14199
|
+
minGapMs: 1200,
|
|
14200
|
+
backchannelPauseMs: 250
|
|
14201
|
+
}
|
|
14202
|
+
};
|
|
14203
|
+
var CHANNEL_MODE_NAMES = Object.keys(CHANNEL_MODES);
|
|
14204
|
+
function parseChannelMode(v) {
|
|
14205
|
+
if (!v) return "normal";
|
|
14206
|
+
if (!(v in CHANNEL_MODES)) throw new Error(`unknown channel mode "${v}" (${CHANNEL_MODE_NAMES.join("|")})`);
|
|
14207
|
+
return v;
|
|
14208
|
+
}
|
|
14074
14209
|
var SecondChannelOptions = class {
|
|
14210
|
+
/** Preset (CHANNEL_MODES) under the explicit options. */
|
|
14211
|
+
mode = "normal";
|
|
14212
|
+
/** Line kinds this channel may speak. */
|
|
14213
|
+
kinds = ["backchannel", "filler", "bridge"];
|
|
14214
|
+
/** A generated (content) backchannel needs this long since the last one; in between, canned light cues. 0 = always. */
|
|
14215
|
+
reactionGapMs = 0;
|
|
14075
14216
|
/** Refiner (Jev/fast LLM): the choice over CHANNEL_ACTIONS, or null on failure. Absent → the rule alone decides.
|
|
14076
14217
|
* `state.rule` is the kind the rule proposed (fastChannelDecider answers yes/no on it). `signal` aborts a late call. */
|
|
14077
14218
|
decide;
|
|
@@ -14153,20 +14294,23 @@ function withBudget(p, ms, what, ac) {
|
|
|
14153
14294
|
let t;
|
|
14154
14295
|
const late = new Promise((r) => {
|
|
14155
14296
|
t = setTimeout(() => {
|
|
14156
|
-
|
|
14297
|
+
log32.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
|
|
14157
14298
|
ac?.abort();
|
|
14158
14299
|
r(null);
|
|
14159
14300
|
}, ms);
|
|
14160
14301
|
});
|
|
14161
14302
|
return Promise.race([p.catch((e) => {
|
|
14162
|
-
|
|
14303
|
+
log32.warn(`${what} failed \u2014 falling back: ${e?.message ?? e}`);
|
|
14163
14304
|
return null;
|
|
14164
14305
|
}), late]).finally(() => clearTimeout(t));
|
|
14165
14306
|
}
|
|
14166
14307
|
var SecondChannel = class {
|
|
14167
14308
|
constructor(options) {
|
|
14168
14309
|
this.options = options;
|
|
14169
|
-
|
|
14310
|
+
const preset = CHANNEL_MODES[options?.mode ?? "normal"] ?? {};
|
|
14311
|
+
const base = new SecondChannelOptions();
|
|
14312
|
+
this.options = { ...base, ...preset, ...options };
|
|
14313
|
+
for (const k of Object.keys(preset)) if (!options || !(k in options)) this.normalTiming[k] = base[k];
|
|
14170
14314
|
this.startedAt = this.o.now();
|
|
14171
14315
|
if (this.o.lang) this.setLanguage(this.o.lang, true);
|
|
14172
14316
|
}
|
|
@@ -14213,6 +14357,10 @@ var SecondChannel = class {
|
|
|
14213
14357
|
reply = "";
|
|
14214
14358
|
// channel-1's text this turn (the plan's context)
|
|
14215
14359
|
lastUser = "";
|
|
14360
|
+
lastReactAt = -Infinity;
|
|
14361
|
+
normalTiming = {};
|
|
14362
|
+
// chatty without aec: the preset's keys back at their defaults
|
|
14363
|
+
degradedLogged = false;
|
|
14216
14364
|
/** The language lines are spoken in (detected from the user's words, or set by the host). */
|
|
14217
14365
|
get language() {
|
|
14218
14366
|
return this.lang;
|
|
@@ -14224,7 +14372,7 @@ var SecondChannel = class {
|
|
|
14224
14372
|
if (host) this.pinned = true;
|
|
14225
14373
|
const lang = primaryLang(tag);
|
|
14226
14374
|
if (!lang || lang === this.lang) return;
|
|
14227
|
-
|
|
14375
|
+
log32.debug(`language ${this.lang} \u2192 ${lang}`);
|
|
14228
14376
|
this.lang = lang;
|
|
14229
14377
|
this.recent = [];
|
|
14230
14378
|
this.picks.clear();
|
|
@@ -14241,7 +14389,13 @@ var SecondChannel = class {
|
|
|
14241
14389
|
return this.lang ? this.o.banks ? this.o.banks.get(this.lang) : this.lang === "en" ? EN_BANK : null : null;
|
|
14242
14390
|
}
|
|
14243
14391
|
get o() {
|
|
14244
|
-
|
|
14392
|
+
const o = this.options;
|
|
14393
|
+
if (o.mode !== "chatty" || o.aec) return o;
|
|
14394
|
+
if (!this.degradedLogged) {
|
|
14395
|
+
this.degradedLogged = true;
|
|
14396
|
+
log32.info("chatty mode without echo cancellation \u2014 normal timing (our lines would re-trigger the mic)");
|
|
14397
|
+
}
|
|
14398
|
+
return { ...o, ...this.normalTiming };
|
|
14245
14399
|
}
|
|
14246
14400
|
/** The user's live partial for the current turn (the whole partial, not a delta). */
|
|
14247
14401
|
onInterim(text) {
|
|
@@ -14354,7 +14508,7 @@ var SecondChannel = class {
|
|
|
14354
14508
|
try {
|
|
14355
14509
|
this.o.cancel();
|
|
14356
14510
|
} catch (e) {
|
|
14357
|
-
|
|
14511
|
+
log32.warn(`cancel threw: ${e.message}`);
|
|
14358
14512
|
}
|
|
14359
14513
|
}
|
|
14360
14514
|
}
|
|
@@ -14377,6 +14531,7 @@ var SecondChannel = class {
|
|
|
14377
14531
|
}
|
|
14378
14532
|
/** Hard gates every line passes, whoever proposed it. */
|
|
14379
14533
|
allowed(k) {
|
|
14534
|
+
if (!this.o.kinds.includes(k)) return false;
|
|
14380
14535
|
if (this.audioLocked || this.inflight || this.o.now() - this.lastAt < this.o.minGapMs || k === this.lastKind && (k !== "backchannel" || this.o.now() - this.lastAt < this.o.backchannelGapMs)) return false;
|
|
14381
14536
|
if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle";
|
|
14382
14537
|
if (k === "filler") return !this.userSpeaking && this.main === "idle";
|
|
@@ -14438,17 +14593,19 @@ var SecondChannel = class {
|
|
|
14438
14593
|
const d = await withBudget(this.o.decide(this.tail(), { ...this.state(pauseMs), rule }, ac.signal), this.o.decisionBudgetMs, "channel decision", ac);
|
|
14439
14594
|
choice = d?.choice;
|
|
14440
14595
|
if (choice && choice in CHANNEL_ACTIONS && (choice === "silence" || this.allowed(choice))) kind = choice;
|
|
14441
|
-
else if (choice && choice in CHANNEL_ACTIONS)
|
|
14596
|
+
else if (choice && choice in CHANNEL_ACTIONS) log32.debug(`decider chose ${choice} for a ${rule} moment \u2014 the rule stands`);
|
|
14442
14597
|
}
|
|
14443
14598
|
if (kind === "silence") return ev({ choice, acted: "silence" });
|
|
14444
14599
|
if (stale()) return ev({ choice, kind, acted: "stale" });
|
|
14445
14600
|
if (!this.allowed(kind)) return ev({ choice, kind, acted: "gated" });
|
|
14446
|
-
const
|
|
14601
|
+
const react = kind === "backchannel" && this.o.now() - this.lastReactAt >= this.o.reactionGapMs;
|
|
14602
|
+
const text = cue && react && !this.recent.some((r) => norm3(r) === norm3(cue.line)) ? cue.line : await this.line(kind, react);
|
|
14447
14603
|
if (!text) return ev({ choice, kind, acted: "no_bank" });
|
|
14448
14604
|
if (stale()) return ev({ choice, kind, text, acted: "stale" });
|
|
14449
14605
|
if (!this.allowed(kind)) return ev({ choice, kind, text, acted: "gated" });
|
|
14450
14606
|
const said = this.say(kind, text);
|
|
14451
14607
|
if (said && cue) this.cueFrom = words2(this.heard).length;
|
|
14608
|
+
if (said && kind === "backchannel" && this.o.reactionGapMs && !this.bank?.backchannel.some((b) => norm3(b) === norm3(text))) this.lastReactAt = this.o.now();
|
|
14452
14609
|
ev({ choice, kind, text, acted: said ? "spoken" : "skipped" });
|
|
14453
14610
|
} finally {
|
|
14454
14611
|
this.busy = false;
|
|
@@ -14465,15 +14622,15 @@ var SecondChannel = class {
|
|
|
14465
14622
|
call_ms: Math.round(now4 - this.startedAt)
|
|
14466
14623
|
};
|
|
14467
14624
|
}
|
|
14468
|
-
async line(kind) {
|
|
14469
|
-
if (this.o.generate && this.o.generateFor.includes(kind)) {
|
|
14625
|
+
async line(kind, react = true) {
|
|
14626
|
+
if (this.o.generate && this.o.generateFor.includes(kind) && (kind !== "backchannel" || react)) {
|
|
14470
14627
|
const ac = new AbortController();
|
|
14471
|
-
const ctx = { kind, heard: this.tail(), state: this.main, avoid: [...this.recent], lang: this.lang };
|
|
14628
|
+
const ctx = { kind, heard: this.tail(), state: this.main, avoid: [...this.recent], lang: this.lang, ...kind === "backchannel" && this.o.reactionGapMs ? { react: true } : {} };
|
|
14472
14629
|
const raw = await withBudget(this.o.generate(ctx, ac.signal), this.o.generateBudgetMs, `${kind} generate`);
|
|
14473
14630
|
if (raw === null) ac.abort();
|
|
14474
14631
|
const text = cleanLine(raw ?? "", this.lang);
|
|
14475
|
-
if (text && !this.recent.some((r) =>
|
|
14476
|
-
if (text)
|
|
14632
|
+
if (text && !this.recent.some((r) => norm3(r) === norm3(text))) return text;
|
|
14633
|
+
if (text) log32.debug(`generated "${text}" repeats a recent line \u2014 canned`);
|
|
14477
14634
|
}
|
|
14478
14635
|
return this.pick(kind);
|
|
14479
14636
|
}
|
|
@@ -14483,8 +14640,8 @@ var SecondChannel = class {
|
|
|
14483
14640
|
const pool = this.bank?.[kind] ?? [];
|
|
14484
14641
|
if (!pool.length) return "";
|
|
14485
14642
|
const hist = this.picks.get(kind) ?? [];
|
|
14486
|
-
const recent = new Set([...hist.slice(-this.o.bankExclude), ...this.recent].map(
|
|
14487
|
-
const fresh = pool.filter((x) => !recent.has(
|
|
14643
|
+
const recent = new Set([...hist.slice(-this.o.bankExclude), ...this.recent].map(norm3));
|
|
14644
|
+
const fresh = pool.filter((x) => !recent.has(norm3(x)));
|
|
14488
14645
|
const from = fresh.length ? fresh : pool;
|
|
14489
14646
|
const p = from[Math.floor(this.o.random() * from.length)];
|
|
14490
14647
|
this.picks.set(kind, [...hist, p].slice(-this.o.bankExclude));
|
|
@@ -14495,7 +14652,7 @@ var SecondChannel = class {
|
|
|
14495
14652
|
try {
|
|
14496
14653
|
r = this.o.speak(text, kind);
|
|
14497
14654
|
} catch (e) {
|
|
14498
|
-
|
|
14655
|
+
log32.warn(`speak threw \u2014 line dropped: ${e.message}`);
|
|
14499
14656
|
return false;
|
|
14500
14657
|
}
|
|
14501
14658
|
if (r === false) return false;
|
|
@@ -14507,7 +14664,7 @@ var SecondChannel = class {
|
|
|
14507
14664
|
this.o.show(text, kind);
|
|
14508
14665
|
const p = Promise.resolve(r).then(() => {
|
|
14509
14666
|
}, (e) => {
|
|
14510
|
-
|
|
14667
|
+
log32.warn(`channel line playback failed: ${e?.message ?? e}`);
|
|
14511
14668
|
}).finally(() => {
|
|
14512
14669
|
if (this.inflight === p) this.inflight = null;
|
|
14513
14670
|
});
|
|
@@ -14543,10 +14700,12 @@ var KIND_HINT2 = {
|
|
|
14543
14700
|
filler: 'a soft listener sound while THEY search for words ("mm\u2026", "mhm", "yeah\u2026") \u2014 never a word that continues their sentence or starts yours',
|
|
14544
14701
|
bridge: 'a neutral one-moment cue while the answer is prepared ("let me see\u2026", "one sec\u2026") \u2014 never the start of a statement or opinion ("the thing is", "well actually")'
|
|
14545
14702
|
};
|
|
14703
|
+
var REACT_HINT = 'a short reaction to their LAST words instead: feeling ("oh no", "ha, nice", "ugh"), surprise ("wait, really?", "huh"), a light laugh if they joke or laugh ("ha!"), or an echo of their key word ("the deploy?") \u2014 still never asks for an answer or takes the turn; if nothing there deserves a reaction, a plain "mm-hm"';
|
|
14546
14704
|
function fastLineGenerator(ai, model) {
|
|
14547
14705
|
return async (c, signal) => {
|
|
14548
14706
|
const prompt = `You voice the tiny side-sounds of a live spoken conversation. Write ONE line of 1-5 words: ${KIND_HINT2[c.kind]}.
|
|
14549
|
-
|
|
14707
|
+
` + (c.react ? `Here the line may be ${REACT_HINT}.
|
|
14708
|
+
` : "") + `The user just said: "${c.heard}"
|
|
14550
14709
|
The assistant is: ${c.state}.
|
|
14551
14710
|
` + (c.avoid.length ? `Do not reuse any of: ${c.avoid.map((a) => `"${a}"`).join(", ")}.
|
|
14552
14711
|
` : "") + `React to their actual words, casually, in ${languageName(c.lang)}. Output only the line.`;
|
|
@@ -14702,7 +14861,7 @@ function applyOverlay(cfg, file) {
|
|
|
14702
14861
|
// cli/hooks-config.ts
|
|
14703
14862
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
14704
14863
|
init_platform();
|
|
14705
|
-
var
|
|
14864
|
+
var log33 = forComponent("hooks");
|
|
14706
14865
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
14707
14866
|
function ruleMatches(rule, toolName) {
|
|
14708
14867
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -14721,7 +14880,7 @@ function runCmd(rule, env) {
|
|
|
14721
14880
|
});
|
|
14722
14881
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
14723
14882
|
} catch (e) {
|
|
14724
|
-
|
|
14883
|
+
log33.debug(`hook command failed: ${rule.command}`, e);
|
|
14725
14884
|
return { code: 1, out: String(e?.message ?? e) };
|
|
14726
14885
|
}
|
|
14727
14886
|
}
|
|
@@ -14829,7 +14988,7 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as rea
|
|
|
14829
14988
|
import { homedir as homedir6 } from "os";
|
|
14830
14989
|
import { join as join11 } from "path";
|
|
14831
14990
|
init_platform();
|
|
14832
|
-
var
|
|
14991
|
+
var log34 = forComponent("session");
|
|
14833
14992
|
var globalDir = () => join11(homedir6(), ".agent", "sessions");
|
|
14834
14993
|
function indexLink(target, link2) {
|
|
14835
14994
|
try {
|
|
@@ -14892,7 +15051,7 @@ var SessionStore = class {
|
|
|
14892
15051
|
}
|
|
14893
15052
|
load(id) {
|
|
14894
15053
|
if (!this.safeId(id)) {
|
|
14895
|
-
|
|
15054
|
+
log34.debug(`rejecting unsafe session id: ${id}`);
|
|
14896
15055
|
return void 0;
|
|
14897
15056
|
}
|
|
14898
15057
|
const path = join11(this.dir, `${id}.json`);
|
|
@@ -14900,7 +15059,7 @@ var SessionStore = class {
|
|
|
14900
15059
|
try {
|
|
14901
15060
|
return JSON.parse(readFileSync8(path, "utf8"));
|
|
14902
15061
|
} catch (e) {
|
|
14903
|
-
|
|
15062
|
+
log34.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
14904
15063
|
return void 0;
|
|
14905
15064
|
}
|
|
14906
15065
|
}
|
|
@@ -14913,7 +15072,7 @@ var SessionStore = class {
|
|
|
14913
15072
|
try {
|
|
14914
15073
|
metas.push(JSON.parse(readFileSync8(join11(this.dir, f), "utf8")).meta);
|
|
14915
15074
|
} catch (e) {
|
|
14916
|
-
|
|
15075
|
+
log34.debug(`skipping unreadable session file ${f}`, e);
|
|
14917
15076
|
}
|
|
14918
15077
|
}
|
|
14919
15078
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -15152,7 +15311,7 @@ import { promisify } from "util";
|
|
|
15152
15311
|
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync9 } from "fs";
|
|
15153
15312
|
import { homedir as homedir7 } from "os";
|
|
15154
15313
|
import { join as join12, resolve as resolve4, sep as sep3, parse as parsePath } from "path";
|
|
15155
|
-
var
|
|
15314
|
+
var log35 = forComponent("checkpoints");
|
|
15156
15315
|
var exec = promisify(execFile4);
|
|
15157
15316
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
15158
15317
|
function isBroadRoot(p) {
|
|
@@ -15198,7 +15357,7 @@ var ShadowRepo = class {
|
|
|
15198
15357
|
writeFileSync6(join12(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
15199
15358
|
this.ready = true;
|
|
15200
15359
|
} catch (e) {
|
|
15201
|
-
|
|
15360
|
+
log35.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
15202
15361
|
this.ready = false;
|
|
15203
15362
|
}
|
|
15204
15363
|
return this.ready;
|
|
@@ -15209,7 +15368,7 @@ var ShadowRepo = class {
|
|
|
15209
15368
|
}
|
|
15210
15369
|
async commit(label, forced = []) {
|
|
15211
15370
|
await this.run("add", "-A");
|
|
15212
|
-
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) =>
|
|
15371
|
+
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log35.debug(`force-add failed: ${p}`, e));
|
|
15213
15372
|
await this.run("commit", "--allow-empty", "-q", "-m", label);
|
|
15214
15373
|
}
|
|
15215
15374
|
/** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
|
|
@@ -15217,8 +15376,8 @@ var ShadowRepo = class {
|
|
|
15217
15376
|
* turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
|
|
15218
15377
|
* point per turn, so the REPL's turn↔frame mapping stays intact. */
|
|
15219
15378
|
async amendForced(paths) {
|
|
15220
|
-
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) =>
|
|
15221
|
-
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) =>
|
|
15379
|
+
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log35.debug(`force-capture failed: ${p}`, e));
|
|
15380
|
+
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log35.debug("amend failed", e));
|
|
15222
15381
|
}
|
|
15223
15382
|
/** Commits on `ref`, oldest-first (canonical index space). */
|
|
15224
15383
|
async log(ref) {
|
|
@@ -15278,7 +15437,7 @@ var ShadowRepo = class {
|
|
|
15278
15437
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
15279
15438
|
});
|
|
15280
15439
|
} catch (e) {
|
|
15281
|
-
|
|
15440
|
+
log35.debug("checkpoint prune failed", e);
|
|
15282
15441
|
}
|
|
15283
15442
|
}
|
|
15284
15443
|
};
|
|
@@ -15346,7 +15505,7 @@ var GitCheckpoints = class {
|
|
|
15346
15505
|
use(sessionId) {
|
|
15347
15506
|
if (sessionId === this.session) return;
|
|
15348
15507
|
this.session = sessionId;
|
|
15349
|
-
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) =>
|
|
15508
|
+
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log35.debug("re-point failed", e));
|
|
15350
15509
|
}
|
|
15351
15510
|
async begin(label) {
|
|
15352
15511
|
if (!await this.start()) return;
|
|
@@ -15366,7 +15525,7 @@ var GitCheckpoints = class {
|
|
|
15366
15525
|
process.stderr.write(`\x1B[2m checkpoints: '${this.repos[i].root}' too large to snapshot in ${Math.round(this.options.opTimeoutMs / 1e3)}s \u2014 file rewind disabled here\x1B[0m
|
|
15367
15526
|
`);
|
|
15368
15527
|
} else {
|
|
15369
|
-
|
|
15528
|
+
log35.debug("checkpoint commit failed", e);
|
|
15370
15529
|
survivors.push(this.repos[i]);
|
|
15371
15530
|
survivorCaches.push(this.caches[i] ?? []);
|
|
15372
15531
|
}
|
|
@@ -15401,7 +15560,7 @@ var GitCheckpoints = class {
|
|
|
15401
15560
|
if (this.forced.has(abs)) continue;
|
|
15402
15561
|
this.forced.add(abs);
|
|
15403
15562
|
const i = this.repoIndexFor(abs);
|
|
15404
|
-
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) =>
|
|
15563
|
+
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log35.debug("amendForced failed", e));
|
|
15405
15564
|
}
|
|
15406
15565
|
}
|
|
15407
15566
|
};
|
|
@@ -17186,7 +17345,7 @@ import { spawnSync as spawnSync6 } from "child_process";
|
|
|
17186
17345
|
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
|
|
17187
17346
|
import { homedir as homedir8 } from "os";
|
|
17188
17347
|
import { join as join14 } from "path";
|
|
17189
|
-
var
|
|
17348
|
+
var log36 = forComponent("os-sched");
|
|
17190
17349
|
var OsScheduler = class {
|
|
17191
17350
|
options;
|
|
17192
17351
|
constructor(options) {
|
|
@@ -17248,7 +17407,7 @@ var OsScheduler = class {
|
|
|
17248
17407
|
}
|
|
17249
17408
|
}
|
|
17250
17409
|
} catch (e) {
|
|
17251
|
-
|
|
17410
|
+
log36.debug(`cancel ${id}`, e);
|
|
17252
17411
|
}
|
|
17253
17412
|
for (const f of [`${id}.json`, `${id}.sh`]) {
|
|
17254
17413
|
try {
|
|
@@ -17366,7 +17525,7 @@ import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unli
|
|
|
17366
17525
|
import { homedir as homedir9 } from "os";
|
|
17367
17526
|
import { join as join15 } from "path";
|
|
17368
17527
|
init_platform();
|
|
17369
|
-
var
|
|
17528
|
+
var log37 = forComponent("remote-trigger");
|
|
17370
17529
|
var TRIGGER_DIR = () => join15(homedir9(), ".agent", "triggers");
|
|
17371
17530
|
var sockPath = (sessionId, dir = TRIGGER_DIR()) => join15(dir, `${sessionId}.sock`);
|
|
17372
17531
|
var TriggerServer = class {
|
|
@@ -17404,13 +17563,13 @@ var TriggerServer = class {
|
|
|
17404
17563
|
conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
|
|
17405
17564
|
}
|
|
17406
17565
|
});
|
|
17407
|
-
conn.on("error", (e) =>
|
|
17566
|
+
conn.on("error", (e) => log37.debug("trigger conn error", e));
|
|
17408
17567
|
});
|
|
17409
|
-
this.server.on("error", (e) =>
|
|
17568
|
+
this.server.on("error", (e) => log37.debug("trigger server error", e));
|
|
17410
17569
|
this.server.listen(p);
|
|
17411
17570
|
this.path = p;
|
|
17412
17571
|
} catch (e) {
|
|
17413
|
-
|
|
17572
|
+
log37.debug("trigger server unavailable", e);
|
|
17414
17573
|
}
|
|
17415
17574
|
}
|
|
17416
17575
|
/** Re-bind on /resume (the session id changed). */
|
|
@@ -17542,7 +17701,7 @@ var strike = C("9");
|
|
|
17542
17701
|
var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
|
|
17543
17702
|
var mdPalette = { bold, dim, cyan, italic, strike, green, link };
|
|
17544
17703
|
var err = (s) => process.stderr.write(s);
|
|
17545
|
-
var
|
|
17704
|
+
var log38 = forComponent("cli");
|
|
17546
17705
|
var VERSION = (() => {
|
|
17547
17706
|
try {
|
|
17548
17707
|
return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -17687,6 +17846,7 @@ function parseArgs(argv) {
|
|
|
17687
17846
|
a.duplex = true;
|
|
17688
17847
|
a.duplexV2 = true;
|
|
17689
17848
|
} else if (x === "--channel-only") a.channelOnly = true;
|
|
17849
|
+
else if (x === "--channel-mode") a.channelMode = parseChannelMode(val(++i, x));
|
|
17690
17850
|
else if (x === "--conversational" || x === "--convo" || x === "--voice") {
|
|
17691
17851
|
a.voice = true;
|
|
17692
17852
|
a.duplex = true;
|
|
@@ -17730,6 +17890,8 @@ function parseArgs(argv) {
|
|
|
17730
17890
|
if (a.channelOnly && !a.inputFormat) throw new Error("--channel-only needs --input-format stream-json --output-format stream-json");
|
|
17731
17891
|
if (a.duplex && a.plan) throw new Error("--plan is not supported in --duplex (workers are non-interactive; a plan could never be approved)");
|
|
17732
17892
|
if (a.voiceSystemPrompt && !a.duplex) throw new Error("--voice-system-prompt only applies with --duplex");
|
|
17893
|
+
if (a.channelMode && !a.duplexV2 && !a.channelOnly) throw new Error("--channel-mode only applies with --duplex-v2 or --channel-only");
|
|
17894
|
+
a.channelMode ??= parseChannelMode(process.env.AGENTX_CHANNEL_MODE);
|
|
17733
17895
|
if (a.voiceModel && !a.duplex && !a.channelOnly) throw new Error("--voice-model only applies with --duplex or --channel-only");
|
|
17734
17896
|
if (a.thinkModel !== void 0 && !a.duplex) throw new Error("--think-model/--no-think only apply with --duplex");
|
|
17735
17897
|
if (!canPrompt && (a.ask || a.plan))
|
|
@@ -17776,6 +17938,9 @@ Flags:
|
|
|
17776
17938
|
enter the agent's history (headless: stdin interim/pause/agent_state, stdout channel)
|
|
17777
17939
|
--channel-only headless (--input-format stream-json): ONLY the second channel, for a host whose main
|
|
17778
17940
|
answer comes from elsewhere (stdin user/text/barge/interim/pause/agent_state, stdout channel)
|
|
17941
|
+
--channel-mode <m> second-channel talkativeness: turn (only a bridge when the answer is slow) | normal
|
|
17942
|
+
(default) | chatty (frequent cues + content reactions; needs echo cancellation)
|
|
17943
|
+
env fallback: AGENTX_CHANNEL_MODE
|
|
17779
17944
|
--conversational duplex with a conversation-native register \u2014 short fast turns, fillers,
|
|
17780
17945
|
impulsive reactions, human pacing (implies --duplex; aliases: --convo, --voice)
|
|
17781
17946
|
with SONIOX_API_KEY + CARTESIA_API_KEY(+VOICE_ID) set: real voice I/O \u2014 mic in,
|
|
@@ -18385,7 +18550,7 @@ function turnCost(model, usage) {
|
|
|
18385
18550
|
const priced = model.startsWith("claude-code/") ? `anthropic/${resolveModelAlias(model.slice("claude-code/".length))}` : model;
|
|
18386
18551
|
return costOf(getModelInfo(priced)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, priced);
|
|
18387
18552
|
}
|
|
18388
|
-
async function evaluateGoal(ai, condition, transcript,
|
|
18553
|
+
async function evaluateGoal(ai, condition, transcript, log39) {
|
|
18389
18554
|
const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
|
|
18390
18555
|
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
18391
18556
|
return text.slice(0, 600);
|
|
@@ -18405,7 +18570,7 @@ ${recent}` }
|
|
|
18405
18570
|
const match = r.content.match(/\{[\s\S]*\}/);
|
|
18406
18571
|
if (match) return JSON.parse(match[0]);
|
|
18407
18572
|
} catch (e) {
|
|
18408
|
-
|
|
18573
|
+
log39(dim(` (goal evaluator error: ${e?.message ?? e})
|
|
18409
18574
|
`));
|
|
18410
18575
|
}
|
|
18411
18576
|
return { met: false, reason: "evaluation unclear" };
|
|
@@ -18631,7 +18796,7 @@ function mcpAgentTools(mounted, opts) {
|
|
|
18631
18796
|
return tools;
|
|
18632
18797
|
}
|
|
18633
18798
|
async function closeMcp(mounted) {
|
|
18634
|
-
await Promise.all(mounted.map((m) => m.client.close().catch((e) =>
|
|
18799
|
+
await Promise.all(mounted.map((m) => m.client.close().catch((e) => log38.debug("mcp close failed", e))));
|
|
18635
18800
|
}
|
|
18636
18801
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
18637
18802
|
function mentionRefs(line) {
|
|
@@ -18690,7 +18855,7 @@ async function expandMentions(fs, line) {
|
|
|
18690
18855
|
if (loaded.includes(ref) || missing.includes(ref)) continue;
|
|
18691
18856
|
if (ref.includes(":") && mcpMentionResolver) {
|
|
18692
18857
|
const body = await mcpMentionResolver(ref).catch((e) => {
|
|
18693
|
-
|
|
18858
|
+
log38.debug("mcp mention resolve failed", e);
|
|
18694
18859
|
return null;
|
|
18695
18860
|
});
|
|
18696
18861
|
if (body != null) {
|
|
@@ -18781,7 +18946,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
18781
18946
|
try {
|
|
18782
18947
|
store.save(session);
|
|
18783
18948
|
} catch (ex) {
|
|
18784
|
-
|
|
18949
|
+
log38.debug("mid-turn session flush failed", ex);
|
|
18785
18950
|
}
|
|
18786
18951
|
}
|
|
18787
18952
|
return origNotify(e);
|
|
@@ -18945,14 +19110,14 @@ var isCancelTeardown = (e) => {
|
|
|
18945
19110
|
function installCancelGuards(mounted) {
|
|
18946
19111
|
process.on("unhandledRejection", (e) => {
|
|
18947
19112
|
if (isCancelTeardown(e)) {
|
|
18948
|
-
|
|
19113
|
+
log38.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
18949
19114
|
return;
|
|
18950
19115
|
}
|
|
18951
|
-
|
|
19116
|
+
log38.error("unhandledRejection", e);
|
|
18952
19117
|
});
|
|
18953
19118
|
process.on("uncaughtException", (e) => {
|
|
18954
19119
|
if (isCancelTeardown(e)) {
|
|
18955
|
-
|
|
19120
|
+
log38.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
18956
19121
|
return;
|
|
18957
19122
|
}
|
|
18958
19123
|
console.error(e);
|
|
@@ -19016,6 +19181,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
19016
19181
|
const decider = duplex ? leakDecider() : void 0;
|
|
19017
19182
|
const instinct = decider && process.env.VOICE_INSTINCT === "1" ? new Instinct({
|
|
19018
19183
|
decider,
|
|
19184
|
+
overlapModel: overlapModelFor(ai),
|
|
19019
19185
|
context: () => ({ agentSaid: dx?.recentExcerpt(1).replace(/^\w+: /, ""), recent: dx?.recentExcerpt(4) }),
|
|
19020
19186
|
onDecision: ({ kind, ...d }) => {
|
|
19021
19187
|
voiceDiag?.push({ t: performance.now(), kind: `instinct_${kind}`, ...d });
|
|
@@ -19381,7 +19547,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
19381
19547
|
try {
|
|
19382
19548
|
mkdirSync12(dir, { recursive: true });
|
|
19383
19549
|
} catch (e) {
|
|
19384
|
-
|
|
19550
|
+
log38.debug("paste image dir create failed", e);
|
|
19385
19551
|
}
|
|
19386
19552
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
19387
19553
|
return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
|
|
@@ -19494,7 +19660,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
19494
19660
|
mkdirSync12(join16(cwd, ".agent"), { recursive: true });
|
|
19495
19661
|
appendFileSync(histPath, line + "\n");
|
|
19496
19662
|
} catch (e) {
|
|
19497
|
-
|
|
19663
|
+
log38.debug("history write failed", e);
|
|
19498
19664
|
}
|
|
19499
19665
|
};
|
|
19500
19666
|
const ago = (t) => {
|
|
@@ -19566,7 +19732,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
19566
19732
|
try {
|
|
19567
19733
|
store.save(session);
|
|
19568
19734
|
} catch (e) {
|
|
19569
|
-
|
|
19735
|
+
log38.debug("session save after rewind failed", e);
|
|
19570
19736
|
}
|
|
19571
19737
|
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
19572
19738
|
`));
|
|
@@ -19601,7 +19767,7 @@ ${task}`;
|
|
|
19601
19767
|
bangContext.length = 0;
|
|
19602
19768
|
}
|
|
19603
19769
|
const delta = await refreshCatalogs().catch((e) => {
|
|
19604
|
-
|
|
19770
|
+
log38.debug("catalog refresh failed", e);
|
|
19605
19771
|
return "";
|
|
19606
19772
|
});
|
|
19607
19773
|
if (delta) {
|
|
@@ -19864,7 +20030,7 @@ ${task}`;
|
|
|
19864
20030
|
desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
|
|
19865
20031
|
run: async () => {
|
|
19866
20032
|
await refreshCatalogs().catch((e) => {
|
|
19867
|
-
|
|
20033
|
+
log38.debug("catalog refresh failed", e);
|
|
19868
20034
|
});
|
|
19869
20035
|
face.reprepare();
|
|
19870
20036
|
err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
|
|
@@ -20367,7 +20533,7 @@ ${task}`;
|
|
|
20367
20533
|
try {
|
|
20368
20534
|
for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
|
|
20369
20535
|
} catch (e) {
|
|
20370
|
-
|
|
20536
|
+
log38.debug(`loadAgents(${d}) failed`, e);
|
|
20371
20537
|
}
|
|
20372
20538
|
}
|
|
20373
20539
|
if (!seen.size) {
|
|
@@ -20455,7 +20621,7 @@ ${task}`;
|
|
|
20455
20621
|
}
|
|
20456
20622
|
if (idx >= 0) {
|
|
20457
20623
|
const old = mounted.splice(idx, 1)[0];
|
|
20458
|
-
await old.client.close().catch((e) =>
|
|
20624
|
+
await old.client.close().catch((e) => log38.debug("mcp close failed", e));
|
|
20459
20625
|
}
|
|
20460
20626
|
try {
|
|
20461
20627
|
const m = await mountMcpServer(name, conf);
|
|
@@ -20483,7 +20649,7 @@ ${task}`;
|
|
|
20483
20649
|
}
|
|
20484
20650
|
const m = mounted.splice(idx, 1)[0];
|
|
20485
20651
|
remountMcpTools();
|
|
20486
|
-
await m.client.close().catch((e) =>
|
|
20652
|
+
await m.client.close().catch((e) => log38.debug("mcp close failed", e));
|
|
20487
20653
|
err(dim(` removed "${name}"
|
|
20488
20654
|
`));
|
|
20489
20655
|
return;
|
|
@@ -20686,7 +20852,7 @@ ${task}`;
|
|
|
20686
20852
|
try {
|
|
20687
20853
|
return readdirSync4(join16(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
20688
20854
|
} catch (e) {
|
|
20689
|
-
|
|
20855
|
+
log38.debug("completion readdir failed", absDir, e);
|
|
20690
20856
|
return null;
|
|
20691
20857
|
}
|
|
20692
20858
|
};
|
|
@@ -20823,7 +20989,7 @@ ${out}
|
|
|
20823
20989
|
return;
|
|
20824
20990
|
}
|
|
20825
20991
|
await refreshCatalogs().catch((e) => {
|
|
20826
|
-
|
|
20992
|
+
log38.debug("catalog refresh failed", e);
|
|
20827
20993
|
});
|
|
20828
20994
|
const sk = skills.find((x) => x.name === name);
|
|
20829
20995
|
if (sk) {
|
|
@@ -20942,7 +21108,7 @@ ${out}
|
|
|
20942
21108
|
if (delivery || cut) dx?.parkCutDelivery(delivery, !!cut, heard);
|
|
20943
21109
|
const cutReply = cut && cut.full.length - cut.heard.length > 40 ? cut : null;
|
|
20944
21110
|
const noteP = replaces ? replaces.note : !cutReply ? Promise.resolve("") : (instinct ? instinct.bargeIntent(text).catch((e) => {
|
|
20945
|
-
|
|
21111
|
+
log38.warn(`barge intent failed \u2014 model decides: ${e.message}`);
|
|
20946
21112
|
return void 0;
|
|
20947
21113
|
}) : Promise.resolve(void 0)).then(async (intent) => interruptionNote(cutReply, intent) + ackedNote(await instinct?.ackFor(text)));
|
|
20948
21114
|
const cutIn = replaces ? replaces.cutIn : interjectNote;
|
|
@@ -20962,6 +21128,10 @@ ${out}
|
|
|
20962
21128
|
}).finally(() => editorRef?.redrawNow());
|
|
20963
21129
|
}
|
|
20964
21130
|
});
|
|
21131
|
+
if (!instinct && decider && args.duplexV2 && softBarge(args.channelMode)) {
|
|
21132
|
+
const v = voiceIO;
|
|
21133
|
+
bindOverlapInstinct(v, new Instinct({ decider, overlapModel: overlapModelFor(ai) }), () => voiceIO === v);
|
|
21134
|
+
}
|
|
20965
21135
|
if (instinct) {
|
|
20966
21136
|
const v = voiceIO, o = v.options;
|
|
20967
21137
|
const { onPartial } = o;
|
|
@@ -20972,7 +21142,7 @@ ${out}
|
|
|
20972
21142
|
if (listening()) instinct.hear(text);
|
|
20973
21143
|
onPartial(text);
|
|
20974
21144
|
};
|
|
20975
|
-
bindOverlapInstinct(v, instinct, () => voiceIO === v);
|
|
21145
|
+
if (!args.duplexV2 || softBarge(args.channelMode)) bindOverlapInstinct(v, instinct, () => voiceIO === v);
|
|
20976
21146
|
bindAckInstinct(v, instinct);
|
|
20977
21147
|
o.onUtterance = (text, meta) => {
|
|
20978
21148
|
instinct.reset();
|
|
@@ -21040,7 +21210,7 @@ ${out}
|
|
|
21040
21210
|
if (dx) dx.options.holdResumeMs = HOLD_RESUME_MS;
|
|
21041
21211
|
if (args.duplexV2 && dx) {
|
|
21042
21212
|
const v = voiceIO;
|
|
21043
|
-
bindSecondChannel(v, makeSecondChannel(ai, dx.options.reflexModel, { show: (t) => {
|
|
21213
|
+
bindSecondChannel(v, makeSecondChannel(ai, dx.options.reflexModel, args.channelMode, { show: (t) => {
|
|
21044
21214
|
err("\r\x1B[0J" + dim(` \u21B3 ${t}
|
|
21045
21215
|
`));
|
|
21046
21216
|
editorRef?.redrawNow();
|
|
@@ -21297,7 +21467,7 @@ function duplexError(e, reflexModel) {
|
|
|
21297
21467
|
}
|
|
21298
21468
|
var HOLD_RESUME_MS = 4e3;
|
|
21299
21469
|
var leakDecider = () => voiceDecider();
|
|
21300
|
-
function makeSecondChannel(ai, reflexModel, opts) {
|
|
21470
|
+
function makeSecondChannel(ai, reflexModel, mode = "normal", opts) {
|
|
21301
21471
|
const decider = leakDecider();
|
|
21302
21472
|
const instinct = decider ? new Instinct({ decider }) : void 0;
|
|
21303
21473
|
const model = process.env.AGENTX_CHANNEL_MODEL || reflexModel;
|
|
@@ -21307,12 +21477,21 @@ function makeSecondChannel(ai, reflexModel, opts) {
|
|
|
21307
21477
|
banks: new ChannelBanks({ generate: fastBankGenerator(ai, model), cacheDir: join16(homedir10(), ".agent", "cache", "channel-banks") }),
|
|
21308
21478
|
decide: channelDecide(ai, instinct),
|
|
21309
21479
|
generate: fastLineGenerator(ai, model),
|
|
21310
|
-
|
|
21311
|
-
|
|
21312
|
-
|
|
21480
|
+
mode,
|
|
21481
|
+
// Anticipated backchannels: on in chatty mode (its preset), else off until the offline replay proves them (tests/voice-bc-anticipate-eval.ts)
|
|
21482
|
+
plan: (c, s) => planner.plan(c, s),
|
|
21483
|
+
...process.env.AGENTX_CHANNEL_ANTICIPATE === "1" ? { anticipate: true } : {},
|
|
21484
|
+
onEvent: (e) => log38.debug(`channel ${e.trigger}: rule=${e.rule} choice=${e.choice ?? "-"} \u2192 ${e.acted}${e.text ? ` "${e.text}"` : ""}${e.cue ? " (planned cue)" : ""} ${e.ms}ms`),
|
|
21313
21485
|
...opts
|
|
21314
21486
|
});
|
|
21315
21487
|
}
|
|
21488
|
+
function overlapResponder(ai, mode, out) {
|
|
21489
|
+
const judge = softBarge(mode) ? new OverlapJudge({ model: overlapModelFor(ai) }) : void 0;
|
|
21490
|
+
return (m) => {
|
|
21491
|
+
const text = String(m.text ?? "");
|
|
21492
|
+
void (judge ? judge.judge(text) : Promise.resolve({ verdict: "cut", by: "mode", ms: 0 })).then((d) => out({ type: "overlap", ...m.id !== void 0 ? { id: m.id } : {}, verdict: d.verdict, by: d.by, ms: d.ms }));
|
|
21493
|
+
};
|
|
21494
|
+
}
|
|
21316
21495
|
async function duplexServe(args, ai, cfg, cwd) {
|
|
21317
21496
|
const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
|
|
21318
21497
|
const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join16(cwd, ".agent", "mcp-auth.json") }));
|
|
@@ -21362,7 +21541,7 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
21362
21541
|
});
|
|
21363
21542
|
if (args.voiceSystemPrompt) dx.voice.options.systemPrompt += "\n\n" + args.voiceSystemPrompt;
|
|
21364
21543
|
let scTurn = 0;
|
|
21365
|
-
const sc = args.duplexV2 ? makeSecondChannel(ai, dx.options.reflexModel, { show: (text, kind) => out({ type: "channel", kind, text }) }) : void 0;
|
|
21544
|
+
const sc = args.duplexV2 ? makeSecondChannel(ai, dx.options.reflexModel, args.channelMode, { show: (text, kind) => out({ type: "channel", kind, text }) }) : void 0;
|
|
21366
21545
|
const store = new SessionStore(cwd);
|
|
21367
21546
|
const session = startSession(args, store, dx.voice, cwd);
|
|
21368
21547
|
persist = () => {
|
|
@@ -21371,10 +21550,11 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
21371
21550
|
try {
|
|
21372
21551
|
store.save(session);
|
|
21373
21552
|
} catch (e) {
|
|
21374
|
-
|
|
21553
|
+
log38.warn("duplex session save failed", e);
|
|
21375
21554
|
}
|
|
21376
21555
|
};
|
|
21377
|
-
|
|
21556
|
+
const overlap2 = overlapResponder(ai, args.duplexV2 ? args.channelMode : void 0, out);
|
|
21557
|
+
out({ type: "ready", sessionId: session.meta.id, reflexModel: dx.options.reflexModel, actModel: dx.options.actModel, softBarge: softBarge(args.duplexV2 ? args.channelMode : void 0), ...notes.length ? { notes } : {} });
|
|
21378
21558
|
const done = new Promise((resolveDone) => {
|
|
21379
21559
|
const rl = createInterface({ input: process.stdin });
|
|
21380
21560
|
rl.on("line", (line) => {
|
|
@@ -21389,7 +21569,8 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
21389
21569
|
dx.userActive();
|
|
21390
21570
|
activeTurn?.abort();
|
|
21391
21571
|
dx.parkInFlightDeliveries();
|
|
21392
|
-
} else if (
|
|
21572
|
+
} else if (m.type === "overlap") overlap2(m);
|
|
21573
|
+
else if (sc && channelInput(sc, m)) {
|
|
21393
21574
|
if (m.type === "interim" && String(m.text ?? "").trim()) dx.userActive();
|
|
21394
21575
|
} else if (m.type === "cancel") out({ type: "task", kind: "cancel_result", id: m.task, message: dx.cancelTask(String(m.task)) });
|
|
21395
21576
|
else if (m.type === "exit") rl.close();
|
|
@@ -21409,7 +21590,7 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
21409
21590
|
done2();
|
|
21410
21591
|
out({ type: "turn_done", id, finishReason: res?.finishReason ?? "error", ...why ? { error: why } : {} });
|
|
21411
21592
|
}).catch((e) => {
|
|
21412
|
-
|
|
21593
|
+
log38.error("duplex turn failed", e);
|
|
21413
21594
|
done2();
|
|
21414
21595
|
out({ type: "turn_done", id, finishReason: "error", error: String(e?.message ?? e) });
|
|
21415
21596
|
});
|
|
@@ -21427,8 +21608,9 @@ async function channelServe(args, ai, cfg) {
|
|
|
21427
21608
|
const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
|
|
21428
21609
|
const { reflexModel, notes } = duplexModels({ voiceModel: args.voiceModel, reflexModel: cfg.reflexModel, thinkModel: false, actModel: String(args.model ?? cfg.model ?? ""), apiKeys: { ...cfg.apiKeys, ...apiKeysFromEnv() } });
|
|
21429
21610
|
const model = reflexModel ?? new DuplexAgentOptions().reflexModel;
|
|
21430
|
-
const sc = makeSecondChannel(ai, model, { show: (text, kind) => out({ type: "channel", kind, text }) });
|
|
21431
|
-
|
|
21611
|
+
const sc = makeSecondChannel(ai, model, args.channelMode, { show: (text, kind) => out({ type: "channel", kind, text }) });
|
|
21612
|
+
const overlap2 = overlapResponder(ai, args.channelMode, out);
|
|
21613
|
+
out({ type: "ready", model, channelMode: args.channelMode, softBarge: softBarge(args.channelMode), ...notes.length ? { notes } : {} });
|
|
21432
21614
|
const rl = createInterface({ input: process.stdin });
|
|
21433
21615
|
rl.on("line", (line) => {
|
|
21434
21616
|
let m;
|
|
@@ -21441,6 +21623,7 @@ async function channelServe(args, ai, cfg) {
|
|
|
21441
21623
|
if (m.type === "user" && typeof m.text === "string") sc.onUserTurn(m.text);
|
|
21442
21624
|
else if (m.type === "text" && typeof m.text === "string") sc.onMainTokens(m.text);
|
|
21443
21625
|
else if (m.type === "barge") sc.onBargeIn();
|
|
21626
|
+
else if (m.type === "overlap") overlap2(m);
|
|
21444
21627
|
else if (m.type === "exit") rl.close();
|
|
21445
21628
|
});
|
|
21446
21629
|
await new Promise((r) => rl.on("close", () => r()));
|