@livx.cc/agentx 0.97.9 → 0.98.1
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 +341 -105
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +218 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1755,7 +1755,7 @@ function makeRealShellTool(options) {
|
|
|
1755
1755
|
proc.stderr?.on("data", collect);
|
|
1756
1756
|
proc.on("error", (err2) => {
|
|
1757
1757
|
if (err2?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
|
|
1758
|
-
|
|
1758
|
+
log14.debug("shell spawn error", err2);
|
|
1759
1759
|
finish(`[exit 1] ${err2?.message ?? err2}${out ? "\n" + clean(out) : ""}`);
|
|
1760
1760
|
});
|
|
1761
1761
|
proc.on("close", (code) => {
|
|
@@ -1817,7 +1817,7 @@ ${clean(out) || "(no output yet)"}`;
|
|
|
1817
1817
|
}
|
|
1818
1818
|
];
|
|
1819
1819
|
}
|
|
1820
|
-
var
|
|
1820
|
+
var log14, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
|
|
1821
1821
|
var init_tools_shell = __esm({
|
|
1822
1822
|
"src/tools.shell.ts"() {
|
|
1823
1823
|
"use strict";
|
|
@@ -1825,7 +1825,7 @@ var init_tools_shell = __esm({
|
|
|
1825
1825
|
init_redact();
|
|
1826
1826
|
init_logging();
|
|
1827
1827
|
init_shell_sandbox();
|
|
1828
|
-
|
|
1828
|
+
log14 = forComponent("shell");
|
|
1829
1829
|
clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
|
|
1830
1830
|
DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
|
|
1831
1831
|
SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
|
|
@@ -4582,6 +4582,188 @@ function digestRun(messages, maxChars) {
|
|
|
4582
4582
|
import { MemFilesystem as MemFilesystem2 } from "@livx.cc/wcli/core";
|
|
4583
4583
|
init_logging();
|
|
4584
4584
|
|
|
4585
|
+
// src/voice/emotion.ts
|
|
4586
|
+
init_logging();
|
|
4587
|
+
var log8 = forComponent("Emotion");
|
|
4588
|
+
var EMOTIONS = [
|
|
4589
|
+
// primary (best results)
|
|
4590
|
+
"neutral",
|
|
4591
|
+
"angry",
|
|
4592
|
+
"excited",
|
|
4593
|
+
"content",
|
|
4594
|
+
"sad",
|
|
4595
|
+
"scared",
|
|
4596
|
+
// extended
|
|
4597
|
+
"happy",
|
|
4598
|
+
"enthusiastic",
|
|
4599
|
+
"elated",
|
|
4600
|
+
"triumphant",
|
|
4601
|
+
"amazed",
|
|
4602
|
+
"surprised",
|
|
4603
|
+
"flirtatious",
|
|
4604
|
+
"curious",
|
|
4605
|
+
"calm",
|
|
4606
|
+
"grateful",
|
|
4607
|
+
"affectionate",
|
|
4608
|
+
"sympathetic",
|
|
4609
|
+
"mysterious",
|
|
4610
|
+
"frustrated",
|
|
4611
|
+
"disgusted",
|
|
4612
|
+
"sarcastic",
|
|
4613
|
+
"disappointed",
|
|
4614
|
+
"hurt",
|
|
4615
|
+
"guilty",
|
|
4616
|
+
"bored",
|
|
4617
|
+
"tired",
|
|
4618
|
+
"nostalgic",
|
|
4619
|
+
"apologetic",
|
|
4620
|
+
"hesitant",
|
|
4621
|
+
"confused",
|
|
4622
|
+
"anxious",
|
|
4623
|
+
"panicked",
|
|
4624
|
+
"proud",
|
|
4625
|
+
"confident",
|
|
4626
|
+
"skeptical",
|
|
4627
|
+
"contemplative",
|
|
4628
|
+
"determined"
|
|
4629
|
+
];
|
|
4630
|
+
var VALID = new Set(EMOTIONS);
|
|
4631
|
+
var ALIASES = {
|
|
4632
|
+
cheerful: "happy",
|
|
4633
|
+
joyful: "happy",
|
|
4634
|
+
joy: "happy",
|
|
4635
|
+
glad: "happy",
|
|
4636
|
+
pleased: "happy",
|
|
4637
|
+
warm: "affectionate",
|
|
4638
|
+
thrilled: "excited",
|
|
4639
|
+
eager: "enthusiastic",
|
|
4640
|
+
ecstatic: "elated",
|
|
4641
|
+
euphoric: "elated",
|
|
4642
|
+
mad: "angry",
|
|
4643
|
+
furious: "angry",
|
|
4644
|
+
annoyed: "frustrated",
|
|
4645
|
+
irritated: "frustrated",
|
|
4646
|
+
agitated: "frustrated",
|
|
4647
|
+
shocked: "surprised",
|
|
4648
|
+
astonished: "amazed",
|
|
4649
|
+
wonder: "amazed",
|
|
4650
|
+
worried: "anxious",
|
|
4651
|
+
nervous: "anxious",
|
|
4652
|
+
afraid: "scared",
|
|
4653
|
+
alarmed: "panicked",
|
|
4654
|
+
unsure: "hesitant",
|
|
4655
|
+
uncertain: "hesitant",
|
|
4656
|
+
doubtful: "skeptical",
|
|
4657
|
+
suspicious: "skeptical",
|
|
4658
|
+
thoughtful: "contemplative",
|
|
4659
|
+
focused: "determined",
|
|
4660
|
+
serious: "determined",
|
|
4661
|
+
playful: "flirtatious",
|
|
4662
|
+
teasing: "flirtatious",
|
|
4663
|
+
ironic: "sarcastic",
|
|
4664
|
+
cheeky: "sarcastic",
|
|
4665
|
+
thankful: "grateful",
|
|
4666
|
+
sorry: "apologetic",
|
|
4667
|
+
down: "sad",
|
|
4668
|
+
melancholic: "sad",
|
|
4669
|
+
gloomy: "sad",
|
|
4670
|
+
peaceful: "calm",
|
|
4671
|
+
serene: "calm",
|
|
4672
|
+
relaxed: "calm",
|
|
4673
|
+
sleepy: "tired"
|
|
4674
|
+
};
|
|
4675
|
+
var NONVERBAL = { laughter: "laughter", laughs: "laughter", laugh: "laughter", laughing: "laughter" };
|
|
4676
|
+
function normalizeEmotion(raw) {
|
|
4677
|
+
const k = raw.trim().toLowerCase();
|
|
4678
|
+
if (VALID.has(k)) return k;
|
|
4679
|
+
return ALIASES[k] ?? null;
|
|
4680
|
+
}
|
|
4681
|
+
function resolveTag(raw) {
|
|
4682
|
+
const k = raw.trim().toLowerCase();
|
|
4683
|
+
if (NONVERBAL[k]) return { kind: "nonverbal", value: NONVERBAL[k] };
|
|
4684
|
+
const e = normalizeEmotion(k);
|
|
4685
|
+
return e ? { kind: "emotion", value: e } : null;
|
|
4686
|
+
}
|
|
4687
|
+
var TAG_RE = /\[([a-zA-Z][a-zA-Z ]{0,24})\]/g;
|
|
4688
|
+
var PARTIAL_RE = /\[[a-zA-Z ]*$/;
|
|
4689
|
+
var cartesiaTag = (t) => t.kind === "nonverbal" ? `[${t.value}]` : `<emotion value="${t.value}"/>`;
|
|
4690
|
+
function renderEmotions(text, opts = { show: true }) {
|
|
4691
|
+
let speech = "", display = "", prose = "", last = 0;
|
|
4692
|
+
TAG_RE.lastIndex = 0;
|
|
4693
|
+
for (let m = TAG_RE.exec(text); m; m = TAG_RE.exec(text)) {
|
|
4694
|
+
const before = text.slice(last, m.index);
|
|
4695
|
+
speech += before;
|
|
4696
|
+
display += before;
|
|
4697
|
+
prose += before;
|
|
4698
|
+
const tag = resolveTag(m[1]);
|
|
4699
|
+
if (tag) {
|
|
4700
|
+
speech += cartesiaTag(tag);
|
|
4701
|
+
if (opts.show) display += m[0];
|
|
4702
|
+
} else {
|
|
4703
|
+
log8.debug(`dropping unknown emotion tag ${m[0]}`);
|
|
4704
|
+
}
|
|
4705
|
+
last = m.index + m[0].length;
|
|
4706
|
+
}
|
|
4707
|
+
const tail = text.slice(last);
|
|
4708
|
+
return { speech: speech + tail, display: display + tail, prose: prose + tail };
|
|
4709
|
+
}
|
|
4710
|
+
var EmotionStream = class {
|
|
4711
|
+
constructor(show = true) {
|
|
4712
|
+
this.show = show;
|
|
4713
|
+
}
|
|
4714
|
+
show;
|
|
4715
|
+
buf = "";
|
|
4716
|
+
pending = null;
|
|
4717
|
+
feed(delta) {
|
|
4718
|
+
this.buf += delta;
|
|
4719
|
+
return this.drain(false);
|
|
4720
|
+
}
|
|
4721
|
+
flush() {
|
|
4722
|
+
return this.drain(true);
|
|
4723
|
+
}
|
|
4724
|
+
drain(final) {
|
|
4725
|
+
let body = this.buf;
|
|
4726
|
+
if (!final) {
|
|
4727
|
+
const p = body.match(PARTIAL_RE);
|
|
4728
|
+
if (p) {
|
|
4729
|
+
this.buf = p[0];
|
|
4730
|
+
body = body.slice(0, body.length - p[0].length);
|
|
4731
|
+
} else this.buf = "";
|
|
4732
|
+
} else this.buf = "";
|
|
4733
|
+
let speech = "", display = "", prose = "", last = 0;
|
|
4734
|
+
TAG_RE.lastIndex = 0;
|
|
4735
|
+
for (let m = TAG_RE.exec(body); m; m = TAG_RE.exec(body)) {
|
|
4736
|
+
this.emit(body.slice(last, m.index), (s, d, p) => {
|
|
4737
|
+
speech += s;
|
|
4738
|
+
display += d;
|
|
4739
|
+
prose += p;
|
|
4740
|
+
});
|
|
4741
|
+
const tag = resolveTag(m[1]);
|
|
4742
|
+
if (tag) {
|
|
4743
|
+
this.pending = tag;
|
|
4744
|
+
if (this.show) display += m[0];
|
|
4745
|
+
} else log8.debug(`dropping unknown emotion tag ${m[0]}`);
|
|
4746
|
+
last = m.index + m[0].length;
|
|
4747
|
+
}
|
|
4748
|
+
this.emit(body.slice(last), (s, d, p) => {
|
|
4749
|
+
speech += s;
|
|
4750
|
+
display += d;
|
|
4751
|
+
prose += p;
|
|
4752
|
+
});
|
|
4753
|
+
return { speech, display, prose };
|
|
4754
|
+
}
|
|
4755
|
+
/** Emit a prose span, flushing any pending tag onto its FRONT (only once real words appear). */
|
|
4756
|
+
emit(text, sink) {
|
|
4757
|
+
if (!text) return;
|
|
4758
|
+
let speech = text;
|
|
4759
|
+
if (this.pending && /[\p{L}\p{N}]/u.test(text)) {
|
|
4760
|
+
speech = cartesiaTag(this.pending) + text;
|
|
4761
|
+
this.pending = null;
|
|
4762
|
+
}
|
|
4763
|
+
sink(speech, text, text);
|
|
4764
|
+
}
|
|
4765
|
+
};
|
|
4766
|
+
|
|
4585
4767
|
// src/voice/spokenSplitter.ts
|
|
4586
4768
|
var OPEN = "<spoken>";
|
|
4587
4769
|
var CLOSE = "</spoken>";
|
|
@@ -4655,7 +4837,7 @@ var SpokenSplitter = class {
|
|
|
4655
4837
|
};
|
|
4656
4838
|
|
|
4657
4839
|
// src/duplex.ts
|
|
4658
|
-
var
|
|
4840
|
+
var log9 = forComponent("DuplexAgent");
|
|
4659
4841
|
function describeCall(call) {
|
|
4660
4842
|
const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
|
|
4661
4843
|
const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
|
|
@@ -4694,6 +4876,9 @@ var DuplexAgentOptions = class {
|
|
|
4694
4876
|
/** Voice register: 'neutral' = clean spoken style; 'conversational' = human-like — fillers,
|
|
4695
4877
|
* backchannels, impulsive first reactions before content (mimics real duplex conversation). */
|
|
4696
4878
|
voiceStyle = "neutral";
|
|
4879
|
+
/** Teach the model to emit inline `[emotion]` tags for Cartesia emotion control. Only set when the
|
|
4880
|
+
* TTS actually speaks them — text-duplex (no TTS) would otherwise print literal tags. */
|
|
4881
|
+
emotionTags = false;
|
|
4697
4882
|
/** Awaited BEFORE a worker spawns — open a per-task checkpoint frame, audit, etc.
|
|
4698
4883
|
* (post-spawn would race the worker's first edits). */
|
|
4699
4884
|
onTaskStart;
|
|
@@ -4726,6 +4911,7 @@ var VOICE_SYSTEM_PROMPT = 'You are a spoken voice assistant \u2014 the user HEAR
|
|
|
4726
4911
|
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.";
|
|
4727
4912
|
var THINK_DISABLED_GUIDANCE = "(Think tier is not available \u2014 use Act for all escalations.)";
|
|
4728
4913
|
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).`;
|
|
4914
|
+
var EMOTION_TAGS_GUIDANCE = `EMOTION: your voice is synthesized with emotion control. Prefix a sentence with an inline [emotion] tag, placed directly before the sentence it colors, to shape how it is spoken. Use it ONLY when the emotion genuinely fits the words (it amplifies real feeling, it cannot fake it) \u2014 do not tag every sentence; reserve it for moments that carry feeling, and vary which one you use. You may also drop [laughter] for a natural laugh. Available emotions: ${EMOTIONS.join(", ")}.`;
|
|
4729
4915
|
var DuplexAgent = class _DuplexAgent {
|
|
4730
4916
|
options;
|
|
4731
4917
|
voice;
|
|
@@ -4795,7 +4981,7 @@ var DuplexAgent = class _DuplexAgent {
|
|
|
4795
4981
|
...new Set(workerToolNames.filter((n) => n.startsWith("mcp__")).map((n) => n.slice(5).split("__")[0]))
|
|
4796
4982
|
];
|
|
4797
4983
|
const workerMcp = mcpNames.length ? `, and it can use these MCP servers: ${[...new Set(mcpNames)].join(", ")}` + (mcpNames.some((n) => /browser/i.test(n)) ? ' \u2014 including driving a REAL browser (open tabs, navigate, click, screenshot), so answer "yes" if asked whether you can control/drive a browser and route an actual browse to Act' : "") : "";
|
|
4798
|
-
const prompt = VOICE_SYSTEM_PROMPT.replace("{{MEMORY_SLOT}}", memSlot).replace("{{THINK_SLOT}}", thinkSlot).replace("{{WORKER_WEB}}", workerWeb + workerMcp) + (o.voiceStyle === "conversational" ? "\n" + VOICE_STYLE_CONVERSATIONAL : "") + `
|
|
4984
|
+
const prompt = VOICE_SYSTEM_PROMPT.replace("{{MEMORY_SLOT}}", memSlot).replace("{{THINK_SLOT}}", thinkSlot).replace("{{WORKER_WEB}}", workerWeb + workerMcp) + (o.voiceStyle === "conversational" ? "\n" + VOICE_STYLE_CONVERSATIONAL : "") + (o.emotionTags ? "\n" + EMOTION_TAGS_GUIDANCE : "") + `
|
|
4799
4985
|
Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
4800
4986
|
const tools = [
|
|
4801
4987
|
...o.reflexOptions?.tools ?? [],
|
|
@@ -4819,7 +5005,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
4819
5005
|
const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
|
|
4820
5006
|
if (m) {
|
|
4821
5007
|
this.fabricationCut = true;
|
|
4822
|
-
|
|
5008
|
+
log9.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
|
|
4823
5009
|
const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
|
|
4824
5010
|
if (!safe) return;
|
|
4825
5011
|
if (safe.trim()) this.spokeThisTurn = true;
|
|
@@ -4921,7 +5107,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
4921
5107
|
try {
|
|
4922
5108
|
await this.voice.send(fallback ? "[reminder] You said nothing to the user this turn. Tell them, in ONE short spoken sentence, what just happened \u2014 no tools." : dispatched ? "[reminder] You dispatched a task but said nothing to the user. Say ONE short spoken acknowledgement now \u2014 no tools." : "[reminder] You said nothing to the user this turn. Give your ONE short spoken reply now \u2014 no tools.");
|
|
4923
5109
|
} catch (e) {
|
|
4924
|
-
|
|
5110
|
+
log9.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
4925
5111
|
} finally {
|
|
4926
5112
|
this.nudging = false;
|
|
4927
5113
|
}
|
|
@@ -5013,7 +5199,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
5013
5199
|
buildBrief(brief, tier = "act", deliver = true) {
|
|
5014
5200
|
const recent = this.voice.transcript.filter((m) => (m.role === "user" || m.role === "assistant") && contentText(m.content).trim()).slice(-this.options.excerptTurns).map((m) => `${m.role}: ${contentText(m.content)}`).join("\n");
|
|
5015
5201
|
const verify = tier === "act" ? "\n\nBefore reporting done: re-read what you changed and check it against EVERY requirement above \u2014 fix any gap first. Your report is trusted without review." : "";
|
|
5016
|
-
const deliverContract = deliver ? "\n\n## DELIVER (spoken delivery)\nYou are reporting back to a user who is LISTENING. Stream your work normally \u2014 your prose is the written work record and detail, and is NOT spoken. Wrap anything the user should HEAR in <spoken>\u2026</spoken> tags. LEAD WITH the actual content they asked for: if they asked for a specific piece of content \u2014 a value, a name, the actual lines, the writing itself \u2014 that content goes INSIDE the <spoken> tags, not a remark about it. Your FIRST <spoken> segment is substantive \u2014 never a greeting or an acknowledgement (the front-end has already acked; do not double-ack). Keep spoken text concise and natural for the ear: short sentences, no markdown." : "";
|
|
5202
|
+
const deliverContract = deliver ? "\n\n## DELIVER (spoken delivery)\nYou are reporting back to a user who is LISTENING. Stream your work normally \u2014 your prose is the written work record and detail, and is NOT spoken. Wrap anything the user should HEAR in <spoken>\u2026</spoken> tags. LEAD WITH the actual content they asked for: if they asked for a specific piece of content \u2014 a value, a name, the actual lines, the writing itself \u2014 that content goes INSIDE the <spoken> tags, not a remark about it. Your FIRST <spoken> segment is substantive \u2014 never a greeting or an acknowledgement (the front-end has already acked; do not double-ack). Keep spoken text concise and natural for the ear: short sentences, no markdown." + (this.options.emotionTags ? " Inside <spoken>, you may prefix a sentence with an inline [emotion] tag (e.g. [excited], [curious]) to color how it is voiced \u2014 only when it genuinely fits, and vary it; [laughter] gives a natural laugh." : "") : "";
|
|
5017
5203
|
return (recent ? `${brief}
|
|
5018
5204
|
|
|
5019
5205
|
## Recent conversation (for context)
|
|
@@ -5129,7 +5315,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
5129
5315
|
this.notify("task_verify", `task ${id}: verifying`, { id });
|
|
5130
5316
|
const cres = await new Agent(checkerOpts).run(checkBrief);
|
|
5131
5317
|
if (cres.finishReason !== "stop") {
|
|
5132
|
-
|
|
5318
|
+
log9.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
|
|
5133
5319
|
this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
|
|
5134
5320
|
}
|
|
5135
5321
|
const sum = (a = 0, b = 0) => a + b;
|
|
@@ -5265,7 +5451,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
5265
5451
|
rec.status = "done";
|
|
5266
5452
|
rec.result = res.text;
|
|
5267
5453
|
const incomplete = res.finishReason !== "stop";
|
|
5268
|
-
|
|
5454
|
+
log9.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
|
|
5269
5455
|
this.notify("task_done", `task ${id} (${rec.label}) completed`, {
|
|
5270
5456
|
id,
|
|
5271
5457
|
text: res.text,
|
|
@@ -5291,7 +5477,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
5291
5477
|
this.dropAsk(rec.id);
|
|
5292
5478
|
rec.status = "error";
|
|
5293
5479
|
rec.result = msg;
|
|
5294
|
-
|
|
5480
|
+
log9.warn(`task ${rec.id} failed: ${msg}`);
|
|
5295
5481
|
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
|
|
5296
5482
|
this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
|
|
5297
5483
|
}
|
|
@@ -5596,7 +5782,7 @@ init_logging();
|
|
|
5596
5782
|
|
|
5597
5783
|
// src/voice/engine.ts
|
|
5598
5784
|
init_logging();
|
|
5599
|
-
var
|
|
5785
|
+
var log10 = forComponent("VoiceEngine");
|
|
5600
5786
|
var now = () => performance.now();
|
|
5601
5787
|
var forSpeech = (t) => t.replace(/[*_`#]+/g, "").replace(/^[ \t]*[-•]\s+/gm, "").replace(/\s*[\u2013\u2014]\s*/g, ", ").replace(/[\u2010\u2011]/g, "-").replace(/\s*\|\s*/g, ", ").replace(/(\d)\s+%/g, "$1%").replace(/\.{3,}/g, ".");
|
|
5602
5788
|
var VoiceEngineOptions = class {
|
|
@@ -5664,6 +5850,11 @@ var VoiceEngineOptions = class {
|
|
|
5664
5850
|
* speech at all, an audible hiccup. Default OFF: the genuine-gated STT partial is the
|
|
5665
5851
|
* mechanism-correct pause trigger; enable only if barge-in onset feels sluggish in a clean-AEC room. */
|
|
5666
5852
|
overlapEnergyHold = false;
|
|
5853
|
+
/** Map inline `[emotion]` tags (emitted by the model, prompt-taught) into Cartesia inline emotion
|
|
5854
|
+
* tags in the spoken transcript (sonic-3 stitches the prosody). false = strip them silently. */
|
|
5855
|
+
emotions = true;
|
|
5856
|
+
/** Show the `[emotion]` tags in the on-screen echo (debug). false = hide (spoken-only). */
|
|
5857
|
+
showEmotions = false;
|
|
5667
5858
|
};
|
|
5668
5859
|
var VoiceEngine = class _VoiceEngine {
|
|
5669
5860
|
options;
|
|
@@ -5709,6 +5900,9 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5709
5900
|
// Central speech queue (above the TTS context): complete worker utterances serialize into ONE
|
|
5710
5901
|
// playback stream, one-at-a-time, never splicing into the live reflex's open utterance.
|
|
5711
5902
|
uttQueue = [];
|
|
5903
|
+
// Per-turn emotion-tag parser (reset on beginSpeech) — converts `[emotion]` → Cartesia inline tags
|
|
5904
|
+
// for TTS, tracks tag-free prose for echo discrimination, and surfaces display text for the screen.
|
|
5905
|
+
emo = null;
|
|
5712
5906
|
constructor(options) {
|
|
5713
5907
|
this.options = { ...new VoiceEngineOptions(), ...options };
|
|
5714
5908
|
const o = this.options;
|
|
@@ -5726,7 +5920,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5726
5920
|
this.stt.onLevel = (rms) => this.handleLevel(rms);
|
|
5727
5921
|
await Promise.all([this.tts.connect(), this.stt.start()]);
|
|
5728
5922
|
this.setState("listening");
|
|
5729
|
-
|
|
5923
|
+
log10.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
|
|
5730
5924
|
}
|
|
5731
5925
|
get usingAec() {
|
|
5732
5926
|
return this.stt.usingAec;
|
|
@@ -5735,6 +5929,10 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5735
5929
|
setBargeIn(on) {
|
|
5736
5930
|
this.options.bargeIn = on;
|
|
5737
5931
|
}
|
|
5932
|
+
/** Show/hide the `[emotion]` debug tags in the echo (next turn's stream picks it up). */
|
|
5933
|
+
setShowEmotions(on) {
|
|
5934
|
+
this.options.showEmotions = on;
|
|
5935
|
+
}
|
|
5738
5936
|
idleWaiters = [];
|
|
5739
5937
|
setState(s) {
|
|
5740
5938
|
if (this.state === s) return;
|
|
@@ -5766,6 +5964,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5766
5964
|
this.ctxOpen = true;
|
|
5767
5965
|
this.spokeDeltas = false;
|
|
5768
5966
|
this.reply = "";
|
|
5967
|
+
this.emo = this.options.emotions ? new EmotionStream(this.options.showEmotions) : null;
|
|
5769
5968
|
this.echoWords = new Set(this.words(this.prevReply));
|
|
5770
5969
|
this.tts.newContext();
|
|
5771
5970
|
if (ack && this.options.ackPhrase) {
|
|
@@ -5776,21 +5975,31 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5776
5975
|
if (!this.turnStartAt) this.turnStartAt = now();
|
|
5777
5976
|
this.setState("thinking");
|
|
5778
5977
|
}
|
|
5978
|
+
/** Feed a spoken delta. Returns the on-screen echo text (emotion tags shown/hidden per config) so the
|
|
5979
|
+
* host renders the SAME stream that was parsed for TTS — no second, state-doubling parse. */
|
|
5779
5980
|
speakDelta(text) {
|
|
5780
|
-
if (this.interrupted) return;
|
|
5981
|
+
if (this.interrupted) return "";
|
|
5781
5982
|
if (!this.speaking || !this.ctxOpen) this.beginSpeech();
|
|
5782
|
-
this.
|
|
5983
|
+
const { speech, display, prose } = this.emo ? this.emo.feed(text) : { speech: text, display: text, prose: text };
|
|
5984
|
+
this.reply += prose;
|
|
5783
5985
|
for (const w of this.words(this.reply)) this.echoWords.add(w);
|
|
5784
|
-
this.tts.speak(forSpeech(
|
|
5785
|
-
if (!this.spokeDeltas && this.turnStartAt)
|
|
5986
|
+
this.tts.speak(forSpeech(speech), true);
|
|
5987
|
+
if (!this.spokeDeltas && this.turnStartAt) log10.debug(`ttft: ${Math.round(now() - this.turnStartAt)}ms`);
|
|
5786
5988
|
this.spokeDeltas = true;
|
|
5787
5989
|
this.setState("speaking");
|
|
5990
|
+
return display;
|
|
5788
5991
|
}
|
|
5789
5992
|
/** close the spoken turn (idempotent); stays audible until ALL audio arrived AND playback drains */
|
|
5790
5993
|
endSpeech() {
|
|
5791
5994
|
this.interrupted = false;
|
|
5792
5995
|
if (!this.speaking) return;
|
|
5793
5996
|
this.ctxOpen = false;
|
|
5997
|
+
if (this.emo) {
|
|
5998
|
+
const t = this.emo.flush();
|
|
5999
|
+
this.emo = null;
|
|
6000
|
+
if (t.prose) this.reply += t.prose;
|
|
6001
|
+
if (t.speech) this.tts.speak(forSpeech(t.speech), true);
|
|
6002
|
+
}
|
|
5794
6003
|
if (this.reply) this.prevReply = this.reply;
|
|
5795
6004
|
const settle = () => {
|
|
5796
6005
|
if (this.ctxOpen) {
|
|
@@ -5803,7 +6012,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5803
6012
|
}
|
|
5804
6013
|
this.drainTimer = null;
|
|
5805
6014
|
this.speaking = false;
|
|
5806
|
-
if (this.turnStartAt)
|
|
6015
|
+
if (this.turnStartAt) log10.debug(`turn: ${Math.round(now() - this.turnStartAt)}ms (incl. playback)`);
|
|
5807
6016
|
this.echoUntil = now() + 2500;
|
|
5808
6017
|
if (!this.usingAec) this.stt.reset();
|
|
5809
6018
|
this.setState("listening");
|
|
@@ -5995,7 +6204,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5995
6204
|
this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
|
|
5996
6205
|
if (this.pendingTimer) clearTimeout(this.pendingTimer);
|
|
5997
6206
|
if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
|
|
5998
|
-
|
|
6207
|
+
log10.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
|
|
5999
6208
|
this.options.onHold();
|
|
6000
6209
|
if (this.options.holdFiller && !this.speaking) {
|
|
6001
6210
|
this.beginSpeech();
|
|
@@ -6094,7 +6303,7 @@ async function resolveAuth(auth) {
|
|
|
6094
6303
|
}
|
|
6095
6304
|
|
|
6096
6305
|
// src/voice/soniox.ts
|
|
6097
|
-
var
|
|
6306
|
+
var log11 = forComponent("SonioxSTT");
|
|
6098
6307
|
var now2 = () => performance.now();
|
|
6099
6308
|
var SonioxSTTOptions = class {
|
|
6100
6309
|
auth = "";
|
|
@@ -6163,9 +6372,9 @@ var SonioxSTT = class {
|
|
|
6163
6372
|
this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
|
|
6164
6373
|
this.ws.onclose = (ev) => {
|
|
6165
6374
|
if (this.stopped) return;
|
|
6166
|
-
|
|
6375
|
+
log11.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
|
|
6167
6376
|
this.reset();
|
|
6168
|
-
this.connectWs().catch((e) =>
|
|
6377
|
+
this.connectWs().catch((e) => log11.error(`soniox reconnect failed: ${e.message}`));
|
|
6169
6378
|
};
|
|
6170
6379
|
}
|
|
6171
6380
|
async start() {
|
|
@@ -6175,7 +6384,7 @@ var SonioxSTT = class {
|
|
|
6175
6384
|
this.endpointTimer = setInterval(() => {
|
|
6176
6385
|
const combined = (this.finalText + this.partialText).trim();
|
|
6177
6386
|
if (!combined || now2() - this.lastChangeAt < this.options.silenceEndpointMs) return;
|
|
6178
|
-
if (this.firstTokenAt)
|
|
6387
|
+
if (this.firstTokenAt) log11.debug(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
|
|
6179
6388
|
this.reset();
|
|
6180
6389
|
this.onUtterance(combined, now2());
|
|
6181
6390
|
}, 120);
|
|
@@ -6187,7 +6396,7 @@ var SonioxSTT = class {
|
|
|
6187
6396
|
if (this.stopped) return;
|
|
6188
6397
|
const ref = this.lastChunkAt || this.startedChunksAt;
|
|
6189
6398
|
if (now2() - ref > noAudioMs) {
|
|
6190
|
-
|
|
6399
|
+
log11.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
|
|
6191
6400
|
this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
|
|
6192
6401
|
this.stop();
|
|
6193
6402
|
}
|
|
@@ -6207,7 +6416,7 @@ var SonioxSTT = class {
|
|
|
6207
6416
|
});
|
|
6208
6417
|
}
|
|
6209
6418
|
handle(m) {
|
|
6210
|
-
if (m.error_message) return
|
|
6419
|
+
if (m.error_message) return log11.error(`soniox: ${m.error_message}`);
|
|
6211
6420
|
let endpoint = false;
|
|
6212
6421
|
for (const t of m.tokens ?? []) {
|
|
6213
6422
|
if (t.text === "<end>") endpoint = true;
|
|
@@ -6223,7 +6432,7 @@ var SonioxSTT = class {
|
|
|
6223
6432
|
this.onPartial(combined);
|
|
6224
6433
|
if (endpoint && this.finalText.trim()) {
|
|
6225
6434
|
const utterance = this.finalText.trim();
|
|
6226
|
-
if (this.firstTokenAt)
|
|
6435
|
+
if (this.firstTokenAt) log11.debug(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
|
|
6227
6436
|
this.reset();
|
|
6228
6437
|
this.onUtterance(utterance, now2());
|
|
6229
6438
|
}
|
|
@@ -6246,7 +6455,7 @@ var SonioxSTT = class {
|
|
|
6246
6455
|
|
|
6247
6456
|
// src/voice/cartesia.ts
|
|
6248
6457
|
init_logging();
|
|
6249
|
-
var
|
|
6458
|
+
var log12 = forComponent("CartesiaTTS");
|
|
6250
6459
|
var now3 = () => performance.now();
|
|
6251
6460
|
var CartesiaTTSOptions = class {
|
|
6252
6461
|
auth = "";
|
|
@@ -6296,9 +6505,9 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
6296
6505
|
this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
|
|
6297
6506
|
});
|
|
6298
6507
|
this.ws.onclose = (ev) => {
|
|
6299
|
-
|
|
6508
|
+
log12.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
|
|
6300
6509
|
if (!this.closed) {
|
|
6301
|
-
this.connecting = this.doConnect().catch((e) =>
|
|
6510
|
+
this.connecting = this.doConnect().catch((e) => log12.error(`cartesia reconnect failed: ${e.message}`));
|
|
6302
6511
|
}
|
|
6303
6512
|
};
|
|
6304
6513
|
this.ws.onmessage = (ev) => {
|
|
@@ -6320,11 +6529,11 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
6320
6529
|
this.down = true;
|
|
6321
6530
|
this.downAt = now3();
|
|
6322
6531
|
this.consecutiveOk = 0;
|
|
6323
|
-
|
|
6532
|
+
log12.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
|
|
6324
6533
|
this.onDone();
|
|
6325
6534
|
this.startProbe();
|
|
6326
6535
|
} else if (!this.down) {
|
|
6327
|
-
|
|
6536
|
+
log12.warn(`cartesia: ${JSON.stringify(m)}`);
|
|
6328
6537
|
}
|
|
6329
6538
|
}
|
|
6330
6539
|
};
|
|
@@ -6338,7 +6547,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
6338
6547
|
this.consecutiveOk = 0;
|
|
6339
6548
|
this.stopProbe();
|
|
6340
6549
|
const downMs = this.downAt ? now3() - this.downAt : 0;
|
|
6341
|
-
(downMs < 2e3 ?
|
|
6550
|
+
(downMs < 2e3 ? log12.debug : log12.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
|
|
6342
6551
|
}
|
|
6343
6552
|
/** Ensure the WS is open before sending — reconnects if idle-closed. */
|
|
6344
6553
|
async ensureConnected() {
|
|
@@ -6418,7 +6627,7 @@ import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor a
|
|
|
6418
6627
|
init_logging();
|
|
6419
6628
|
import { spawn } from "child_process";
|
|
6420
6629
|
import { createHash } from "crypto";
|
|
6421
|
-
var
|
|
6630
|
+
var log13 = forComponent("mcp");
|
|
6422
6631
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
6423
6632
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
6424
6633
|
var StdioTransport = class {
|
|
@@ -6437,7 +6646,7 @@ var StdioTransport = class {
|
|
|
6437
6646
|
proc.stdout.setEncoding("utf8");
|
|
6438
6647
|
proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
6439
6648
|
proc.stderr.setEncoding("utf8");
|
|
6440
|
-
proc.stderr.on("data", (chunk) =>
|
|
6649
|
+
proc.stderr.on("data", (chunk) => log13.debug(`[${command}] stderr:`, chunk.trimEnd()));
|
|
6441
6650
|
proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
|
|
6442
6651
|
proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
|
|
6443
6652
|
}
|
|
@@ -6451,7 +6660,7 @@ var StdioTransport = class {
|
|
|
6451
6660
|
try {
|
|
6452
6661
|
this.dispatch(JSON.parse(line));
|
|
6453
6662
|
} catch (e) {
|
|
6454
|
-
|
|
6663
|
+
log13.debug("dropping non-JSON line from MCP server:", line, e);
|
|
6455
6664
|
}
|
|
6456
6665
|
}
|
|
6457
6666
|
}
|
|
@@ -6500,7 +6709,7 @@ var StdioTransport = class {
|
|
|
6500
6709
|
try {
|
|
6501
6710
|
this.proc?.stdin?.end();
|
|
6502
6711
|
} catch (e) {
|
|
6503
|
-
|
|
6712
|
+
log13.debug("stdin end failed", e);
|
|
6504
6713
|
}
|
|
6505
6714
|
this.proc?.kill();
|
|
6506
6715
|
}
|
|
@@ -6569,7 +6778,7 @@ function parseSseResponse(body) {
|
|
|
6569
6778
|
const obj = JSON.parse(trimmed.slice(5).trim());
|
|
6570
6779
|
if (obj && (obj.result !== void 0 || obj.error !== void 0)) return obj;
|
|
6571
6780
|
} catch (e) {
|
|
6572
|
-
|
|
6781
|
+
log13.debug("skipping unparseable SSE data line", e);
|
|
6573
6782
|
}
|
|
6574
6783
|
}
|
|
6575
6784
|
return {};
|
|
@@ -6637,7 +6846,7 @@ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
|
|
|
6637
6846
|
return { name, client, tools, specs, serverInfo: init?.serverInfo, config: cfg };
|
|
6638
6847
|
})(), mountTimeoutMs, name);
|
|
6639
6848
|
} catch (e) {
|
|
6640
|
-
await client.close().catch((err2) =>
|
|
6849
|
+
await client.close().catch((err2) => log13.debug(`close after failed mount of "${name}": ${err2}`));
|
|
6641
6850
|
throw e;
|
|
6642
6851
|
}
|
|
6643
6852
|
}
|
|
@@ -6648,15 +6857,15 @@ function validEntries(servers) {
|
|
|
6648
6857
|
return Object.entries(servers).filter(([name, cfg]) => {
|
|
6649
6858
|
if (!cfg || cfg.disabled) return false;
|
|
6650
6859
|
if (!cfg.command && !cfg.url) {
|
|
6651
|
-
|
|
6860
|
+
log13.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
|
|
6652
6861
|
return false;
|
|
6653
6862
|
}
|
|
6654
6863
|
return true;
|
|
6655
6864
|
});
|
|
6656
6865
|
}
|
|
6657
6866
|
function logMountFailure(name, e) {
|
|
6658
|
-
if (e instanceof McpAuthError)
|
|
6659
|
-
else
|
|
6867
|
+
if (e instanceof McpAuthError) log13.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
|
|
6868
|
+
else log13.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
|
|
6660
6869
|
}
|
|
6661
6870
|
async function mountMcpServers(servers = {}, opts = {}) {
|
|
6662
6871
|
const entries = validEntries(servers);
|
|
@@ -6666,7 +6875,7 @@ async function mountMcpServers(servers = {}, opts = {}) {
|
|
|
6666
6875
|
const name = entries[i][0];
|
|
6667
6876
|
if (r.status === "fulfilled") {
|
|
6668
6877
|
out.push(r.value);
|
|
6669
|
-
|
|
6878
|
+
log13.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
|
|
6670
6879
|
} else logMountFailure(name, r.reason);
|
|
6671
6880
|
});
|
|
6672
6881
|
return out;
|
|
@@ -6706,7 +6915,7 @@ var McpPool = class {
|
|
|
6706
6915
|
const prev = this.warm.get(key);
|
|
6707
6916
|
if (prev) {
|
|
6708
6917
|
clearTimeout(prev.timer);
|
|
6709
|
-
if (prev.client !== client) void prev.client.close().catch((err2) =>
|
|
6918
|
+
if (prev.client !== client) void prev.client.close().catch((err2) => log13.debug(`warm-pool replace close failed: ${err2}`));
|
|
6710
6919
|
}
|
|
6711
6920
|
const e = { client, timer: void 0 };
|
|
6712
6921
|
this.warm.set(key, e);
|
|
@@ -6723,7 +6932,7 @@ var McpPool = class {
|
|
|
6723
6932
|
const e = this.warm.get(key);
|
|
6724
6933
|
if (!e) return;
|
|
6725
6934
|
this.warm.delete(key);
|
|
6726
|
-
await e.client.close().catch((err2) =>
|
|
6935
|
+
await e.client.close().catch((err2) => log13.debug(`warm-pool evict close failed: ${err2}`));
|
|
6727
6936
|
}
|
|
6728
6937
|
async closeAll() {
|
|
6729
6938
|
for (const e of this.warm.values()) {
|
|
@@ -6941,7 +7150,7 @@ init_tools_shell();
|
|
|
6941
7150
|
// src/tools.notify.ts
|
|
6942
7151
|
init_logging();
|
|
6943
7152
|
import { execFile } from "child_process";
|
|
6944
|
-
var
|
|
7153
|
+
var log15 = forComponent("notify");
|
|
6945
7154
|
function makeNotifyTool(opts = {}) {
|
|
6946
7155
|
const platform2 = opts.platform ?? process.platform;
|
|
6947
7156
|
const run = opts.exec ?? execFile;
|
|
@@ -6965,7 +7174,7 @@ function makeNotifyTool(opts = {}) {
|
|
|
6965
7174
|
return new Promise((resolve4) => {
|
|
6966
7175
|
run(argv[0], argv[1], { timeout: 5e3 }, (e) => {
|
|
6967
7176
|
if (e) {
|
|
6968
|
-
|
|
7177
|
+
log15.debug("notification failed", e);
|
|
6969
7178
|
resolve4(`Notification failed: ${e.message}`);
|
|
6970
7179
|
} else resolve4("Notification shown.");
|
|
6971
7180
|
});
|
|
@@ -6981,7 +7190,7 @@ import { BodDB as BodDB2 } from "@bod.ee/db";
|
|
|
6981
7190
|
init_logging();
|
|
6982
7191
|
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
6983
7192
|
import { homedir } from "os";
|
|
6984
|
-
var
|
|
7193
|
+
var log16 = forComponent("cli-util");
|
|
6985
7194
|
function dotDirs(base, sub, opts = {}) {
|
|
6986
7195
|
const home = opts.home ?? homedir();
|
|
6987
7196
|
const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
|
|
@@ -6998,7 +7207,7 @@ function parseJson(text, fallback, what = "json") {
|
|
|
6998
7207
|
try {
|
|
6999
7208
|
return JSON.parse(text);
|
|
7000
7209
|
} catch (e) {
|
|
7001
|
-
|
|
7210
|
+
log16.debug(`parseJson(${what}) failed: ${e.message}`);
|
|
7002
7211
|
return fallback;
|
|
7003
7212
|
}
|
|
7004
7213
|
}
|
|
@@ -7008,7 +7217,7 @@ function readJsonFile(path, fallback) {
|
|
|
7008
7217
|
try {
|
|
7009
7218
|
text = readFileSync3(path, "utf8");
|
|
7010
7219
|
} catch (e) {
|
|
7011
|
-
|
|
7220
|
+
log16.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
|
|
7012
7221
|
return fallback;
|
|
7013
7222
|
}
|
|
7014
7223
|
return parseJson(text, fallback, path);
|
|
@@ -7261,7 +7470,7 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as rea
|
|
|
7261
7470
|
import { homedir as homedir3 } from "os";
|
|
7262
7471
|
import { dirname as dirname3, join as join6 } from "path";
|
|
7263
7472
|
import { fileURLToPath } from "url";
|
|
7264
|
-
var
|
|
7473
|
+
var log17 = forComponent("VoiceIO");
|
|
7265
7474
|
var now4 = () => performance.now();
|
|
7266
7475
|
var Player = class {
|
|
7267
7476
|
proc = null;
|
|
@@ -7275,7 +7484,7 @@ var Player = class {
|
|
|
7275
7484
|
["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
|
|
7276
7485
|
{ stdio: ["pipe", "ignore", "ignore"] }
|
|
7277
7486
|
);
|
|
7278
|
-
this.proc.on("error", (e) =>
|
|
7487
|
+
this.proc.on("error", (e) => log17.warn(`ffplay error: ${e.message}`));
|
|
7279
7488
|
this.proc.stdin.on("error", () => {
|
|
7280
7489
|
});
|
|
7281
7490
|
this.bytesWritten = 0;
|
|
@@ -7313,7 +7522,7 @@ function detectFfmpegMic() {
|
|
|
7313
7522
|
const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
|
|
7314
7523
|
const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
|
|
7315
7524
|
if (!mic) throw new Error("no audio input device found");
|
|
7316
|
-
|
|
7525
|
+
log17.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
|
|
7317
7526
|
return `:${mic.idx}`;
|
|
7318
7527
|
}
|
|
7319
7528
|
function detectedInputDevice() {
|
|
@@ -7349,15 +7558,15 @@ function resolveAecBinary() {
|
|
|
7349
7558
|
if (existsSync4(bin) && statSync3(bin).mtimeMs >= statSync3(src).mtimeMs) return bin;
|
|
7350
7559
|
if (spawnSync2("which", ["swiftc"]).status !== 0) return null;
|
|
7351
7560
|
mkdirSync5(cacheDir, { recursive: true });
|
|
7352
|
-
|
|
7561
|
+
log17.info("compiling AEC mic helper (first run)\u2026");
|
|
7353
7562
|
const build = spawnSync2("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
|
|
7354
7563
|
if (build.status !== 0) {
|
|
7355
|
-
|
|
7564
|
+
log17.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
|
|
7356
7565
|
return null;
|
|
7357
7566
|
}
|
|
7358
7567
|
const sign = spawnSync2("codesign", ["-fs", "-", bin], { encoding: "utf8" });
|
|
7359
7568
|
if (sign.status !== 0) {
|
|
7360
|
-
|
|
7569
|
+
log17.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
|
|
7361
7570
|
return null;
|
|
7362
7571
|
}
|
|
7363
7572
|
return bin;
|
|
@@ -7399,16 +7608,16 @@ var NodeMicSource = class {
|
|
|
7399
7608
|
this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
|
|
7400
7609
|
} else {
|
|
7401
7610
|
if (spawnSync2("which", ["ffmpeg"]).status !== 0) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
|
|
7402
|
-
|
|
7611
|
+
log17.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
|
|
7403
7612
|
this.proc = spawn2(
|
|
7404
7613
|
"ffmpeg",
|
|
7405
7614
|
["-loglevel", "error", "-f", "avfoundation", "-i", detectFfmpegMic(), "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
|
|
7406
7615
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
7407
7616
|
);
|
|
7408
|
-
this.proc.stderr.on("data", (d) =>
|
|
7617
|
+
this.proc.stderr.on("data", (d) => log17.warn(`ffmpeg: ${String(d).trim()}`));
|
|
7409
7618
|
}
|
|
7410
7619
|
this.proc.on("exit", (c) => {
|
|
7411
|
-
if (c && !this.stopped)
|
|
7620
|
+
if (c && !this.stopped) log17.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
|
|
7412
7621
|
});
|
|
7413
7622
|
this.proc.stdout.on("data", (chunk) => onChunk(chunk));
|
|
7414
7623
|
}
|
|
@@ -7474,7 +7683,7 @@ var AecDuplexAudio = class {
|
|
|
7474
7683
|
this.proc.stdin.on("error", () => {
|
|
7475
7684
|
});
|
|
7476
7685
|
this.proc.on("exit", (c) => {
|
|
7477
|
-
if (c && !this.stopped)
|
|
7686
|
+
if (c && !this.stopped) log17.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
|
|
7478
7687
|
});
|
|
7479
7688
|
this.proc.stdout.on("data", (chunk) => {
|
|
7480
7689
|
this.gotChunk = true;
|
|
@@ -7490,7 +7699,7 @@ var AecDuplexAudio = class {
|
|
|
7490
7699
|
openMicSettings();
|
|
7491
7700
|
this.onFatal?.("microphone permission denied \u2014 enable it in System Settings \u2192 Privacy & Security \u2192 Microphone for your terminal, then restart it");
|
|
7492
7701
|
}
|
|
7493
|
-
} else
|
|
7702
|
+
} else log17.debug(`mic-aec: ${s}`);
|
|
7494
7703
|
}
|
|
7495
7704
|
});
|
|
7496
7705
|
if (!this.noVpio && !this.triedFallback) {
|
|
@@ -7499,7 +7708,7 @@ var AecDuplexAudio = class {
|
|
|
7499
7708
|
this.triedFallback = true;
|
|
7500
7709
|
this.noVpio = true;
|
|
7501
7710
|
this._aec = false;
|
|
7502
|
-
|
|
7711
|
+
log17.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)");
|
|
7503
7712
|
this.onDegrade?.();
|
|
7504
7713
|
this.killProc();
|
|
7505
7714
|
this.spawnHelper();
|
|
@@ -7607,6 +7816,10 @@ var VoiceIOOptions = class extends VoiceEngineOptions {
|
|
|
7607
7816
|
sonioxApiKey = process.env.SONIOX_API_KEY ?? "";
|
|
7608
7817
|
cartesiaApiKey = process.env.CARTESIA_API_KEY ?? "";
|
|
7609
7818
|
cartesiaVoiceId = process.env.CARTESIA_VOICE_ID ?? "";
|
|
7819
|
+
emotions = process.env.VOICE_EMOTIONS !== "0";
|
|
7820
|
+
// Cartesia inline emotion tags (sonic-3)
|
|
7821
|
+
showEmotions = process.env.VOICE_SHOW_EMOTIONS === "1";
|
|
7822
|
+
// surface the tags in the on-screen echo (debug; opt-in)
|
|
7610
7823
|
};
|
|
7611
7824
|
var VoiceIO = class extends VoiceEngine {
|
|
7612
7825
|
duplexSource;
|
|
@@ -7750,7 +7963,7 @@ async function loadConfig(cwd) {
|
|
|
7750
7963
|
|
|
7751
7964
|
// cli/hooks-config.ts
|
|
7752
7965
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
7753
|
-
var
|
|
7966
|
+
var log18 = forComponent("hooks");
|
|
7754
7967
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
7755
7968
|
function ruleMatches(rule, toolName) {
|
|
7756
7969
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -7767,7 +7980,7 @@ function runCmd(rule, env) {
|
|
|
7767
7980
|
});
|
|
7768
7981
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
7769
7982
|
} catch (e) {
|
|
7770
|
-
|
|
7983
|
+
log18.debug(`hook command failed: ${rule.command}`, e);
|
|
7771
7984
|
return { code: 1, out: String(e?.message ?? e) };
|
|
7772
7985
|
}
|
|
7773
7986
|
}
|
|
@@ -7874,7 +8087,7 @@ function formatDiff(ops, opts = {}) {
|
|
|
7874
8087
|
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4, readdirSync, renameSync, symlinkSync as symlinkSync2, unlinkSync, readlinkSync } from "fs";
|
|
7875
8088
|
import { homedir as homedir5 } from "os";
|
|
7876
8089
|
import { join as join8 } from "path";
|
|
7877
|
-
var
|
|
8090
|
+
var log19 = forComponent("session");
|
|
7878
8091
|
var globalDir = () => join8(homedir5(), ".agent", "sessions");
|
|
7879
8092
|
var SessionStore = class {
|
|
7880
8093
|
dir;
|
|
@@ -7919,7 +8132,7 @@ var SessionStore = class {
|
|
|
7919
8132
|
}
|
|
7920
8133
|
load(id) {
|
|
7921
8134
|
if (!this.safeId(id)) {
|
|
7922
|
-
|
|
8135
|
+
log19.debug(`rejecting unsafe session id: ${id}`);
|
|
7923
8136
|
return void 0;
|
|
7924
8137
|
}
|
|
7925
8138
|
const path = join8(this.dir, `${id}.json`);
|
|
@@ -7927,7 +8140,7 @@ var SessionStore = class {
|
|
|
7927
8140
|
try {
|
|
7928
8141
|
return JSON.parse(readFileSync6(path, "utf8"));
|
|
7929
8142
|
} catch (e) {
|
|
7930
|
-
|
|
8143
|
+
log19.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
7931
8144
|
return void 0;
|
|
7932
8145
|
}
|
|
7933
8146
|
}
|
|
@@ -7940,7 +8153,7 @@ var SessionStore = class {
|
|
|
7940
8153
|
try {
|
|
7941
8154
|
metas.push(JSON.parse(readFileSync6(join8(this.dir, f), "utf8")).meta);
|
|
7942
8155
|
} catch (e) {
|
|
7943
|
-
|
|
8156
|
+
log19.debug(`skipping unreadable session file ${f}`, e);
|
|
7944
8157
|
}
|
|
7945
8158
|
}
|
|
7946
8159
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -8108,7 +8321,7 @@ import { execFile as execFile3 } from "child_process";
|
|
|
8108
8321
|
import { promisify } from "util";
|
|
8109
8322
|
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync7 } from "fs";
|
|
8110
8323
|
import { join as join9, resolve as resolve2, sep as sep2 } from "path";
|
|
8111
|
-
var
|
|
8324
|
+
var log20 = forComponent("checkpoints");
|
|
8112
8325
|
var exec = promisify(execFile3);
|
|
8113
8326
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
8114
8327
|
var ShadowRepo = class {
|
|
@@ -8146,7 +8359,7 @@ var ShadowRepo = class {
|
|
|
8146
8359
|
writeFileSync5(join9(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
8147
8360
|
this.ready = true;
|
|
8148
8361
|
} catch (e) {
|
|
8149
|
-
|
|
8362
|
+
log20.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
8150
8363
|
this.ready = false;
|
|
8151
8364
|
}
|
|
8152
8365
|
return this.ready;
|
|
@@ -8157,7 +8370,7 @@ var ShadowRepo = class {
|
|
|
8157
8370
|
}
|
|
8158
8371
|
async commit(label, forced = []) {
|
|
8159
8372
|
await this.run("add", "-A");
|
|
8160
|
-
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) =>
|
|
8373
|
+
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log20.debug(`force-add failed: ${p}`, e));
|
|
8161
8374
|
await this.run("commit", "--allow-empty", "-q", "-m", label);
|
|
8162
8375
|
}
|
|
8163
8376
|
/** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
|
|
@@ -8165,8 +8378,8 @@ var ShadowRepo = class {
|
|
|
8165
8378
|
* turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
|
|
8166
8379
|
* point per turn, so the REPL's turn↔frame mapping stays intact. */
|
|
8167
8380
|
async amendForced(paths) {
|
|
8168
|
-
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) =>
|
|
8169
|
-
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) =>
|
|
8381
|
+
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log20.debug(`force-capture failed: ${p}`, e));
|
|
8382
|
+
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log20.debug("amend failed", e));
|
|
8170
8383
|
}
|
|
8171
8384
|
/** Commits on `ref`, oldest-first (canonical index space). */
|
|
8172
8385
|
async log(ref) {
|
|
@@ -8226,7 +8439,7 @@ var ShadowRepo = class {
|
|
|
8226
8439
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
8227
8440
|
});
|
|
8228
8441
|
} catch (e) {
|
|
8229
|
-
|
|
8442
|
+
log20.debug("checkpoint prune failed", e);
|
|
8230
8443
|
}
|
|
8231
8444
|
}
|
|
8232
8445
|
};
|
|
@@ -8285,7 +8498,7 @@ var GitCheckpoints = class {
|
|
|
8285
8498
|
use(sessionId) {
|
|
8286
8499
|
if (sessionId === this.session) return;
|
|
8287
8500
|
this.session = sessionId;
|
|
8288
|
-
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) =>
|
|
8501
|
+
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log20.debug("re-point failed", e));
|
|
8289
8502
|
}
|
|
8290
8503
|
async begin(label) {
|
|
8291
8504
|
if (!await this.start()) return;
|
|
@@ -8297,7 +8510,7 @@ var GitCheckpoints = class {
|
|
|
8297
8510
|
try {
|
|
8298
8511
|
await this.repos[i].commit(msg, forced);
|
|
8299
8512
|
} catch (e) {
|
|
8300
|
-
|
|
8513
|
+
log20.debug("checkpoint commit failed", e);
|
|
8301
8514
|
}
|
|
8302
8515
|
}
|
|
8303
8516
|
if (slow) clearTimeout(slow);
|
|
@@ -8327,7 +8540,7 @@ var GitCheckpoints = class {
|
|
|
8327
8540
|
if (this.forced.has(abs)) continue;
|
|
8328
8541
|
this.forced.add(abs);
|
|
8329
8542
|
const i = this.repoIndexFor(abs);
|
|
8330
|
-
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) =>
|
|
8543
|
+
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log20.debug("amendForced failed", e));
|
|
8331
8544
|
}
|
|
8332
8545
|
}
|
|
8333
8546
|
};
|
|
@@ -10033,7 +10246,7 @@ import { spawnSync as spawnSync5 } from "child_process";
|
|
|
10033
10246
|
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync9, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
|
|
10034
10247
|
import { homedir as homedir7 } from "os";
|
|
10035
10248
|
import { join as join12 } from "path";
|
|
10036
|
-
var
|
|
10249
|
+
var log21 = forComponent("os-sched");
|
|
10037
10250
|
var OsScheduler = class {
|
|
10038
10251
|
options;
|
|
10039
10252
|
constructor(options) {
|
|
@@ -10095,7 +10308,7 @@ var OsScheduler = class {
|
|
|
10095
10308
|
}
|
|
10096
10309
|
}
|
|
10097
10310
|
} catch (e) {
|
|
10098
|
-
|
|
10311
|
+
log21.debug(`cancel ${id}`, e);
|
|
10099
10312
|
}
|
|
10100
10313
|
for (const f of [`${id}.json`, `${id}.sh`]) {
|
|
10101
10314
|
try {
|
|
@@ -10212,7 +10425,7 @@ import { spawn as spawn3 } from "child_process";
|
|
|
10212
10425
|
import { existsSync as existsSync8, mkdirSync as mkdirSync10, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "fs";
|
|
10213
10426
|
import { homedir as homedir8 } from "os";
|
|
10214
10427
|
import { join as join13 } from "path";
|
|
10215
|
-
var
|
|
10428
|
+
var log22 = forComponent("remote-trigger");
|
|
10216
10429
|
var TRIGGER_DIR = () => join13(homedir8(), ".agent", "triggers");
|
|
10217
10430
|
var sockPath = (sessionId, dir = TRIGGER_DIR()) => join13(dir, `${sessionId}.sock`);
|
|
10218
10431
|
var TriggerServer = class {
|
|
@@ -10250,13 +10463,13 @@ var TriggerServer = class {
|
|
|
10250
10463
|
conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
|
|
10251
10464
|
}
|
|
10252
10465
|
});
|
|
10253
|
-
conn.on("error", (e) =>
|
|
10466
|
+
conn.on("error", (e) => log22.debug("trigger conn error", e));
|
|
10254
10467
|
});
|
|
10255
|
-
this.server.on("error", (e) =>
|
|
10468
|
+
this.server.on("error", (e) => log22.debug("trigger server error", e));
|
|
10256
10469
|
this.server.listen(p);
|
|
10257
10470
|
this.path = p;
|
|
10258
10471
|
} catch (e) {
|
|
10259
|
-
|
|
10472
|
+
log22.debug("trigger server unavailable", e);
|
|
10260
10473
|
}
|
|
10261
10474
|
}
|
|
10262
10475
|
/** Re-bind on /resume (the session id changed). */
|
|
@@ -10377,7 +10590,7 @@ var italic = C("3");
|
|
|
10377
10590
|
var strike = C("9");
|
|
10378
10591
|
var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
|
|
10379
10592
|
var err = (s) => process.stderr.write(s);
|
|
10380
|
-
var
|
|
10593
|
+
var log23 = forComponent("cli");
|
|
10381
10594
|
var VERSION = (() => {
|
|
10382
10595
|
try {
|
|
10383
10596
|
return JSON.parse(readFileSync8(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -11016,7 +11229,7 @@ function costOf(pricing, promptTokens = 0, completionTokens = 0, cacheCreationTo
|
|
|
11016
11229
|
function turnCost(model, usage) {
|
|
11017
11230
|
return costOf(getModelInfo(model)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, model);
|
|
11018
11231
|
}
|
|
11019
|
-
async function evaluateGoal(ai, condition, transcript,
|
|
11232
|
+
async function evaluateGoal(ai, condition, transcript, log24) {
|
|
11020
11233
|
const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
|
|
11021
11234
|
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
11022
11235
|
return text.slice(0, 600);
|
|
@@ -11036,7 +11249,7 @@ ${recent}` }
|
|
|
11036
11249
|
const match = r.content.match(/\{[\s\S]*\}/);
|
|
11037
11250
|
if (match) return JSON.parse(match[0]);
|
|
11038
11251
|
} catch (e) {
|
|
11039
|
-
|
|
11252
|
+
log24(dim(` (goal evaluator error: ${e?.message ?? e})
|
|
11040
11253
|
`));
|
|
11041
11254
|
}
|
|
11042
11255
|
return { met: false, reason: "evaluation unclear" };
|
|
@@ -11248,7 +11461,7 @@ function mcpAgentTools(mounted, opts) {
|
|
|
11248
11461
|
return tools;
|
|
11249
11462
|
}
|
|
11250
11463
|
async function closeMcp(mounted) {
|
|
11251
|
-
await Promise.all(mounted.map((m) => m.client.close().catch((e) =>
|
|
11464
|
+
await Promise.all(mounted.map((m) => m.client.close().catch((e) => log23.debug("mcp close failed", e))));
|
|
11252
11465
|
}
|
|
11253
11466
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
11254
11467
|
function mentionRefs(line) {
|
|
@@ -11298,7 +11511,7 @@ async function expandMentions(fs, line) {
|
|
|
11298
11511
|
if (loaded.includes(ref) || missing.includes(ref)) continue;
|
|
11299
11512
|
if (ref.includes(":") && mcpMentionResolver) {
|
|
11300
11513
|
const body = await mcpMentionResolver(ref).catch((e) => {
|
|
11301
|
-
|
|
11514
|
+
log23.debug("mcp mention resolve failed", e);
|
|
11302
11515
|
return null;
|
|
11303
11516
|
});
|
|
11304
11517
|
if (body != null) {
|
|
@@ -11381,7 +11594,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
11381
11594
|
try {
|
|
11382
11595
|
store.save(session);
|
|
11383
11596
|
} catch (ex) {
|
|
11384
|
-
|
|
11597
|
+
log23.debug("mid-turn session flush failed", ex);
|
|
11385
11598
|
}
|
|
11386
11599
|
}
|
|
11387
11600
|
return origNotify(e);
|
|
@@ -11528,14 +11741,14 @@ var isCancelTeardown = (e) => {
|
|
|
11528
11741
|
function installCancelGuards(mounted) {
|
|
11529
11742
|
process.on("unhandledRejection", (e) => {
|
|
11530
11743
|
if (isCancelTeardown(e)) {
|
|
11531
|
-
|
|
11744
|
+
log23.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
11532
11745
|
return;
|
|
11533
11746
|
}
|
|
11534
|
-
|
|
11747
|
+
log23.error("unhandledRejection", e);
|
|
11535
11748
|
});
|
|
11536
11749
|
process.on("uncaughtException", (e) => {
|
|
11537
11750
|
if (isCancelTeardown(e)) {
|
|
11538
|
-
|
|
11751
|
+
log23.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
11539
11752
|
return;
|
|
11540
11753
|
}
|
|
11541
11754
|
console.error(e);
|
|
@@ -11595,6 +11808,8 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
11595
11808
|
let dx;
|
|
11596
11809
|
let voiceIO;
|
|
11597
11810
|
let voiceLineOpen = false;
|
|
11811
|
+
const emotionsOn = process.env.VOICE_EMOTIONS !== "0";
|
|
11812
|
+
let showEmotions = process.env.VOICE_SHOW_EMOTIONS === "1";
|
|
11598
11813
|
const voiceEcho = (text) => {
|
|
11599
11814
|
const s = forSpeech(text);
|
|
11600
11815
|
if (!s) return;
|
|
@@ -11669,7 +11884,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
11669
11884
|
spinner.stop();
|
|
11670
11885
|
voiceIO.enqueueUtterance(e.message);
|
|
11671
11886
|
editorRef?.suspend();
|
|
11672
|
-
voiceEcho(e.message);
|
|
11887
|
+
voiceEcho(emotionsOn ? renderEmotions(e.message, { show: showEmotions }).display : e.message);
|
|
11673
11888
|
voiceEchoEnd();
|
|
11674
11889
|
editorRef?.resume();
|
|
11675
11890
|
editorRef?.redrawNow();
|
|
@@ -11683,9 +11898,9 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
11683
11898
|
}
|
|
11684
11899
|
if (e.kind === "text_delta" && voiceIO) {
|
|
11685
11900
|
spinner.stop();
|
|
11686
|
-
voiceIO.speakDelta(e.message);
|
|
11901
|
+
const echo = voiceIO.speakDelta(e.message);
|
|
11687
11902
|
editorRef?.suspend();
|
|
11688
|
-
voiceEcho(
|
|
11903
|
+
voiceEcho(echo);
|
|
11689
11904
|
return;
|
|
11690
11905
|
} else if (e.kind === "text_delta" && stashText()) {
|
|
11691
11906
|
process.stdout.write("\r\x1B[K");
|
|
@@ -11759,8 +11974,8 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
11759
11974
|
providerOptionsFor: (m) => cursorProviderOptions(m, cwd, cfg.mcpServers),
|
|
11760
11975
|
...(args.thinkModel ?? cfg.thinkModel) !== void 0 ? { thinkModel: (args.thinkModel ?? cfg.thinkModel) === false ? false : resolveModelOrNewest(String(args.thinkModel ?? cfg.thinkModel)) } : {},
|
|
11761
11976
|
host,
|
|
11762
|
-
...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true } : {},
|
|
11763
|
-
// voice: progress asides + worker questions relayed through the conversation
|
|
11977
|
+
...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true, emotionTags: emotionsOn } : {},
|
|
11978
|
+
// voice: progress asides + worker questions relayed through the conversation; emotion tags taught only with a TTS that speaks them
|
|
11764
11979
|
// Per-TASK checkpoint frames (the natural undo unit in duplex = one delegation): opened BEFORE
|
|
11765
11980
|
// the worker spawns (post-spawn would race its first edits). `checkpoints` is bound below.
|
|
11766
11981
|
onTaskStart: async (_id, label) => {
|
|
@@ -12012,7 +12227,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
12012
12227
|
mkdirSync11(join14(cwd, ".agent"), { recursive: true });
|
|
12013
12228
|
appendFileSync(histPath, line + "\n");
|
|
12014
12229
|
} catch (e) {
|
|
12015
|
-
|
|
12230
|
+
log23.debug("history write failed", e);
|
|
12016
12231
|
}
|
|
12017
12232
|
};
|
|
12018
12233
|
const ago = (t) => {
|
|
@@ -12083,7 +12298,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
12083
12298
|
try {
|
|
12084
12299
|
store.save(session);
|
|
12085
12300
|
} catch (e) {
|
|
12086
|
-
|
|
12301
|
+
log23.debug("session save after rewind failed", e);
|
|
12087
12302
|
}
|
|
12088
12303
|
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
12089
12304
|
`));
|
|
@@ -12117,7 +12332,7 @@ ${task}`;
|
|
|
12117
12332
|
bangContext.length = 0;
|
|
12118
12333
|
}
|
|
12119
12334
|
const delta = await refreshCatalogs().catch((e) => {
|
|
12120
|
-
|
|
12335
|
+
log23.debug("catalog refresh failed", e);
|
|
12121
12336
|
return "";
|
|
12122
12337
|
});
|
|
12123
12338
|
if (delta) {
|
|
@@ -12380,7 +12595,7 @@ ${task}`;
|
|
|
12380
12595
|
desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
|
|
12381
12596
|
run: async () => {
|
|
12382
12597
|
await refreshCatalogs().catch((e) => {
|
|
12383
|
-
|
|
12598
|
+
log23.debug("catalog refresh failed", e);
|
|
12384
12599
|
});
|
|
12385
12600
|
face.reprepare();
|
|
12386
12601
|
err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
|
|
@@ -12450,6 +12665,24 @@ ${task}`;
|
|
|
12450
12665
|
}
|
|
12451
12666
|
await toggleVoice();
|
|
12452
12667
|
}
|
|
12668
|
+
}, "voice-emotions": {
|
|
12669
|
+
desc: "show/hide the [emotion] debug tags in the echo \u2014 /voice-emotions <on|off> (TTS emotion control stays on regardless)",
|
|
12670
|
+
run: async (a) => {
|
|
12671
|
+
if (!emotionsOn) {
|
|
12672
|
+
err(dim(" (emotion control is off \u2014 VOICE_EMOTIONS=0)\n"));
|
|
12673
|
+
return;
|
|
12674
|
+
}
|
|
12675
|
+
const v = a[0]?.toLowerCase();
|
|
12676
|
+
if (v === "on" || v === "off") {
|
|
12677
|
+
showEmotions = v === "on";
|
|
12678
|
+
voiceIO?.setShowEmotions(showEmotions);
|
|
12679
|
+
err(green(` \u2713 emotion tags ${showEmotions ? "shown" : "hidden"} in echo
|
|
12680
|
+
`));
|
|
12681
|
+
return;
|
|
12682
|
+
}
|
|
12683
|
+
err(dim(` emotion tags in echo: ${showEmotions ? "shown" : "hidden"} (use /voice-emotions on|off)
|
|
12684
|
+
`));
|
|
12685
|
+
}
|
|
12453
12686
|
}, "voice-model": {
|
|
12454
12687
|
desc: "switch the reflex (voice) model \u2014 /voice-model <id>, or alone for a picker",
|
|
12455
12688
|
run: async (a) => {
|
|
@@ -12842,7 +13075,7 @@ ${task}`;
|
|
|
12842
13075
|
try {
|
|
12843
13076
|
for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
|
|
12844
13077
|
} catch (e) {
|
|
12845
|
-
|
|
13078
|
+
log23.debug(`loadAgents(${d}) failed`, e);
|
|
12846
13079
|
}
|
|
12847
13080
|
}
|
|
12848
13081
|
if (!seen.size) {
|
|
@@ -12930,7 +13163,7 @@ ${task}`;
|
|
|
12930
13163
|
}
|
|
12931
13164
|
if (idx >= 0) {
|
|
12932
13165
|
const old = mounted.splice(idx, 1)[0];
|
|
12933
|
-
await old.client.close().catch((e) =>
|
|
13166
|
+
await old.client.close().catch((e) => log23.debug("mcp close failed", e));
|
|
12934
13167
|
}
|
|
12935
13168
|
try {
|
|
12936
13169
|
const m = await mountMcpServer(name, conf);
|
|
@@ -12958,7 +13191,7 @@ ${task}`;
|
|
|
12958
13191
|
}
|
|
12959
13192
|
const m = mounted.splice(idx, 1)[0];
|
|
12960
13193
|
remountMcpTools();
|
|
12961
|
-
await m.client.close().catch((e) =>
|
|
13194
|
+
await m.client.close().catch((e) => log23.debug("mcp close failed", e));
|
|
12962
13195
|
err(dim(` removed "${name}"
|
|
12963
13196
|
`));
|
|
12964
13197
|
return;
|
|
@@ -13178,7 +13411,7 @@ ${task}`;
|
|
|
13178
13411
|
try {
|
|
13179
13412
|
return readdirSync4(join14(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
13180
13413
|
} catch (e) {
|
|
13181
|
-
|
|
13414
|
+
log23.debug("completion readdir failed", absDir, e);
|
|
13182
13415
|
return null;
|
|
13183
13416
|
}
|
|
13184
13417
|
};
|
|
@@ -13305,7 +13538,7 @@ ${out}
|
|
|
13305
13538
|
return;
|
|
13306
13539
|
}
|
|
13307
13540
|
await refreshCatalogs().catch((e) => {
|
|
13308
|
-
|
|
13541
|
+
log23.debug("catalog refresh failed", e);
|
|
13309
13542
|
});
|
|
13310
13543
|
const sk = skills.find((x) => x.name === name);
|
|
13311
13544
|
if (sk) {
|
|
@@ -13348,6 +13581,9 @@ ${out}
|
|
|
13348
13581
|
const fakeVoice = process.env.AGENTX_VOICE_FAKE ? fakeVoiceParts(process.env.AGENTX_VOICE_FAKE) : null;
|
|
13349
13582
|
voiceIO = new VoiceIO({
|
|
13350
13583
|
...fakeVoice ?? {},
|
|
13584
|
+
emotions: emotionsOn,
|
|
13585
|
+
showEmotions,
|
|
13586
|
+
// local is authoritative (a /voice-emotions before mic-on still applies)
|
|
13351
13587
|
// No ack phrase by default: a fixed "Mm-hm," every turn reads robotic, Haiku's TTFT doesn't
|
|
13352
13588
|
// need masking (~0.7-1.2s full turns), and the conversational register already opens with a
|
|
13353
13589
|
// natural reaction. The mechanism (+ echo-leak guard) stays for slower voice models.
|