@livx.cc/agentx 0.99.85 → 0.99.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +838 -138
- package/dist/cli.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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 norm3 = (s) => s.trim();
|
|
713
713
|
const cl = content.split("\n");
|
|
714
|
-
const ol = oldStr.split("\n").map(
|
|
714
|
+
const ol = oldStr.split("\n").map(norm3);
|
|
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 (norm3(cl[i2 + j]) !== ol[j]) {
|
|
722
722
|
ok = false;
|
|
723
723
|
break;
|
|
724
724
|
}
|
|
@@ -3431,7 +3431,7 @@ function copyTextToClipboard(text, platform2 = process.platform) {
|
|
|
3431
3431
|
}
|
|
3432
3432
|
|
|
3433
3433
|
// cli/cli.ts
|
|
3434
|
-
import { join as
|
|
3434
|
+
import { join as join16, resolve as resolve5, basename as basename4, extname, dirname as dirname6 } from "path";
|
|
3435
3435
|
import { AIClient, DecideClient, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported, disposeCursorSessions, disposeClaudeCodeSessions } from "ai.libx.js";
|
|
3436
3436
|
|
|
3437
3437
|
// src/ccBridge.ts
|
|
@@ -6046,14 +6046,14 @@ function summarize(messages, focus) {
|
|
|
6046
6046
|
if (lastAssistant) lines.push(`last assistant: ${lastAssistant}`);
|
|
6047
6047
|
if (carried.length) lines.push("earlier recap:", ...carried.map((l) => ` ${l}`));
|
|
6048
6048
|
if (focus?.trim()) {
|
|
6049
|
-
const
|
|
6049
|
+
const words3 = focus.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
6050
6050
|
const kept = [];
|
|
6051
6051
|
const ordered = [...messages.filter((m) => !isSyntheticSummary(m)), ...messages.filter(isSyntheticSummary)];
|
|
6052
6052
|
outer: for (const msg of ordered) {
|
|
6053
6053
|
if (msg.role === "tool") continue;
|
|
6054
6054
|
for (const line of contentText(msg.content).split("\n")) {
|
|
6055
6055
|
const l = line.toLowerCase();
|
|
6056
|
-
if (line.trim() &&
|
|
6056
|
+
if (line.trim() && words3.some((w) => l.includes(w))) {
|
|
6057
6057
|
kept.push(line.trim().slice(0, 200));
|
|
6058
6058
|
if (kept.length >= 12) break outer;
|
|
6059
6059
|
}
|
|
@@ -7101,13 +7101,13 @@ function isTranscriptComplete(text) {
|
|
|
7101
7101
|
const trimmed = text.trim();
|
|
7102
7102
|
if (!trimmed) return false;
|
|
7103
7103
|
if (/[.?!]$/.test(trimmed)) return true;
|
|
7104
|
-
const
|
|
7105
|
-
const lastWord =
|
|
7104
|
+
const words3 = trimmed.toLowerCase().split(/\s+/);
|
|
7105
|
+
const lastWord = words3[words3.length - 1];
|
|
7106
7106
|
if (COMPLETE_ENDINGS.has(lastWord)) return true;
|
|
7107
7107
|
if (INCOMPLETE_ENDINGS.has(lastWord)) return false;
|
|
7108
7108
|
const lower = trimmed.toLowerCase();
|
|
7109
7109
|
for (const marker of CONTINUATION_MARKERS) if (lower.endsWith(marker)) return false;
|
|
7110
|
-
if (
|
|
7110
|
+
if (words3.length <= 2) return false;
|
|
7111
7111
|
return true;
|
|
7112
7112
|
}
|
|
7113
7113
|
var MAX_HOLD_MS = 2e3;
|
|
@@ -7508,13 +7508,13 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7508
7508
|
};
|
|
7509
7509
|
if (this.options.revealMode === "word" || this.options.heardTimestamps) {
|
|
7510
7510
|
this.tts.wantTimestamps = true;
|
|
7511
|
-
this.tts.onTimestamps = (
|
|
7511
|
+
this.tts.onTimestamps = (words3, start) => {
|
|
7512
7512
|
if (this.interrupted) return;
|
|
7513
7513
|
start.forEach((s, i) => {
|
|
7514
|
-
if (this.wordAnchorPending) this.wordPending.push([
|
|
7514
|
+
if (this.wordAnchorPending) this.wordPending.push([words3[i] ?? "", s]);
|
|
7515
7515
|
else {
|
|
7516
7516
|
this.wordStarts.push(this.wordOrigin + s);
|
|
7517
|
-
this.wordTexts.push(
|
|
7517
|
+
this.wordTexts.push(words3[i] ?? "");
|
|
7518
7518
|
}
|
|
7519
7519
|
});
|
|
7520
7520
|
};
|
|
@@ -7662,9 +7662,9 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7662
7662
|
this.ackProbe = null;
|
|
7663
7663
|
this.adaptiveAckFired = false;
|
|
7664
7664
|
const lead = m ? m[1] : bare;
|
|
7665
|
-
const
|
|
7665
|
+
const norm3 = lead.replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
|
|
7666
7666
|
const same = this.ackPickedFired ? bare.match(_VoiceEngine.leadingPhrase(this.lastAck)) : null;
|
|
7667
|
-
if (_VoiceEngine.ACK_LEADING.test(
|
|
7667
|
+
if (_VoiceEngine.ACK_LEADING.test(norm3)) {
|
|
7668
7668
|
const rest = m ? m[2] : "";
|
|
7669
7669
|
this.diag("reflex_ack_suppressed", { lead: lead.trim() });
|
|
7670
7670
|
if (!rest.trim()) return "";
|
|
@@ -7750,8 +7750,8 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7750
7750
|
while (n < this.wordStarts.length && this.wordStarts[n] <= playedSec) n++;
|
|
7751
7751
|
if (n !== this.revealedN) {
|
|
7752
7752
|
this.revealedN = n;
|
|
7753
|
-
const
|
|
7754
|
-
this.options.onReveal(
|
|
7753
|
+
const words3 = this.revealText.trim().split(/\s+/).filter(Boolean);
|
|
7754
|
+
this.options.onReveal(words3.slice(0, n).join(" "));
|
|
7755
7755
|
}
|
|
7756
7756
|
this.revealPoll = this.clock.setTimeout(tick, 60);
|
|
7757
7757
|
};
|
|
@@ -7778,8 +7778,8 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7778
7778
|
this.adaptiveAckFired = false;
|
|
7779
7779
|
this.ackProbe = null;
|
|
7780
7780
|
if (heldProbe) {
|
|
7781
|
-
const
|
|
7782
|
-
if (_VoiceEngine.ACK_LEADING.test(
|
|
7781
|
+
const norm3 = heldProbe.replace(/\[emotion[^\]]*\]/gi, "").replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
|
|
7782
|
+
if (_VoiceEngine.ACK_LEADING.test(norm3)) this.diag("reflex_ack_suppressed", { lead: heldProbe.trim() });
|
|
7783
7783
|
else this.speakDelta(heldProbe);
|
|
7784
7784
|
}
|
|
7785
7785
|
if (!this.speaking) return;
|
|
@@ -8317,14 +8317,14 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
8317
8317
|
if (!prev) return next;
|
|
8318
8318
|
if (!next) return prev;
|
|
8319
8319
|
const pw = prev.split(/\s+/), nw = next.split(/\s+/);
|
|
8320
|
-
const
|
|
8320
|
+
const norm3 = _VoiceEngine.normWord;
|
|
8321
8321
|
if (pw.length >= 2 && pw.length <= nw.length) {
|
|
8322
8322
|
let pfx = true;
|
|
8323
|
-
for (let i = 0; i < pw.length - 1; i++) if (
|
|
8323
|
+
for (let i = 0; i < pw.length - 1; i++) if (norm3(pw[i]) !== norm3(nw[i])) {
|
|
8324
8324
|
pfx = false;
|
|
8325
8325
|
break;
|
|
8326
8326
|
}
|
|
8327
|
-
if (pfx &&
|
|
8327
|
+
if (pfx && norm3(nw[pw.length - 1]).startsWith(norm3(pw[pw.length - 1]))) return next;
|
|
8328
8328
|
}
|
|
8329
8329
|
if (_VoiceEngine.restates(prev, next)) {
|
|
8330
8330
|
this.diag("merge_restated", { dropped: prev.slice(0, 60) });
|
|
@@ -8333,7 +8333,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
8333
8333
|
const max = Math.min(pw.length, nw.length);
|
|
8334
8334
|
for (let k = max; k > 0; k--) {
|
|
8335
8335
|
let match = true;
|
|
8336
|
-
for (let i = 0; i < k; i++) if (
|
|
8336
|
+
for (let i = 0; i < k; i++) if (norm3(pw[pw.length - k + i]) !== norm3(nw[i])) {
|
|
8337
8337
|
match = false;
|
|
8338
8338
|
break;
|
|
8339
8339
|
}
|
|
@@ -8385,11 +8385,11 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
8385
8385
|
* race), never worse. Fillers ("mhm") deliberately don't count as replies — see speakFiller. */
|
|
8386
8386
|
refinalizes(next) {
|
|
8387
8387
|
const pw = this.lastDispatchWords, nw = next.trim().split(/\s+/);
|
|
8388
|
-
const
|
|
8388
|
+
const norm3 = _VoiceEngine.normWord;
|
|
8389
8389
|
if (pw.length < 3 || nw.length < pw.length) return false;
|
|
8390
8390
|
if (this.clock.now() - this.lastDispatchAt >= _VoiceEngine.DUP_FINAL_MS) return false;
|
|
8391
|
-
for (let i = 0; i < pw.length - 1; i++) if (
|
|
8392
|
-
const pLast =
|
|
8391
|
+
for (let i = 0; i < pw.length - 1; i++) if (norm3(pw[i]) !== norm3(nw[i])) return false;
|
|
8392
|
+
const pLast = norm3(pw[pw.length - 1]), nSame = norm3(nw[pw.length - 1]);
|
|
8393
8393
|
if (!nSame.startsWith(pLast)) return false;
|
|
8394
8394
|
return nw.length > pw.length || nSame.length > pLast.length;
|
|
8395
8395
|
}
|
|
@@ -9285,6 +9285,17 @@ var ACK_BANK = {
|
|
|
9285
9285
|
steer: ["Ah, okay, switching\u2014", "Ah, got it, so\u2014", "Oh, gotcha, sure\u2014"],
|
|
9286
9286
|
resume: ["Right, so\u2014", "Anyway, so\u2014", "So, as I was saying\u2014"]
|
|
9287
9287
|
};
|
|
9288
|
+
var CHANNEL_ACTIONS = {
|
|
9289
|
+
silence: 'the default \u2014 nothing to add. ALWAYS for a greeting, thanks, a yes/no, or any short complete turn the real reply will answer instantly ("hey", "how are you?", "thanks"); also mid-word or mid-clause',
|
|
9290
|
+
backchannel: 'they are mid-way through a longer story or explanation (several clauses) and just closed a clause: a tiny "mm-hm"/"right" shows you follow, without taking the turn',
|
|
9291
|
+
filler: 'they paused mid-thought, searching for words, and a soft "hmm" keeps the line warm',
|
|
9292
|
+
bridge: 'they finished a substantial request or hard question and the real answer takes a moment: a short "let me check\u2014" lead-in'
|
|
9293
|
+
};
|
|
9294
|
+
var CHANNEL_BANK = {
|
|
9295
|
+
backchannel: ["Mm-hm.", "Right.", "Mm, yeah.", "Uh-huh.", "I see.", "Mm."],
|
|
9296
|
+
filler: ["Hmm\u2026", "Mm\u2026", "Hm, mm\u2026", "Ahh\u2026"],
|
|
9297
|
+
bridge: ["Let me check\u2014", "One sec, looking\u2014", "Hm, let me see\u2014", "Okay, give me a moment\u2014", "Let me think on that\u2014"]
|
|
9298
|
+
};
|
|
9288
9299
|
var INTENT_ACK_OVER = {
|
|
9289
9300
|
stop: Object.keys(ACK_SITUATIONS).filter((s) => s !== "silent"),
|
|
9290
9301
|
steer: ["command"],
|
|
@@ -9502,6 +9513,17 @@ var Instinct = class {
|
|
|
9502
9513
|
if (pick?.text !== text.trim()) return null;
|
|
9503
9514
|
return Promise.race([pick.p.then((p) => p ? pick.spoken : null), pick.spoken]);
|
|
9504
9515
|
}
|
|
9516
|
+
/** Duplex v2 second channel: which (if any) listener line fits THIS instant — one Choice over CHANNEL_ACTIONS.
|
|
9517
|
+
* `heard` is the micro-context tail only (never history). Never rejects: a failed call → null (the rule decides). */
|
|
9518
|
+
channel(heard, state) {
|
|
9519
|
+
return this.ask(
|
|
9520
|
+
"channel",
|
|
9521
|
+
heard,
|
|
9522
|
+
{ heard_so_far: heard, ...state },
|
|
9523
|
+
FRAME + "You are the quick side-voice of the conversation, NOT the one who answers. Pick the tiny sound a person would make right now, or none. Silence is the default: most moments call for nothing.",
|
|
9524
|
+
CHANNEL_ACTIONS
|
|
9525
|
+
);
|
|
9526
|
+
}
|
|
9505
9527
|
/** A random phrase from `pool`, never the one said last (no back-to-back repeat across keys). */
|
|
9506
9528
|
rotate(pool) {
|
|
9507
9529
|
const fresh = pool.filter((x) => x !== this.lastAckSaid);
|
|
@@ -13022,8 +13044,8 @@ var CutInAbsorber = class {
|
|
|
13022
13044
|
const cut = this.cut || (meta?.supersedes !== void 0 && meta.supersedes === this.absorbed?.text ? this.absorbed.cut : "");
|
|
13023
13045
|
this.cut = "";
|
|
13024
13046
|
this.absorbed = void 0;
|
|
13025
|
-
const
|
|
13026
|
-
if (!cut ||
|
|
13047
|
+
const words3 = (s) => s.trim().split(/\s+/).filter(Boolean).length;
|
|
13048
|
+
if (!cut || words3(text) - words3(cut) >= 3) return false;
|
|
13027
13049
|
this.absorbed = { text, cut };
|
|
13028
13050
|
return true;
|
|
13029
13051
|
}
|
|
@@ -13082,6 +13104,79 @@ function bindAckInstinct(v, instinct) {
|
|
|
13082
13104
|
onDiag(ev);
|
|
13083
13105
|
};
|
|
13084
13106
|
}
|
|
13107
|
+
function bindSecondChannel(v, sc, live) {
|
|
13108
|
+
const o = v.options;
|
|
13109
|
+
const { onPartial, onUtterance, onState, onBargeIn, onBackchannel } = o;
|
|
13110
|
+
const { show } = sc.options;
|
|
13111
|
+
let own = false;
|
|
13112
|
+
let blip = "";
|
|
13113
|
+
o.onBackchannel = (p) => {
|
|
13114
|
+
blip = p;
|
|
13115
|
+
onBackchannel(p);
|
|
13116
|
+
};
|
|
13117
|
+
const take = v.takeInterruptedReply.bind(v);
|
|
13118
|
+
v.takeInterruptedReply = () => {
|
|
13119
|
+
const r = take();
|
|
13120
|
+
if (!r || !sc.spoke(r.full)) return r;
|
|
13121
|
+
return r.queued.length ? { ...r, full: "", heard: "" } : null;
|
|
13122
|
+
};
|
|
13123
|
+
o.onPartial = (t) => {
|
|
13124
|
+
if (live() && !v.audible) sc.onInterim(t);
|
|
13125
|
+
onPartial(t);
|
|
13126
|
+
};
|
|
13127
|
+
o.onUtterance = ((t, meta) => {
|
|
13128
|
+
if (live() && t.trim()) sc.onUserTurn(t);
|
|
13129
|
+
onUtterance(t, meta);
|
|
13130
|
+
});
|
|
13131
|
+
o.onState = (s) => {
|
|
13132
|
+
if (live() && !own) {
|
|
13133
|
+
if (s === "speaking") sc.onAgentState("first_audio");
|
|
13134
|
+
else if (s === "listening") sc.onAgentState("idle");
|
|
13135
|
+
}
|
|
13136
|
+
onState(s);
|
|
13137
|
+
};
|
|
13138
|
+
o.onBargeIn = (phase) => {
|
|
13139
|
+
sc.onBargeIn();
|
|
13140
|
+
onBargeIn(phase);
|
|
13141
|
+
};
|
|
13142
|
+
Object.assign(sc.options, {
|
|
13143
|
+
aec: v.usingAec,
|
|
13144
|
+
speak: (text, kind) => {
|
|
13145
|
+
if (!live() || v.audible) return false;
|
|
13146
|
+
if (kind === "backchannel") {
|
|
13147
|
+
if (sc.language !== "en") return false;
|
|
13148
|
+
blip = "";
|
|
13149
|
+
v.fireBackchannel();
|
|
13150
|
+
return;
|
|
13151
|
+
}
|
|
13152
|
+
own = true;
|
|
13153
|
+
v.speakFiller(text);
|
|
13154
|
+
return v.awaitIdle().finally(() => {
|
|
13155
|
+
own = false;
|
|
13156
|
+
});
|
|
13157
|
+
},
|
|
13158
|
+
show: (text, kind) => show(kind === "backchannel" && blip ? blip : text, kind)
|
|
13159
|
+
});
|
|
13160
|
+
}
|
|
13161
|
+
function channelInput(sc, m) {
|
|
13162
|
+
if (m.type === "interim") {
|
|
13163
|
+
if (typeof m.aec === "boolean") sc.options.aec = m.aec;
|
|
13164
|
+
if (typeof m.lang === "string") sc.setLanguage(m.lang, true);
|
|
13165
|
+
if (typeof m.text === "string") sc.onInterim(m.text);
|
|
13166
|
+
return true;
|
|
13167
|
+
}
|
|
13168
|
+
if (m.type === "pause") {
|
|
13169
|
+
sc.onPause(Number(m.ms) || 0);
|
|
13170
|
+
return true;
|
|
13171
|
+
}
|
|
13172
|
+
if (m.type === "agent_state") {
|
|
13173
|
+
if (typeof m.state === "string" && MAIN_STATES.has(m.state)) sc.onAgentState(m.state);
|
|
13174
|
+
else log27.warn(`agent_state: unknown state ${JSON.stringify(m.state)} \u2014 ignored`);
|
|
13175
|
+
return true;
|
|
13176
|
+
}
|
|
13177
|
+
return false;
|
|
13178
|
+
}
|
|
13179
|
+
var MAIN_STATES = /* @__PURE__ */ new Set(["idle", "queued", "thinking", "streaming_text", "first_audio", "speaking"]);
|
|
13085
13180
|
var VoiceIOOptions = class extends VoiceEngineOptions {
|
|
13086
13181
|
sonioxApiKey = process.env.SONIOX_API_KEY ?? "";
|
|
13087
13182
|
cartesiaApiKey = process.env.CARTESIA_API_KEY ?? "";
|
|
@@ -13251,16 +13346,581 @@ var JsonlDiagSink = class {
|
|
|
13251
13346
|
}
|
|
13252
13347
|
};
|
|
13253
13348
|
|
|
13349
|
+
// src/voice/secondChannel.ts
|
|
13350
|
+
init_logging();
|
|
13351
|
+
|
|
13352
|
+
// src/voice/channelLang.ts
|
|
13353
|
+
init_logging();
|
|
13354
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
13355
|
+
import { join as join8 } from "path";
|
|
13356
|
+
var log29 = forComponent("ChannelBanks");
|
|
13357
|
+
var LINE_KINDS = ["backchannel", "filler", "bridge"];
|
|
13358
|
+
var BANK_KEYS = [...LINE_KINDS, "phatic"];
|
|
13359
|
+
var EN_PHATIC = [
|
|
13360
|
+
"hey",
|
|
13361
|
+
"hi",
|
|
13362
|
+
"hello",
|
|
13363
|
+
"yo",
|
|
13364
|
+
"hiya",
|
|
13365
|
+
"morning",
|
|
13366
|
+
"good morning",
|
|
13367
|
+
"good evening",
|
|
13368
|
+
"good afternoon",
|
|
13369
|
+
"thanks",
|
|
13370
|
+
"thank you",
|
|
13371
|
+
"ok",
|
|
13372
|
+
"okay",
|
|
13373
|
+
"yes",
|
|
13374
|
+
"yeah",
|
|
13375
|
+
"yep",
|
|
13376
|
+
"no",
|
|
13377
|
+
"nope",
|
|
13378
|
+
"sure",
|
|
13379
|
+
"bye",
|
|
13380
|
+
"goodbye",
|
|
13381
|
+
"how are you",
|
|
13382
|
+
"what's up",
|
|
13383
|
+
"whats up",
|
|
13384
|
+
"can you hear me"
|
|
13385
|
+
];
|
|
13386
|
+
var EN_BANK = { ...CHANNEL_BANK, phatic: EN_PHATIC };
|
|
13387
|
+
var primaryLang = (tag) => tag.trim().toLowerCase().split(/[-_]/)[0];
|
|
13388
|
+
function isPhatic(text, bank) {
|
|
13389
|
+
const t = norm2(text);
|
|
13390
|
+
return !!t && !!bank?.phatic?.some((p) => {
|
|
13391
|
+
const q2 = norm2(p);
|
|
13392
|
+
return !!q2 && (t === q2 || t.startsWith(q2 + " "));
|
|
13393
|
+
});
|
|
13394
|
+
}
|
|
13395
|
+
var SCRIPTS = [
|
|
13396
|
+
[new RegExp("\\p{Script=Hebrew}", "gu"), "he"],
|
|
13397
|
+
[new RegExp("\\p{Script=Arabic}", "gu"), "ar"],
|
|
13398
|
+
[new RegExp("\\p{Script=Cyrillic}", "gu"), "ru"],
|
|
13399
|
+
[new RegExp("\\p{Script=Greek}", "gu"), "el"],
|
|
13400
|
+
[/[\p{Script=Hiragana}\p{Script=Katakana}]/gu, "ja"],
|
|
13401
|
+
[new RegExp("\\p{Script=Hangul}", "gu"), "ko"],
|
|
13402
|
+
[new RegExp("\\p{Script=Han}", "gu"), "zh"],
|
|
13403
|
+
[new RegExp("\\p{Script=Thai}", "gu"), "th"],
|
|
13404
|
+
[new RegExp("\\p{Script=Devanagari}", "gu"), "hi"],
|
|
13405
|
+
[new RegExp("\\p{Script=Latin}", "gu"), "latin"]
|
|
13406
|
+
];
|
|
13407
|
+
var STOP3 = Object.fromEntries(Object.entries({
|
|
13408
|
+
en: "hi hello hey yes thanks let see check one sec just give right well look the and is are you i it to of that what this was have with for not but my me",
|
|
13409
|
+
es: "hola s\xED gracias bueno a me te lo se le del al el la los las que es y de en un una por para con no pero mi yo qu\xE9 est\xE1",
|
|
13410
|
+
fr: "bonjour salut oui merci le la les est et de des un une que je tu il pas pour avec mais mon qui",
|
|
13411
|
+
de: "hallo ja danke der die das und ist ich du nicht ein eine mit zu was aber mein wie auch",
|
|
13412
|
+
it: "ciao grazie s\xEC a mi ti si il lo la gli che \xE8 e di un una non per con ma mio io sono",
|
|
13413
|
+
pt: "ol\xE1 oi sim obrigado obrigada o a os as que \xE9 e de em um uma n\xE3o para com mas meu eu voc\xEA",
|
|
13414
|
+
nl: "hoi dank de het een en is ik je niet van dat met voor maar mijn wat"
|
|
13415
|
+
}).map(([k, v]) => [k, new Set(v.split(" "))]));
|
|
13416
|
+
var scriptOf = (w) => SCRIPTS.find(([re]) => {
|
|
13417
|
+
re.lastIndex = 0;
|
|
13418
|
+
return re.test(w);
|
|
13419
|
+
})?.[1] ?? null;
|
|
13420
|
+
function detectLanguage(text) {
|
|
13421
|
+
const ws = text.toLowerCase().match(new RegExp("\\p{L}+", "gu")) ?? [];
|
|
13422
|
+
const n = /* @__PURE__ */ new Map();
|
|
13423
|
+
for (const w of ws) {
|
|
13424
|
+
const s = scriptOf(w);
|
|
13425
|
+
if (s) n.set(s, (n.get(s) ?? 0) + 1);
|
|
13426
|
+
}
|
|
13427
|
+
const [best, c] = [...n].sort((a2, b2) => b2[1] - a2[1])[0] ?? [null, 0];
|
|
13428
|
+
if (best !== "latin") return c >= 1 && (best === "zh" || best === "ja" || c >= 2 || ws.length === 1) ? best : null;
|
|
13429
|
+
const score = Object.entries(STOP3).map(([l2, s]) => [l2, ws.filter((w) => s.has(w)).length]).sort((a2, b2) => b2[1] - a2[1]);
|
|
13430
|
+
const [[l, a], [, b]] = score;
|
|
13431
|
+
return a > b && (a >= 2 || a === 1 && ws.length <= 3) ? l : null;
|
|
13432
|
+
}
|
|
13433
|
+
var NAMES = new Intl.DisplayNames(["en"], { type: "language" });
|
|
13434
|
+
var languageName = (lang) => {
|
|
13435
|
+
try {
|
|
13436
|
+
return NAMES.of(lang) ?? lang;
|
|
13437
|
+
} catch {
|
|
13438
|
+
return lang;
|
|
13439
|
+
}
|
|
13440
|
+
};
|
|
13441
|
+
function fitsLanguage(line, lang) {
|
|
13442
|
+
const own = SCRIPTS.find(([, l]) => l === lang)?.[0];
|
|
13443
|
+
if (own) return (line.match(own)?.length ?? 0) > 0 && !new RegExp("\\p{Script=Latin}{2,}", "u").test(line);
|
|
13444
|
+
if (/[^\p{Script=Latin}\p{M}\p{P}\p{Z}\p{N}\p{S}]/u.test(line)) return false;
|
|
13445
|
+
const mine = STOP3[lang];
|
|
13446
|
+
const ws = line.toLowerCase().match(new RegExp("\\p{L}+", "gu")) ?? [];
|
|
13447
|
+
return !ws.some((w) => !mine?.has(w) && Object.entries(STOP3).some(([l, s]) => l !== lang && s.has(w)));
|
|
13448
|
+
}
|
|
13449
|
+
var norm2 = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}' ]/gu, "").replace(/\s+/g, " ").trim();
|
|
13450
|
+
function cleanLine(s, lang, maxWords = 6) {
|
|
13451
|
+
const raw = s.trim().split("\n")[0];
|
|
13452
|
+
const l = raw.replace(/^[-*•\d.)\s]+/, "").replace(/^["'“”«»]+|["'“”«»]+$/g, "").replace(new RegExp("^\\p{L}+:\\s*", "u"), "").trim();
|
|
13453
|
+
if (!l || isLeakSuspect(raw) || isLeakSuspect(l)) {
|
|
13454
|
+
if (l) log29.debug(`line "${raw}" reads as meta \u2014 rejected`);
|
|
13455
|
+
return "";
|
|
13456
|
+
}
|
|
13457
|
+
if (lang && !fitsLanguage(l, lang)) {
|
|
13458
|
+
log29.debug(`line "${l}" is not ${lang} \u2014 rejected`);
|
|
13459
|
+
return "";
|
|
13460
|
+
}
|
|
13461
|
+
return l.split(/\s+/).length > maxWords || l.length > 60 ? "" : l;
|
|
13462
|
+
}
|
|
13463
|
+
function validateBank(raw, lang, minPerKind = 3) {
|
|
13464
|
+
if (!raw || typeof raw !== "object") return null;
|
|
13465
|
+
const out = {};
|
|
13466
|
+
for (const k of BANK_KEYS) {
|
|
13467
|
+
const v = raw[k];
|
|
13468
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13469
|
+
out[k] = (Array.isArray(v) ? v : []).filter((x) => typeof x === "string").map((x) => cleanLine(x, lang)).filter((x) => x && !seen.has(norm2(x)) && seen.add(norm2(x)));
|
|
13470
|
+
if (out[k].length < minPerKind) {
|
|
13471
|
+
log29.warn(`bank ${lang}: only ${out[k].length} valid ${k} lines \u2014 rejected`);
|
|
13472
|
+
return null;
|
|
13473
|
+
}
|
|
13474
|
+
}
|
|
13475
|
+
return out;
|
|
13476
|
+
}
|
|
13477
|
+
var KIND_HINT = {
|
|
13478
|
+
backchannel: 'tiny listener cues while they keep talking (like "mm-hm", "right") \u2014 never takes the turn',
|
|
13479
|
+
filler: 'soft thinking sounds at their pause (like "hmm\u2026")',
|
|
13480
|
+
bridge: 'quick lead-ins before the real answer while it is prepared (like "let me check\u2014")',
|
|
13481
|
+
phatic: 'greetings, thanks and yes/no openers a person starts a turn with (like "hi", "thanks", "how are you")'
|
|
13482
|
+
};
|
|
13483
|
+
function fastBankGenerator(ai, model, perKind = 12) {
|
|
13484
|
+
return async (lang, avoid, signal) => {
|
|
13485
|
+
const name = languageName(lang);
|
|
13486
|
+
const prompt = `Write short spoken side-sounds for a live voice conversation, in ${name} (natural, casual, how native ${name} speakers really talk \u2014 not translated English). ${perKind} varied lines per kind, 1-4 words each \u2014 short, idiomatic, grammatical everyday phrases a native would actually say:
|
|
13487
|
+
${BANK_KEYS.map((k) => `- ${k}: ${KIND_HINT[k]}`).join("\n")}
|
|
13488
|
+
` + (avoid.length ? `Do not repeat these existing ones: ${avoid.slice(0, 60).join(" | ")}
|
|
13489
|
+
` : "") + `Reply with JSON only: {"backchannel":[...],"filler":[...],"bridge":[...],"phatic":[...]}. Every line in ${name}; no explanations.`;
|
|
13490
|
+
const reasoning = /gpt-oss/.test(model) ? { providerOptions: { reasoning_effort: "low" } } : {};
|
|
13491
|
+
const r = await ai.chat({ model, messages: [{ role: "user", content: prompt }], stream: false, signal, maxTokens: 2e3, ...reasoning });
|
|
13492
|
+
const m = (r.content ?? "").match(/\{[\s\S]*\}/);
|
|
13493
|
+
if (!m) throw new Error(`no JSON in bank reply: ${(r.content ?? "").slice(0, 80)}`);
|
|
13494
|
+
return JSON.parse(m[0]);
|
|
13495
|
+
};
|
|
13496
|
+
}
|
|
13497
|
+
var ChannelBanksOptions = class {
|
|
13498
|
+
/** Raw bank for a language (fastBankGenerator). Absent → English fallback only. */
|
|
13499
|
+
generate;
|
|
13500
|
+
/** Disk cache dir (one `<lang>.json` per language). Absent → in-memory only. */
|
|
13501
|
+
cacheDir;
|
|
13502
|
+
/** A cached bank older than this is still used, and topped up by one background call. */
|
|
13503
|
+
refreshMs = 24 * 36e5;
|
|
13504
|
+
/** Lines kept per kind (newest win) — the cap on growth. */
|
|
13505
|
+
maxPerKind = 40;
|
|
13506
|
+
timeoutMs = 2e4;
|
|
13507
|
+
/** After a failed cold generation, the next attempt waits this long. */
|
|
13508
|
+
retryMs = 6e4;
|
|
13509
|
+
/** Cold-start/offline banks (English only by default). */
|
|
13510
|
+
fallback = { en: EN_BANK };
|
|
13511
|
+
now = () => Date.now();
|
|
13512
|
+
};
|
|
13513
|
+
var ChannelBanks = class {
|
|
13514
|
+
constructor(options) {
|
|
13515
|
+
this.options = options;
|
|
13516
|
+
this.options = { ...new ChannelBanksOptions(), ...options };
|
|
13517
|
+
}
|
|
13518
|
+
options;
|
|
13519
|
+
banks = /* @__PURE__ */ new Map();
|
|
13520
|
+
pending = /* @__PURE__ */ new Map();
|
|
13521
|
+
get o() {
|
|
13522
|
+
return this.options;
|
|
13523
|
+
}
|
|
13524
|
+
/** The bank to speak from for `lang` now — generated/cached, else the fallback, else null (= stay silent). */
|
|
13525
|
+
get(lang) {
|
|
13526
|
+
return this.banks.get(lang) ?? this.o.fallback[lang] ?? null;
|
|
13527
|
+
}
|
|
13528
|
+
/** Load (cache) and/or generate `lang`'s bank in the background. Never throws, never blocks the caller. */
|
|
13529
|
+
ensure(lang) {
|
|
13530
|
+
if (!this.pending.has(lang)) this.pending.set(lang, this.load(lang));
|
|
13531
|
+
return this.pending.get(lang);
|
|
13532
|
+
}
|
|
13533
|
+
file(lang) {
|
|
13534
|
+
return this.o.cacheDir ? join8(this.o.cacheDir, `${lang.replace(/[^\w-]/g, "")}.json`) : null;
|
|
13535
|
+
}
|
|
13536
|
+
async load(lang) {
|
|
13537
|
+
const f = this.file(lang);
|
|
13538
|
+
let cached = null;
|
|
13539
|
+
if (f) cached = await readFile(f, "utf8").then((s) => JSON.parse(s)).catch((e) => {
|
|
13540
|
+
if (e.code !== "ENOENT") log29.warn(`bank cache ${f} unreadable \u2014 regenerating: ${e.message}`);
|
|
13541
|
+
return null;
|
|
13542
|
+
});
|
|
13543
|
+
const valid = cached && validateBank(cached.bank, lang, 1);
|
|
13544
|
+
if (valid) {
|
|
13545
|
+
this.banks.set(lang, valid);
|
|
13546
|
+
log29.debug(`bank ${lang}: cache hit (${BANK_KEYS.map((k) => valid[k].length).join("/")})`);
|
|
13547
|
+
}
|
|
13548
|
+
if (valid && this.o.now() - cached.at < this.o.refreshMs) return;
|
|
13549
|
+
await this.generate(lang, valid);
|
|
13550
|
+
}
|
|
13551
|
+
async generate(lang, have) {
|
|
13552
|
+
if (!this.o.generate) return;
|
|
13553
|
+
const ac = new AbortController();
|
|
13554
|
+
const t = setTimeout(() => ac.abort(), this.o.timeoutMs);
|
|
13555
|
+
const t0 = this.o.now();
|
|
13556
|
+
try {
|
|
13557
|
+
const fresh = validateBank(await this.o.generate(lang, have ? BANK_KEYS.flatMap((k) => have[k]) : [], ac.signal), lang);
|
|
13558
|
+
if (!fresh) return;
|
|
13559
|
+
const bank = {};
|
|
13560
|
+
for (const k of BANK_KEYS) {
|
|
13561
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13562
|
+
bank[k] = [...fresh[k], ...have?.[k] ?? []].filter((x) => !seen.has(norm2(x)) && seen.add(norm2(x))).slice(0, this.o.maxPerKind);
|
|
13563
|
+
}
|
|
13564
|
+
this.banks.set(lang, bank);
|
|
13565
|
+
log29.info(`bank ${lang}: ${have ? "refreshed" : "generated"} (${BANK_KEYS.map((k) => bank[k].length).join("/")}) in ${Math.round(this.o.now() - t0)}ms`);
|
|
13566
|
+
const f = this.file(lang);
|
|
13567
|
+
if (f) await mkdir(this.o.cacheDir, { recursive: true }).then(() => writeFile(f, JSON.stringify({ at: this.o.now(), bank }))).catch((e) => log29.warn(`bank cache write ${f} failed: ${e.message}`));
|
|
13568
|
+
} catch (e) {
|
|
13569
|
+
log29.warn(`bank ${lang}: generation failed \u2014 ${have ? "keeping the cached bank" : lang in this.o.fallback ? "fallback bank" : "silent"}: ${e.message}`);
|
|
13570
|
+
} finally {
|
|
13571
|
+
clearTimeout(t);
|
|
13572
|
+
if (!this.banks.has(lang)) setTimeout(() => this.pending.delete(lang), this.o.retryMs).unref?.();
|
|
13573
|
+
}
|
|
13574
|
+
}
|
|
13575
|
+
};
|
|
13576
|
+
|
|
13577
|
+
// src/voice/secondChannel.ts
|
|
13578
|
+
var log30 = forComponent("SecondChannel");
|
|
13579
|
+
var SecondChannelOptions = class {
|
|
13580
|
+
/** Refiner (Jev/fast LLM): the choice over CHANNEL_ACTIONS, or null on failure. Absent → the rule alone decides. */
|
|
13581
|
+
decide;
|
|
13582
|
+
/** Fast-model line writer (see fastLineGenerator). Absent or late → the canned bank. */
|
|
13583
|
+
generate;
|
|
13584
|
+
/** Per-language banks (ChannelBanks). Absent → the hardcoded English bank, and silence in any other language. */
|
|
13585
|
+
banks;
|
|
13586
|
+
/** The host's language (BCP-47; pinned — detection never overrides it). Absent → unknown until the user's words
|
|
13587
|
+
* tell it confidently; the channel is silent until then (never English by default). */
|
|
13588
|
+
lang;
|
|
13589
|
+
/** Kinds worth generating (content-conditioned); the rest are canned sounds. */
|
|
13590
|
+
generateFor = ["backchannel", "bridge"];
|
|
13591
|
+
/** Host TTS on the second channel; resolves when the line finished playing. `false` = the host could not play it. */
|
|
13592
|
+
speak = () => {
|
|
13593
|
+
};
|
|
13594
|
+
/** Host UI (caption / muted line) — never history. */
|
|
13595
|
+
show = () => {
|
|
13596
|
+
};
|
|
13597
|
+
/** Stop the in-flight line's audio now (barge-in only). */
|
|
13598
|
+
cancel = () => {
|
|
13599
|
+
};
|
|
13600
|
+
onEvent = () => {
|
|
13601
|
+
};
|
|
13602
|
+
/** Echo cancellation on: backchannels may play while the user speaks. Off (half-duplex) → fillers/bridges in silences only. */
|
|
13603
|
+
aec = false;
|
|
13604
|
+
decideEveryWords = 3;
|
|
13605
|
+
decisionBudgetMs = 300;
|
|
13606
|
+
generateBudgetMs = 500;
|
|
13607
|
+
minGapMs = 2500;
|
|
13608
|
+
/** Backchannel after backchannel needs this gap (v1 Instinct's cadence, ≤12/min). */
|
|
13609
|
+
backchannelGapMs = 5e3;
|
|
13610
|
+
/** A decision whose partial grew by this many words while in flight is stale → dropped. */
|
|
13611
|
+
staleWords = 3;
|
|
13612
|
+
microWords = 30;
|
|
13613
|
+
avoidLines = 4;
|
|
13614
|
+
/** Canned picks exclude the last N picks of the same kind. */
|
|
13615
|
+
bankExclude = 2;
|
|
13616
|
+
backchannelMinWords = 8;
|
|
13617
|
+
/** No new interim for this long → a pause (hosts may also call onPause). */
|
|
13618
|
+
fillerPauseMs = 700;
|
|
13619
|
+
fillerMinWords = 4;
|
|
13620
|
+
bridgeMinWords = 5;
|
|
13621
|
+
now = () => performance.now();
|
|
13622
|
+
random = Math.random;
|
|
13623
|
+
};
|
|
13624
|
+
var words2 = (s) => s.trim() ? s.trim().split(/\s+/) : [];
|
|
13625
|
+
function withBudget(p, ms, what) {
|
|
13626
|
+
let t;
|
|
13627
|
+
const late = new Promise((r) => {
|
|
13628
|
+
t = setTimeout(() => {
|
|
13629
|
+
log30.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
|
|
13630
|
+
r(null);
|
|
13631
|
+
}, ms);
|
|
13632
|
+
});
|
|
13633
|
+
return Promise.race([p.catch((e) => {
|
|
13634
|
+
log30.warn(`${what} failed \u2014 falling back: ${e?.message ?? e}`);
|
|
13635
|
+
return null;
|
|
13636
|
+
}), late]).finally(() => clearTimeout(t));
|
|
13637
|
+
}
|
|
13638
|
+
var SecondChannel = class {
|
|
13639
|
+
constructor(options) {
|
|
13640
|
+
this.options = options;
|
|
13641
|
+
this.options = { ...new SecondChannelOptions(), ...options };
|
|
13642
|
+
this.startedAt = this.o.now();
|
|
13643
|
+
if (this.o.lang) this.setLanguage(this.o.lang, true);
|
|
13644
|
+
}
|
|
13645
|
+
options;
|
|
13646
|
+
heard = "";
|
|
13647
|
+
userSpeaking = false;
|
|
13648
|
+
main = "idle";
|
|
13649
|
+
lead = "";
|
|
13650
|
+
gen = 0;
|
|
13651
|
+
// bumped by barge-in / a new user turn → in-flight decisions drop
|
|
13652
|
+
decidedAt = 0;
|
|
13653
|
+
// word count at the last decision
|
|
13654
|
+
busy = false;
|
|
13655
|
+
pauseTimer = null;
|
|
13656
|
+
lastAt = -Infinity;
|
|
13657
|
+
lastKind = null;
|
|
13658
|
+
live = null;
|
|
13659
|
+
// our last line, until channel-1 audio takes the floor
|
|
13660
|
+
cutLine = null;
|
|
13661
|
+
// the line the last barge actually cut (spoke())
|
|
13662
|
+
bridged = false;
|
|
13663
|
+
// one bridge per main turn
|
|
13664
|
+
recent = [];
|
|
13665
|
+
picks = /* @__PURE__ */ new Map();
|
|
13666
|
+
inflight = null;
|
|
13667
|
+
startedAt;
|
|
13668
|
+
lang = null;
|
|
13669
|
+
pinned = false;
|
|
13670
|
+
/** The language lines are spoken in (detected from the user's words, or set by the host). */
|
|
13671
|
+
get language() {
|
|
13672
|
+
return this.lang;
|
|
13673
|
+
}
|
|
13674
|
+
/** Switch language: its bank starts loading in the background. `host` = the host's STT-reported language — it pins
|
|
13675
|
+
* the language (later detection never overrides it; only another host call does). Tags normalize to 'he-IL' → 'he'. */
|
|
13676
|
+
setLanguage(tag, host = false) {
|
|
13677
|
+
if (!tag || this.pinned && !host) return;
|
|
13678
|
+
if (host) this.pinned = true;
|
|
13679
|
+
const lang = primaryLang(tag);
|
|
13680
|
+
if (!lang || lang === this.lang) return;
|
|
13681
|
+
log30.info(`language ${this.lang} \u2192 ${lang}`);
|
|
13682
|
+
this.lang = lang;
|
|
13683
|
+
this.recent = [];
|
|
13684
|
+
this.picks.clear();
|
|
13685
|
+
void this.o.banks?.ensure(lang);
|
|
13686
|
+
}
|
|
13687
|
+
detect(text) {
|
|
13688
|
+
if (this.pinned) return;
|
|
13689
|
+
const l = detectLanguage(text);
|
|
13690
|
+
if (l) this.setLanguage(l);
|
|
13691
|
+
}
|
|
13692
|
+
get bank() {
|
|
13693
|
+
return this.lang ? this.o.banks ? this.o.banks.get(this.lang) : this.lang === "en" ? EN_BANK : null : null;
|
|
13694
|
+
}
|
|
13695
|
+
get o() {
|
|
13696
|
+
return this.options;
|
|
13697
|
+
}
|
|
13698
|
+
/** The user's live partial for the current turn (the whole partial, not a delta). */
|
|
13699
|
+
onInterim(text) {
|
|
13700
|
+
const t = text.trim();
|
|
13701
|
+
if (!t || t === this.heard) return;
|
|
13702
|
+
if (this.main !== "idle") this.onAgentState("idle");
|
|
13703
|
+
this.heard = t;
|
|
13704
|
+
this.detect(t);
|
|
13705
|
+
this.userSpeaking = true;
|
|
13706
|
+
if (this.pauseTimer) clearTimeout(this.pauseTimer);
|
|
13707
|
+
this.pauseTimer = setTimeout(() => {
|
|
13708
|
+
this.pauseTimer = null;
|
|
13709
|
+
this.onPause(this.o.fillerPauseMs);
|
|
13710
|
+
}, this.o.fillerPauseMs);
|
|
13711
|
+
if (words2(t).length - this.decidedAt >= this.o.decideEveryWords) void this.consider("interim");
|
|
13712
|
+
}
|
|
13713
|
+
/** The user went quiet for `ms` (host VAD/STT pause, or the internal interim timer). */
|
|
13714
|
+
onPause(ms) {
|
|
13715
|
+
if (this.pauseTimer) {
|
|
13716
|
+
clearTimeout(this.pauseTimer);
|
|
13717
|
+
this.pauseTimer = null;
|
|
13718
|
+
}
|
|
13719
|
+
this.userSpeaking = false;
|
|
13720
|
+
void this.consider("pause", ms);
|
|
13721
|
+
}
|
|
13722
|
+
/** Channel-1 lifecycle. `queued` = the user's turn was dispatched to the main agent. */
|
|
13723
|
+
onAgentState(s) {
|
|
13724
|
+
if (s === this.main) return;
|
|
13725
|
+
this.main = s;
|
|
13726
|
+
if (s === "first_audio" || s === "speaking") this.live = null;
|
|
13727
|
+
if (s === "idle") {
|
|
13728
|
+
this.heard = "";
|
|
13729
|
+
this.decidedAt = 0;
|
|
13730
|
+
this.lead = "";
|
|
13731
|
+
this.bridged = false;
|
|
13732
|
+
return;
|
|
13733
|
+
}
|
|
13734
|
+
if (s === "queued") {
|
|
13735
|
+
this.gen++;
|
|
13736
|
+
this.userSpeaking = false;
|
|
13737
|
+
this.lead = "";
|
|
13738
|
+
this.bridged = false;
|
|
13739
|
+
if (this.pauseTimer) clearTimeout(this.pauseTimer);
|
|
13740
|
+
this.pauseTimer = null;
|
|
13741
|
+
}
|
|
13742
|
+
if (s === "queued" || s === "thinking" || s === "streaming_text") void this.consider("working");
|
|
13743
|
+
}
|
|
13744
|
+
/** The user's final line was dispatched to the main agent (its words are the bridge's micro-context). */
|
|
13745
|
+
onUserTurn(text) {
|
|
13746
|
+
this.heard = text.trim();
|
|
13747
|
+
this.detect(this.heard);
|
|
13748
|
+
this.main = "idle";
|
|
13749
|
+
this.onAgentState("queued");
|
|
13750
|
+
}
|
|
13751
|
+
/** The main answer's streamed text (before its audio): a bridge can lead into it. */
|
|
13752
|
+
onMainTokens(text) {
|
|
13753
|
+
if (this.lead.length < 120) this.lead += text;
|
|
13754
|
+
if (this.main === "queued" || this.main === "thinking") this.onAgentState("streaming_text");
|
|
13755
|
+
}
|
|
13756
|
+
/** The user barged in: cut the in-flight line now and drop every pending decision. */
|
|
13757
|
+
onBargeIn() {
|
|
13758
|
+
this.gen++;
|
|
13759
|
+
this.cutLine = this.inflight || this.live ? this.live : null;
|
|
13760
|
+
if (this.inflight) {
|
|
13761
|
+
this.inflight = null;
|
|
13762
|
+
try {
|
|
13763
|
+
this.o.cancel();
|
|
13764
|
+
} catch (e) {
|
|
13765
|
+
log30.warn(`cancel threw: ${e.message}`);
|
|
13766
|
+
}
|
|
13767
|
+
}
|
|
13768
|
+
}
|
|
13769
|
+
/** Is `text` the line the last barge-in cut (ours, still holding the floor)? Hosts strip it from the cut reply — it is
|
|
13770
|
+
* not the agent's speech. By identity, not by text: a real reply that merely repeats a recent line is kept. Consumed once. */
|
|
13771
|
+
spoke(text) {
|
|
13772
|
+
const c = this.cutLine;
|
|
13773
|
+
this.cutLine = null;
|
|
13774
|
+
return !!c && c === text.trim();
|
|
13775
|
+
}
|
|
13776
|
+
/** Resolves when the in-flight line finished playing (immediately when none) — hosts enqueue channel-1 audio behind it. */
|
|
13777
|
+
drain() {
|
|
13778
|
+
return this.inflight ?? Promise.resolve();
|
|
13779
|
+
}
|
|
13780
|
+
get audioLocked() {
|
|
13781
|
+
return this.main === "first_audio" || this.main === "speaking";
|
|
13782
|
+
}
|
|
13783
|
+
get working() {
|
|
13784
|
+
return this.main === "queued" || this.main === "thinking" || this.main === "streaming_text";
|
|
13785
|
+
}
|
|
13786
|
+
tail() {
|
|
13787
|
+
return words2(this.heard).slice(-this.o.microWords).join(" ");
|
|
13788
|
+
}
|
|
13789
|
+
/** Hard gates every line passes, whoever proposed it. */
|
|
13790
|
+
allowed(k) {
|
|
13791
|
+
if (this.audioLocked || this.inflight || this.o.now() - this.lastAt < this.o.minGapMs || k === this.lastKind && (k !== "backchannel" || this.o.now() - this.lastAt < this.o.backchannelGapMs)) return false;
|
|
13792
|
+
if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle";
|
|
13793
|
+
if (k === "filler") return !this.userSpeaking && this.main === "idle";
|
|
13794
|
+
return this.working && !this.bridged;
|
|
13795
|
+
}
|
|
13796
|
+
/** 0ms rule: the candidate line for this trigger, or silence. */
|
|
13797
|
+
rule(trigger, pauseMs = 0) {
|
|
13798
|
+
const w = words2(this.heard);
|
|
13799
|
+
if (!this.lang) return "silence";
|
|
13800
|
+
if (w.length <= 3 || w.length <= 6 && isPhatic(this.heard, this.bank)) return "silence";
|
|
13801
|
+
const k = trigger === "interim" ? w.length >= this.o.backchannelMinWords && /[,.;:!?…]$/.test(this.heard) ? "backchannel" : null : trigger === "pause" ? pauseMs >= this.o.fillerPauseMs && w.length >= this.o.fillerMinWords ? "filler" : null : w.length >= this.o.bridgeMinWords ? "bridge" : null;
|
|
13802
|
+
return k && this.allowed(k) ? k : "silence";
|
|
13803
|
+
}
|
|
13804
|
+
async consider(trigger, pauseMs) {
|
|
13805
|
+
if (this.busy) return;
|
|
13806
|
+
const n0 = words2(this.heard).length;
|
|
13807
|
+
this.decidedAt = n0;
|
|
13808
|
+
const rule = this.rule(trigger, pauseMs);
|
|
13809
|
+
if (rule === "silence") return;
|
|
13810
|
+
this.busy = true;
|
|
13811
|
+
const gen = this.gen, t0 = this.o.now();
|
|
13812
|
+
const ev = (e) => this.o.onEvent({ trigger, rule, ms: Math.round(this.o.now() - t0), ...e });
|
|
13813
|
+
const stale = () => gen !== this.gen || words2(this.heard).length - n0 >= this.o.staleWords;
|
|
13814
|
+
try {
|
|
13815
|
+
let kind = rule;
|
|
13816
|
+
let choice;
|
|
13817
|
+
if (this.o.decide) {
|
|
13818
|
+
const d = await withBudget(this.o.decide(this.tail(), this.state(pauseMs)), this.o.decisionBudgetMs, "channel decision");
|
|
13819
|
+
choice = d?.choice;
|
|
13820
|
+
if (choice && choice in CHANNEL_ACTIONS) kind = choice;
|
|
13821
|
+
}
|
|
13822
|
+
if (kind === "silence") return ev({ choice, acted: "silence" });
|
|
13823
|
+
if (stale()) return ev({ choice, kind, acted: "stale" });
|
|
13824
|
+
if (!this.allowed(kind)) return ev({ choice, kind, acted: "gated" });
|
|
13825
|
+
const text = await this.line(kind);
|
|
13826
|
+
if (!text) return ev({ choice, kind, acted: "no_bank" });
|
|
13827
|
+
if (stale()) return ev({ choice, kind, text, acted: "stale" });
|
|
13828
|
+
if (!this.allowed(kind)) return ev({ choice, kind, text, acted: "gated" });
|
|
13829
|
+
ev({ choice, kind, text, acted: this.say(kind, text) ? "spoken" : "skipped" });
|
|
13830
|
+
} finally {
|
|
13831
|
+
this.busy = false;
|
|
13832
|
+
}
|
|
13833
|
+
}
|
|
13834
|
+
state(pauseMs) {
|
|
13835
|
+
const now4 = this.o.now();
|
|
13836
|
+
return {
|
|
13837
|
+
user_speaking: this.userSpeaking,
|
|
13838
|
+
pause_ms: pauseMs ?? null,
|
|
13839
|
+
agent_state: this.main,
|
|
13840
|
+
last_line: this.recent.at(-1) ?? null,
|
|
13841
|
+
ms_since_last_line: Number.isFinite(this.lastAt) ? Math.round(now4 - this.lastAt) : null,
|
|
13842
|
+
call_ms: Math.round(now4 - this.startedAt)
|
|
13843
|
+
};
|
|
13844
|
+
}
|
|
13845
|
+
async line(kind) {
|
|
13846
|
+
if (this.o.generate && this.o.generateFor.includes(kind)) {
|
|
13847
|
+
const ac = new AbortController();
|
|
13848
|
+
const ctx = { kind, heard: this.tail(), state: this.main, avoid: [...this.recent], lang: this.lang, ...this.lead ? { lead: this.lead } : {} };
|
|
13849
|
+
const raw = await withBudget(this.o.generate(ctx, ac.signal), this.o.generateBudgetMs, `${kind} generate`);
|
|
13850
|
+
if (raw === null) ac.abort();
|
|
13851
|
+
const text = cleanLine(raw ?? "", this.lang);
|
|
13852
|
+
if (text && !this.recent.some((r) => norm2(r) === norm2(text))) return text;
|
|
13853
|
+
if (text) log30.debug(`generated "${text}" repeats a recent line \u2014 canned`);
|
|
13854
|
+
}
|
|
13855
|
+
return this.pick(kind);
|
|
13856
|
+
}
|
|
13857
|
+
/** Canned rotation in the user's language: never one of the last `bankExclude` picks of this kind, nor a recent
|
|
13858
|
+
* line. '' = no bank for this language yet (silence). */
|
|
13859
|
+
pick(kind) {
|
|
13860
|
+
const pool = this.bank?.[kind] ?? [];
|
|
13861
|
+
if (!pool.length) return "";
|
|
13862
|
+
const hist = this.picks.get(kind) ?? [];
|
|
13863
|
+
const recent = new Set([...hist.slice(-this.o.bankExclude), ...this.recent].map(norm2));
|
|
13864
|
+
const fresh = pool.filter((x) => !recent.has(norm2(x)));
|
|
13865
|
+
const from = fresh.length ? fresh : pool;
|
|
13866
|
+
const p = from[Math.floor(this.o.random() * from.length)];
|
|
13867
|
+
this.picks.set(kind, [...hist, p].slice(-this.o.bankExclude));
|
|
13868
|
+
return p;
|
|
13869
|
+
}
|
|
13870
|
+
say(kind, text) {
|
|
13871
|
+
let r;
|
|
13872
|
+
try {
|
|
13873
|
+
r = this.o.speak(text, kind);
|
|
13874
|
+
} catch (e) {
|
|
13875
|
+
log30.warn(`speak threw \u2014 line dropped: ${e.message}`);
|
|
13876
|
+
return false;
|
|
13877
|
+
}
|
|
13878
|
+
if (r === false) return false;
|
|
13879
|
+
this.lastAt = this.o.now();
|
|
13880
|
+
this.lastKind = kind;
|
|
13881
|
+
if (kind === "bridge") this.bridged = true;
|
|
13882
|
+
this.recent = [...this.recent, text].slice(-this.o.avoidLines);
|
|
13883
|
+
this.live = text.trim();
|
|
13884
|
+
this.o.show(text, kind);
|
|
13885
|
+
const p = Promise.resolve(r).then(() => {
|
|
13886
|
+
}, (e) => {
|
|
13887
|
+
log30.warn(`channel line playback failed: ${e?.message ?? e}`);
|
|
13888
|
+
}).finally(() => {
|
|
13889
|
+
if (this.inflight === p) this.inflight = null;
|
|
13890
|
+
});
|
|
13891
|
+
this.inflight = p;
|
|
13892
|
+
return true;
|
|
13893
|
+
}
|
|
13894
|
+
};
|
|
13895
|
+
var KIND_HINT2 = {
|
|
13896
|
+
backchannel: "a tiny listener cue while they keep talking \u2014 shows you follow THEIR point, does not take the turn",
|
|
13897
|
+
filler: "a soft thinking sound at their pause",
|
|
13898
|
+
bridge: "a quick lead-in before the real answer, while it is being prepared"
|
|
13899
|
+
};
|
|
13900
|
+
function fastLineGenerator(ai, model) {
|
|
13901
|
+
return async (c, signal) => {
|
|
13902
|
+
const prompt = `You voice the tiny side-sounds of a live spoken conversation. Write ONE line of 1-5 words: ${KIND_HINT2[c.kind]}.
|
|
13903
|
+
The user just said: "${c.heard}"
|
|
13904
|
+
The assistant is: ${c.state}.
|
|
13905
|
+
` + (c.lead ? `The real answer begins: "${c.lead.trim()}" \u2014 lead naturally into it; do not repeat it.
|
|
13906
|
+
` : "") + (c.avoid.length ? `Do not reuse any of: ${c.avoid.map((a) => `"${a}"`).join(", ")}.
|
|
13907
|
+
` : "") + `React to their actual words, casually, in ${languageName(c.lang)}. Output only the line.`;
|
|
13908
|
+
const reasoning = /gpt-oss/.test(model) ? { providerOptions: { reasoning_effort: "low" } } : {};
|
|
13909
|
+
const r = await ai.chat({ model, messages: [{ role: "user", content: prompt }], stream: false, signal, maxTokens: 150, ...reasoning });
|
|
13910
|
+
return r.content ?? "";
|
|
13911
|
+
};
|
|
13912
|
+
}
|
|
13913
|
+
|
|
13254
13914
|
// cli/config.ts
|
|
13255
13915
|
import { homedir as homedir5 } from "os";
|
|
13256
13916
|
import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
|
|
13257
|
-
import { join as
|
|
13917
|
+
import { join as join10 } from "path";
|
|
13258
13918
|
import { pathToFileURL } from "url";
|
|
13259
13919
|
|
|
13260
13920
|
// cli/permissions.ts
|
|
13261
13921
|
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
|
|
13262
13922
|
import { homedir as homedir4 } from "os";
|
|
13263
|
-
import { join as
|
|
13923
|
+
import { join as join9 } from "path";
|
|
13264
13924
|
var RULE_RE = /^(\w[\w*-]*)(?:\((.+)\))?$/;
|
|
13265
13925
|
function parseOne(raw, decision) {
|
|
13266
13926
|
const m = RULE_RE.exec(raw.trim());
|
|
@@ -13282,13 +13942,13 @@ function parsePermRules(perms, explicit = false) {
|
|
|
13282
13942
|
function describeRule(r) {
|
|
13283
13943
|
return `${r.decision.padEnd(5)} ${r.tool ?? "*"}${r.pathGlob ? `(${r.pathGlob})` : ""}`;
|
|
13284
13944
|
}
|
|
13285
|
-
var PERM_FILE = (cwd) =>
|
|
13945
|
+
var PERM_FILE = (cwd) => join9(cwd, ".agent", "permissions.json");
|
|
13286
13946
|
function loadPersistedRules(cwd) {
|
|
13287
13947
|
const j = readJsonFile(PERM_FILE(cwd), null);
|
|
13288
13948
|
return j ? { allow: j.allow ?? [], ask: j.ask ?? [], deny: j.deny ?? [] } : {};
|
|
13289
13949
|
}
|
|
13290
13950
|
function loadClaudeSettings(cwd, home = homedir4()) {
|
|
13291
|
-
const files = [
|
|
13951
|
+
const files = [join9(home, ".claude", "settings.json"), join9(cwd, ".claude", "settings.json"), join9(cwd, ".claude", "settings.local.json")];
|
|
13292
13952
|
let out = {};
|
|
13293
13953
|
for (const p of files) {
|
|
13294
13954
|
const perms = readJsonFile(p, null)?.permissions;
|
|
@@ -13301,7 +13961,7 @@ function persistRule(cwd, decision, ruleStr) {
|
|
|
13301
13961
|
const list = cur[decision] ??= [];
|
|
13302
13962
|
if (!list.includes(ruleStr)) list.push(ruleStr);
|
|
13303
13963
|
try {
|
|
13304
|
-
mkdirSync7(
|
|
13964
|
+
mkdirSync7(join9(cwd, ".agent"), { recursive: true });
|
|
13305
13965
|
writeFileSync4(PERM_FILE(cwd), JSON.stringify(cur, null, 2) + "\n");
|
|
13306
13966
|
} catch {
|
|
13307
13967
|
}
|
|
@@ -13315,7 +13975,7 @@ function mergePerms(a, b) {
|
|
|
13315
13975
|
}
|
|
13316
13976
|
return Object.keys(out).length ? out : void 0;
|
|
13317
13977
|
}
|
|
13318
|
-
var TRUST_FILE =
|
|
13978
|
+
var TRUST_FILE = join9(homedir4(), ".agent", "trusted.json");
|
|
13319
13979
|
function isTrusted(cwd, file = TRUST_FILE) {
|
|
13320
13980
|
const list = readJsonFile(file, []);
|
|
13321
13981
|
return Array.isArray(list) && list.includes(cwd);
|
|
@@ -13325,7 +13985,7 @@ function trustDir(cwd, file = TRUST_FILE) {
|
|
|
13325
13985
|
if (!Array.isArray(list)) list = [];
|
|
13326
13986
|
if (!list.includes(cwd)) list.push(cwd);
|
|
13327
13987
|
try {
|
|
13328
|
-
mkdirSync7(
|
|
13988
|
+
mkdirSync7(join9(file, ".."), { recursive: true });
|
|
13329
13989
|
writeFileSync4(file, JSON.stringify(list, null, 2) + "\n");
|
|
13330
13990
|
} catch {
|
|
13331
13991
|
}
|
|
@@ -13335,7 +13995,7 @@ function trustDir(cwd, file = TRUST_FILE) {
|
|
|
13335
13995
|
var FILES = ["config.ts", "config.js", "config.mjs", "config.json"];
|
|
13336
13996
|
async function loadFrom(dir) {
|
|
13337
13997
|
for (const f of FILES) {
|
|
13338
|
-
const p =
|
|
13998
|
+
const p = join10(dir, ".agent", f);
|
|
13339
13999
|
if (!existsSync5(p)) continue;
|
|
13340
14000
|
try {
|
|
13341
14001
|
const mod = await import(pathToFileURL(p).href, f.endsWith(".json") ? { with: { type: "json" } } : void 0);
|
|
@@ -13348,7 +14008,7 @@ async function loadFrom(dir) {
|
|
|
13348
14008
|
return {};
|
|
13349
14009
|
}
|
|
13350
14010
|
function loadSettings(dir) {
|
|
13351
|
-
const p =
|
|
14011
|
+
const p = join10(dir, ".agent", "settings.json");
|
|
13352
14012
|
if (!existsSync5(p)) return {};
|
|
13353
14013
|
try {
|
|
13354
14014
|
const raw = JSON.parse(readFileSync7(p, "utf8"));
|
|
@@ -13397,7 +14057,7 @@ function applyOverlay(cfg, file) {
|
|
|
13397
14057
|
// cli/hooks-config.ts
|
|
13398
14058
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
13399
14059
|
init_platform();
|
|
13400
|
-
var
|
|
14060
|
+
var log31 = forComponent("hooks");
|
|
13401
14061
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
13402
14062
|
function ruleMatches(rule, toolName) {
|
|
13403
14063
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -13416,7 +14076,7 @@ function runCmd(rule, env) {
|
|
|
13416
14076
|
});
|
|
13417
14077
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
13418
14078
|
} catch (e) {
|
|
13419
|
-
|
|
14079
|
+
log31.debug(`hook command failed: ${rule.command}`, e);
|
|
13420
14080
|
return { code: 1, out: String(e?.message ?? e) };
|
|
13421
14081
|
}
|
|
13422
14082
|
}
|
|
@@ -13522,10 +14182,10 @@ function formatDiff(ops, opts = {}) {
|
|
|
13522
14182
|
// cli/session.ts
|
|
13523
14183
|
import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync5, readdirSync, renameSync, symlinkSync as symlinkSync2, unlinkSync, readlinkSync, statSync as statSync7 } from "fs";
|
|
13524
14184
|
import { homedir as homedir6 } from "os";
|
|
13525
|
-
import { join as
|
|
14185
|
+
import { join as join11 } from "path";
|
|
13526
14186
|
init_platform();
|
|
13527
|
-
var
|
|
13528
|
-
var globalDir = () =>
|
|
14187
|
+
var log32 = forComponent("session");
|
|
14188
|
+
var globalDir = () => join11(homedir6(), ".agent", "sessions");
|
|
13529
14189
|
function indexLink(target, link2) {
|
|
13530
14190
|
try {
|
|
13531
14191
|
symlinkSync2(target, link2);
|
|
@@ -13547,7 +14207,7 @@ function readIndexLink(link2) {
|
|
|
13547
14207
|
var SessionStore = class {
|
|
13548
14208
|
dir;
|
|
13549
14209
|
constructor(cwd) {
|
|
13550
|
-
this.dir =
|
|
14210
|
+
this.dir = join11(cwd, ".agent", "sessions");
|
|
13551
14211
|
}
|
|
13552
14212
|
/** Sortable, human-readable id: `YYYYMMDD-HHMMSS-<folder>`. */
|
|
13553
14213
|
newId(now4 = Date.now(), cwd) {
|
|
@@ -13555,10 +14215,10 @@ var SessionStore = class {
|
|
|
13555
14215
|
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
13556
14216
|
const slug2 = pathTail(cwd ?? process.cwd())?.replace(/[^A-Za-z0-9_-]/g, "") || "session";
|
|
13557
14217
|
let id = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${slug2}`;
|
|
13558
|
-
if (existsSync6(this.dir) && existsSync6(
|
|
14218
|
+
if (existsSync6(this.dir) && existsSync6(join11(this.dir, `${id}.json`))) {
|
|
13559
14219
|
for (let i = 2; i <= 99; i++) {
|
|
13560
14220
|
const c = `${id}-${i}`;
|
|
13561
|
-
if (!existsSync6(
|
|
14221
|
+
if (!existsSync6(join11(this.dir, `${c}.json`))) {
|
|
13562
14222
|
id = c;
|
|
13563
14223
|
break;
|
|
13564
14224
|
}
|
|
@@ -13573,29 +14233,29 @@ var SessionStore = class {
|
|
|
13573
14233
|
save(data) {
|
|
13574
14234
|
if (!this.safeId(data.meta.id)) throw new Error(`unsafe session id: ${data.meta.id}`);
|
|
13575
14235
|
if (!existsSync6(this.dir)) mkdirSync8(this.dir, { recursive: true });
|
|
13576
|
-
const path =
|
|
14236
|
+
const path = join11(this.dir, `${data.meta.id}.json`);
|
|
13577
14237
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
13578
14238
|
writeFileSync5(tmp, JSON.stringify(data));
|
|
13579
14239
|
renameSync(tmp, path);
|
|
13580
14240
|
try {
|
|
13581
14241
|
const gd = globalDir();
|
|
13582
14242
|
if (!existsSync6(gd)) mkdirSync8(gd, { recursive: true });
|
|
13583
|
-
const link2 =
|
|
14243
|
+
const link2 = join11(gd, `${data.meta.id}.json`);
|
|
13584
14244
|
if (!existsSync6(link2)) indexLink(path, link2);
|
|
13585
14245
|
} catch {
|
|
13586
14246
|
}
|
|
13587
14247
|
}
|
|
13588
14248
|
load(id) {
|
|
13589
14249
|
if (!this.safeId(id)) {
|
|
13590
|
-
|
|
14250
|
+
log32.debug(`rejecting unsafe session id: ${id}`);
|
|
13591
14251
|
return void 0;
|
|
13592
14252
|
}
|
|
13593
|
-
const path =
|
|
14253
|
+
const path = join11(this.dir, `${id}.json`);
|
|
13594
14254
|
if (!existsSync6(path)) return void 0;
|
|
13595
14255
|
try {
|
|
13596
14256
|
return JSON.parse(readFileSync8(path, "utf8"));
|
|
13597
14257
|
} catch (e) {
|
|
13598
|
-
|
|
14258
|
+
log32.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
13599
14259
|
return void 0;
|
|
13600
14260
|
}
|
|
13601
14261
|
}
|
|
@@ -13606,9 +14266,9 @@ var SessionStore = class {
|
|
|
13606
14266
|
for (const f of readdirSync(this.dir)) {
|
|
13607
14267
|
if (!f.endsWith(".json")) continue;
|
|
13608
14268
|
try {
|
|
13609
|
-
metas.push(JSON.parse(readFileSync8(
|
|
14269
|
+
metas.push(JSON.parse(readFileSync8(join11(this.dir, f), "utf8")).meta);
|
|
13610
14270
|
} catch (e) {
|
|
13611
|
-
|
|
14271
|
+
log32.debug(`skipping unreadable session file ${f}`, e);
|
|
13612
14272
|
}
|
|
13613
14273
|
}
|
|
13614
14274
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -13621,7 +14281,7 @@ var SessionStore = class {
|
|
|
13621
14281
|
for (const f of readdirSync(this.dir)) {
|
|
13622
14282
|
if (!f.endsWith(".json")) continue;
|
|
13623
14283
|
try {
|
|
13624
|
-
const st = statSync7(
|
|
14284
|
+
const st = statSync7(join11(this.dir, f));
|
|
13625
14285
|
out.push({ id: f.slice(0, -5), mtime: st.mtimeMs, size: st.size });
|
|
13626
14286
|
} catch {
|
|
13627
14287
|
}
|
|
@@ -13640,7 +14300,7 @@ var SessionStore = class {
|
|
|
13640
14300
|
function globalSessionLoad(idOrPrefix) {
|
|
13641
14301
|
const gd = globalDir();
|
|
13642
14302
|
if (!existsSync6(gd)) return void 0;
|
|
13643
|
-
const exact =
|
|
14303
|
+
const exact = join11(gd, `${idOrPrefix}.json`);
|
|
13644
14304
|
if (existsSync6(exact)) {
|
|
13645
14305
|
try {
|
|
13646
14306
|
const target = readIndexLink(exact);
|
|
@@ -13654,7 +14314,7 @@ function globalSessionLoad(idOrPrefix) {
|
|
|
13654
14314
|
if (!f.endsWith(".json")) continue;
|
|
13655
14315
|
const base = f.slice(0, -5);
|
|
13656
14316
|
if (base.includes(idOrPrefix) || base.endsWith(idOrPrefix)) {
|
|
13657
|
-
const target = readIndexLink(
|
|
14317
|
+
const target = readIndexLink(join11(gd, f));
|
|
13658
14318
|
return JSON.parse(readFileSync8(target, "utf8"));
|
|
13659
14319
|
}
|
|
13660
14320
|
}
|
|
@@ -13669,10 +14329,10 @@ function globalSessionList() {
|
|
|
13669
14329
|
for (const f of readdirSync(gd)) {
|
|
13670
14330
|
if (!f.endsWith(".json")) continue;
|
|
13671
14331
|
try {
|
|
13672
|
-
const target = readIndexLink(
|
|
14332
|
+
const target = readIndexLink(join11(gd, f));
|
|
13673
14333
|
if (!existsSync6(target)) {
|
|
13674
14334
|
try {
|
|
13675
|
-
unlinkSync(
|
|
14335
|
+
unlinkSync(join11(gd, f));
|
|
13676
14336
|
} catch {
|
|
13677
14337
|
}
|
|
13678
14338
|
continue;
|
|
@@ -13846,8 +14506,8 @@ import { execFile as execFile4 } from "child_process";
|
|
|
13846
14506
|
import { promisify } from "util";
|
|
13847
14507
|
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync9 } from "fs";
|
|
13848
14508
|
import { homedir as homedir7 } from "os";
|
|
13849
|
-
import { join as
|
|
13850
|
-
var
|
|
14509
|
+
import { join as join12, resolve as resolve4, sep as sep3, parse as parsePath } from "path";
|
|
14510
|
+
var log33 = forComponent("checkpoints");
|
|
13851
14511
|
var exec = promisify(execFile4);
|
|
13852
14512
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
13853
14513
|
function isBroadRoot(p) {
|
|
@@ -13889,11 +14549,11 @@ var ShadowRepo = class {
|
|
|
13889
14549
|
mkdirSync9(this.gitDir, { recursive: true });
|
|
13890
14550
|
const valid = await this.run("rev-parse", "--git-dir").then(() => true, () => false);
|
|
13891
14551
|
if (!valid) await this.run("init", "-q");
|
|
13892
|
-
mkdirSync9(
|
|
13893
|
-
writeFileSync6(
|
|
14552
|
+
mkdirSync9(join12(this.gitDir, "info"), { recursive: true });
|
|
14553
|
+
writeFileSync6(join12(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
13894
14554
|
this.ready = true;
|
|
13895
14555
|
} catch (e) {
|
|
13896
|
-
|
|
14556
|
+
log33.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
13897
14557
|
this.ready = false;
|
|
13898
14558
|
}
|
|
13899
14559
|
return this.ready;
|
|
@@ -13904,7 +14564,7 @@ var ShadowRepo = class {
|
|
|
13904
14564
|
}
|
|
13905
14565
|
async commit(label, forced = []) {
|
|
13906
14566
|
await this.run("add", "-A");
|
|
13907
|
-
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) =>
|
|
14567
|
+
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log33.debug(`force-add failed: ${p}`, e));
|
|
13908
14568
|
await this.run("commit", "--allow-empty", "-q", "-m", label);
|
|
13909
14569
|
}
|
|
13910
14570
|
/** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
|
|
@@ -13912,8 +14572,8 @@ var ShadowRepo = class {
|
|
|
13912
14572
|
* turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
|
|
13913
14573
|
* point per turn, so the REPL's turn↔frame mapping stays intact. */
|
|
13914
14574
|
async amendForced(paths) {
|
|
13915
|
-
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) =>
|
|
13916
|
-
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) =>
|
|
14575
|
+
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log33.debug(`force-capture failed: ${p}`, e));
|
|
14576
|
+
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log33.debug("amend failed", e));
|
|
13917
14577
|
}
|
|
13918
14578
|
/** Commits on `ref`, oldest-first (canonical index space). */
|
|
13919
14579
|
async log(ref) {
|
|
@@ -13973,7 +14633,7 @@ var ShadowRepo = class {
|
|
|
13973
14633
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
13974
14634
|
});
|
|
13975
14635
|
} catch (e) {
|
|
13976
|
-
|
|
14636
|
+
log33.debug("checkpoint prune failed", e);
|
|
13977
14637
|
}
|
|
13978
14638
|
}
|
|
13979
14639
|
};
|
|
@@ -14005,7 +14665,7 @@ var GitCheckpoints = class {
|
|
|
14005
14665
|
const abs = resolve4(d);
|
|
14006
14666
|
if (abs === cwd || abs.startsWith(cwd + sep3)) continue;
|
|
14007
14667
|
if (cwd.startsWith(abs + sep3)) continue;
|
|
14008
|
-
out.push({ workTree: abs, gitDir:
|
|
14668
|
+
out.push({ workTree: abs, gitDir: join12(abs, ".agent", "checkpoints.git") });
|
|
14009
14669
|
}
|
|
14010
14670
|
return out;
|
|
14011
14671
|
}
|
|
@@ -14041,7 +14701,7 @@ var GitCheckpoints = class {
|
|
|
14041
14701
|
use(sessionId) {
|
|
14042
14702
|
if (sessionId === this.session) return;
|
|
14043
14703
|
this.session = sessionId;
|
|
14044
|
-
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) =>
|
|
14704
|
+
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log33.debug("re-point failed", e));
|
|
14045
14705
|
}
|
|
14046
14706
|
async begin(label) {
|
|
14047
14707
|
if (!await this.start()) return;
|
|
@@ -14061,7 +14721,7 @@ var GitCheckpoints = class {
|
|
|
14061
14721
|
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
|
|
14062
14722
|
`);
|
|
14063
14723
|
} else {
|
|
14064
|
-
|
|
14724
|
+
log33.debug("checkpoint commit failed", e);
|
|
14065
14725
|
survivors.push(this.repos[i]);
|
|
14066
14726
|
survivorCaches.push(this.caches[i] ?? []);
|
|
14067
14727
|
}
|
|
@@ -14096,7 +14756,7 @@ var GitCheckpoints = class {
|
|
|
14096
14756
|
if (this.forced.has(abs)) continue;
|
|
14097
14757
|
this.forced.add(abs);
|
|
14098
14758
|
const i = this.repoIndexFor(abs);
|
|
14099
|
-
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) =>
|
|
14759
|
+
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log33.debug("amendForced failed", e));
|
|
14100
14760
|
}
|
|
14101
14761
|
}
|
|
14102
14762
|
};
|
|
@@ -14148,7 +14808,7 @@ var GitCheckpointsOptions = class {
|
|
|
14148
14808
|
/** Real working tree to snapshot (the launch cwd). */
|
|
14149
14809
|
workTree = process.cwd();
|
|
14150
14810
|
/** Isolated git dir for the cwd shadow repo (kept out of the user's real .git). */
|
|
14151
|
-
gitDir =
|
|
14811
|
+
gitDir = join12(process.cwd(), ".agent", "checkpoints.git");
|
|
14152
14812
|
/** Extra mounted dirs (`--add-dir`); those outside cwd each get their own shadow repo. */
|
|
14153
14813
|
addDirs = [];
|
|
14154
14814
|
/** Conversation id → per-session restore-point ref. */
|
|
@@ -14224,7 +14884,7 @@ import { emitKeypressEvents } from "readline";
|
|
|
14224
14884
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
14225
14885
|
import { writeFileSync as writeFileSync7, readFileSync as readFileSync9, unlinkSync as unlinkSync2 } from "fs";
|
|
14226
14886
|
import { tmpdir as tmpdir2 } from "os";
|
|
14227
|
-
import { join as
|
|
14887
|
+
import { join as join13 } from "path";
|
|
14228
14888
|
|
|
14229
14889
|
// cli/bidi.ts
|
|
14230
14890
|
var RTL_RE = /[\u0590-\u05ff\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb1d-\ufdff\ufe70-\ufeff]/;
|
|
@@ -15412,7 +16072,7 @@ function createLineEditor(out, input = keyInput) {
|
|
|
15412
16072
|
function externalEdit(s) {
|
|
15413
16073
|
const spec = process.env.VISUAL || process.env.EDITOR || DEFAULT_EDITOR;
|
|
15414
16074
|
const [cmd, ...cargs] = spec.split(" ").filter(Boolean);
|
|
15415
|
-
const file =
|
|
16075
|
+
const file = join13(tmpdir2(), `agentx-edit-${process.pid}-${Date.now()}.md`);
|
|
15416
16076
|
try {
|
|
15417
16077
|
writeFileSync7(file, s.buf);
|
|
15418
16078
|
process.stdin.setRawMode(false);
|
|
@@ -15880,21 +16540,21 @@ init_hostPath();
|
|
|
15880
16540
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
15881
16541
|
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
|
|
15882
16542
|
import { homedir as homedir8 } from "os";
|
|
15883
|
-
import { join as
|
|
15884
|
-
var
|
|
16543
|
+
import { join as join14 } from "path";
|
|
16544
|
+
var log34 = forComponent("os-sched");
|
|
15885
16545
|
var OsScheduler = class {
|
|
15886
16546
|
options;
|
|
15887
16547
|
constructor(options) {
|
|
15888
16548
|
this.options = { ...new OsSchedulerOptions(), ...options };
|
|
15889
16549
|
}
|
|
15890
16550
|
get dir() {
|
|
15891
|
-
return
|
|
16551
|
+
return join14(this.options.home, ".agent", "sched");
|
|
15892
16552
|
}
|
|
15893
16553
|
label(id) {
|
|
15894
16554
|
return `cc.livx.agentx.sched-${id}`;
|
|
15895
16555
|
}
|
|
15896
16556
|
plistPath(id) {
|
|
15897
|
-
return
|
|
16557
|
+
return join14(this.options.home, "Library", "LaunchAgents", `${this.label(id)}.plist`);
|
|
15898
16558
|
}
|
|
15899
16559
|
run(cmd, args, input) {
|
|
15900
16560
|
return this.options.exec(cmd, args, input);
|
|
@@ -15910,11 +16570,11 @@ var OsScheduler = class {
|
|
|
15910
16570
|
const script = this.writeScript(spec, oneOff);
|
|
15911
16571
|
const mechanism = this.options.platform === "darwin" ? this.scheduleDarwin(spec, script) : this.scheduleLinux(spec, script, oneOff);
|
|
15912
16572
|
const meta = { ...spec, created: Date.now(), mechanism };
|
|
15913
|
-
writeFileSync8(
|
|
16573
|
+
writeFileSync8(join14(this.dir, `${spec.id}.json`), JSON.stringify(meta, null, 2));
|
|
15914
16574
|
return mechanism;
|
|
15915
16575
|
}
|
|
15916
16576
|
cancel(id) {
|
|
15917
|
-
const meta = readJsonFile(
|
|
16577
|
+
const meta = readJsonFile(join14(this.dir, `${id}.json`), null);
|
|
15918
16578
|
if (!meta) return false;
|
|
15919
16579
|
try {
|
|
15920
16580
|
if (this.options.platform === "darwin") {
|
|
@@ -15943,11 +16603,11 @@ var OsScheduler = class {
|
|
|
15943
16603
|
}
|
|
15944
16604
|
}
|
|
15945
16605
|
} catch (e) {
|
|
15946
|
-
|
|
16606
|
+
log34.debug(`cancel ${id}`, e);
|
|
15947
16607
|
}
|
|
15948
16608
|
for (const f of [`${id}.json`, `${id}.sh`]) {
|
|
15949
16609
|
try {
|
|
15950
|
-
unlinkSync3(
|
|
16610
|
+
unlinkSync3(join14(this.dir, f));
|
|
15951
16611
|
} catch {
|
|
15952
16612
|
}
|
|
15953
16613
|
}
|
|
@@ -15955,19 +16615,19 @@ var OsScheduler = class {
|
|
|
15955
16615
|
}
|
|
15956
16616
|
list() {
|
|
15957
16617
|
if (!existsSync7(this.dir)) return [];
|
|
15958
|
-
return readdirSync2(this.dir).filter((f) => f.endsWith(".json")).map((f) => readJsonFile(
|
|
16618
|
+
return readdirSync2(this.dir).filter((f) => f.endsWith(".json")).map((f) => readJsonFile(join14(this.dir, f), null)).filter(Boolean);
|
|
15959
16619
|
}
|
|
15960
16620
|
/** The per-job runner script: cd to the project, headless-resume the session, log, notify. */
|
|
15961
16621
|
writeScript(spec, oneOff) {
|
|
15962
|
-
const p =
|
|
16622
|
+
const p = join14(this.dir, `${spec.id}.sh`);
|
|
15963
16623
|
const q2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
15964
|
-
const cleanup = oneOff ? this.options.platform === "darwin" ? `launchctl remove ${this.label(spec.id)} 2>/dev/null; rm -f ${q2(this.plistPath(spec.id))} ${q2(
|
|
15965
|
-
` : `rm -f ${q2(
|
|
16624
|
+
const cleanup = oneOff ? this.options.platform === "darwin" ? `launchctl remove ${this.label(spec.id)} 2>/dev/null; rm -f ${q2(this.plistPath(spec.id))} ${q2(join14(this.dir, `${spec.id}.json`))} ${q2(p)}
|
|
16625
|
+
` : `rm -f ${q2(join14(this.dir, `${spec.id}.json`))} ${q2(p)}
|
|
15966
16626
|
` : "";
|
|
15967
16627
|
writeFileSync8(p, `#!/bin/sh
|
|
15968
16628
|
# agentx scheduled job ${spec.id}${spec.label ? ` \u2014 ${spec.label}` : ""}
|
|
15969
16629
|
cd ${q2(spec.cwd)} || exit 1
|
|
15970
|
-
${this.options.agentx} -p ${q2(spec.prompt)} --resume ${q2(spec.sessionId)} --yes >> ${q2(
|
|
16630
|
+
${this.options.agentx} -p ${q2(spec.prompt)} --resume ${q2(spec.sessionId)} --yes >> ${q2(join14(this.dir, `${spec.id}.log`))} 2>&1
|
|
15971
16631
|
${cleanup}`);
|
|
15972
16632
|
chmodSync(p, 493);
|
|
15973
16633
|
return p;
|
|
@@ -16004,7 +16664,7 @@ ${trigger}
|
|
|
16004
16664
|
<key>RunAtLoad</key><false/>
|
|
16005
16665
|
</dict></plist>
|
|
16006
16666
|
`;
|
|
16007
|
-
mkdirSync10(
|
|
16667
|
+
mkdirSync10(join14(this.options.home, "Library", "LaunchAgents"), { recursive: true });
|
|
16008
16668
|
writeFileSync8(this.plistPath(spec.id), plist);
|
|
16009
16669
|
this.run("launchctl", ["load", this.plistPath(spec.id)]);
|
|
16010
16670
|
return `launchd:${this.label(spec.id)}`;
|
|
@@ -16059,11 +16719,11 @@ import { createServer as createServer2, createConnection } from "net";
|
|
|
16059
16719
|
import { spawn as spawn3 } from "child_process";
|
|
16060
16720
|
import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "fs";
|
|
16061
16721
|
import { homedir as homedir9 } from "os";
|
|
16062
|
-
import { join as
|
|
16722
|
+
import { join as join15 } from "path";
|
|
16063
16723
|
init_platform();
|
|
16064
|
-
var
|
|
16065
|
-
var TRIGGER_DIR = () =>
|
|
16066
|
-
var sockPath = (sessionId, dir = TRIGGER_DIR()) =>
|
|
16724
|
+
var log35 = forComponent("remote-trigger");
|
|
16725
|
+
var TRIGGER_DIR = () => join15(homedir9(), ".agent", "triggers");
|
|
16726
|
+
var sockPath = (sessionId, dir = TRIGGER_DIR()) => join15(dir, `${sessionId}.sock`);
|
|
16067
16727
|
var TriggerServer = class {
|
|
16068
16728
|
constructor(onTrigger) {
|
|
16069
16729
|
this.onTrigger = onTrigger;
|
|
@@ -16099,13 +16759,13 @@ var TriggerServer = class {
|
|
|
16099
16759
|
conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
|
|
16100
16760
|
}
|
|
16101
16761
|
});
|
|
16102
|
-
conn.on("error", (e) =>
|
|
16762
|
+
conn.on("error", (e) => log35.debug("trigger conn error", e));
|
|
16103
16763
|
});
|
|
16104
|
-
this.server.on("error", (e) =>
|
|
16764
|
+
this.server.on("error", (e) => log35.debug("trigger server error", e));
|
|
16105
16765
|
this.server.listen(p);
|
|
16106
16766
|
this.path = p;
|
|
16107
16767
|
} catch (e) {
|
|
16108
|
-
|
|
16768
|
+
log35.debug("trigger server unavailable", e);
|
|
16109
16769
|
}
|
|
16110
16770
|
}
|
|
16111
16771
|
/** Re-bind on /resume (the session id changed). */
|
|
@@ -16237,7 +16897,7 @@ var strike = C("9");
|
|
|
16237
16897
|
var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
|
|
16238
16898
|
var mdPalette = { bold, dim, cyan, italic, strike, green, link };
|
|
16239
16899
|
var err = (s) => process.stderr.write(s);
|
|
16240
|
-
var
|
|
16900
|
+
var log36 = forComponent("cli");
|
|
16241
16901
|
var VERSION = (() => {
|
|
16242
16902
|
try {
|
|
16243
16903
|
return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -16377,7 +17037,10 @@ function parseArgs(argv) {
|
|
|
16377
17037
|
a.hardenNet = true;
|
|
16378
17038
|
} else if (x === "--subagents") a.subagents = true;
|
|
16379
17039
|
else if (x === "--duplex") a.duplex = true;
|
|
16380
|
-
else if (x === "--
|
|
17040
|
+
else if (x === "--duplex-v2") {
|
|
17041
|
+
a.duplex = true;
|
|
17042
|
+
a.duplexV2 = true;
|
|
17043
|
+
} else if (x === "--conversational" || x === "--convo" || x === "--voice") {
|
|
16381
17044
|
a.voice = true;
|
|
16382
17045
|
a.duplex = true;
|
|
16383
17046
|
} else if (x === "--voice-model") a.voiceModel = val(++i, x);
|
|
@@ -16461,6 +17124,8 @@ Flags:
|
|
|
16461
17124
|
--append-system-prompt <t> extra instructions appended to the system prompt for this run
|
|
16462
17125
|
--duplex duplex mode: a fast voice model replies instantly and delegates real work
|
|
16463
17126
|
to a background worker agent (-m model); results are re-voiced when ready
|
|
17127
|
+
--duplex-v2 duplex + a second voice channel: backchannels, fillers and bridges that never
|
|
17128
|
+
enter the agent's history (headless: stdin interim/pause/agent_state, stdout channel)
|
|
16464
17129
|
--conversational duplex with a conversation-native register \u2014 short fast turns, fillers,
|
|
16465
17130
|
impulsive reactions, human pacing (implies --duplex; aliases: --convo, --voice)
|
|
16466
17131
|
with SONIOX_API_KEY + CARTESIA_API_KEY(+VOICE_ID) set: real voice I/O \u2014 mic in,
|
|
@@ -16563,10 +17228,10 @@ function loadEnvFile(file) {
|
|
|
16563
17228
|
}
|
|
16564
17229
|
function loadInstallEnv() {
|
|
16565
17230
|
let dir = dirname6(import.meta.path);
|
|
16566
|
-
for (let i = 0; i < 5 && !existsSync9(
|
|
17231
|
+
for (let i = 0; i < 5 && !existsSync9(join16(dir, "package.json")); i++) dir = dirname6(dir);
|
|
16567
17232
|
for (const name of [".env", ".env.local"]) {
|
|
16568
|
-
loadEnvFile(
|
|
16569
|
-
loadEnvFile(
|
|
17233
|
+
loadEnvFile(join16(dir, name));
|
|
17234
|
+
loadEnvFile(join16(homedir10(), ".agent", name));
|
|
16570
17235
|
}
|
|
16571
17236
|
}
|
|
16572
17237
|
async function loadBodifySecrets() {
|
|
@@ -17063,7 +17728,7 @@ function turnCost(model, usage) {
|
|
|
17063
17728
|
const priced = model.startsWith("claude-code/") ? `anthropic/${resolveModelAlias(model.slice("claude-code/".length))}` : model;
|
|
17064
17729
|
return costOf(getModelInfo(priced)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, priced);
|
|
17065
17730
|
}
|
|
17066
|
-
async function evaluateGoal(ai, condition, transcript,
|
|
17731
|
+
async function evaluateGoal(ai, condition, transcript, log37) {
|
|
17067
17732
|
const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
|
|
17068
17733
|
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
17069
17734
|
return text.slice(0, 600);
|
|
@@ -17083,7 +17748,7 @@ ${recent}` }
|
|
|
17083
17748
|
const match = r.content.match(/\{[\s\S]*\}/);
|
|
17084
17749
|
if (match) return JSON.parse(match[0]);
|
|
17085
17750
|
} catch (e) {
|
|
17086
|
-
|
|
17751
|
+
log37(dim(` (goal evaluator error: ${e?.message ?? e})
|
|
17087
17752
|
`));
|
|
17088
17753
|
}
|
|
17089
17754
|
return { met: false, reason: "evaluation unclear" };
|
|
@@ -17309,13 +17974,13 @@ function mcpAgentTools(mounted, opts) {
|
|
|
17309
17974
|
return tools;
|
|
17310
17975
|
}
|
|
17311
17976
|
async function closeMcp(mounted) {
|
|
17312
|
-
await Promise.all(mounted.map((m) => m.client.close().catch((e) =>
|
|
17977
|
+
await Promise.all(mounted.map((m) => m.client.close().catch((e) => log36.debug("mcp close failed", e))));
|
|
17313
17978
|
}
|
|
17314
17979
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
17315
17980
|
function mentionRefs(line) {
|
|
17316
17981
|
return [...line.matchAll(/(?:^|\s)@(?:"([^"]+)"|(\S+))/g)].map((m) => m[1] ?? m[2].replace(/[?!.,;:)\]}'">]+$/, "")).filter(Boolean);
|
|
17317
17982
|
}
|
|
17318
|
-
var untilde = (p) => p.startsWith("~/") || IS_WIN && p.startsWith("~\\") ?
|
|
17983
|
+
var untilde = (p) => p.startsWith("~/") || IS_WIN && p.startsWith("~\\") ? join16(homedir10(), p.slice(2)) : p;
|
|
17319
17984
|
var PATH_LIKE = IS_WIN ? /^(\/|~[\\/]|\.\.?[\\/]|[A-Za-z]:[\\/]|\\\\)/ : /^(\/|~\/|\.\/|\.\.\/)/;
|
|
17320
17985
|
var userPath = (cwd, p) => {
|
|
17321
17986
|
const t = untilde(p);
|
|
@@ -17368,7 +18033,7 @@ async function expandMentions(fs, line) {
|
|
|
17368
18033
|
if (loaded.includes(ref) || missing.includes(ref)) continue;
|
|
17369
18034
|
if (ref.includes(":") && mcpMentionResolver) {
|
|
17370
18035
|
const body = await mcpMentionResolver(ref).catch((e) => {
|
|
17371
|
-
|
|
18036
|
+
log36.debug("mcp mention resolve failed", e);
|
|
17372
18037
|
return null;
|
|
17373
18038
|
});
|
|
17374
18039
|
if (body != null) {
|
|
@@ -17459,7 +18124,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
17459
18124
|
try {
|
|
17460
18125
|
store.save(session);
|
|
17461
18126
|
} catch (ex) {
|
|
17462
|
-
|
|
18127
|
+
log36.debug("mid-turn session flush failed", ex);
|
|
17463
18128
|
}
|
|
17464
18129
|
}
|
|
17465
18130
|
return origNotify(e);
|
|
@@ -17527,7 +18192,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
17527
18192
|
}
|
|
17528
18193
|
return { ok, res };
|
|
17529
18194
|
}
|
|
17530
|
-
var remintSessionImages = (data, store, cwd) => remintImageRefs(data.messages, [
|
|
18195
|
+
var remintSessionImages = (data, store, cwd) => remintImageRefs(data.messages, [join16(store.dir, `${data.meta.id}.images`), cwd]);
|
|
17531
18196
|
function startSession(args, store, agent, cwd) {
|
|
17532
18197
|
if (args.resume || args.cont) {
|
|
17533
18198
|
const data = args.resume ? store.load(args.resume) ?? globalSessionLoad(args.resume) : store.latestData();
|
|
@@ -17574,19 +18239,19 @@ var AGENTS_MD_TEMPLATE = `# ${"${name}"}
|
|
|
17574
18239
|
`;
|
|
17575
18240
|
function initInstructions(cwd) {
|
|
17576
18241
|
for (const f of ["AGENTS.md", "CLAUDE.md"]) {
|
|
17577
|
-
if (existsSync9(
|
|
18242
|
+
if (existsSync9(join16(cwd, f))) {
|
|
17578
18243
|
err(yellow(` ${f} already exists \u2014 leaving it as-is
|
|
17579
18244
|
`));
|
|
17580
18245
|
return;
|
|
17581
18246
|
}
|
|
17582
18247
|
}
|
|
17583
|
-
const path =
|
|
18248
|
+
const path = join16(cwd, "AGENTS.md");
|
|
17584
18249
|
writeFileSync9(path, AGENTS_MD_TEMPLATE.replace("${name}", basename4(cwd)));
|
|
17585
18250
|
err(green(` created ${path}
|
|
17586
18251
|
`) + dim(" edit it, then it auto-loads into every run.\n"));
|
|
17587
18252
|
}
|
|
17588
18253
|
function persistSetting(cwd, key, value) {
|
|
17589
|
-
const path =
|
|
18254
|
+
const path = join16(cwd, ".agent", "settings.json");
|
|
17590
18255
|
try {
|
|
17591
18256
|
const obj = existsSync9(path) ? JSON.parse(readFileSync10(path, "utf8")) : {};
|
|
17592
18257
|
if (obj[key] === value) return;
|
|
@@ -17608,14 +18273,14 @@ var isCancelTeardown = (e) => {
|
|
|
17608
18273
|
function installCancelGuards(mounted) {
|
|
17609
18274
|
process.on("unhandledRejection", (e) => {
|
|
17610
18275
|
if (isCancelTeardown(e)) {
|
|
17611
|
-
|
|
18276
|
+
log36.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
17612
18277
|
return;
|
|
17613
18278
|
}
|
|
17614
|
-
|
|
18279
|
+
log36.error("unhandledRejection", e);
|
|
17615
18280
|
});
|
|
17616
18281
|
process.on("uncaughtException", (e) => {
|
|
17617
18282
|
if (isCancelTeardown(e)) {
|
|
17618
|
-
|
|
18283
|
+
log36.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
17619
18284
|
return;
|
|
17620
18285
|
}
|
|
17621
18286
|
console.error(e);
|
|
@@ -17624,7 +18289,7 @@ function installCancelGuards(mounted) {
|
|
|
17624
18289
|
});
|
|
17625
18290
|
}
|
|
17626
18291
|
async function repl(args, ai, cfg, cwd) {
|
|
17627
|
-
const oauth = new McpOAuth({ storePath:
|
|
18292
|
+
const oauth = new McpOAuth({ storePath: join16(cwd, ".agent", "mcp-auth.json") });
|
|
17628
18293
|
const mounted = await mountMcp(cfg, oauth);
|
|
17629
18294
|
let mcpToolNames = [];
|
|
17630
18295
|
const initialMcpTools = mcpAgentTools(mounted);
|
|
@@ -17902,7 +18567,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
17902
18567
|
quickLook: {
|
|
17903
18568
|
branch: () => {
|
|
17904
18569
|
try {
|
|
17905
|
-
const head = readFileSync10(
|
|
18570
|
+
const head = readFileSync10(join16(cwd, ".git", "HEAD"), "utf8").trim();
|
|
17906
18571
|
return head.startsWith("ref: refs/heads/") ? `branch: ${head.slice("ref: refs/heads/".length)}` : `detached HEAD at ${head.slice(0, 12)}`;
|
|
17907
18572
|
} catch {
|
|
17908
18573
|
return "not a git repository";
|
|
@@ -18036,11 +18701,11 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
18036
18701
|
const pendingImages = [];
|
|
18037
18702
|
const bangContext = [];
|
|
18038
18703
|
const grabClipboardAttachment = () => {
|
|
18039
|
-
const dir =
|
|
18704
|
+
const dir = join16(store.dir, `${session.meta.id}.images`);
|
|
18040
18705
|
try {
|
|
18041
18706
|
mkdirSync12(dir, { recursive: true });
|
|
18042
18707
|
} catch (e) {
|
|
18043
|
-
|
|
18708
|
+
log36.debug("paste image dir create failed", e);
|
|
18044
18709
|
}
|
|
18045
18710
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
18046
18711
|
return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
|
|
@@ -18094,7 +18759,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
18094
18759
|
err(dim(` \u23F0 ${scheduler.size} scheduled job(s) re-armed
|
|
18095
18760
|
`));
|
|
18096
18761
|
}
|
|
18097
|
-
const checkpoints = args.vfs || args.boddb ? new CheckpointStack(agent.options.fs) : new GitCheckpoints({ workTree: cwd, gitDir:
|
|
18762
|
+
const checkpoints = args.vfs || args.boddb ? new CheckpointStack(agent.options.fs) : new GitCheckpoints({ workTree: cwd, gitDir: join16(cwd, ".agent", "checkpoints.git"), addDirs: args.addDirs, sessionId: session.meta.id });
|
|
18098
18763
|
const cpHooks = checkpoints.hooks?.();
|
|
18099
18764
|
if (cpHooks) {
|
|
18100
18765
|
agent.options.hooks = composeHooks(agent.options.hooks, cpHooks);
|
|
@@ -18146,14 +18811,14 @@ ${lines.join("\n")}
|
|
|
18146
18811
|
Added entries are loadable now via the Skill/SlashCommand tools; removed ones are gone even if still listed in the system prompt.
|
|
18147
18812
|
</system-reminder>`;
|
|
18148
18813
|
};
|
|
18149
|
-
const histPath =
|
|
18814
|
+
const histPath = join16(cwd, ".agent", "history");
|
|
18150
18815
|
const history = existsSync9(histPath) ? readFileSync10(histPath, "utf8").split("\n").filter(Boolean).reverse().slice(0, 500) : [];
|
|
18151
18816
|
const remember2 = (line) => {
|
|
18152
18817
|
try {
|
|
18153
|
-
mkdirSync12(
|
|
18818
|
+
mkdirSync12(join16(cwd, ".agent"), { recursive: true });
|
|
18154
18819
|
appendFileSync(histPath, line + "\n");
|
|
18155
18820
|
} catch (e) {
|
|
18156
|
-
|
|
18821
|
+
log36.debug("history write failed", e);
|
|
18157
18822
|
}
|
|
18158
18823
|
};
|
|
18159
18824
|
const ago = (t) => {
|
|
@@ -18225,7 +18890,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
18225
18890
|
try {
|
|
18226
18891
|
store.save(session);
|
|
18227
18892
|
} catch (e) {
|
|
18228
|
-
|
|
18893
|
+
log36.debug("session save after rewind failed", e);
|
|
18229
18894
|
}
|
|
18230
18895
|
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
18231
18896
|
`));
|
|
@@ -18260,7 +18925,7 @@ ${task}`;
|
|
|
18260
18925
|
bangContext.length = 0;
|
|
18261
18926
|
}
|
|
18262
18927
|
const delta = await refreshCatalogs().catch((e) => {
|
|
18263
|
-
|
|
18928
|
+
log36.debug("catalog refresh failed", e);
|
|
18264
18929
|
return "";
|
|
18265
18930
|
});
|
|
18266
18931
|
if (delta) {
|
|
@@ -18523,7 +19188,7 @@ ${task}`;
|
|
|
18523
19188
|
desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
|
|
18524
19189
|
run: async () => {
|
|
18525
19190
|
await refreshCatalogs().catch((e) => {
|
|
18526
|
-
|
|
19191
|
+
log36.debug("catalog refresh failed", e);
|
|
18527
19192
|
});
|
|
18528
19193
|
face.reprepare();
|
|
18529
19194
|
err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
|
|
@@ -19026,7 +19691,7 @@ ${task}`;
|
|
|
19026
19691
|
try {
|
|
19027
19692
|
for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
|
|
19028
19693
|
} catch (e) {
|
|
19029
|
-
|
|
19694
|
+
log36.debug(`loadAgents(${d}) failed`, e);
|
|
19030
19695
|
}
|
|
19031
19696
|
}
|
|
19032
19697
|
if (!seen.size) {
|
|
@@ -19114,7 +19779,7 @@ ${task}`;
|
|
|
19114
19779
|
}
|
|
19115
19780
|
if (idx >= 0) {
|
|
19116
19781
|
const old = mounted.splice(idx, 1)[0];
|
|
19117
|
-
await old.client.close().catch((e) =>
|
|
19782
|
+
await old.client.close().catch((e) => log36.debug("mcp close failed", e));
|
|
19118
19783
|
}
|
|
19119
19784
|
try {
|
|
19120
19785
|
const m = await mountMcpServer(name, conf);
|
|
@@ -19142,7 +19807,7 @@ ${task}`;
|
|
|
19142
19807
|
}
|
|
19143
19808
|
const m = mounted.splice(idx, 1)[0];
|
|
19144
19809
|
remountMcpTools();
|
|
19145
|
-
await m.client.close().catch((e) =>
|
|
19810
|
+
await m.client.close().catch((e) => log36.debug("mcp close failed", e));
|
|
19146
19811
|
err(dim(` removed "${name}"
|
|
19147
19812
|
`));
|
|
19148
19813
|
return;
|
|
@@ -19257,7 +19922,7 @@ ${task}`;
|
|
|
19257
19922
|
return;
|
|
19258
19923
|
}
|
|
19259
19924
|
const md = exportMarkdown(session.meta, shown);
|
|
19260
|
-
const name = a[0] ? extname(a[0]) ? a[0] : a[0] + ".md" :
|
|
19925
|
+
const name = a[0] ? extname(a[0]) ? a[0] : a[0] + ".md" : join16(".agent", "exports", `${session.meta.id}.md`);
|
|
19261
19926
|
const path = resolve5(cwd, name);
|
|
19262
19927
|
try {
|
|
19263
19928
|
mkdirSync12(dirname6(path), { recursive: true });
|
|
@@ -19343,9 +20008,9 @@ ${task}`;
|
|
|
19343
20008
|
});
|
|
19344
20009
|
const listDir = (absDir) => {
|
|
19345
20010
|
try {
|
|
19346
|
-
return readdirSync4(
|
|
20011
|
+
return readdirSync4(join16(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
19347
20012
|
} catch (e) {
|
|
19348
|
-
|
|
20013
|
+
log36.debug("completion readdir failed", absDir, e);
|
|
19349
20014
|
return null;
|
|
19350
20015
|
}
|
|
19351
20016
|
};
|
|
@@ -19482,7 +20147,7 @@ ${out}
|
|
|
19482
20147
|
return;
|
|
19483
20148
|
}
|
|
19484
20149
|
await refreshCatalogs().catch((e) => {
|
|
19485
|
-
|
|
20150
|
+
log36.debug("catalog refresh failed", e);
|
|
19486
20151
|
});
|
|
19487
20152
|
const sk = skills.find((x) => x.name === name);
|
|
19488
20153
|
if (sk) {
|
|
@@ -19528,7 +20193,7 @@ ${out}
|
|
|
19528
20193
|
return false;
|
|
19529
20194
|
}
|
|
19530
20195
|
const fakeVoice = process.env.AGENTX_VOICE_FAKE ? fakeVoiceParts(process.env.AGENTX_VOICE_FAKE) : null;
|
|
19531
|
-
voiceDiag = new JsonlDiagSink(
|
|
20196
|
+
voiceDiag = new JsonlDiagSink(join16(store.dir, `${session.meta.id}.voice.jsonl`));
|
|
19532
20197
|
voiceIO = new VoiceIO({
|
|
19533
20198
|
...fakeVoice ?? {},
|
|
19534
20199
|
onDiag: (ev) => voiceDiag?.push(ev),
|
|
@@ -19593,7 +20258,7 @@ ${out}
|
|
|
19593
20258
|
if (delivery || cut) dx?.parkCutDelivery(delivery, !!cut, heard);
|
|
19594
20259
|
const cutReply = cut && cut.full.length - cut.heard.length > 40 ? cut : null;
|
|
19595
20260
|
const noteP = replaces ? replaces.note : !cutReply ? Promise.resolve("") : (instinct ? instinct.bargeIntent(text).catch((e) => {
|
|
19596
|
-
|
|
20261
|
+
log36.warn(`barge intent failed \u2014 model decides: ${e.message}`);
|
|
19597
20262
|
return void 0;
|
|
19598
20263
|
}) : Promise.resolve(void 0)).then(async (intent) => interruptionNote(cutReply, intent) + ackedNote(await instinct?.ackFor(text)));
|
|
19599
20264
|
const cutIn = replaces ? replaces.cutIn : interjectNote;
|
|
@@ -19688,6 +20353,14 @@ ${out}
|
|
|
19688
20353
|
};
|
|
19689
20354
|
try {
|
|
19690
20355
|
await voiceIO.start();
|
|
20356
|
+
if (args.duplexV2 && dx) {
|
|
20357
|
+
const v = voiceIO;
|
|
20358
|
+
bindSecondChannel(v, makeSecondChannel(ai, dx.options.reflexModel, { show: (t) => {
|
|
20359
|
+
err("\r\x1B[0J" + dim(` \u21B3 ${t}
|
|
20360
|
+
`));
|
|
20361
|
+
editorRef?.redrawNow();
|
|
20362
|
+
} }), () => voiceIO === v);
|
|
20363
|
+
}
|
|
19691
20364
|
const inDev = voiceIO.usingAec ? detectedInputDevice() : null;
|
|
19692
20365
|
const inNote = inDev ? `, in: ${inDev.name}` : "";
|
|
19693
20366
|
err(dim(` \u{1F3A4} voice on (${voiceIO.usingAec ? "echo-cancelled" : "heuristic echo \u2014 headphones recommended"}${inNote}) \u2014 just talk; speak over it to interrupt; say "voice off" to stop
|
|
@@ -19938,17 +20611,32 @@ function duplexError(e, reflexModel) {
|
|
|
19938
20611
|
return code && !msg.includes(String(code)) ? `${msg} (${code})` : msg;
|
|
19939
20612
|
}
|
|
19940
20613
|
var leakDecider = () => process.env.TYPESAFE_AI_KEY ? new DecideClient({ apiKey: process.env.TYPESAFE_AI_KEY }) : void 0;
|
|
20614
|
+
function makeSecondChannel(ai, reflexModel, opts) {
|
|
20615
|
+
const decider = leakDecider();
|
|
20616
|
+
const instinct = decider ? new Instinct({ decider }) : void 0;
|
|
20617
|
+
const model = process.env.AGENTX_CHANNEL_MODEL || reflexModel;
|
|
20618
|
+
return new SecondChannel({
|
|
20619
|
+
// Banks in the user's language: generated once per language, cached + topped up across sessions (~/.agent/cache).
|
|
20620
|
+
banks: new ChannelBanks({ generate: fastBankGenerator(ai, model), cacheDir: join16(homedir10(), ".agent", "cache", "channel-banks") }),
|
|
20621
|
+
decide: instinct ? (heard, state) => instinct.channel(heard, state) : void 0,
|
|
20622
|
+
generate: fastLineGenerator(ai, model),
|
|
20623
|
+
onEvent: (e) => log36.debug(`channel ${e.trigger}: rule=${e.rule} choice=${e.choice ?? "-"} \u2192 ${e.acted}${e.text ? ` "${e.text}"` : ""} ${e.ms}ms`),
|
|
20624
|
+
...opts
|
|
20625
|
+
});
|
|
20626
|
+
}
|
|
19941
20627
|
async function duplexServe(args, ai, cfg, cwd) {
|
|
19942
20628
|
const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
|
|
19943
|
-
const mounted = await mountMcp(cfg, new McpOAuth({ storePath:
|
|
20629
|
+
const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join16(cwd, ".agent", "mcp-auth.json") }));
|
|
19944
20630
|
installCancelGuards(mounted);
|
|
19945
20631
|
const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted, { quiet: true }), mounted.map((m) => m.name));
|
|
19946
20632
|
const { host: _h, stream: _s, signal: _g, providerOptions: _po, ...wo } = agent.options;
|
|
19947
20633
|
const host = {
|
|
19948
20634
|
notify(e) {
|
|
19949
20635
|
const k = String(e.kind);
|
|
19950
|
-
if (k === "text_delta")
|
|
19951
|
-
|
|
20636
|
+
if (k === "text_delta") {
|
|
20637
|
+
out({ type: "text", text: e.message });
|
|
20638
|
+
sc?.onMainTokens(String(e.message ?? ""));
|
|
20639
|
+
} else if (k === "thinking_delta") out({ type: "thinking", text: e.message });
|
|
19952
20640
|
else if (k === "speak_utterance") out({ type: "speak", text: e.message });
|
|
19953
20641
|
else if (k === "hold_filler") out({ type: "filler", text: e.message });
|
|
19954
20642
|
else if (k === "turn_replace") out({ type: "replaced", ...e.data });
|
|
@@ -19980,6 +20668,8 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
19980
20668
|
reflexOptions: { fs: agent.options.fs, tools: [exitSessionTool(() => out({ type: "exit" }))] }
|
|
19981
20669
|
});
|
|
19982
20670
|
if (args.voiceSystemPrompt) dx.voice.options.systemPrompt += "\n\n" + args.voiceSystemPrompt;
|
|
20671
|
+
let scTurn = 0;
|
|
20672
|
+
const sc = args.duplexV2 ? makeSecondChannel(ai, dx.options.reflexModel, { show: (text, kind) => out({ type: "channel", kind, text }) }) : void 0;
|
|
19983
20673
|
const store = new SessionStore(cwd);
|
|
19984
20674
|
const session = startSession(args, store, dx.voice, cwd);
|
|
19985
20675
|
persist = () => {
|
|
@@ -19988,7 +20678,7 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
19988
20678
|
try {
|
|
19989
20679
|
store.save(session);
|
|
19990
20680
|
} catch (e) {
|
|
19991
|
-
|
|
20681
|
+
log36.warn("duplex session save failed", e);
|
|
19992
20682
|
}
|
|
19993
20683
|
};
|
|
19994
20684
|
out({ type: "ready", sessionId: session.meta.id, reflexModel: dx.options.reflexModel, actModel: dx.options.actModel, ...notes.length ? { notes } : {} });
|
|
@@ -20002,20 +20692,30 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
20002
20692
|
return;
|
|
20003
20693
|
}
|
|
20004
20694
|
if (m.type === "barge") {
|
|
20695
|
+
sc?.onBargeIn();
|
|
20005
20696
|
activeTurn?.abort();
|
|
20006
20697
|
dx.parkInFlightDeliveries();
|
|
20698
|
+
} else if (sc && channelInput(sc, m)) {
|
|
20007
20699
|
} else if (m.type === "cancel") out({ type: "task", kind: "cancel_result", id: m.task, message: dx.cancelTask(String(m.task)) });
|
|
20008
20700
|
else if (m.type === "exit") rl.close();
|
|
20009
20701
|
else if (m.type === "user" && typeof m.text === "string" && m.text.trim()) {
|
|
20010
|
-
const {
|
|
20702
|
+
const cut = m.cut && sc?.spoke(String(m.cut.full ?? "")) ? { ...m.cut, full: "", heard: "" } : m.cut;
|
|
20703
|
+
const { reply, delivery, heard } = splitCut(cut ?? null);
|
|
20011
20704
|
if (delivery || reply) dx.parkCutDelivery(delivery, !!reply, heard);
|
|
20012
20705
|
const note = reply && reply.full.length - reply.heard.length > 40 ? interruptionNote(reply) : "";
|
|
20013
20706
|
const id = m.id ? String(m.id) : void 0;
|
|
20707
|
+
sc?.onUserTurn(m.text);
|
|
20708
|
+
const seq = ++scTurn;
|
|
20709
|
+
const done2 = () => {
|
|
20710
|
+
if (seq === scTurn) sc?.onAgentState("idle");
|
|
20711
|
+
};
|
|
20014
20712
|
void runTurn(dx.voice, store, session, m.text + note, void 0, cwd, (c) => dx.send(c, { id, replaces: m.replaces ? String(m.replaces) : void 0, followUp: typeof m.followUp === "string" || m.followUp && typeof m.followUp === "object" ? m.followUp : void 0 })).then(({ res, error }) => {
|
|
20015
20713
|
const why = (res?.finishReason ?? "error") === "error" ? duplexError(res?.error ?? error, dx.options.reflexModel) : "";
|
|
20714
|
+
done2();
|
|
20016
20715
|
out({ type: "turn_done", id, finishReason: res?.finishReason ?? "error", ...why ? { error: why } : {} });
|
|
20017
20716
|
}).catch((e) => {
|
|
20018
|
-
|
|
20717
|
+
log36.error("duplex turn failed", e);
|
|
20718
|
+
done2();
|
|
20019
20719
|
out({ type: "turn_done", id, finishReason: "error", error: String(e?.message ?? e) });
|
|
20020
20720
|
});
|
|
20021
20721
|
}
|
|
@@ -20175,7 +20875,7 @@ async function main() {
|
|
|
20175
20875
|
worktreeCleanup?.();
|
|
20176
20876
|
process.exit(0);
|
|
20177
20877
|
}
|
|
20178
|
-
const mounted = await mountMcp(cfg, new McpOAuth({ storePath:
|
|
20878
|
+
const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join16(cwd, ".agent", "mcp-auth.json") }));
|
|
20179
20879
|
const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted), mounted.map((m) => m.name));
|
|
20180
20880
|
const store = new SessionStore(cwd);
|
|
20181
20881
|
const session = startSession(args, store, agent, cwd);
|