@livx.cc/agentx 0.99.101 → 0.99.103
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 +338 -196
- 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,98 @@ 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
|
+
if (!model && !env.GROQ_API_KEY) return void 0;
|
|
9335
|
+
const m = fastOverlapModel(ai, model);
|
|
9336
|
+
const t0 = performance.now();
|
|
9337
|
+
void m("yeah", AbortSignal.timeout(1e4)).then(
|
|
9338
|
+
() => log19.debug(`fast model warm in ${Math.round(performance.now() - t0)}ms`),
|
|
9339
|
+
(e) => log19.warn(`fast model warm-up failed (the first overlap verdict may be late): ${e?.message ?? e}`)
|
|
9340
|
+
);
|
|
9341
|
+
return m;
|
|
9342
|
+
}
|
|
9343
|
+
var softBarge = (mode) => mode !== "turn";
|
|
9344
|
+
|
|
9345
|
+
// src/voice/instinct.ts
|
|
9346
|
+
var log20 = forComponent("Instinct");
|
|
9256
9347
|
function voiceDeciderOptions(env = process.env) {
|
|
9257
9348
|
if (env.TYPESAFE_AI_KEY) return { apiKey: env.TYPESAFE_AI_KEY };
|
|
9258
9349
|
if (env.GROQ_API_KEY) return { provider: "llm", model: "groq/openai/gpt-oss-120b", apiKey: env.GROQ_API_KEY };
|
|
@@ -9285,6 +9376,10 @@ var InstinctOptions = class {
|
|
|
9285
9376
|
* (~300-400ms) and can never act. A hold that survives this window is the undecided case (a 1-word overlap
|
|
9286
9377
|
* the engine would RESUME over at overlapResumeMs=700) — the one where a yield actually changes the outcome. */
|
|
9287
9378
|
overlapSettleMs = 250;
|
|
9379
|
+
/** Soft barge-in's fast yes/no model (overlap.ts fastOverlapModel) for overlaps the 0ms rules leave open, inside
|
|
9380
|
+
* `overlapBudgetMs`; late/failed → yield. Absent → the generic Choice decides them (slower; no verdict → engine default). */
|
|
9381
|
+
overlapModel;
|
|
9382
|
+
overlapBudgetMs = 300;
|
|
9288
9383
|
/** ackFor waits this long for the host's ackOutcome before treating the ack as unspoken. Hosts set it to the engine's
|
|
9289
9384
|
* last chance to fire (bod-voice: 2×adaptiveAckMs — past that a pending pick is dropped as pick_late). */
|
|
9290
9385
|
ackSettleMs = 1500;
|
|
@@ -9457,7 +9552,8 @@ var Instinct = class {
|
|
|
9457
9552
|
const gen = ++this.overlapGen;
|
|
9458
9553
|
if (this.o.overlapSettleMs > 0) await new Promise((r) => setTimeout(r, this.o.overlapSettleMs));
|
|
9459
9554
|
if (gen !== this.overlapGen) return;
|
|
9460
|
-
const
|
|
9555
|
+
const fast = overlapRule(heard) ?? this.o.overlapModel;
|
|
9556
|
+
const d = fast ? await this.judgeOverlap(heard) : await this.ask(
|
|
9461
9557
|
"overlap",
|
|
9462
9558
|
heard,
|
|
9463
9559
|
{ user_said_over_us: heard },
|
|
@@ -9474,6 +9570,12 @@ var Instinct = class {
|
|
|
9474
9570
|
this.o.onDecision({ ...d, acted: acted("keep_talking", this.o.onKeepTalking(heard)) });
|
|
9475
9571
|
} else if (d) this.o.onDecision(d);
|
|
9476
9572
|
}
|
|
9573
|
+
/** Rules + fast model (OverlapJudge) as an overlap decision; undecided → yield (today's hard barge). */
|
|
9574
|
+
async judgeOverlap(heard) {
|
|
9575
|
+
const j = await new OverlapJudge({ model: this.o.overlapModel, budgetMs: this.o.overlapBudgetMs, now: this.o.now }).judge(heard);
|
|
9576
|
+
const choice = j.verdict === "keep" ? "keep_talking" : "yield";
|
|
9577
|
+
return { kind: "overlap", heard, choice, probabilities: { [choice]: 1 }, ms: j.ms };
|
|
9578
|
+
}
|
|
9477
9579
|
/** The engine's hold ended (it ceded the floor, resumed over a stalled overlap, began a new turn, or was
|
|
9478
9580
|
* interrupted) — drop any pending/in-flight overlap consult. Hosts call this on every hold exit. */
|
|
9479
9581
|
overlapEnded() {
|
|
@@ -9512,13 +9614,13 @@ var Instinct = class {
|
|
|
9512
9614
|
* unfinished partial is no turn to react to — the interjection itself is the reply. */
|
|
9513
9615
|
pickAck(ctx) {
|
|
9514
9616
|
if (this.interjecting) {
|
|
9515
|
-
|
|
9617
|
+
log20.debug(`ack: cut-in turn \u2192 silent (engine context "${ctx.text.slice(0, 40)}" is the previous final)`);
|
|
9516
9618
|
return Promise.resolve(null);
|
|
9517
9619
|
}
|
|
9518
9620
|
const text = ctx.text.trim();
|
|
9519
9621
|
const p = (async () => {
|
|
9520
9622
|
const intentP = ctx.barged ? this.bargeIntent(text).catch((e) => {
|
|
9521
|
-
|
|
9623
|
+
log20.warn(`barge intent for the ack failed \u2014 situational ack: ${e.message}`);
|
|
9522
9624
|
return null;
|
|
9523
9625
|
}) : null;
|
|
9524
9626
|
const d = await this.ask(
|
|
@@ -9540,7 +9642,7 @@ var Instinct = class {
|
|
|
9540
9642
|
settle = r;
|
|
9541
9643
|
});
|
|
9542
9644
|
const timer = setTimeout(() => {
|
|
9543
|
-
|
|
9645
|
+
log20.debug(`ack: no engine outcome in ${this.o.ackSettleMs}ms \u2014 not claimed`);
|
|
9544
9646
|
settle(null);
|
|
9545
9647
|
}, this.o.ackSettleMs);
|
|
9546
9648
|
void spoken.then(() => clearTimeout(timer));
|
|
@@ -9589,7 +9691,7 @@ var Instinct = class {
|
|
|
9589
9691
|
});
|
|
9590
9692
|
return { kind, heard, choice: answers.act.choice, probabilities: answers.act.probabilities, ms: Math.round(this.o.now() - t0) };
|
|
9591
9693
|
} catch (e) {
|
|
9592
|
-
|
|
9694
|
+
log20.warn(`${kind} decision failed \u2014 falling back to engine defaults: ${e.message}`);
|
|
9593
9695
|
if (rethrow) throw e;
|
|
9594
9696
|
return null;
|
|
9595
9697
|
}
|
|
@@ -9598,7 +9700,7 @@ var Instinct = class {
|
|
|
9598
9700
|
|
|
9599
9701
|
// src/voice/unsaid.ts
|
|
9600
9702
|
init_logging();
|
|
9601
|
-
var
|
|
9703
|
+
var log21 = forComponent("Unsaid");
|
|
9602
9704
|
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
9705
|
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
9706
|
var SAID_OVERLAP = 0.6;
|
|
@@ -9638,7 +9740,7 @@ async function unsaidClauses(pieces, spoken, decider, timeoutMs = 5e3) {
|
|
|
9638
9740
|
return p < CONVEYED_MIN;
|
|
9639
9741
|
}).map((x) => x.c);
|
|
9640
9742
|
} catch (e) {
|
|
9641
|
-
|
|
9743
|
+
log21.warn(`unsaid judge failed \u2014 only near-zero-overlap clauses count as unsaid: ${e.message}`);
|
|
9642
9744
|
return fallback();
|
|
9643
9745
|
} finally {
|
|
9644
9746
|
clearTimeout(timer);
|
|
@@ -9722,13 +9824,13 @@ function scrubSpokenJunk(text, final = false) {
|
|
|
9722
9824
|
return s;
|
|
9723
9825
|
}
|
|
9724
9826
|
function isRepeatOf(sentence, spoken) {
|
|
9725
|
-
const
|
|
9726
|
-
const s =
|
|
9727
|
-
return s.replace(/ /g, "").length >= 16 && spoken.split(SENTENCE_SPLIT).some((x) =>
|
|
9827
|
+
const norm4 = (x) => x.replace(/\[[a-z_ ]+\]/gi, "").replace(/[^\p{L}\p{N}]+/gu, " ").trim().toLowerCase();
|
|
9828
|
+
const s = norm4(sentence);
|
|
9829
|
+
return s.replace(/ /g, "").length >= 16 && spoken.split(SENTENCE_SPLIT).some((x) => norm4(x) === s);
|
|
9728
9830
|
}
|
|
9729
9831
|
|
|
9730
9832
|
// src/duplex.ts
|
|
9731
|
-
var
|
|
9833
|
+
var log22 = forComponent("DuplexAgent");
|
|
9732
9834
|
function describeCall(call) {
|
|
9733
9835
|
const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
|
|
9734
9836
|
const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
|
|
@@ -9916,7 +10018,7 @@ function isOfferAcceptance(text) {
|
|
|
9916
10018
|
}
|
|
9917
10019
|
var isQuestion = (s) => /\?["'”’)\]]*$/.test(s.trim());
|
|
9918
10020
|
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.';
|
|
10021
|
+
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
10022
|
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
10023
|
var THINK_DISABLED_GUIDANCE = "(Think tier is not available \u2014 use Act for all escalations.)";
|
|
9922
10024
|
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 +10185,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10083
10185
|
const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
|
|
10084
10186
|
if (m) {
|
|
10085
10187
|
this.fabricationCut = true;
|
|
10086
|
-
|
|
10188
|
+
log22.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
|
|
10087
10189
|
this.reflexBuf = this.reflexBuf.slice(0, Math.max(m.index, this.reflexForwarded));
|
|
10088
10190
|
this.pumpReflex(true);
|
|
10089
10191
|
return;
|
|
@@ -10207,7 +10309,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10207
10309
|
this.leakHit = true;
|
|
10208
10310
|
this.leakCuts.push(leak.trim().slice(0, 40));
|
|
10209
10311
|
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
|
-
|
|
10312
|
+
log22.warn(`reflex ${why} ("${leak.trim().slice(0, 40)}\u2026") \u2014 cut before TTS`);
|
|
10211
10313
|
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
10314
|
const next = this.stepMarks.find((k) => k > at);
|
|
10213
10315
|
if (next === void 0) {
|
|
@@ -10224,6 +10326,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10224
10326
|
if (!out) return;
|
|
10225
10327
|
if (out.trim()) this.spokeThisTurn = true;
|
|
10226
10328
|
this.turnSpoken += out;
|
|
10329
|
+
this.noteVoiced(this.turnSpoken);
|
|
10227
10330
|
this.emitHost({ kind: "text_delta", message: out });
|
|
10228
10331
|
}
|
|
10229
10332
|
/** Leak verdict for one complete sentence: false = speak, true = cut, undefined = the judge is still out.
|
|
@@ -10247,7 +10350,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10247
10350
|
this.notify("diag", "leak_judge", { text: s.slice(0, 80), p: Math.round(p * 100) / 100, ms: Math.round(performance.now() - t0) });
|
|
10248
10351
|
return p >= LEAK_JUDGE_THRESHOLD;
|
|
10249
10352
|
}, (e) => {
|
|
10250
|
-
|
|
10353
|
+
log22.warn(`leak judge failed \u2014 speaking the sentence: ${e.message}`);
|
|
10251
10354
|
return false;
|
|
10252
10355
|
}).then((leak) => {
|
|
10253
10356
|
clearTimeout(timer);
|
|
@@ -10319,7 +10422,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10319
10422
|
let i = t.length - 1;
|
|
10320
10423
|
while (i >= this.turnBase && !(t[i].role === "assistant" && typeof t[i].content === "string" && t[i].content.includes(key))) i--;
|
|
10321
10424
|
if (i < this.turnBase) {
|
|
10322
|
-
|
|
10425
|
+
log22.warn("analysis leak cut, but its text was not found in the transcript to scrub");
|
|
10323
10426
|
continue;
|
|
10324
10427
|
}
|
|
10325
10428
|
const m = t[i];
|
|
@@ -10374,7 +10477,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10374
10477
|
const res = await this.voice.send(content);
|
|
10375
10478
|
const fb = this.options.reflexFallbackModel, model = this.voice.options.model;
|
|
10376
10479
|
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
|
-
|
|
10480
|
+
log22.warn(`reflex ${model} rate-limited \u2014 re-running the turn on ${fb}`);
|
|
10378
10481
|
this.notify("diag", "reflex_fallback", { from: model, to: fb });
|
|
10379
10482
|
this.voice.transcript.length = base;
|
|
10380
10483
|
this.voice.options.model = fb;
|
|
@@ -10407,7 +10510,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10407
10510
|
try {
|
|
10408
10511
|
await this.voice.send(reminder);
|
|
10409
10512
|
} catch (e) {
|
|
10410
|
-
|
|
10513
|
+
log22.warn(`follow-through nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
10411
10514
|
} finally {
|
|
10412
10515
|
const i = this.voice.transcript.findIndex((m, k) => k >= base && m.role === "user" && m.content === reminder);
|
|
10413
10516
|
if (i >= 0) this.voice.transcript.splice(i, 1);
|
|
@@ -10468,7 +10571,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10468
10571
|
return {
|
|
10469
10572
|
preToolUse: (call) => {
|
|
10470
10573
|
if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
|
|
10471
|
-
|
|
10574
|
+
log22.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
|
|
10472
10575
|
this.abortSpeculation();
|
|
10473
10576
|
return { block: true, reason: "Speculative turn aborted." };
|
|
10474
10577
|
}
|
|
@@ -10542,7 +10645,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10542
10645
|
try {
|
|
10543
10646
|
await this.voice.send(reminder);
|
|
10544
10647
|
} catch (e) {
|
|
10545
|
-
|
|
10648
|
+
log22.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
10546
10649
|
} finally {
|
|
10547
10650
|
this.nudging = false;
|
|
10548
10651
|
const i = this.voice.transcript.findIndex((m, k) => k >= base && m.role === "user" && m.content === reminder);
|
|
@@ -10597,7 +10700,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10597
10700
|
const accepted = this.offerOpen && this.parkedRedeliver.length > 0 && this.parkedRedeliver.every((p) => p.task === this.offer.task) && (isOfferAcceptance(content) || isTrivialBarge(content));
|
|
10598
10701
|
const judge = accepted ? void 0 : this.options.bargeIntent;
|
|
10599
10702
|
return (accepted ? Promise.resolve("resume") : judge ? judge(content).catch((e) => {
|
|
10600
|
-
|
|
10703
|
+
log22.warn(`bargeIntent judge failed \u2014 using the allowlist: ${e.message}`);
|
|
10601
10704
|
return fallback();
|
|
10602
10705
|
}) : Promise.resolve(fallback())).then((intent) => {
|
|
10603
10706
|
this.notify("diag", "barge_intent", { intent, text: content.slice(0, 80) });
|
|
@@ -10622,7 +10725,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10622
10725
|
return this.sendTurn(content, false, rec, ext);
|
|
10623
10726
|
});
|
|
10624
10727
|
}
|
|
10625
|
-
return this.sendTurn(content, false, rec, ext);
|
|
10728
|
+
return this.sendTurn(typeof content === "string" ? content + this.acceptNote(content) : content, false, rec, ext);
|
|
10626
10729
|
}
|
|
10627
10730
|
/** Host-named user turns (send opts.id), so a later send can replace one. Bounded; a replace that arrives
|
|
10628
10731
|
* BEFORE its target (the host's dispatch hops can reorder) leaves a pre-superseded record behind. */
|
|
@@ -10793,7 +10896,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10793
10896
|
try {
|
|
10794
10897
|
res = await this.voice.send(spec.text);
|
|
10795
10898
|
} catch (e) {
|
|
10796
|
-
|
|
10899
|
+
log22.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
|
|
10797
10900
|
} finally {
|
|
10798
10901
|
this.voice.options.signal = prevSignal;
|
|
10799
10902
|
}
|
|
@@ -10807,7 +10910,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10807
10910
|
if (d === "abort") {
|
|
10808
10911
|
if (this.voice.transcript.length > base) this.voice.transcript.length = base;
|
|
10809
10912
|
this.speculativeAbortedCalls++;
|
|
10810
|
-
|
|
10913
|
+
log22.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
|
|
10811
10914
|
if (this.spec === spec) this.spec = void 0;
|
|
10812
10915
|
return res ?? empty;
|
|
10813
10916
|
}
|
|
@@ -10875,6 +10978,7 @@ Update: ${brief}`;
|
|
|
10875
10978
|
this.bargeMark = this.parkedRedeliver.length;
|
|
10876
10979
|
this.reflexCut = false;
|
|
10877
10980
|
this.offerOpen = false;
|
|
10981
|
+
this.asked = "";
|
|
10878
10982
|
}
|
|
10879
10983
|
const parked = [];
|
|
10880
10984
|
for (const rec of this.tasks.values())
|
|
@@ -10918,6 +11022,7 @@ Update: ${brief}`;
|
|
|
10918
11022
|
if (say) {
|
|
10919
11023
|
this.offerOpen = false;
|
|
10920
11024
|
this.options.host?.notify?.({ kind: "speak_utterance", message: say });
|
|
11025
|
+
this.noteVoiced(say);
|
|
10921
11026
|
if (rec) {
|
|
10922
11027
|
this.voicedBy.set(say.trim(), rec.id);
|
|
10923
11028
|
if (this.voicedBy.size > 64) this.voicedBy.delete(this.voicedBy.keys().next().value);
|
|
@@ -10930,6 +11035,7 @@ Update: ${brief}`;
|
|
|
10930
11035
|
this.offer = { text: rec.capOffer = this.options.spokenOffer, task: rec.id };
|
|
10931
11036
|
this.offerOpen = true;
|
|
10932
11037
|
this.options.host?.notify?.({ kind: "speak_utterance", message: this.offer.text });
|
|
11038
|
+
this.noteVoiced(this.offer.text);
|
|
10933
11039
|
}
|
|
10934
11040
|
this.notify("diag", "delivery_budget", { task: rec.id, spoke: rec.voicedChars, parked: rest.length });
|
|
10935
11041
|
} else if (rec.deliveryParked === "barge") this.parkedRedeliver.push({ text: seg, task: rec.id });
|
|
@@ -10956,6 +11062,20 @@ Update: ${brief}`;
|
|
|
10956
11062
|
offer = { text: "", task: "" };
|
|
10957
11063
|
/** The cap offer was the last thing voiced (no delivery or turn has spoken since) — a plain yes accepts it (send). */
|
|
10958
11064
|
offerOpen = false;
|
|
11065
|
+
/** The last thing voiced (reflex reply or delivery), when it ended on a question — a plain yes answers it (acceptNote). */
|
|
11066
|
+
asked = "";
|
|
11067
|
+
noteVoiced(text) {
|
|
11068
|
+
const last = text.trim().split(SENTENCE_SPLIT).at(-1) ?? "";
|
|
11069
|
+
this.asked = isQuestion(last) ? last.trim() : "";
|
|
11070
|
+
}
|
|
11071
|
+
/** 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
|
|
11072
|
+
* an offer read as ambiguous and the offer was asked again instead of done (Para phone call 2026-10-13). */
|
|
11073
|
+
acceptNote(content) {
|
|
11074
|
+
const q2 = this.asked;
|
|
11075
|
+
this.asked = "";
|
|
11076
|
+
return q2 && isOfferAcceptance(content) ? `
|
|
11077
|
+
[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.]` : "";
|
|
11078
|
+
}
|
|
10959
11079
|
/** The task's outcome now belongs to its integration turn (failed / stopped early): take back its parked pieces and
|
|
10960
11080
|
* keep it from being revived — a "go on" must not replay raw segments of a result the reflex is about to explain. */
|
|
10961
11081
|
dropParked(rec) {
|
|
@@ -10982,6 +11102,7 @@ Update: ${brief}`;
|
|
|
10982
11102
|
if (!text) return;
|
|
10983
11103
|
this.offerOpen = false;
|
|
10984
11104
|
this.options.host?.notify?.({ kind: "speak_utterance", message: text });
|
|
11105
|
+
this.noteVoiced(text);
|
|
10985
11106
|
this.notify("diag", "parked_redelivered", { chars: text.length });
|
|
10986
11107
|
}
|
|
10987
11108
|
/** Resolve when all queued voice turns AND all in-flight worker tasks have settled (tests, graceful shutdown). */
|
|
@@ -11172,7 +11293,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11172
11293
|
this.notify("task_verify", `task ${id}: verifying`, { id });
|
|
11173
11294
|
const cres = await new Agent(checkerOpts).run(checkBrief);
|
|
11174
11295
|
if (cres.finishReason !== "stop") {
|
|
11175
|
-
|
|
11296
|
+
log22.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
|
|
11176
11297
|
this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
|
|
11177
11298
|
}
|
|
11178
11299
|
const sum = (a = 0, b = 0) => a + b;
|
|
@@ -11308,7 +11429,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11308
11429
|
rec.status = "done";
|
|
11309
11430
|
rec.result = res.text;
|
|
11310
11431
|
const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
|
|
11311
|
-
|
|
11432
|
+
log22.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
|
|
11312
11433
|
this.notify("task_done", `task ${id} (${rec.label}) completed`, {
|
|
11313
11434
|
id,
|
|
11314
11435
|
text: res.text,
|
|
@@ -11342,14 +11463,14 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11342
11463
|
foldIfSuperseded(rec, note) {
|
|
11343
11464
|
if (rec.deliveryParked !== "moved_on") return false;
|
|
11344
11465
|
if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
|
|
11345
|
-
|
|
11466
|
+
log22.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
|
|
11346
11467
|
return true;
|
|
11347
11468
|
}
|
|
11348
11469
|
failTask(rec, msg) {
|
|
11349
11470
|
this.dropAsk(rec.id);
|
|
11350
11471
|
rec.status = "error";
|
|
11351
11472
|
rec.result = msg;
|
|
11352
|
-
|
|
11473
|
+
log22.warn(`task ${rec.id} failed: ${msg}`);
|
|
11353
11474
|
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`, { id: rec.id });
|
|
11354
11475
|
if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
|
|
11355
11476
|
this.dropParked(rec);
|
|
@@ -11647,7 +11768,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11647
11768
|
init_llm();
|
|
11648
11769
|
init_logging();
|
|
11649
11770
|
init_models();
|
|
11650
|
-
var
|
|
11771
|
+
var log23 = forComponent("ModelRouter");
|
|
11651
11772
|
var ModelRouterOptions = class {
|
|
11652
11773
|
classifier = jevClassifier();
|
|
11653
11774
|
/** Capability tiers → model id (any provider). basic/standard run the main session; premium is only ever an offload. */
|
|
@@ -11731,10 +11852,10 @@ var ModelRouter = class {
|
|
|
11731
11852
|
if (tier === "premium") d.offload = this.state.offload && (c.sameThread ?? 0) >= o.sameThreadAt ? "continue" : "fresh";
|
|
11732
11853
|
} catch (e) {
|
|
11733
11854
|
const msg = e instanceof Error ? e.message : String(e);
|
|
11734
|
-
|
|
11855
|
+
log23.warn(`classifier failed, falling back to standard: ${msg}`);
|
|
11735
11856
|
d = { tier: "standard", model: o.models.standard, probs: {}, fallback: msg, classifyMs: Math.round(performance.now() - t0) };
|
|
11736
11857
|
}
|
|
11737
|
-
|
|
11858
|
+
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
11859
|
return d;
|
|
11739
11860
|
}
|
|
11740
11861
|
/** Run one user turn on `agent` with routing. Returns a RunResult carrying `costUsd` + `routing`. */
|
|
@@ -11786,7 +11907,7 @@ ${text}`;
|
|
|
11786
11907
|
const answer = res.text || `(premium sub-agent finished with no reply; finishReason=${res.finishReason})`;
|
|
11787
11908
|
agent.transcript.push({ role: "user", content }, { role: "assistant", content: answer });
|
|
11788
11909
|
this.state.offload = { messages: child.transcript, mainLen: agent.transcript.length, question: text };
|
|
11789
|
-
|
|
11910
|
+
log23.info(`offload ${cont ? "continue" : "fresh"} done \xB7 ${res.finishReason} \xB7 ${res.steps} steps`);
|
|
11790
11911
|
const costUsd = o.costOf(main2, briefUsage) + o.costOf(o.models.premium, res.usage);
|
|
11791
11912
|
return { ...res, text: answer, messages: agent.transcript, usage: addUsage(briefUsage, res.usage), costUsd, routing: d };
|
|
11792
11913
|
}
|
|
@@ -11802,7 +11923,7 @@ The request:
|
|
|
11802
11923
|
${text}`);
|
|
11803
11924
|
return { text: r.text.trim() || text, usage: r.usage };
|
|
11804
11925
|
} catch (e) {
|
|
11805
|
-
|
|
11926
|
+
log23.warn(`brief failed, sending the raw request: ${e instanceof Error ? e.message : String(e)}`);
|
|
11806
11927
|
return { text };
|
|
11807
11928
|
} finally {
|
|
11808
11929
|
agent.transcript = snap;
|
|
@@ -11847,7 +11968,7 @@ async function jevAvailable(opts = {}) {
|
|
|
11847
11968
|
const { env } = await loadJev(opts);
|
|
11848
11969
|
return JEV_KEYS.some((k) => env[k]?.trim());
|
|
11849
11970
|
} catch (e) {
|
|
11850
|
-
|
|
11971
|
+
log23.debug(`jev unavailable: ${e instanceof Error ? e.message : String(e)}`);
|
|
11851
11972
|
return false;
|
|
11852
11973
|
}
|
|
11853
11974
|
}
|
|
@@ -12076,7 +12197,7 @@ init_logging();
|
|
|
12076
12197
|
init_platform();
|
|
12077
12198
|
import { spawn } from "child_process";
|
|
12078
12199
|
import { createHash } from "crypto";
|
|
12079
|
-
var
|
|
12200
|
+
var log24 = forComponent("mcp");
|
|
12080
12201
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
12081
12202
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
12082
12203
|
var DEFAULT_CALL_TIMEOUT_MS = 30 * 6e4;
|
|
@@ -12108,7 +12229,7 @@ var StdioTransport = class {
|
|
|
12108
12229
|
proc.stdout.setEncoding("utf8");
|
|
12109
12230
|
proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
12110
12231
|
proc.stderr.setEncoding("utf8");
|
|
12111
|
-
proc.stderr.on("data", (chunk) =>
|
|
12232
|
+
proc.stderr.on("data", (chunk) => log24.debug(`[${command}] stderr:`, chunk.trimEnd()));
|
|
12112
12233
|
proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
|
|
12113
12234
|
proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
|
|
12114
12235
|
}
|
|
@@ -12122,7 +12243,7 @@ var StdioTransport = class {
|
|
|
12122
12243
|
try {
|
|
12123
12244
|
this.dispatch(JSON.parse(line));
|
|
12124
12245
|
} catch (e) {
|
|
12125
|
-
|
|
12246
|
+
log24.debug("dropping non-JSON line from MCP server:", line, e);
|
|
12126
12247
|
}
|
|
12127
12248
|
}
|
|
12128
12249
|
}
|
|
@@ -12132,7 +12253,7 @@ var StdioTransport = class {
|
|
|
12132
12253
|
try {
|
|
12133
12254
|
p2?.onProgress?.(msg.params ?? {});
|
|
12134
12255
|
} catch (e) {
|
|
12135
|
-
|
|
12256
|
+
log24.debug("onProgress handler threw", e);
|
|
12136
12257
|
}
|
|
12137
12258
|
return;
|
|
12138
12259
|
}
|
|
@@ -12170,7 +12291,7 @@ var StdioTransport = class {
|
|
|
12170
12291
|
const onAbort = () => {
|
|
12171
12292
|
if (!this.pending.delete(id)) return;
|
|
12172
12293
|
clearTimeout(timer);
|
|
12173
|
-
this.notify("notifications/cancelled", { requestId: id, reason: String(signal?.reason?.message ?? "cancelled") }).catch((e) =>
|
|
12294
|
+
this.notify("notifications/cancelled", { requestId: id, reason: String(signal?.reason?.message ?? "cancelled") }).catch((e) => log24.debug("cancel notify failed", e));
|
|
12174
12295
|
reject(abortError(signal));
|
|
12175
12296
|
};
|
|
12176
12297
|
const done = () => signal?.removeEventListener("abort", onAbort);
|
|
@@ -12194,7 +12315,7 @@ var StdioTransport = class {
|
|
|
12194
12315
|
try {
|
|
12195
12316
|
this.proc?.stdin?.end();
|
|
12196
12317
|
} catch (e) {
|
|
12197
|
-
|
|
12318
|
+
log24.debug("stdin end failed", e);
|
|
12198
12319
|
}
|
|
12199
12320
|
if (!(IS_WIN && killTree(this.proc?.pid, "SIGTERM"))) this.proc?.kill();
|
|
12200
12321
|
}
|
|
@@ -12229,7 +12350,7 @@ var HttpTransport = class {
|
|
|
12229
12350
|
return await this.post({ jsonrpc: "2.0", id, method, params: opts.onProgress ? withProgressToken(params, id) : params }, false, signal, opts.onProgress);
|
|
12230
12351
|
} catch (e) {
|
|
12231
12352
|
if (opts.signal?.aborted) {
|
|
12232
|
-
void this.notify("notifications/cancelled", { requestId: id, reason: String(opts.signal.reason?.message ?? "cancelled") }).catch((err2) =>
|
|
12353
|
+
void this.notify("notifications/cancelled", { requestId: id, reason: String(opts.signal.reason?.message ?? "cancelled") }).catch((err2) => log24.debug("cancel notify failed", err2));
|
|
12233
12354
|
throw abortError(opts.signal);
|
|
12234
12355
|
}
|
|
12235
12356
|
throw e;
|
|
@@ -12272,7 +12393,7 @@ function sseMessage(line) {
|
|
|
12272
12393
|
try {
|
|
12273
12394
|
return JSON.parse(trimmed.slice(5).trim());
|
|
12274
12395
|
} catch (e) {
|
|
12275
|
-
|
|
12396
|
+
log24.debug("skipping unparseable SSE data line", e);
|
|
12276
12397
|
return void 0;
|
|
12277
12398
|
}
|
|
12278
12399
|
}
|
|
@@ -12282,7 +12403,7 @@ async function readSseResponse(resp, id, onProgress) {
|
|
|
12282
12403
|
try {
|
|
12283
12404
|
onProgress?.(obj.params ?? {});
|
|
12284
12405
|
} catch (e) {
|
|
12285
|
-
|
|
12406
|
+
log24.debug("onProgress handler threw", e);
|
|
12286
12407
|
}
|
|
12287
12408
|
return void 0;
|
|
12288
12409
|
}
|
|
@@ -12381,7 +12502,7 @@ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
|
|
|
12381
12502
|
return { name, client, tools, specs, serverInfo: init?.serverInfo, config: cfg };
|
|
12382
12503
|
})(), mountTimeoutMs, name);
|
|
12383
12504
|
} catch (e) {
|
|
12384
|
-
await client.close().catch((err2) =>
|
|
12505
|
+
await client.close().catch((err2) => log24.debug(`close after failed mount of "${name}": ${err2}`));
|
|
12385
12506
|
throw e;
|
|
12386
12507
|
}
|
|
12387
12508
|
}
|
|
@@ -12392,15 +12513,15 @@ function validEntries(servers) {
|
|
|
12392
12513
|
return Object.entries(servers).filter(([name, cfg]) => {
|
|
12393
12514
|
if (!cfg || cfg.disabled) return false;
|
|
12394
12515
|
if (!cfg.command && !cfg.url) {
|
|
12395
|
-
|
|
12516
|
+
log24.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
|
|
12396
12517
|
return false;
|
|
12397
12518
|
}
|
|
12398
12519
|
return true;
|
|
12399
12520
|
});
|
|
12400
12521
|
}
|
|
12401
12522
|
function logMountFailure(name, e) {
|
|
12402
|
-
if (e instanceof McpAuthError)
|
|
12403
|
-
else
|
|
12523
|
+
if (e instanceof McpAuthError) log24.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
|
|
12524
|
+
else log24.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
|
|
12404
12525
|
}
|
|
12405
12526
|
async function mountMcpServers(servers = {}, opts = {}) {
|
|
12406
12527
|
const entries = validEntries(servers);
|
|
@@ -12410,7 +12531,7 @@ async function mountMcpServers(servers = {}, opts = {}) {
|
|
|
12410
12531
|
const name = entries[i][0];
|
|
12411
12532
|
if (r.status === "fulfilled") {
|
|
12412
12533
|
out.push(r.value);
|
|
12413
|
-
|
|
12534
|
+
log24.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
|
|
12414
12535
|
} else logMountFailure(name, r.reason);
|
|
12415
12536
|
});
|
|
12416
12537
|
return out;
|
|
@@ -12450,7 +12571,7 @@ var McpPool = class {
|
|
|
12450
12571
|
const prev = this.warm.get(key);
|
|
12451
12572
|
if (prev) {
|
|
12452
12573
|
clearTimeout(prev.timer);
|
|
12453
|
-
if (prev.client !== client) void prev.client.close().catch((err2) =>
|
|
12574
|
+
if (prev.client !== client) void prev.client.close().catch((err2) => log24.debug(`warm-pool replace close failed: ${err2}`));
|
|
12454
12575
|
}
|
|
12455
12576
|
const e = { client, timer: void 0 };
|
|
12456
12577
|
this.warm.set(key, e);
|
|
@@ -12467,7 +12588,7 @@ var McpPool = class {
|
|
|
12467
12588
|
const e = this.warm.get(key);
|
|
12468
12589
|
if (!e) return;
|
|
12469
12590
|
this.warm.delete(key);
|
|
12470
|
-
await e.client.close().catch((err2) =>
|
|
12591
|
+
await e.client.close().catch((err2) => log24.debug(`warm-pool evict close failed: ${err2}`));
|
|
12471
12592
|
}
|
|
12472
12593
|
async closeAll() {
|
|
12473
12594
|
for (const e of this.warm.values()) {
|
|
@@ -12686,7 +12807,7 @@ init_tools_shell();
|
|
|
12686
12807
|
// src/tools.notify.ts
|
|
12687
12808
|
init_logging();
|
|
12688
12809
|
import { execFile as execFile2 } from "child_process";
|
|
12689
|
-
var
|
|
12810
|
+
var log26 = forComponent("notify");
|
|
12690
12811
|
var WIN_TOAST = [
|
|
12691
12812
|
"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null",
|
|
12692
12813
|
"$t = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)",
|
|
@@ -12717,7 +12838,7 @@ function makeNotifyTool(opts = {}) {
|
|
|
12717
12838
|
return new Promise((resolve6) => {
|
|
12718
12839
|
run(argv[0], argv[1], { timeout: 5e3, ...extra }, (e) => {
|
|
12719
12840
|
if (e) {
|
|
12720
|
-
|
|
12841
|
+
log26.debug("notification failed", e);
|
|
12721
12842
|
resolve6(`Notification failed: ${e.message}`);
|
|
12722
12843
|
} else resolve6("Notification shown.");
|
|
12723
12844
|
});
|
|
@@ -12733,7 +12854,7 @@ import { BodDB as BodDB2 } from "@bod.ee/db";
|
|
|
12733
12854
|
init_logging();
|
|
12734
12855
|
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
12735
12856
|
import { homedir } from "os";
|
|
12736
|
-
var
|
|
12857
|
+
var log27 = forComponent("cli-util");
|
|
12737
12858
|
function dotDirs(base, sub, opts = {}) {
|
|
12738
12859
|
const home = opts.home ?? homedir();
|
|
12739
12860
|
const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
|
|
@@ -12750,7 +12871,7 @@ function parseJson(text, fallback, what = "json") {
|
|
|
12750
12871
|
try {
|
|
12751
12872
|
return JSON.parse(text);
|
|
12752
12873
|
} catch (e) {
|
|
12753
|
-
|
|
12874
|
+
log27.debug(`parseJson(${what}) failed: ${e.message}`);
|
|
12754
12875
|
return fallback;
|
|
12755
12876
|
}
|
|
12756
12877
|
}
|
|
@@ -12760,7 +12881,7 @@ function readJsonFile(path, fallback) {
|
|
|
12760
12881
|
try {
|
|
12761
12882
|
text = readFileSync5(path, "utf8");
|
|
12762
12883
|
} catch (e) {
|
|
12763
|
-
|
|
12884
|
+
log27.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
|
|
12764
12885
|
return fallback;
|
|
12765
12886
|
}
|
|
12766
12887
|
return parseJson(text, fallback, path);
|
|
@@ -13064,7 +13185,7 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as rea
|
|
|
13064
13185
|
import { homedir as homedir3 } from "os";
|
|
13065
13186
|
import { dirname as dirname4, join as join7 } from "path";
|
|
13066
13187
|
import { fileURLToPath } from "url";
|
|
13067
|
-
var
|
|
13188
|
+
var log28 = forComponent("VoiceIO");
|
|
13068
13189
|
var now3 = () => performance.now();
|
|
13069
13190
|
var Player = class {
|
|
13070
13191
|
proc = null;
|
|
@@ -13078,7 +13199,7 @@ var Player = class {
|
|
|
13078
13199
|
["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
|
|
13079
13200
|
{ stdio: ["pipe", "ignore", "ignore"] }
|
|
13080
13201
|
);
|
|
13081
|
-
this.proc.on("error", (e) =>
|
|
13202
|
+
this.proc.on("error", (e) => log28.warn(`ffplay error: ${e.message}`));
|
|
13082
13203
|
this.proc.stdin.on("error", () => {
|
|
13083
13204
|
});
|
|
13084
13205
|
this.bytesWritten = 0;
|
|
@@ -13116,7 +13237,7 @@ function detectFfmpegMic() {
|
|
|
13116
13237
|
const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
|
|
13117
13238
|
const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
|
|
13118
13239
|
if (!mic) throw new Error("no audio input device found");
|
|
13119
|
-
|
|
13240
|
+
log28.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
|
|
13120
13241
|
return `:${mic.idx}`;
|
|
13121
13242
|
}
|
|
13122
13243
|
function detectDshowMic() {
|
|
@@ -13127,7 +13248,7 @@ function detectDshowMic() {
|
|
|
13127
13248
|
const devices = [...(r.stderr ?? "").matchAll(/"([^"]+)"\s*\(audio\)/g)].map((m) => m[1]);
|
|
13128
13249
|
const mic = devices.find((d) => !/stereo mix|loopback|virtual|cable|teams/i.test(d)) ?? devices[0];
|
|
13129
13250
|
if (!mic) throw new Error("no audio input device found (set MIC_DEVICE to the DirectShow device name)");
|
|
13130
|
-
|
|
13251
|
+
log28.debug(`ffmpeg dshow mic: ${mic}`);
|
|
13131
13252
|
return `audio=${mic}`;
|
|
13132
13253
|
}
|
|
13133
13254
|
function detectedInputDevice() {
|
|
@@ -13163,15 +13284,15 @@ function resolveAecBinary() {
|
|
|
13163
13284
|
if (existsSync4(bin) && statSync6(bin).mtimeMs >= statSync6(src).mtimeMs) return bin;
|
|
13164
13285
|
if (spawnSync3("which", ["swiftc"]).status !== 0) return null;
|
|
13165
13286
|
mkdirSync5(cacheDir, { recursive: true });
|
|
13166
|
-
|
|
13287
|
+
log28.info("compiling AEC mic helper (first run)\u2026");
|
|
13167
13288
|
const build = spawnSync3("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
|
|
13168
13289
|
if (build.status !== 0) {
|
|
13169
|
-
|
|
13290
|
+
log28.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
|
|
13170
13291
|
return null;
|
|
13171
13292
|
}
|
|
13172
13293
|
const sign = spawnSync3("codesign", ["-fs", "-", bin], { encoding: "utf8" });
|
|
13173
13294
|
if (sign.status !== 0) {
|
|
13174
|
-
|
|
13295
|
+
log28.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
|
|
13175
13296
|
return null;
|
|
13176
13297
|
}
|
|
13177
13298
|
return bin;
|
|
@@ -13213,17 +13334,17 @@ var NodeMicSource = class {
|
|
|
13213
13334
|
this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
|
|
13214
13335
|
} else {
|
|
13215
13336
|
if (!hasCommand("ffmpeg")) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
|
|
13216
|
-
|
|
13337
|
+
log28.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
|
|
13217
13338
|
const input = process.platform === "win32" ? ["-f", "dshow", "-i", detectDshowMic()] : ["-f", "avfoundation", "-i", detectFfmpegMic()];
|
|
13218
13339
|
this.proc = spawn2(
|
|
13219
13340
|
"ffmpeg",
|
|
13220
13341
|
["-loglevel", "error", ...input, "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
|
|
13221
13342
|
{ stdio: ["ignore", "pipe", "pipe"], ...process.platform === "win32" ? { windowsHide: true } : {} }
|
|
13222
13343
|
);
|
|
13223
|
-
this.proc.stderr.on("data", (d) =>
|
|
13344
|
+
this.proc.stderr.on("data", (d) => log28.warn(`ffmpeg: ${String(d).trim()}`));
|
|
13224
13345
|
}
|
|
13225
13346
|
this.proc.on("exit", (c) => {
|
|
13226
|
-
if (c && !this.stopped)
|
|
13347
|
+
if (c && !this.stopped) log28.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
|
|
13227
13348
|
});
|
|
13228
13349
|
this.proc.stdout.on("data", (chunk) => onChunk(chunk));
|
|
13229
13350
|
}
|
|
@@ -13289,7 +13410,7 @@ var AecDuplexAudio = class {
|
|
|
13289
13410
|
this.proc.stdin.on("error", () => {
|
|
13290
13411
|
});
|
|
13291
13412
|
this.proc.on("exit", (c) => {
|
|
13292
|
-
if (c && !this.stopped)
|
|
13413
|
+
if (c && !this.stopped) log28.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
|
|
13293
13414
|
});
|
|
13294
13415
|
this.proc.stdout.on("data", (chunk) => {
|
|
13295
13416
|
this.gotChunk = true;
|
|
@@ -13305,7 +13426,7 @@ var AecDuplexAudio = class {
|
|
|
13305
13426
|
openMicSettings();
|
|
13306
13427
|
this.onFatal?.("microphone permission denied \u2014 enable it in System Settings \u2192 Privacy & Security \u2192 Microphone for your terminal, then restart it");
|
|
13307
13428
|
}
|
|
13308
|
-
} else
|
|
13429
|
+
} else log28.debug(`mic-aec: ${s}`);
|
|
13309
13430
|
}
|
|
13310
13431
|
});
|
|
13311
13432
|
if (!this.noVpio && !this.triedFallback) {
|
|
@@ -13314,7 +13435,7 @@ var AecDuplexAudio = class {
|
|
|
13314
13435
|
this.triedFallback = true;
|
|
13315
13436
|
this.noVpio = true;
|
|
13316
13437
|
this._aec = false;
|
|
13317
|
-
|
|
13438
|
+
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
13439
|
this.onDegrade?.();
|
|
13319
13440
|
this.killProc();
|
|
13320
13441
|
this.spawnHelper();
|
|
@@ -13485,21 +13606,15 @@ function bindAckInstinct(v, instinct) {
|
|
|
13485
13606
|
if (ev.kind === "ack_fired") instinct.ackOutcome(String(ev.phrase));
|
|
13486
13607
|
else if (ev.kind === "ack_skipped" || ev.kind === "ack_cancelled") instinct.ackOutcome(null);
|
|
13487
13608
|
} catch (e) {
|
|
13488
|
-
|
|
13609
|
+
log28.warn(`ack outcome tap failed: ${e instanceof Error ? e.message : e}`);
|
|
13489
13610
|
}
|
|
13490
13611
|
onDiag(ev);
|
|
13491
13612
|
};
|
|
13492
13613
|
}
|
|
13493
13614
|
function bindSecondChannel(v, sc, live) {
|
|
13494
13615
|
const o = v.options;
|
|
13495
|
-
const { onPartial, onUtterance, onState, onBargeIn
|
|
13496
|
-
const { show } = sc.options;
|
|
13616
|
+
const { onPartial, onUtterance, onState, onBargeIn } = o;
|
|
13497
13617
|
let own = false;
|
|
13498
|
-
let blip = "";
|
|
13499
|
-
o.onBackchannel = (p) => {
|
|
13500
|
-
blip = p;
|
|
13501
|
-
onBackchannel(p);
|
|
13502
|
-
};
|
|
13503
13618
|
const take = v.takeInterruptedReply.bind(v);
|
|
13504
13619
|
v.takeInterruptedReply = () => {
|
|
13505
13620
|
const r = take();
|
|
@@ -13507,7 +13622,7 @@ function bindSecondChannel(v, sc, live) {
|
|
|
13507
13622
|
return r.queued.length ? { ...r, full: "", heard: "" } : null;
|
|
13508
13623
|
};
|
|
13509
13624
|
o.onPartial = (t) => {
|
|
13510
|
-
if (live() && !v.audible) sc.onInterim(t);
|
|
13625
|
+
if (live() && (own || !v.audible)) sc.onInterim(t);
|
|
13511
13626
|
onPartial(t);
|
|
13512
13627
|
};
|
|
13513
13628
|
o.onUtterance = ((t, meta) => {
|
|
@@ -13530,25 +13645,34 @@ function bindSecondChannel(v, sc, live) {
|
|
|
13530
13645
|
if (live()) sc.noteReply(t);
|
|
13531
13646
|
return speakDelta(t, ...rest);
|
|
13532
13647
|
});
|
|
13648
|
+
Object.defineProperty(sc.options, "aec", { get: () => v.usingAec, set: () => {
|
|
13649
|
+
}, enumerable: true, configurable: true });
|
|
13533
13650
|
Object.assign(sc.options, {
|
|
13534
|
-
aec: v.usingAec,
|
|
13535
13651
|
speak: (text, kind) => {
|
|
13536
13652
|
if (!live() || v.audible) return false;
|
|
13537
|
-
if (kind === "backchannel")
|
|
13538
|
-
if (sc.language !== "en") return false;
|
|
13539
|
-
blip = "";
|
|
13540
|
-
v.fireBackchannel();
|
|
13541
|
-
return;
|
|
13542
|
-
}
|
|
13653
|
+
if (kind === "backchannel") return speakBlip(v, text);
|
|
13543
13654
|
own = true;
|
|
13544
13655
|
v.speakFiller(text);
|
|
13545
13656
|
return v.awaitIdle().finally(() => {
|
|
13546
13657
|
own = false;
|
|
13547
13658
|
});
|
|
13548
|
-
}
|
|
13549
|
-
show: (text, kind) => show(kind === "backchannel" && blip ? blip : text, kind)
|
|
13659
|
+
}
|
|
13550
13660
|
});
|
|
13551
13661
|
}
|
|
13662
|
+
function speakBlip(v, text) {
|
|
13663
|
+
const E = VoiceEngine;
|
|
13664
|
+
const pool = E.BACKCHANNELS;
|
|
13665
|
+
E.BACKCHANNELS = [text];
|
|
13666
|
+
try {
|
|
13667
|
+
v.fireBackchannel();
|
|
13668
|
+
return true;
|
|
13669
|
+
} catch (e) {
|
|
13670
|
+
log28.warn(`backchannel blip failed \u2014 line dropped: ${e.message}`);
|
|
13671
|
+
return false;
|
|
13672
|
+
} finally {
|
|
13673
|
+
E.BACKCHANNELS = pool;
|
|
13674
|
+
}
|
|
13675
|
+
}
|
|
13552
13676
|
function channelInput(sc, m) {
|
|
13553
13677
|
if (m.type === "interim") {
|
|
13554
13678
|
if (typeof m.aec === "boolean") sc.options.aec = m.aec;
|
|
@@ -13562,7 +13686,7 @@ function channelInput(sc, m) {
|
|
|
13562
13686
|
}
|
|
13563
13687
|
if (m.type === "agent_state") {
|
|
13564
13688
|
if (typeof m.state === "string" && MAIN_STATES.has(m.state)) sc.onAgentState(m.state);
|
|
13565
|
-
else
|
|
13689
|
+
else log28.warn(`agent_state: unknown state ${JSON.stringify(m.state)} \u2014 ignored`);
|
|
13566
13690
|
return true;
|
|
13567
13691
|
}
|
|
13568
13692
|
return false;
|
|
@@ -13686,7 +13810,7 @@ function fakeVoiceParts(uttFile) {
|
|
|
13686
13810
|
init_logging();
|
|
13687
13811
|
import { appendFile, mkdirSync as mkdirSync6 } from "fs";
|
|
13688
13812
|
import { dirname as dirname5 } from "path";
|
|
13689
|
-
var
|
|
13813
|
+
var log29 = forComponent("VoiceDiag");
|
|
13690
13814
|
var JsonlDiagSink = class {
|
|
13691
13815
|
constructor(path) {
|
|
13692
13816
|
this.path = path;
|
|
@@ -13727,7 +13851,7 @@ var JsonlDiagSink = class {
|
|
|
13727
13851
|
disable(e) {
|
|
13728
13852
|
this.disabled = true;
|
|
13729
13853
|
this.queue.length = 0;
|
|
13730
|
-
|
|
13854
|
+
log29.debug(`voice diag sink disabled (${this.path}): ${e instanceof Error ? e.message : e}`);
|
|
13731
13855
|
}
|
|
13732
13856
|
/** True once a failure permanently disabled the sink (tests/inspection). */
|
|
13733
13857
|
get isDisabled() {
|
|
@@ -13749,7 +13873,7 @@ init_logging();
|
|
|
13749
13873
|
init_logging();
|
|
13750
13874
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
13751
13875
|
import { join as join8 } from "path";
|
|
13752
|
-
var
|
|
13876
|
+
var log30 = forComponent("ChannelBanks");
|
|
13753
13877
|
var LINE_KINDS = ["backchannel", "filler", "bridge"];
|
|
13754
13878
|
var BANK_KEYS = [...LINE_KINDS, "phatic"];
|
|
13755
13879
|
var EN_PHATIC = [
|
|
@@ -13782,9 +13906,9 @@ var EN_PHATIC = [
|
|
|
13782
13906
|
var EN_BANK = { ...CHANNEL_BANK, phatic: EN_PHATIC };
|
|
13783
13907
|
var primaryLang = (tag) => tag.trim().toLowerCase().split(/[-_]/)[0];
|
|
13784
13908
|
function isPhatic(text, bank) {
|
|
13785
|
-
const t =
|
|
13909
|
+
const t = norm3(text);
|
|
13786
13910
|
return !!t && !!bank?.phatic?.some((p) => {
|
|
13787
|
-
const q2 =
|
|
13911
|
+
const q2 = norm3(p);
|
|
13788
13912
|
return !!q2 && (t === q2 || t.startsWith(q2 + " "));
|
|
13789
13913
|
});
|
|
13790
13914
|
}
|
|
@@ -13803,8 +13927,10 @@ function requestOpener(text) {
|
|
|
13803
13927
|
return ASK.includes(w[0]) || WANT.test(w.slice(0, 4).join(" ")) || new RegExp("^\u05EA\\p{Script=Hebrew}{2,}$", "u").test(w[0]);
|
|
13804
13928
|
}
|
|
13805
13929
|
function pendingQuestion(text, lastWords = 6) {
|
|
13806
|
-
const
|
|
13807
|
-
|
|
13930
|
+
const all = text.toLowerCase().replace(/[^\p{L}\p{N}'\s.!?]+/gu, " ");
|
|
13931
|
+
const sentence = all.split(/[.!?]/).at(-1).split(/\s+/).filter(Boolean);
|
|
13932
|
+
const t = sentence.slice(-lastWords).join(" ");
|
|
13933
|
+
return /\b(can|could|would|will|do|does|did|is|are|should)\s+(you|we|i|it|there|they|he|she)\b/.test(t) || /(^|\s)(האם|אתה יכול|את יכולה)(\s|$)/u.test(t) || /\b(what|why|how|where|when|who|which)\s+(do|does|did|is|are|should|would|can|could|will)\s+(you|we|i|it|they|he|she)\b/.test(sentence.slice(-4 * lastWords).join(" "));
|
|
13808
13934
|
}
|
|
13809
13935
|
var SCRIPTS = [
|
|
13810
13936
|
[new RegExp("\\p{Script=Hebrew}", "gu"), "he"],
|
|
@@ -13862,16 +13988,16 @@ function fitsLanguage(line, lang) {
|
|
|
13862
13988
|
const ws = line.toLowerCase().match(new RegExp("\\p{L}+", "gu")) ?? [];
|
|
13863
13989
|
return !ws.some((w) => !mine?.has(w) && Object.entries(STOP3).some(([l, s]) => l !== lang && s.has(w)));
|
|
13864
13990
|
}
|
|
13865
|
-
var
|
|
13991
|
+
var norm3 = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}' ]/gu, "").replace(/\s+/g, " ").trim();
|
|
13866
13992
|
function cleanLine(s, lang, maxWords = 6) {
|
|
13867
13993
|
const raw = s.trim().split("\n")[0];
|
|
13868
13994
|
const l = raw.replace(/^[-*•\d.)\s]+/, "").replace(/^["'“”«»]+|["'“”«»]+$/g, "").replace(new RegExp("^\\p{L}+:\\s*", "u"), "").trim();
|
|
13869
13995
|
if (!l || isLeakSuspect(raw) || isLeakSuspect(l)) {
|
|
13870
|
-
if (l)
|
|
13996
|
+
if (l) log30.debug(`line "${raw}" reads as meta \u2014 rejected`);
|
|
13871
13997
|
return "";
|
|
13872
13998
|
}
|
|
13873
13999
|
if (lang && !fitsLanguage(l, lang)) {
|
|
13874
|
-
|
|
14000
|
+
log30.debug(`line "${l}" is not ${lang} \u2014 rejected`);
|
|
13875
14001
|
return "";
|
|
13876
14002
|
}
|
|
13877
14003
|
return l.split(/\s+/).length > maxWords || l.length > 60 ? "" : l;
|
|
@@ -13883,9 +14009,9 @@ function validateBank(raw, lang, minPerKind = 3) {
|
|
|
13883
14009
|
for (const k of BANK_KEYS) {
|
|
13884
14010
|
const v = raw[k];
|
|
13885
14011
|
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(
|
|
14012
|
+
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
14013
|
if (out[k].length < minPerKind) {
|
|
13888
|
-
|
|
14014
|
+
log30.warn(`bank ${lang}: only ${out[k].length} valid ${k} lines \u2014 rejected`);
|
|
13889
14015
|
return null;
|
|
13890
14016
|
}
|
|
13891
14017
|
}
|
|
@@ -13955,11 +14081,11 @@ var ChannelBanks = class {
|
|
|
13955
14081
|
const f = this.file(lang);
|
|
13956
14082
|
let cached = null;
|
|
13957
14083
|
if (f) cached = await readFile(f, "utf8").then((s) => JSON.parse(s)).catch((e) => {
|
|
13958
|
-
if (e.code !== "ENOENT")
|
|
14084
|
+
if (e.code !== "ENOENT") log30.warn(`bank cache ${f} unreadable \u2014 regenerating: ${e.message}`);
|
|
13959
14085
|
return null;
|
|
13960
14086
|
});
|
|
13961
14087
|
if (cached && cached.v !== BANK_VERSION) {
|
|
13962
|
-
|
|
14088
|
+
log30.debug(`bank ${lang}: cache v${cached.v ?? 1} predates v${BANK_VERSION} \u2014 regenerating`);
|
|
13963
14089
|
cached = null;
|
|
13964
14090
|
}
|
|
13965
14091
|
const valid = cached && validateBank(cached.bank, lang, 0);
|
|
@@ -13967,7 +14093,7 @@ var ChannelBanks = class {
|
|
|
13967
14093
|
if (valid) {
|
|
13968
14094
|
for (const k of BANK_KEYS) if (!valid[k].length) valid[k] = [...this.o.fallback[lang]?.[k] ?? []];
|
|
13969
14095
|
this.banks.set(lang, valid);
|
|
13970
|
-
|
|
14096
|
+
log30.debug(`bank ${lang}: cache hit (${BANK_KEYS.map((k) => valid[k].length).join("/")})${complete ? "" : " \u2014 missing kinds, refreshing"}`);
|
|
13971
14097
|
}
|
|
13972
14098
|
if (complete && this.o.now() - cached.at < this.o.refreshMs) return;
|
|
13973
14099
|
await this.generate(lang, valid);
|
|
@@ -13983,14 +14109,14 @@ var ChannelBanks = class {
|
|
|
13983
14109
|
const bank = {};
|
|
13984
14110
|
for (const k of BANK_KEYS) {
|
|
13985
14111
|
const seen = /* @__PURE__ */ new Set();
|
|
13986
|
-
bank[k] = [...fresh[k], ...have?.[k] ?? []].filter((x) => !seen.has(
|
|
14112
|
+
bank[k] = [...fresh[k], ...have?.[k] ?? []].filter((x) => !seen.has(norm3(x)) && seen.add(norm3(x))).slice(0, this.o.maxPerKind);
|
|
13987
14113
|
}
|
|
13988
14114
|
this.banks.set(lang, bank);
|
|
13989
|
-
|
|
14115
|
+
log30.debug(`bank ${lang}: ${have ? "refreshed" : "generated"} (${BANK_KEYS.map((k) => bank[k].length).join("/")}) in ${Math.round(this.o.now() - t0)}ms`);
|
|
13990
14116
|
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) =>
|
|
14117
|
+
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
14118
|
} catch (e) {
|
|
13993
|
-
|
|
14119
|
+
log30.warn(`bank ${lang}: generation failed \u2014 ${have ? "keeping the cached bank" : lang in this.o.fallback ? "fallback bank" : "silent"}: ${e.message}`);
|
|
13994
14120
|
} finally {
|
|
13995
14121
|
clearTimeout(t);
|
|
13996
14122
|
if (!this.banks.has(lang)) setTimeout(() => this.pending.delete(lang), this.o.retryMs).unref?.();
|
|
@@ -13999,7 +14125,7 @@ var ChannelBanks = class {
|
|
|
13999
14125
|
};
|
|
14000
14126
|
|
|
14001
14127
|
// src/voice/backchannelPlan.ts
|
|
14002
|
-
var
|
|
14128
|
+
var log31 = forComponent("BackchannelPlan");
|
|
14003
14129
|
var TURN_TYPES = ["story", "list", "complaint", "explanation", "short_command", "question", "other"];
|
|
14004
14130
|
var DENSITIES = ["none", "low", "normal", "high"];
|
|
14005
14131
|
var silentTurn = (p) => p.density === "none" || p.turn === "short_command" || p.turn === "question";
|
|
@@ -14064,13 +14190,13 @@ Reply JSON only: {"turn":"...","density":"...","cues":[{"when":["phrase",...],"l
|
|
|
14064
14190
|
const reasoning = /gpt-oss/.test(o.model) ? { providerOptions: { reasoning_effort: "low" } } : {};
|
|
14065
14191
|
const r = await this.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false, signal, maxTokens: o.maxTokens, ...reasoning });
|
|
14066
14192
|
const p = parsePlan(r.content ?? "", c.lang);
|
|
14067
|
-
if (!p)
|
|
14193
|
+
if (!p) log31.debug(`unusable plan: ${(r.content ?? "").slice(0, 120)}`);
|
|
14068
14194
|
return p;
|
|
14069
14195
|
}
|
|
14070
14196
|
};
|
|
14071
14197
|
|
|
14072
14198
|
// src/voice/secondChannel.ts
|
|
14073
|
-
var
|
|
14199
|
+
var log32 = forComponent("SecondChannel");
|
|
14074
14200
|
var CHANNEL_MODES = {
|
|
14075
14201
|
turn: { kinds: ["bridge"], bridgeDelayMs: 1500 },
|
|
14076
14202
|
normal: {},
|
|
@@ -14180,13 +14306,13 @@ function withBudget(p, ms, what, ac) {
|
|
|
14180
14306
|
let t;
|
|
14181
14307
|
const late = new Promise((r) => {
|
|
14182
14308
|
t = setTimeout(() => {
|
|
14183
|
-
|
|
14309
|
+
log32.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
|
|
14184
14310
|
ac?.abort();
|
|
14185
14311
|
r(null);
|
|
14186
14312
|
}, ms);
|
|
14187
14313
|
});
|
|
14188
14314
|
return Promise.race([p.catch((e) => {
|
|
14189
|
-
|
|
14315
|
+
log32.warn(`${what} failed \u2014 falling back: ${e?.message ?? e}`);
|
|
14190
14316
|
return null;
|
|
14191
14317
|
}), late]).finally(() => clearTimeout(t));
|
|
14192
14318
|
}
|
|
@@ -14258,7 +14384,7 @@ var SecondChannel = class {
|
|
|
14258
14384
|
if (host) this.pinned = true;
|
|
14259
14385
|
const lang = primaryLang(tag);
|
|
14260
14386
|
if (!lang || lang === this.lang) return;
|
|
14261
|
-
|
|
14387
|
+
log32.debug(`language ${this.lang} \u2192 ${lang}`);
|
|
14262
14388
|
this.lang = lang;
|
|
14263
14389
|
this.recent = [];
|
|
14264
14390
|
this.picks.clear();
|
|
@@ -14276,12 +14402,7 @@ var SecondChannel = class {
|
|
|
14276
14402
|
}
|
|
14277
14403
|
get o() {
|
|
14278
14404
|
const o = this.options;
|
|
14279
|
-
|
|
14280
|
-
if (!this.degradedLogged) {
|
|
14281
|
-
this.degradedLogged = true;
|
|
14282
|
-
log31.info("chatty mode without echo cancellation \u2014 normal timing (our lines would re-trigger the mic)");
|
|
14283
|
-
}
|
|
14284
|
-
return { ...o, ...this.normalTiming };
|
|
14405
|
+
return o.mode !== "chatty" || o.aec ? o : { ...o, ...this.normalTiming };
|
|
14285
14406
|
}
|
|
14286
14407
|
/** The user's live partial for the current turn (the whole partial, not a delta). */
|
|
14287
14408
|
onInterim(text) {
|
|
@@ -14394,7 +14515,7 @@ var SecondChannel = class {
|
|
|
14394
14515
|
try {
|
|
14395
14516
|
this.o.cancel();
|
|
14396
14517
|
} catch (e) {
|
|
14397
|
-
|
|
14518
|
+
log32.warn(`cancel threw: ${e.message}`);
|
|
14398
14519
|
}
|
|
14399
14520
|
}
|
|
14400
14521
|
}
|
|
@@ -14465,6 +14586,11 @@ var SecondChannel = class {
|
|
|
14465
14586
|
const n0 = words2(this.heard).length;
|
|
14466
14587
|
this.decidedAt = n0;
|
|
14467
14588
|
if (!(this.o.anticipate && this.plan && trigger === "interim")) cue = null;
|
|
14589
|
+
const opts = this.options;
|
|
14590
|
+
if (opts.mode === "chatty" && !opts.aec && !this.degradedLogged) {
|
|
14591
|
+
this.degradedLogged = true;
|
|
14592
|
+
log32.info("chatty mode without echo cancellation \u2014 normal timing (our lines would re-trigger the mic)");
|
|
14593
|
+
}
|
|
14468
14594
|
const rule = this.rule(trigger, pauseMs, breath, cue);
|
|
14469
14595
|
if (rule === "silence") return;
|
|
14470
14596
|
this.busy = true;
|
|
@@ -14479,19 +14605,19 @@ var SecondChannel = class {
|
|
|
14479
14605
|
const d = await withBudget(this.o.decide(this.tail(), { ...this.state(pauseMs), rule }, ac.signal), this.o.decisionBudgetMs, "channel decision", ac);
|
|
14480
14606
|
choice = d?.choice;
|
|
14481
14607
|
if (choice && choice in CHANNEL_ACTIONS && (choice === "silence" || this.allowed(choice))) kind = choice;
|
|
14482
|
-
else if (choice && choice in CHANNEL_ACTIONS)
|
|
14608
|
+
else if (choice && choice in CHANNEL_ACTIONS) log32.debug(`decider chose ${choice} for a ${rule} moment \u2014 the rule stands`);
|
|
14483
14609
|
}
|
|
14484
14610
|
if (kind === "silence") return ev({ choice, acted: "silence" });
|
|
14485
14611
|
if (stale()) return ev({ choice, kind, acted: "stale" });
|
|
14486
14612
|
if (!this.allowed(kind)) return ev({ choice, kind, acted: "gated" });
|
|
14487
14613
|
const react = kind === "backchannel" && this.o.now() - this.lastReactAt >= this.o.reactionGapMs;
|
|
14488
|
-
const text = cue && react && !this.recent.some((r) =>
|
|
14614
|
+
const text = cue && react && !this.recent.some((r) => norm3(r) === norm3(cue.line)) ? cue.line : await this.line(kind, react);
|
|
14489
14615
|
if (!text) return ev({ choice, kind, acted: "no_bank" });
|
|
14490
14616
|
if (stale()) return ev({ choice, kind, text, acted: "stale" });
|
|
14491
14617
|
if (!this.allowed(kind)) return ev({ choice, kind, text, acted: "gated" });
|
|
14492
14618
|
const said = this.say(kind, text);
|
|
14493
14619
|
if (said && cue) this.cueFrom = words2(this.heard).length;
|
|
14494
|
-
if (said && kind === "backchannel" && this.o.reactionGapMs && !this.bank?.backchannel.some((b) =>
|
|
14620
|
+
if (said && kind === "backchannel" && this.o.reactionGapMs && !this.bank?.backchannel.some((b) => norm3(b) === norm3(text))) this.lastReactAt = this.o.now();
|
|
14495
14621
|
ev({ choice, kind, text, acted: said ? "spoken" : "skipped" });
|
|
14496
14622
|
} finally {
|
|
14497
14623
|
this.busy = false;
|
|
@@ -14515,8 +14641,8 @@ var SecondChannel = class {
|
|
|
14515
14641
|
const raw = await withBudget(this.o.generate(ctx, ac.signal), this.o.generateBudgetMs, `${kind} generate`);
|
|
14516
14642
|
if (raw === null) ac.abort();
|
|
14517
14643
|
const text = cleanLine(raw ?? "", this.lang);
|
|
14518
|
-
if (text && !this.recent.some((r) =>
|
|
14519
|
-
if (text)
|
|
14644
|
+
if (text && !this.recent.some((r) => norm3(r) === norm3(text))) return text;
|
|
14645
|
+
if (text) log32.debug(`generated "${text}" repeats a recent line \u2014 canned`);
|
|
14520
14646
|
}
|
|
14521
14647
|
return this.pick(kind);
|
|
14522
14648
|
}
|
|
@@ -14526,8 +14652,8 @@ var SecondChannel = class {
|
|
|
14526
14652
|
const pool = this.bank?.[kind] ?? [];
|
|
14527
14653
|
if (!pool.length) return "";
|
|
14528
14654
|
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(
|
|
14655
|
+
const recent = new Set([...hist.slice(-this.o.bankExclude), ...this.recent].map(norm3));
|
|
14656
|
+
const fresh = pool.filter((x) => !recent.has(norm3(x)));
|
|
14531
14657
|
const from = fresh.length ? fresh : pool;
|
|
14532
14658
|
const p = from[Math.floor(this.o.random() * from.length)];
|
|
14533
14659
|
this.picks.set(kind, [...hist, p].slice(-this.o.bankExclude));
|
|
@@ -14538,7 +14664,7 @@ var SecondChannel = class {
|
|
|
14538
14664
|
try {
|
|
14539
14665
|
r = this.o.speak(text, kind);
|
|
14540
14666
|
} catch (e) {
|
|
14541
|
-
|
|
14667
|
+
log32.warn(`speak threw \u2014 line dropped: ${e.message}`);
|
|
14542
14668
|
return false;
|
|
14543
14669
|
}
|
|
14544
14670
|
if (r === false) return false;
|
|
@@ -14550,7 +14676,7 @@ var SecondChannel = class {
|
|
|
14550
14676
|
this.o.show(text, kind);
|
|
14551
14677
|
const p = Promise.resolve(r).then(() => {
|
|
14552
14678
|
}, (e) => {
|
|
14553
|
-
|
|
14679
|
+
log32.warn(`channel line playback failed: ${e?.message ?? e}`);
|
|
14554
14680
|
}).finally(() => {
|
|
14555
14681
|
if (this.inflight === p) this.inflight = null;
|
|
14556
14682
|
});
|
|
@@ -14747,7 +14873,7 @@ function applyOverlay(cfg, file) {
|
|
|
14747
14873
|
// cli/hooks-config.ts
|
|
14748
14874
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
14749
14875
|
init_platform();
|
|
14750
|
-
var
|
|
14876
|
+
var log33 = forComponent("hooks");
|
|
14751
14877
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
14752
14878
|
function ruleMatches(rule, toolName) {
|
|
14753
14879
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -14766,7 +14892,7 @@ function runCmd(rule, env) {
|
|
|
14766
14892
|
});
|
|
14767
14893
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
14768
14894
|
} catch (e) {
|
|
14769
|
-
|
|
14895
|
+
log33.debug(`hook command failed: ${rule.command}`, e);
|
|
14770
14896
|
return { code: 1, out: String(e?.message ?? e) };
|
|
14771
14897
|
}
|
|
14772
14898
|
}
|
|
@@ -14874,7 +15000,7 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as rea
|
|
|
14874
15000
|
import { homedir as homedir6 } from "os";
|
|
14875
15001
|
import { join as join11 } from "path";
|
|
14876
15002
|
init_platform();
|
|
14877
|
-
var
|
|
15003
|
+
var log34 = forComponent("session");
|
|
14878
15004
|
var globalDir = () => join11(homedir6(), ".agent", "sessions");
|
|
14879
15005
|
function indexLink(target, link2) {
|
|
14880
15006
|
try {
|
|
@@ -14937,7 +15063,7 @@ var SessionStore = class {
|
|
|
14937
15063
|
}
|
|
14938
15064
|
load(id) {
|
|
14939
15065
|
if (!this.safeId(id)) {
|
|
14940
|
-
|
|
15066
|
+
log34.debug(`rejecting unsafe session id: ${id}`);
|
|
14941
15067
|
return void 0;
|
|
14942
15068
|
}
|
|
14943
15069
|
const path = join11(this.dir, `${id}.json`);
|
|
@@ -14945,7 +15071,7 @@ var SessionStore = class {
|
|
|
14945
15071
|
try {
|
|
14946
15072
|
return JSON.parse(readFileSync8(path, "utf8"));
|
|
14947
15073
|
} catch (e) {
|
|
14948
|
-
|
|
15074
|
+
log34.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
14949
15075
|
return void 0;
|
|
14950
15076
|
}
|
|
14951
15077
|
}
|
|
@@ -14958,7 +15084,7 @@ var SessionStore = class {
|
|
|
14958
15084
|
try {
|
|
14959
15085
|
metas.push(JSON.parse(readFileSync8(join11(this.dir, f), "utf8")).meta);
|
|
14960
15086
|
} catch (e) {
|
|
14961
|
-
|
|
15087
|
+
log34.debug(`skipping unreadable session file ${f}`, e);
|
|
14962
15088
|
}
|
|
14963
15089
|
}
|
|
14964
15090
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -15197,7 +15323,7 @@ import { promisify } from "util";
|
|
|
15197
15323
|
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync9 } from "fs";
|
|
15198
15324
|
import { homedir as homedir7 } from "os";
|
|
15199
15325
|
import { join as join12, resolve as resolve4, sep as sep3, parse as parsePath } from "path";
|
|
15200
|
-
var
|
|
15326
|
+
var log35 = forComponent("checkpoints");
|
|
15201
15327
|
var exec = promisify(execFile4);
|
|
15202
15328
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
15203
15329
|
function isBroadRoot(p) {
|
|
@@ -15243,7 +15369,7 @@ var ShadowRepo = class {
|
|
|
15243
15369
|
writeFileSync6(join12(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
15244
15370
|
this.ready = true;
|
|
15245
15371
|
} catch (e) {
|
|
15246
|
-
|
|
15372
|
+
log35.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
15247
15373
|
this.ready = false;
|
|
15248
15374
|
}
|
|
15249
15375
|
return this.ready;
|
|
@@ -15254,7 +15380,7 @@ var ShadowRepo = class {
|
|
|
15254
15380
|
}
|
|
15255
15381
|
async commit(label, forced = []) {
|
|
15256
15382
|
await this.run("add", "-A");
|
|
15257
|
-
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) =>
|
|
15383
|
+
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log35.debug(`force-add failed: ${p}`, e));
|
|
15258
15384
|
await this.run("commit", "--allow-empty", "-q", "-m", label);
|
|
15259
15385
|
}
|
|
15260
15386
|
/** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
|
|
@@ -15262,8 +15388,8 @@ var ShadowRepo = class {
|
|
|
15262
15388
|
* turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
|
|
15263
15389
|
* point per turn, so the REPL's turn↔frame mapping stays intact. */
|
|
15264
15390
|
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) =>
|
|
15391
|
+
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log35.debug(`force-capture failed: ${p}`, e));
|
|
15392
|
+
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log35.debug("amend failed", e));
|
|
15267
15393
|
}
|
|
15268
15394
|
/** Commits on `ref`, oldest-first (canonical index space). */
|
|
15269
15395
|
async log(ref) {
|
|
@@ -15323,7 +15449,7 @@ var ShadowRepo = class {
|
|
|
15323
15449
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
15324
15450
|
});
|
|
15325
15451
|
} catch (e) {
|
|
15326
|
-
|
|
15452
|
+
log35.debug("checkpoint prune failed", e);
|
|
15327
15453
|
}
|
|
15328
15454
|
}
|
|
15329
15455
|
};
|
|
@@ -15391,7 +15517,7 @@ var GitCheckpoints = class {
|
|
|
15391
15517
|
use(sessionId) {
|
|
15392
15518
|
if (sessionId === this.session) return;
|
|
15393
15519
|
this.session = sessionId;
|
|
15394
|
-
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) =>
|
|
15520
|
+
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log35.debug("re-point failed", e));
|
|
15395
15521
|
}
|
|
15396
15522
|
async begin(label) {
|
|
15397
15523
|
if (!await this.start()) return;
|
|
@@ -15411,7 +15537,7 @@ var GitCheckpoints = class {
|
|
|
15411
15537
|
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
15538
|
`);
|
|
15413
15539
|
} else {
|
|
15414
|
-
|
|
15540
|
+
log35.debug("checkpoint commit failed", e);
|
|
15415
15541
|
survivors.push(this.repos[i]);
|
|
15416
15542
|
survivorCaches.push(this.caches[i] ?? []);
|
|
15417
15543
|
}
|
|
@@ -15446,7 +15572,7 @@ var GitCheckpoints = class {
|
|
|
15446
15572
|
if (this.forced.has(abs)) continue;
|
|
15447
15573
|
this.forced.add(abs);
|
|
15448
15574
|
const i = this.repoIndexFor(abs);
|
|
15449
|
-
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) =>
|
|
15575
|
+
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log35.debug("amendForced failed", e));
|
|
15450
15576
|
}
|
|
15451
15577
|
}
|
|
15452
15578
|
};
|
|
@@ -17231,7 +17357,7 @@ import { spawnSync as spawnSync6 } from "child_process";
|
|
|
17231
17357
|
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
|
|
17232
17358
|
import { homedir as homedir8 } from "os";
|
|
17233
17359
|
import { join as join14 } from "path";
|
|
17234
|
-
var
|
|
17360
|
+
var log36 = forComponent("os-sched");
|
|
17235
17361
|
var OsScheduler = class {
|
|
17236
17362
|
options;
|
|
17237
17363
|
constructor(options) {
|
|
@@ -17293,7 +17419,7 @@ var OsScheduler = class {
|
|
|
17293
17419
|
}
|
|
17294
17420
|
}
|
|
17295
17421
|
} catch (e) {
|
|
17296
|
-
|
|
17422
|
+
log36.debug(`cancel ${id}`, e);
|
|
17297
17423
|
}
|
|
17298
17424
|
for (const f of [`${id}.json`, `${id}.sh`]) {
|
|
17299
17425
|
try {
|
|
@@ -17411,7 +17537,7 @@ import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unli
|
|
|
17411
17537
|
import { homedir as homedir9 } from "os";
|
|
17412
17538
|
import { join as join15 } from "path";
|
|
17413
17539
|
init_platform();
|
|
17414
|
-
var
|
|
17540
|
+
var log37 = forComponent("remote-trigger");
|
|
17415
17541
|
var TRIGGER_DIR = () => join15(homedir9(), ".agent", "triggers");
|
|
17416
17542
|
var sockPath = (sessionId, dir = TRIGGER_DIR()) => join15(dir, `${sessionId}.sock`);
|
|
17417
17543
|
var TriggerServer = class {
|
|
@@ -17449,13 +17575,13 @@ var TriggerServer = class {
|
|
|
17449
17575
|
conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
|
|
17450
17576
|
}
|
|
17451
17577
|
});
|
|
17452
|
-
conn.on("error", (e) =>
|
|
17578
|
+
conn.on("error", (e) => log37.debug("trigger conn error", e));
|
|
17453
17579
|
});
|
|
17454
|
-
this.server.on("error", (e) =>
|
|
17580
|
+
this.server.on("error", (e) => log37.debug("trigger server error", e));
|
|
17455
17581
|
this.server.listen(p);
|
|
17456
17582
|
this.path = p;
|
|
17457
17583
|
} catch (e) {
|
|
17458
|
-
|
|
17584
|
+
log37.debug("trigger server unavailable", e);
|
|
17459
17585
|
}
|
|
17460
17586
|
}
|
|
17461
17587
|
/** Re-bind on /resume (the session id changed). */
|
|
@@ -17587,7 +17713,7 @@ var strike = C("9");
|
|
|
17587
17713
|
var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
|
|
17588
17714
|
var mdPalette = { bold, dim, cyan, italic, strike, green, link };
|
|
17589
17715
|
var err = (s) => process.stderr.write(s);
|
|
17590
|
-
var
|
|
17716
|
+
var log38 = forComponent("cli");
|
|
17591
17717
|
var VERSION = (() => {
|
|
17592
17718
|
try {
|
|
17593
17719
|
return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -18436,7 +18562,7 @@ function turnCost(model, usage) {
|
|
|
18436
18562
|
const priced = model.startsWith("claude-code/") ? `anthropic/${resolveModelAlias(model.slice("claude-code/".length))}` : model;
|
|
18437
18563
|
return costOf(getModelInfo(priced)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, priced);
|
|
18438
18564
|
}
|
|
18439
|
-
async function evaluateGoal(ai, condition, transcript,
|
|
18565
|
+
async function evaluateGoal(ai, condition, transcript, log39) {
|
|
18440
18566
|
const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
|
|
18441
18567
|
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
18442
18568
|
return text.slice(0, 600);
|
|
@@ -18456,7 +18582,7 @@ ${recent}` }
|
|
|
18456
18582
|
const match = r.content.match(/\{[\s\S]*\}/);
|
|
18457
18583
|
if (match) return JSON.parse(match[0]);
|
|
18458
18584
|
} catch (e) {
|
|
18459
|
-
|
|
18585
|
+
log39(dim(` (goal evaluator error: ${e?.message ?? e})
|
|
18460
18586
|
`));
|
|
18461
18587
|
}
|
|
18462
18588
|
return { met: false, reason: "evaluation unclear" };
|
|
@@ -18682,7 +18808,7 @@ function mcpAgentTools(mounted, opts) {
|
|
|
18682
18808
|
return tools;
|
|
18683
18809
|
}
|
|
18684
18810
|
async function closeMcp(mounted) {
|
|
18685
|
-
await Promise.all(mounted.map((m) => m.client.close().catch((e) =>
|
|
18811
|
+
await Promise.all(mounted.map((m) => m.client.close().catch((e) => log38.debug("mcp close failed", e))));
|
|
18686
18812
|
}
|
|
18687
18813
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
18688
18814
|
function mentionRefs(line) {
|
|
@@ -18741,7 +18867,7 @@ async function expandMentions(fs, line) {
|
|
|
18741
18867
|
if (loaded.includes(ref) || missing.includes(ref)) continue;
|
|
18742
18868
|
if (ref.includes(":") && mcpMentionResolver) {
|
|
18743
18869
|
const body = await mcpMentionResolver(ref).catch((e) => {
|
|
18744
|
-
|
|
18870
|
+
log38.debug("mcp mention resolve failed", e);
|
|
18745
18871
|
return null;
|
|
18746
18872
|
});
|
|
18747
18873
|
if (body != null) {
|
|
@@ -18832,7 +18958,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
18832
18958
|
try {
|
|
18833
18959
|
store.save(session);
|
|
18834
18960
|
} catch (ex) {
|
|
18835
|
-
|
|
18961
|
+
log38.debug("mid-turn session flush failed", ex);
|
|
18836
18962
|
}
|
|
18837
18963
|
}
|
|
18838
18964
|
return origNotify(e);
|
|
@@ -18996,14 +19122,14 @@ var isCancelTeardown = (e) => {
|
|
|
18996
19122
|
function installCancelGuards(mounted) {
|
|
18997
19123
|
process.on("unhandledRejection", (e) => {
|
|
18998
19124
|
if (isCancelTeardown(e)) {
|
|
18999
|
-
|
|
19125
|
+
log38.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
19000
19126
|
return;
|
|
19001
19127
|
}
|
|
19002
|
-
|
|
19128
|
+
log38.error("unhandledRejection", e);
|
|
19003
19129
|
});
|
|
19004
19130
|
process.on("uncaughtException", (e) => {
|
|
19005
19131
|
if (isCancelTeardown(e)) {
|
|
19006
|
-
|
|
19132
|
+
log38.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
19007
19133
|
return;
|
|
19008
19134
|
}
|
|
19009
19135
|
console.error(e);
|
|
@@ -19067,6 +19193,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
19067
19193
|
const decider = duplex ? leakDecider() : void 0;
|
|
19068
19194
|
const instinct = decider && process.env.VOICE_INSTINCT === "1" ? new Instinct({
|
|
19069
19195
|
decider,
|
|
19196
|
+
overlapModel: overlapModelFor(ai),
|
|
19070
19197
|
context: () => ({ agentSaid: dx?.recentExcerpt(1).replace(/^\w+: /, ""), recent: dx?.recentExcerpt(4) }),
|
|
19071
19198
|
onDecision: ({ kind, ...d }) => {
|
|
19072
19199
|
voiceDiag?.push({ t: performance.now(), kind: `instinct_${kind}`, ...d });
|
|
@@ -19432,7 +19559,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
19432
19559
|
try {
|
|
19433
19560
|
mkdirSync12(dir, { recursive: true });
|
|
19434
19561
|
} catch (e) {
|
|
19435
|
-
|
|
19562
|
+
log38.debug("paste image dir create failed", e);
|
|
19436
19563
|
}
|
|
19437
19564
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
19438
19565
|
return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
|
|
@@ -19545,7 +19672,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
19545
19672
|
mkdirSync12(join16(cwd, ".agent"), { recursive: true });
|
|
19546
19673
|
appendFileSync(histPath, line + "\n");
|
|
19547
19674
|
} catch (e) {
|
|
19548
|
-
|
|
19675
|
+
log38.debug("history write failed", e);
|
|
19549
19676
|
}
|
|
19550
19677
|
};
|
|
19551
19678
|
const ago = (t) => {
|
|
@@ -19617,7 +19744,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
19617
19744
|
try {
|
|
19618
19745
|
store.save(session);
|
|
19619
19746
|
} catch (e) {
|
|
19620
|
-
|
|
19747
|
+
log38.debug("session save after rewind failed", e);
|
|
19621
19748
|
}
|
|
19622
19749
|
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
19623
19750
|
`));
|
|
@@ -19652,7 +19779,7 @@ ${task}`;
|
|
|
19652
19779
|
bangContext.length = 0;
|
|
19653
19780
|
}
|
|
19654
19781
|
const delta = await refreshCatalogs().catch((e) => {
|
|
19655
|
-
|
|
19782
|
+
log38.debug("catalog refresh failed", e);
|
|
19656
19783
|
return "";
|
|
19657
19784
|
});
|
|
19658
19785
|
if (delta) {
|
|
@@ -19915,7 +20042,7 @@ ${task}`;
|
|
|
19915
20042
|
desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
|
|
19916
20043
|
run: async () => {
|
|
19917
20044
|
await refreshCatalogs().catch((e) => {
|
|
19918
|
-
|
|
20045
|
+
log38.debug("catalog refresh failed", e);
|
|
19919
20046
|
});
|
|
19920
20047
|
face.reprepare();
|
|
19921
20048
|
err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
|
|
@@ -20418,7 +20545,7 @@ ${task}`;
|
|
|
20418
20545
|
try {
|
|
20419
20546
|
for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
|
|
20420
20547
|
} catch (e) {
|
|
20421
|
-
|
|
20548
|
+
log38.debug(`loadAgents(${d}) failed`, e);
|
|
20422
20549
|
}
|
|
20423
20550
|
}
|
|
20424
20551
|
if (!seen.size) {
|
|
@@ -20506,7 +20633,7 @@ ${task}`;
|
|
|
20506
20633
|
}
|
|
20507
20634
|
if (idx >= 0) {
|
|
20508
20635
|
const old = mounted.splice(idx, 1)[0];
|
|
20509
|
-
await old.client.close().catch((e) =>
|
|
20636
|
+
await old.client.close().catch((e) => log38.debug("mcp close failed", e));
|
|
20510
20637
|
}
|
|
20511
20638
|
try {
|
|
20512
20639
|
const m = await mountMcpServer(name, conf);
|
|
@@ -20534,7 +20661,7 @@ ${task}`;
|
|
|
20534
20661
|
}
|
|
20535
20662
|
const m = mounted.splice(idx, 1)[0];
|
|
20536
20663
|
remountMcpTools();
|
|
20537
|
-
await m.client.close().catch((e) =>
|
|
20664
|
+
await m.client.close().catch((e) => log38.debug("mcp close failed", e));
|
|
20538
20665
|
err(dim(` removed "${name}"
|
|
20539
20666
|
`));
|
|
20540
20667
|
return;
|
|
@@ -20737,7 +20864,7 @@ ${task}`;
|
|
|
20737
20864
|
try {
|
|
20738
20865
|
return readdirSync4(join16(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
20739
20866
|
} catch (e) {
|
|
20740
|
-
|
|
20867
|
+
log38.debug("completion readdir failed", absDir, e);
|
|
20741
20868
|
return null;
|
|
20742
20869
|
}
|
|
20743
20870
|
};
|
|
@@ -20874,7 +21001,7 @@ ${out}
|
|
|
20874
21001
|
return;
|
|
20875
21002
|
}
|
|
20876
21003
|
await refreshCatalogs().catch((e) => {
|
|
20877
|
-
|
|
21004
|
+
log38.debug("catalog refresh failed", e);
|
|
20878
21005
|
});
|
|
20879
21006
|
const sk = skills.find((x) => x.name === name);
|
|
20880
21007
|
if (sk) {
|
|
@@ -20993,7 +21120,7 @@ ${out}
|
|
|
20993
21120
|
if (delivery || cut) dx?.parkCutDelivery(delivery, !!cut, heard);
|
|
20994
21121
|
const cutReply = cut && cut.full.length - cut.heard.length > 40 ? cut : null;
|
|
20995
21122
|
const noteP = replaces ? replaces.note : !cutReply ? Promise.resolve("") : (instinct ? instinct.bargeIntent(text).catch((e) => {
|
|
20996
|
-
|
|
21123
|
+
log38.warn(`barge intent failed \u2014 model decides: ${e.message}`);
|
|
20997
21124
|
return void 0;
|
|
20998
21125
|
}) : Promise.resolve(void 0)).then(async (intent) => interruptionNote(cutReply, intent) + ackedNote(await instinct?.ackFor(text)));
|
|
20999
21126
|
const cutIn = replaces ? replaces.cutIn : interjectNote;
|
|
@@ -21013,6 +21140,10 @@ ${out}
|
|
|
21013
21140
|
}).finally(() => editorRef?.redrawNow());
|
|
21014
21141
|
}
|
|
21015
21142
|
});
|
|
21143
|
+
if (!instinct && decider && args.duplexV2 && softBarge(args.channelMode)) {
|
|
21144
|
+
const v = voiceIO;
|
|
21145
|
+
bindOverlapInstinct(v, new Instinct({ decider, overlapModel: overlapModelFor(ai) }), () => voiceIO === v);
|
|
21146
|
+
}
|
|
21016
21147
|
if (instinct) {
|
|
21017
21148
|
const v = voiceIO, o = v.options;
|
|
21018
21149
|
const { onPartial } = o;
|
|
@@ -21023,7 +21154,7 @@ ${out}
|
|
|
21023
21154
|
if (listening()) instinct.hear(text);
|
|
21024
21155
|
onPartial(text);
|
|
21025
21156
|
};
|
|
21026
|
-
bindOverlapInstinct(v, instinct, () => voiceIO === v);
|
|
21157
|
+
if (!args.duplexV2 || softBarge(args.channelMode)) bindOverlapInstinct(v, instinct, () => voiceIO === v);
|
|
21027
21158
|
bindAckInstinct(v, instinct);
|
|
21028
21159
|
o.onUtterance = (text, meta) => {
|
|
21029
21160
|
instinct.reset();
|
|
@@ -21362,10 +21493,17 @@ function makeSecondChannel(ai, reflexModel, mode = "normal", opts) {
|
|
|
21362
21493
|
// Anticipated backchannels: on in chatty mode (its preset), else off until the offline replay proves them (tests/voice-bc-anticipate-eval.ts)
|
|
21363
21494
|
plan: (c, s) => planner.plan(c, s),
|
|
21364
21495
|
...process.env.AGENTX_CHANNEL_ANTICIPATE === "1" ? { anticipate: true } : {},
|
|
21365
|
-
onEvent: (e) =>
|
|
21496
|
+
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
21497
|
...opts
|
|
21367
21498
|
});
|
|
21368
21499
|
}
|
|
21500
|
+
function overlapResponder(ai, mode, out) {
|
|
21501
|
+
const judge = softBarge(mode) ? new OverlapJudge({ model: overlapModelFor(ai) }) : void 0;
|
|
21502
|
+
return (m) => {
|
|
21503
|
+
const text = String(m.text ?? "");
|
|
21504
|
+
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 }));
|
|
21505
|
+
};
|
|
21506
|
+
}
|
|
21369
21507
|
async function duplexServe(args, ai, cfg, cwd) {
|
|
21370
21508
|
const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
|
|
21371
21509
|
const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join16(cwd, ".agent", "mcp-auth.json") }));
|
|
@@ -21424,10 +21562,11 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
21424
21562
|
try {
|
|
21425
21563
|
store.save(session);
|
|
21426
21564
|
} catch (e) {
|
|
21427
|
-
|
|
21565
|
+
log38.warn("duplex session save failed", e);
|
|
21428
21566
|
}
|
|
21429
21567
|
};
|
|
21430
|
-
|
|
21568
|
+
const overlap2 = overlapResponder(ai, args.duplexV2 ? args.channelMode : void 0, out);
|
|
21569
|
+
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
21570
|
const done = new Promise((resolveDone) => {
|
|
21432
21571
|
const rl = createInterface({ input: process.stdin });
|
|
21433
21572
|
rl.on("line", (line) => {
|
|
@@ -21442,7 +21581,8 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
21442
21581
|
dx.userActive();
|
|
21443
21582
|
activeTurn?.abort();
|
|
21444
21583
|
dx.parkInFlightDeliveries();
|
|
21445
|
-
} else if (
|
|
21584
|
+
} else if (m.type === "overlap") overlap2(m);
|
|
21585
|
+
else if (sc && channelInput(sc, m)) {
|
|
21446
21586
|
if (m.type === "interim" && String(m.text ?? "").trim()) dx.userActive();
|
|
21447
21587
|
} else if (m.type === "cancel") out({ type: "task", kind: "cancel_result", id: m.task, message: dx.cancelTask(String(m.task)) });
|
|
21448
21588
|
else if (m.type === "exit") rl.close();
|
|
@@ -21462,7 +21602,7 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
21462
21602
|
done2();
|
|
21463
21603
|
out({ type: "turn_done", id, finishReason: res?.finishReason ?? "error", ...why ? { error: why } : {} });
|
|
21464
21604
|
}).catch((e) => {
|
|
21465
|
-
|
|
21605
|
+
log38.error("duplex turn failed", e);
|
|
21466
21606
|
done2();
|
|
21467
21607
|
out({ type: "turn_done", id, finishReason: "error", error: String(e?.message ?? e) });
|
|
21468
21608
|
});
|
|
@@ -21481,7 +21621,8 @@ async function channelServe(args, ai, cfg) {
|
|
|
21481
21621
|
const { reflexModel, notes } = duplexModels({ voiceModel: args.voiceModel, reflexModel: cfg.reflexModel, thinkModel: false, actModel: String(args.model ?? cfg.model ?? ""), apiKeys: { ...cfg.apiKeys, ...apiKeysFromEnv() } });
|
|
21482
21622
|
const model = reflexModel ?? new DuplexAgentOptions().reflexModel;
|
|
21483
21623
|
const sc = makeSecondChannel(ai, model, args.channelMode, { show: (text, kind) => out({ type: "channel", kind, text }) });
|
|
21484
|
-
|
|
21624
|
+
const overlap2 = overlapResponder(ai, args.channelMode, out);
|
|
21625
|
+
out({ type: "ready", model, channelMode: args.channelMode, softBarge: softBarge(args.channelMode), ...notes.length ? { notes } : {} });
|
|
21485
21626
|
const rl = createInterface({ input: process.stdin });
|
|
21486
21627
|
rl.on("line", (line) => {
|
|
21487
21628
|
let m;
|
|
@@ -21494,6 +21635,7 @@ async function channelServe(args, ai, cfg) {
|
|
|
21494
21635
|
if (m.type === "user" && typeof m.text === "string") sc.onUserTurn(m.text);
|
|
21495
21636
|
else if (m.type === "text" && typeof m.text === "string") sc.onMainTokens(m.text);
|
|
21496
21637
|
else if (m.type === "barge") sc.onBargeIn();
|
|
21638
|
+
else if (m.type === "overlap") overlap2(m);
|
|
21497
21639
|
else if (m.type === "exit") rl.close();
|
|
21498
21640
|
});
|
|
21499
21641
|
await new Promise((r) => rl.on("close", () => r()));
|