@livx.cc/agentx 0.99.101 → 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.js +302 -172
- 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,13 +14178,13 @@ 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");
|
|
14074
14188
|
var CHANNEL_MODES = {
|
|
14075
14189
|
turn: { kinds: ["bridge"], bridgeDelayMs: 1500 },
|
|
14076
14190
|
normal: {},
|
|
@@ -14180,13 +14294,13 @@ function withBudget(p, ms, what, ac) {
|
|
|
14180
14294
|
let t;
|
|
14181
14295
|
const late = new Promise((r) => {
|
|
14182
14296
|
t = setTimeout(() => {
|
|
14183
|
-
|
|
14297
|
+
log32.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
|
|
14184
14298
|
ac?.abort();
|
|
14185
14299
|
r(null);
|
|
14186
14300
|
}, ms);
|
|
14187
14301
|
});
|
|
14188
14302
|
return Promise.race([p.catch((e) => {
|
|
14189
|
-
|
|
14303
|
+
log32.warn(`${what} failed \u2014 falling back: ${e?.message ?? e}`);
|
|
14190
14304
|
return null;
|
|
14191
14305
|
}), late]).finally(() => clearTimeout(t));
|
|
14192
14306
|
}
|
|
@@ -14258,7 +14372,7 @@ var SecondChannel = class {
|
|
|
14258
14372
|
if (host) this.pinned = true;
|
|
14259
14373
|
const lang = primaryLang(tag);
|
|
14260
14374
|
if (!lang || lang === this.lang) return;
|
|
14261
|
-
|
|
14375
|
+
log32.debug(`language ${this.lang} \u2192 ${lang}`);
|
|
14262
14376
|
this.lang = lang;
|
|
14263
14377
|
this.recent = [];
|
|
14264
14378
|
this.picks.clear();
|
|
@@ -14279,7 +14393,7 @@ var SecondChannel = class {
|
|
|
14279
14393
|
if (o.mode !== "chatty" || o.aec) return o;
|
|
14280
14394
|
if (!this.degradedLogged) {
|
|
14281
14395
|
this.degradedLogged = true;
|
|
14282
|
-
|
|
14396
|
+
log32.info("chatty mode without echo cancellation \u2014 normal timing (our lines would re-trigger the mic)");
|
|
14283
14397
|
}
|
|
14284
14398
|
return { ...o, ...this.normalTiming };
|
|
14285
14399
|
}
|
|
@@ -14394,7 +14508,7 @@ var SecondChannel = class {
|
|
|
14394
14508
|
try {
|
|
14395
14509
|
this.o.cancel();
|
|
14396
14510
|
} catch (e) {
|
|
14397
|
-
|
|
14511
|
+
log32.warn(`cancel threw: ${e.message}`);
|
|
14398
14512
|
}
|
|
14399
14513
|
}
|
|
14400
14514
|
}
|
|
@@ -14479,19 +14593,19 @@ var SecondChannel = class {
|
|
|
14479
14593
|
const d = await withBudget(this.o.decide(this.tail(), { ...this.state(pauseMs), rule }, ac.signal), this.o.decisionBudgetMs, "channel decision", ac);
|
|
14480
14594
|
choice = d?.choice;
|
|
14481
14595
|
if (choice && choice in CHANNEL_ACTIONS && (choice === "silence" || this.allowed(choice))) kind = choice;
|
|
14482
|
-
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`);
|
|
14483
14597
|
}
|
|
14484
14598
|
if (kind === "silence") return ev({ choice, acted: "silence" });
|
|
14485
14599
|
if (stale()) return ev({ choice, kind, acted: "stale" });
|
|
14486
14600
|
if (!this.allowed(kind)) return ev({ choice, kind, acted: "gated" });
|
|
14487
14601
|
const react = kind === "backchannel" && this.o.now() - this.lastReactAt >= this.o.reactionGapMs;
|
|
14488
|
-
const text = cue && react && !this.recent.some((r) =>
|
|
14602
|
+
const text = cue && react && !this.recent.some((r) => norm3(r) === norm3(cue.line)) ? cue.line : await this.line(kind, react);
|
|
14489
14603
|
if (!text) return ev({ choice, kind, acted: "no_bank" });
|
|
14490
14604
|
if (stale()) return ev({ choice, kind, text, acted: "stale" });
|
|
14491
14605
|
if (!this.allowed(kind)) return ev({ choice, kind, text, acted: "gated" });
|
|
14492
14606
|
const said = this.say(kind, text);
|
|
14493
14607
|
if (said && cue) this.cueFrom = words2(this.heard).length;
|
|
14494
|
-
if (said && kind === "backchannel" && this.o.reactionGapMs && !this.bank?.backchannel.some((b) =>
|
|
14608
|
+
if (said && kind === "backchannel" && this.o.reactionGapMs && !this.bank?.backchannel.some((b) => norm3(b) === norm3(text))) this.lastReactAt = this.o.now();
|
|
14495
14609
|
ev({ choice, kind, text, acted: said ? "spoken" : "skipped" });
|
|
14496
14610
|
} finally {
|
|
14497
14611
|
this.busy = false;
|
|
@@ -14515,8 +14629,8 @@ var SecondChannel = class {
|
|
|
14515
14629
|
const raw = await withBudget(this.o.generate(ctx, ac.signal), this.o.generateBudgetMs, `${kind} generate`);
|
|
14516
14630
|
if (raw === null) ac.abort();
|
|
14517
14631
|
const text = cleanLine(raw ?? "", this.lang);
|
|
14518
|
-
if (text && !this.recent.some((r) =>
|
|
14519
|
-
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`);
|
|
14520
14634
|
}
|
|
14521
14635
|
return this.pick(kind);
|
|
14522
14636
|
}
|
|
@@ -14526,8 +14640,8 @@ var SecondChannel = class {
|
|
|
14526
14640
|
const pool = this.bank?.[kind] ?? [];
|
|
14527
14641
|
if (!pool.length) return "";
|
|
14528
14642
|
const hist = this.picks.get(kind) ?? [];
|
|
14529
|
-
const recent = new Set([...hist.slice(-this.o.bankExclude), ...this.recent].map(
|
|
14530
|
-
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)));
|
|
14531
14645
|
const from = fresh.length ? fresh : pool;
|
|
14532
14646
|
const p = from[Math.floor(this.o.random() * from.length)];
|
|
14533
14647
|
this.picks.set(kind, [...hist, p].slice(-this.o.bankExclude));
|
|
@@ -14538,7 +14652,7 @@ var SecondChannel = class {
|
|
|
14538
14652
|
try {
|
|
14539
14653
|
r = this.o.speak(text, kind);
|
|
14540
14654
|
} catch (e) {
|
|
14541
|
-
|
|
14655
|
+
log32.warn(`speak threw \u2014 line dropped: ${e.message}`);
|
|
14542
14656
|
return false;
|
|
14543
14657
|
}
|
|
14544
14658
|
if (r === false) return false;
|
|
@@ -14550,7 +14664,7 @@ var SecondChannel = class {
|
|
|
14550
14664
|
this.o.show(text, kind);
|
|
14551
14665
|
const p = Promise.resolve(r).then(() => {
|
|
14552
14666
|
}, (e) => {
|
|
14553
|
-
|
|
14667
|
+
log32.warn(`channel line playback failed: ${e?.message ?? e}`);
|
|
14554
14668
|
}).finally(() => {
|
|
14555
14669
|
if (this.inflight === p) this.inflight = null;
|
|
14556
14670
|
});
|
|
@@ -14747,7 +14861,7 @@ function applyOverlay(cfg, file) {
|
|
|
14747
14861
|
// cli/hooks-config.ts
|
|
14748
14862
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
14749
14863
|
init_platform();
|
|
14750
|
-
var
|
|
14864
|
+
var log33 = forComponent("hooks");
|
|
14751
14865
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
14752
14866
|
function ruleMatches(rule, toolName) {
|
|
14753
14867
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -14766,7 +14880,7 @@ function runCmd(rule, env) {
|
|
|
14766
14880
|
});
|
|
14767
14881
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
14768
14882
|
} catch (e) {
|
|
14769
|
-
|
|
14883
|
+
log33.debug(`hook command failed: ${rule.command}`, e);
|
|
14770
14884
|
return { code: 1, out: String(e?.message ?? e) };
|
|
14771
14885
|
}
|
|
14772
14886
|
}
|
|
@@ -14874,7 +14988,7 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as rea
|
|
|
14874
14988
|
import { homedir as homedir6 } from "os";
|
|
14875
14989
|
import { join as join11 } from "path";
|
|
14876
14990
|
init_platform();
|
|
14877
|
-
var
|
|
14991
|
+
var log34 = forComponent("session");
|
|
14878
14992
|
var globalDir = () => join11(homedir6(), ".agent", "sessions");
|
|
14879
14993
|
function indexLink(target, link2) {
|
|
14880
14994
|
try {
|
|
@@ -14937,7 +15051,7 @@ var SessionStore = class {
|
|
|
14937
15051
|
}
|
|
14938
15052
|
load(id) {
|
|
14939
15053
|
if (!this.safeId(id)) {
|
|
14940
|
-
|
|
15054
|
+
log34.debug(`rejecting unsafe session id: ${id}`);
|
|
14941
15055
|
return void 0;
|
|
14942
15056
|
}
|
|
14943
15057
|
const path = join11(this.dir, `${id}.json`);
|
|
@@ -14945,7 +15059,7 @@ var SessionStore = class {
|
|
|
14945
15059
|
try {
|
|
14946
15060
|
return JSON.parse(readFileSync8(path, "utf8"));
|
|
14947
15061
|
} catch (e) {
|
|
14948
|
-
|
|
15062
|
+
log34.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
14949
15063
|
return void 0;
|
|
14950
15064
|
}
|
|
14951
15065
|
}
|
|
@@ -14958,7 +15072,7 @@ var SessionStore = class {
|
|
|
14958
15072
|
try {
|
|
14959
15073
|
metas.push(JSON.parse(readFileSync8(join11(this.dir, f), "utf8")).meta);
|
|
14960
15074
|
} catch (e) {
|
|
14961
|
-
|
|
15075
|
+
log34.debug(`skipping unreadable session file ${f}`, e);
|
|
14962
15076
|
}
|
|
14963
15077
|
}
|
|
14964
15078
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -15197,7 +15311,7 @@ import { promisify } from "util";
|
|
|
15197
15311
|
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync9 } from "fs";
|
|
15198
15312
|
import { homedir as homedir7 } from "os";
|
|
15199
15313
|
import { join as join12, resolve as resolve4, sep as sep3, parse as parsePath } from "path";
|
|
15200
|
-
var
|
|
15314
|
+
var log35 = forComponent("checkpoints");
|
|
15201
15315
|
var exec = promisify(execFile4);
|
|
15202
15316
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
15203
15317
|
function isBroadRoot(p) {
|
|
@@ -15243,7 +15357,7 @@ var ShadowRepo = class {
|
|
|
15243
15357
|
writeFileSync6(join12(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
15244
15358
|
this.ready = true;
|
|
15245
15359
|
} catch (e) {
|
|
15246
|
-
|
|
15360
|
+
log35.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
15247
15361
|
this.ready = false;
|
|
15248
15362
|
}
|
|
15249
15363
|
return this.ready;
|
|
@@ -15254,7 +15368,7 @@ var ShadowRepo = class {
|
|
|
15254
15368
|
}
|
|
15255
15369
|
async commit(label, forced = []) {
|
|
15256
15370
|
await this.run("add", "-A");
|
|
15257
|
-
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));
|
|
15258
15372
|
await this.run("commit", "--allow-empty", "-q", "-m", label);
|
|
15259
15373
|
}
|
|
15260
15374
|
/** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
|
|
@@ -15262,8 +15376,8 @@ var ShadowRepo = class {
|
|
|
15262
15376
|
* turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
|
|
15263
15377
|
* point per turn, so the REPL's turn↔frame mapping stays intact. */
|
|
15264
15378
|
async amendForced(paths) {
|
|
15265
|
-
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) =>
|
|
15266
|
-
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));
|
|
15267
15381
|
}
|
|
15268
15382
|
/** Commits on `ref`, oldest-first (canonical index space). */
|
|
15269
15383
|
async log(ref) {
|
|
@@ -15323,7 +15437,7 @@ var ShadowRepo = class {
|
|
|
15323
15437
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
15324
15438
|
});
|
|
15325
15439
|
} catch (e) {
|
|
15326
|
-
|
|
15440
|
+
log35.debug("checkpoint prune failed", e);
|
|
15327
15441
|
}
|
|
15328
15442
|
}
|
|
15329
15443
|
};
|
|
@@ -15391,7 +15505,7 @@ var GitCheckpoints = class {
|
|
|
15391
15505
|
use(sessionId) {
|
|
15392
15506
|
if (sessionId === this.session) return;
|
|
15393
15507
|
this.session = sessionId;
|
|
15394
|
-
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));
|
|
15395
15509
|
}
|
|
15396
15510
|
async begin(label) {
|
|
15397
15511
|
if (!await this.start()) return;
|
|
@@ -15411,7 +15525,7 @@ var GitCheckpoints = class {
|
|
|
15411
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
|
|
15412
15526
|
`);
|
|
15413
15527
|
} else {
|
|
15414
|
-
|
|
15528
|
+
log35.debug("checkpoint commit failed", e);
|
|
15415
15529
|
survivors.push(this.repos[i]);
|
|
15416
15530
|
survivorCaches.push(this.caches[i] ?? []);
|
|
15417
15531
|
}
|
|
@@ -15446,7 +15560,7 @@ var GitCheckpoints = class {
|
|
|
15446
15560
|
if (this.forced.has(abs)) continue;
|
|
15447
15561
|
this.forced.add(abs);
|
|
15448
15562
|
const i = this.repoIndexFor(abs);
|
|
15449
|
-
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));
|
|
15450
15564
|
}
|
|
15451
15565
|
}
|
|
15452
15566
|
};
|
|
@@ -17231,7 +17345,7 @@ import { spawnSync as spawnSync6 } from "child_process";
|
|
|
17231
17345
|
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
|
|
17232
17346
|
import { homedir as homedir8 } from "os";
|
|
17233
17347
|
import { join as join14 } from "path";
|
|
17234
|
-
var
|
|
17348
|
+
var log36 = forComponent("os-sched");
|
|
17235
17349
|
var OsScheduler = class {
|
|
17236
17350
|
options;
|
|
17237
17351
|
constructor(options) {
|
|
@@ -17293,7 +17407,7 @@ var OsScheduler = class {
|
|
|
17293
17407
|
}
|
|
17294
17408
|
}
|
|
17295
17409
|
} catch (e) {
|
|
17296
|
-
|
|
17410
|
+
log36.debug(`cancel ${id}`, e);
|
|
17297
17411
|
}
|
|
17298
17412
|
for (const f of [`${id}.json`, `${id}.sh`]) {
|
|
17299
17413
|
try {
|
|
@@ -17411,7 +17525,7 @@ import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unli
|
|
|
17411
17525
|
import { homedir as homedir9 } from "os";
|
|
17412
17526
|
import { join as join15 } from "path";
|
|
17413
17527
|
init_platform();
|
|
17414
|
-
var
|
|
17528
|
+
var log37 = forComponent("remote-trigger");
|
|
17415
17529
|
var TRIGGER_DIR = () => join15(homedir9(), ".agent", "triggers");
|
|
17416
17530
|
var sockPath = (sessionId, dir = TRIGGER_DIR()) => join15(dir, `${sessionId}.sock`);
|
|
17417
17531
|
var TriggerServer = class {
|
|
@@ -17449,13 +17563,13 @@ var TriggerServer = class {
|
|
|
17449
17563
|
conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
|
|
17450
17564
|
}
|
|
17451
17565
|
});
|
|
17452
|
-
conn.on("error", (e) =>
|
|
17566
|
+
conn.on("error", (e) => log37.debug("trigger conn error", e));
|
|
17453
17567
|
});
|
|
17454
|
-
this.server.on("error", (e) =>
|
|
17568
|
+
this.server.on("error", (e) => log37.debug("trigger server error", e));
|
|
17455
17569
|
this.server.listen(p);
|
|
17456
17570
|
this.path = p;
|
|
17457
17571
|
} catch (e) {
|
|
17458
|
-
|
|
17572
|
+
log37.debug("trigger server unavailable", e);
|
|
17459
17573
|
}
|
|
17460
17574
|
}
|
|
17461
17575
|
/** Re-bind on /resume (the session id changed). */
|
|
@@ -17587,7 +17701,7 @@ var strike = C("9");
|
|
|
17587
17701
|
var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
|
|
17588
17702
|
var mdPalette = { bold, dim, cyan, italic, strike, green, link };
|
|
17589
17703
|
var err = (s) => process.stderr.write(s);
|
|
17590
|
-
var
|
|
17704
|
+
var log38 = forComponent("cli");
|
|
17591
17705
|
var VERSION = (() => {
|
|
17592
17706
|
try {
|
|
17593
17707
|
return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -18436,7 +18550,7 @@ function turnCost(model, usage) {
|
|
|
18436
18550
|
const priced = model.startsWith("claude-code/") ? `anthropic/${resolveModelAlias(model.slice("claude-code/".length))}` : model;
|
|
18437
18551
|
return costOf(getModelInfo(priced)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, priced);
|
|
18438
18552
|
}
|
|
18439
|
-
async function evaluateGoal(ai, condition, transcript,
|
|
18553
|
+
async function evaluateGoal(ai, condition, transcript, log39) {
|
|
18440
18554
|
const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
|
|
18441
18555
|
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
18442
18556
|
return text.slice(0, 600);
|
|
@@ -18456,7 +18570,7 @@ ${recent}` }
|
|
|
18456
18570
|
const match = r.content.match(/\{[\s\S]*\}/);
|
|
18457
18571
|
if (match) return JSON.parse(match[0]);
|
|
18458
18572
|
} catch (e) {
|
|
18459
|
-
|
|
18573
|
+
log39(dim(` (goal evaluator error: ${e?.message ?? e})
|
|
18460
18574
|
`));
|
|
18461
18575
|
}
|
|
18462
18576
|
return { met: false, reason: "evaluation unclear" };
|
|
@@ -18682,7 +18796,7 @@ function mcpAgentTools(mounted, opts) {
|
|
|
18682
18796
|
return tools;
|
|
18683
18797
|
}
|
|
18684
18798
|
async function closeMcp(mounted) {
|
|
18685
|
-
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))));
|
|
18686
18800
|
}
|
|
18687
18801
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
18688
18802
|
function mentionRefs(line) {
|
|
@@ -18741,7 +18855,7 @@ async function expandMentions(fs, line) {
|
|
|
18741
18855
|
if (loaded.includes(ref) || missing.includes(ref)) continue;
|
|
18742
18856
|
if (ref.includes(":") && mcpMentionResolver) {
|
|
18743
18857
|
const body = await mcpMentionResolver(ref).catch((e) => {
|
|
18744
|
-
|
|
18858
|
+
log38.debug("mcp mention resolve failed", e);
|
|
18745
18859
|
return null;
|
|
18746
18860
|
});
|
|
18747
18861
|
if (body != null) {
|
|
@@ -18832,7 +18946,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
18832
18946
|
try {
|
|
18833
18947
|
store.save(session);
|
|
18834
18948
|
} catch (ex) {
|
|
18835
|
-
|
|
18949
|
+
log38.debug("mid-turn session flush failed", ex);
|
|
18836
18950
|
}
|
|
18837
18951
|
}
|
|
18838
18952
|
return origNotify(e);
|
|
@@ -18996,14 +19110,14 @@ var isCancelTeardown = (e) => {
|
|
|
18996
19110
|
function installCancelGuards(mounted) {
|
|
18997
19111
|
process.on("unhandledRejection", (e) => {
|
|
18998
19112
|
if (isCancelTeardown(e)) {
|
|
18999
|
-
|
|
19113
|
+
log38.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
19000
19114
|
return;
|
|
19001
19115
|
}
|
|
19002
|
-
|
|
19116
|
+
log38.error("unhandledRejection", e);
|
|
19003
19117
|
});
|
|
19004
19118
|
process.on("uncaughtException", (e) => {
|
|
19005
19119
|
if (isCancelTeardown(e)) {
|
|
19006
|
-
|
|
19120
|
+
log38.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
19007
19121
|
return;
|
|
19008
19122
|
}
|
|
19009
19123
|
console.error(e);
|
|
@@ -19067,6 +19181,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
19067
19181
|
const decider = duplex ? leakDecider() : void 0;
|
|
19068
19182
|
const instinct = decider && process.env.VOICE_INSTINCT === "1" ? new Instinct({
|
|
19069
19183
|
decider,
|
|
19184
|
+
overlapModel: overlapModelFor(ai),
|
|
19070
19185
|
context: () => ({ agentSaid: dx?.recentExcerpt(1).replace(/^\w+: /, ""), recent: dx?.recentExcerpt(4) }),
|
|
19071
19186
|
onDecision: ({ kind, ...d }) => {
|
|
19072
19187
|
voiceDiag?.push({ t: performance.now(), kind: `instinct_${kind}`, ...d });
|
|
@@ -19432,7 +19547,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
19432
19547
|
try {
|
|
19433
19548
|
mkdirSync12(dir, { recursive: true });
|
|
19434
19549
|
} catch (e) {
|
|
19435
|
-
|
|
19550
|
+
log38.debug("paste image dir create failed", e);
|
|
19436
19551
|
}
|
|
19437
19552
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
19438
19553
|
return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
|
|
@@ -19545,7 +19660,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
19545
19660
|
mkdirSync12(join16(cwd, ".agent"), { recursive: true });
|
|
19546
19661
|
appendFileSync(histPath, line + "\n");
|
|
19547
19662
|
} catch (e) {
|
|
19548
|
-
|
|
19663
|
+
log38.debug("history write failed", e);
|
|
19549
19664
|
}
|
|
19550
19665
|
};
|
|
19551
19666
|
const ago = (t) => {
|
|
@@ -19617,7 +19732,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
19617
19732
|
try {
|
|
19618
19733
|
store.save(session);
|
|
19619
19734
|
} catch (e) {
|
|
19620
|
-
|
|
19735
|
+
log38.debug("session save after rewind failed", e);
|
|
19621
19736
|
}
|
|
19622
19737
|
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
19623
19738
|
`));
|
|
@@ -19652,7 +19767,7 @@ ${task}`;
|
|
|
19652
19767
|
bangContext.length = 0;
|
|
19653
19768
|
}
|
|
19654
19769
|
const delta = await refreshCatalogs().catch((e) => {
|
|
19655
|
-
|
|
19770
|
+
log38.debug("catalog refresh failed", e);
|
|
19656
19771
|
return "";
|
|
19657
19772
|
});
|
|
19658
19773
|
if (delta) {
|
|
@@ -19915,7 +20030,7 @@ ${task}`;
|
|
|
19915
20030
|
desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
|
|
19916
20031
|
run: async () => {
|
|
19917
20032
|
await refreshCatalogs().catch((e) => {
|
|
19918
|
-
|
|
20033
|
+
log38.debug("catalog refresh failed", e);
|
|
19919
20034
|
});
|
|
19920
20035
|
face.reprepare();
|
|
19921
20036
|
err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
|
|
@@ -20418,7 +20533,7 @@ ${task}`;
|
|
|
20418
20533
|
try {
|
|
20419
20534
|
for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
|
|
20420
20535
|
} catch (e) {
|
|
20421
|
-
|
|
20536
|
+
log38.debug(`loadAgents(${d}) failed`, e);
|
|
20422
20537
|
}
|
|
20423
20538
|
}
|
|
20424
20539
|
if (!seen.size) {
|
|
@@ -20506,7 +20621,7 @@ ${task}`;
|
|
|
20506
20621
|
}
|
|
20507
20622
|
if (idx >= 0) {
|
|
20508
20623
|
const old = mounted.splice(idx, 1)[0];
|
|
20509
|
-
await old.client.close().catch((e) =>
|
|
20624
|
+
await old.client.close().catch((e) => log38.debug("mcp close failed", e));
|
|
20510
20625
|
}
|
|
20511
20626
|
try {
|
|
20512
20627
|
const m = await mountMcpServer(name, conf);
|
|
@@ -20534,7 +20649,7 @@ ${task}`;
|
|
|
20534
20649
|
}
|
|
20535
20650
|
const m = mounted.splice(idx, 1)[0];
|
|
20536
20651
|
remountMcpTools();
|
|
20537
|
-
await m.client.close().catch((e) =>
|
|
20652
|
+
await m.client.close().catch((e) => log38.debug("mcp close failed", e));
|
|
20538
20653
|
err(dim(` removed "${name}"
|
|
20539
20654
|
`));
|
|
20540
20655
|
return;
|
|
@@ -20737,7 +20852,7 @@ ${task}`;
|
|
|
20737
20852
|
try {
|
|
20738
20853
|
return readdirSync4(join16(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
20739
20854
|
} catch (e) {
|
|
20740
|
-
|
|
20855
|
+
log38.debug("completion readdir failed", absDir, e);
|
|
20741
20856
|
return null;
|
|
20742
20857
|
}
|
|
20743
20858
|
};
|
|
@@ -20874,7 +20989,7 @@ ${out}
|
|
|
20874
20989
|
return;
|
|
20875
20990
|
}
|
|
20876
20991
|
await refreshCatalogs().catch((e) => {
|
|
20877
|
-
|
|
20992
|
+
log38.debug("catalog refresh failed", e);
|
|
20878
20993
|
});
|
|
20879
20994
|
const sk = skills.find((x) => x.name === name);
|
|
20880
20995
|
if (sk) {
|
|
@@ -20993,7 +21108,7 @@ ${out}
|
|
|
20993
21108
|
if (delivery || cut) dx?.parkCutDelivery(delivery, !!cut, heard);
|
|
20994
21109
|
const cutReply = cut && cut.full.length - cut.heard.length > 40 ? cut : null;
|
|
20995
21110
|
const noteP = replaces ? replaces.note : !cutReply ? Promise.resolve("") : (instinct ? instinct.bargeIntent(text).catch((e) => {
|
|
20996
|
-
|
|
21111
|
+
log38.warn(`barge intent failed \u2014 model decides: ${e.message}`);
|
|
20997
21112
|
return void 0;
|
|
20998
21113
|
}) : Promise.resolve(void 0)).then(async (intent) => interruptionNote(cutReply, intent) + ackedNote(await instinct?.ackFor(text)));
|
|
20999
21114
|
const cutIn = replaces ? replaces.cutIn : interjectNote;
|
|
@@ -21013,6 +21128,10 @@ ${out}
|
|
|
21013
21128
|
}).finally(() => editorRef?.redrawNow());
|
|
21014
21129
|
}
|
|
21015
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
|
+
}
|
|
21016
21135
|
if (instinct) {
|
|
21017
21136
|
const v = voiceIO, o = v.options;
|
|
21018
21137
|
const { onPartial } = o;
|
|
@@ -21023,7 +21142,7 @@ ${out}
|
|
|
21023
21142
|
if (listening()) instinct.hear(text);
|
|
21024
21143
|
onPartial(text);
|
|
21025
21144
|
};
|
|
21026
|
-
bindOverlapInstinct(v, instinct, () => voiceIO === v);
|
|
21145
|
+
if (!args.duplexV2 || softBarge(args.channelMode)) bindOverlapInstinct(v, instinct, () => voiceIO === v);
|
|
21027
21146
|
bindAckInstinct(v, instinct);
|
|
21028
21147
|
o.onUtterance = (text, meta) => {
|
|
21029
21148
|
instinct.reset();
|
|
@@ -21362,10 +21481,17 @@ function makeSecondChannel(ai, reflexModel, mode = "normal", opts) {
|
|
|
21362
21481
|
// Anticipated backchannels: on in chatty mode (its preset), else off until the offline replay proves them (tests/voice-bc-anticipate-eval.ts)
|
|
21363
21482
|
plan: (c, s) => planner.plan(c, s),
|
|
21364
21483
|
...process.env.AGENTX_CHANNEL_ANTICIPATE === "1" ? { anticipate: true } : {},
|
|
21365
|
-
onEvent: (e) =>
|
|
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`),
|
|
21366
21485
|
...opts
|
|
21367
21486
|
});
|
|
21368
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
|
+
}
|
|
21369
21495
|
async function duplexServe(args, ai, cfg, cwd) {
|
|
21370
21496
|
const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
|
|
21371
21497
|
const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join16(cwd, ".agent", "mcp-auth.json") }));
|
|
@@ -21424,10 +21550,11 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
21424
21550
|
try {
|
|
21425
21551
|
store.save(session);
|
|
21426
21552
|
} catch (e) {
|
|
21427
|
-
|
|
21553
|
+
log38.warn("duplex session save failed", e);
|
|
21428
21554
|
}
|
|
21429
21555
|
};
|
|
21430
|
-
|
|
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 } : {} });
|
|
21431
21558
|
const done = new Promise((resolveDone) => {
|
|
21432
21559
|
const rl = createInterface({ input: process.stdin });
|
|
21433
21560
|
rl.on("line", (line) => {
|
|
@@ -21442,7 +21569,8 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
21442
21569
|
dx.userActive();
|
|
21443
21570
|
activeTurn?.abort();
|
|
21444
21571
|
dx.parkInFlightDeliveries();
|
|
21445
|
-
} else if (
|
|
21572
|
+
} else if (m.type === "overlap") overlap2(m);
|
|
21573
|
+
else if (sc && channelInput(sc, m)) {
|
|
21446
21574
|
if (m.type === "interim" && String(m.text ?? "").trim()) dx.userActive();
|
|
21447
21575
|
} else if (m.type === "cancel") out({ type: "task", kind: "cancel_result", id: m.task, message: dx.cancelTask(String(m.task)) });
|
|
21448
21576
|
else if (m.type === "exit") rl.close();
|
|
@@ -21462,7 +21590,7 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
21462
21590
|
done2();
|
|
21463
21591
|
out({ type: "turn_done", id, finishReason: res?.finishReason ?? "error", ...why ? { error: why } : {} });
|
|
21464
21592
|
}).catch((e) => {
|
|
21465
|
-
|
|
21593
|
+
log38.error("duplex turn failed", e);
|
|
21466
21594
|
done2();
|
|
21467
21595
|
out({ type: "turn_done", id, finishReason: "error", error: String(e?.message ?? e) });
|
|
21468
21596
|
});
|
|
@@ -21481,7 +21609,8 @@ async function channelServe(args, ai, cfg) {
|
|
|
21481
21609
|
const { reflexModel, notes } = duplexModels({ voiceModel: args.voiceModel, reflexModel: cfg.reflexModel, thinkModel: false, actModel: String(args.model ?? cfg.model ?? ""), apiKeys: { ...cfg.apiKeys, ...apiKeysFromEnv() } });
|
|
21482
21610
|
const model = reflexModel ?? new DuplexAgentOptions().reflexModel;
|
|
21483
21611
|
const sc = makeSecondChannel(ai, model, args.channelMode, { show: (text, kind) => out({ type: "channel", kind, text }) });
|
|
21484
|
-
|
|
21612
|
+
const overlap2 = overlapResponder(ai, args.channelMode, out);
|
|
21613
|
+
out({ type: "ready", model, channelMode: args.channelMode, softBarge: softBarge(args.channelMode), ...notes.length ? { notes } : {} });
|
|
21485
21614
|
const rl = createInterface({ input: process.stdin });
|
|
21486
21615
|
rl.on("line", (line) => {
|
|
21487
21616
|
let m;
|
|
@@ -21494,6 +21623,7 @@ async function channelServe(args, ai, cfg) {
|
|
|
21494
21623
|
if (m.type === "user" && typeof m.text === "string") sc.onUserTurn(m.text);
|
|
21495
21624
|
else if (m.type === "text" && typeof m.text === "string") sc.onMainTokens(m.text);
|
|
21496
21625
|
else if (m.type === "barge") sc.onBargeIn();
|
|
21626
|
+
else if (m.type === "overlap") overlap2(m);
|
|
21497
21627
|
else if (m.type === "exit") rl.close();
|
|
21498
21628
|
});
|
|
21499
21629
|
await new Promise((r) => rl.on("close", () => r()));
|