agent.libx.js 0.93.10 → 0.93.11

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/cli/cli.ts CHANGED
@@ -1034,13 +1034,25 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
1034
1034
  const duplexAsk = async (call: ToolUse): Promise<{ decision: 'allow' | 'deny' }> => {
1035
1035
  if (args.voice && dx) {
1036
1036
  const hint = summarizeCall(call.name, call.args).slice(0, 80);
1037
- // 'menu' mode: approve like a normal session — suspend the editor, pop the picker.
1038
- if ((cfg as any).voiceAskUi === 'menu') {
1037
+ // Default: approve like a normal session — suspend the editor, pop an interactive picker
1038
+ // (Allow once / always / Deny). Set `voiceAskUi: 'relay'` to opt into the spoken park/relay flow.
1039
+ if ((cfg as any).voiceAskUi !== 'relay') {
1039
1040
  editorRef?.suspend();
1040
- const v = await selectMenu(process.stderr, { title: `? background worker asks to run ${call.name} ${hint}`, items: [{ label: 'Allow', value: 'y' }, { label: 'Deny', value: 'n' }], current: 'n' });
1041
+ const v = await selectMenu(process.stderr, {
1042
+ title: `? background worker asks to run ${call.name} ${hint}`,
1043
+ items: [{ label: 'Allow once', value: 'y' }, { label: 'Allow always', value: 'a' }, { label: 'Deny', value: 'n' }],
1044
+ current: 'y',
1045
+ });
1041
1046
  editorRef?.resume();
1042
1047
  editorRef?.redrawNow();
1043
- return { decision: v === 'y' ? 'allow' : 'deny' };
1048
+ if (v === 'a') {
1049
+ // Remember a command-scoped allow: a live session rule (wins next ask; glob has no `*`
1050
+ // → exact-command match) + persist to .agent/permissions.json for future sessions.
1051
+ const cmd = typeof call.args?.command === 'string' ? call.args.command : null;
1052
+ work.permissions?.options.rules.unshift(cmd ? { tool: call.name, pathGlob: cmd, decision: 'allow' } : { tool: call.name, decision: 'allow' });
1053
+ persistRule(cwd, 'allow', cmd ? `${call.name}(${cmd})` : call.name);
1054
+ }
1055
+ return { decision: v === 'y' || v === 'a' ? 'allow' : 'deny' };
1044
1056
  }
1045
1057
  // NB: perm asks are keyed perm-N (PermissionPolicy.ask carries no task identity), so a
1046
1058
  // cancelled task can't clean its parked perm question — bounded by askTimeoutMs → deny.
@@ -2120,7 +2132,28 @@ async function repl(args: Args, ai: ChatLike, cfg: Partial<AgentConfig>, cwd: st
2120
2132
  err(dim(` … cancelled ${running.length} background task(s)\n`));
2121
2133
  } else if (running.length) {
2122
2134
  err(dim(` … waiting for ${running.length} background task(s) (Ctrl-C to force quit)\n`));
2123
- await dx.idle();
2135
+ // stdin is still in raw mode here, so Ctrl-C arrives as a 0x03 byte (no SIGINT).
2136
+ // Race the drain against a raw Ctrl-C: on press, abort all workers and bail.
2137
+ let forced = false;
2138
+ let onCtrlC = () => {};
2139
+ const onByte = (b: Buffer) => {
2140
+ if (!b.includes(0x03)) return; // Ctrl-C
2141
+ forced = true;
2142
+ for (const t of running) { t.status = 'cancelled'; t.controller.abort(); }
2143
+ err(dim(`\n … force-quit — cancelled ${running.length} background task(s)\n`));
2144
+ onCtrlC();
2145
+ };
2146
+ process.stdin.on('data', onByte);
2147
+ await Promise.race([dx.idle(), new Promise<void>((res) => { onCtrlC = res; })]);
2148
+ process.stdin.off('data', onByte);
2149
+ if (forced) {
2150
+ // User force-quit: tear down and hard-exit — don't trust the event loop to drain
2151
+ // (voice children / sockets / MCP handles can keep the process alive otherwise).
2152
+ voiceIO?.stop();
2153
+ releaseStdin();
2154
+ await closeMcp(mounted);
2155
+ process.exit(130);
2156
+ }
2124
2157
  (face.options.host as { flushText?: () => void } | undefined)?.flushText?.();
2125
2158
  duplexPersist();
2126
2159
  }
package/dist/cli.js CHANGED
@@ -3520,7 +3520,7 @@ var DuplexAgentOptions = class {
3520
3520
  reflexModel = "groq/openai/gpt-oss-20b";
3521
3521
  actModel = "anthropic/claude-sonnet-4-6";
3522
3522
  /** Premium reasoning model. Set to `false` to disable the Think tier entirely. */
3523
- thinkModel = "anthropic/claude-opus-4-6";
3523
+ thinkModel = "anthropic/claude-opus-4-8";
3524
3524
  /** Escape hatches merged over the derived per-agent options. */
3525
3525
  reflexOptions;
3526
3526
  actOptions;
@@ -4125,7 +4125,7 @@ var VoiceEngine = class _VoiceEngine {
4125
4125
  this.stt.onLevel = (rms) => this.handleLevel(rms);
4126
4126
  await Promise.all([this.tts.connect(), this.stt.start()]);
4127
4127
  this.setState("listening");
4128
- log7.info(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
4128
+ log7.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
4129
4129
  }
4130
4130
  get usingAec() {
4131
4131
  return this.stt.usingAec;
@@ -4168,7 +4168,7 @@ var VoiceEngine = class _VoiceEngine {
4168
4168
  this.spokeDeltas = true;
4169
4169
  this.ackAt = now();
4170
4170
  }
4171
- this.turnStartAt = now();
4171
+ if (!this.turnStartAt) this.turnStartAt = now();
4172
4172
  this.setState("thinking");
4173
4173
  }
4174
4174
  speakDelta(text) {
@@ -4177,7 +4177,7 @@ var VoiceEngine = class _VoiceEngine {
4177
4177
  this.reply += text;
4178
4178
  for (const w of this.words(this.reply)) this.echoWords.add(w);
4179
4179
  this.tts.speak(text, true);
4180
- if (!this.spokeDeltas && this.turnStartAt) log7.info(`ttft: ${Math.round(now() - this.turnStartAt)}ms`);
4180
+ if (!this.spokeDeltas && this.turnStartAt) log7.debug(`ttft: ${Math.round(now() - this.turnStartAt)}ms`);
4181
4181
  this.spokeDeltas = true;
4182
4182
  this.setState("speaking");
4183
4183
  }
@@ -4198,7 +4198,7 @@ var VoiceEngine = class _VoiceEngine {
4198
4198
  }
4199
4199
  this.drainTimer = null;
4200
4200
  this.speaking = false;
4201
- if (this.turnStartAt) log7.info(`turn: ${Math.round(now() - this.turnStartAt)}ms (incl. playback)`);
4201
+ if (this.turnStartAt) log7.debug(`turn: ${Math.round(now() - this.turnStartAt)}ms (incl. playback)`);
4202
4202
  this.echoUntil = now() + 2500;
4203
4203
  if (!this.usingAec) this.stt.reset();
4204
4204
  this.setState("listening");
@@ -4353,7 +4353,10 @@ var VoiceEngine = class _VoiceEngine {
4353
4353
  }
4354
4354
  const text = this.pendingUtt;
4355
4355
  this.pendingUtt = "";
4356
- if (text) this.options.onUtterance(text);
4356
+ if (text) {
4357
+ this.turnStartAt = now();
4358
+ this.options.onUtterance(text);
4359
+ }
4357
4360
  }
4358
4361
  get overlapCapable() {
4359
4362
  return this.usingAec && this.options.overlapPause && !!this.player.pause && !!this.player.resume;
@@ -4494,7 +4497,7 @@ var SonioxSTT = class {
4494
4497
  this.endpointTimer = setInterval(() => {
4495
4498
  const combined = (this.finalText + this.partialText).trim();
4496
4499
  if (!combined || now2() - this.lastChangeAt < this.options.silenceEndpointMs) return;
4497
- if (this.firstTokenAt) log8.info(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
4500
+ if (this.firstTokenAt) log8.debug(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
4498
4501
  this.reset();
4499
4502
  this.onUtterance(combined, now2());
4500
4503
  }, 120);
@@ -4527,7 +4530,7 @@ var SonioxSTT = class {
4527
4530
  this.onPartial(combined);
4528
4531
  if (endpoint && this.finalText.trim()) {
4529
4532
  const utterance = this.finalText.trim();
4530
- if (this.firstTokenAt) log8.info(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
4533
+ if (this.firstTokenAt) log8.debug(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
4531
4534
  this.reset();
4532
4535
  this.onUtterance(utterance, now2());
4533
4536
  }
@@ -8350,12 +8353,21 @@ async function repl(args, ai, cfg, cwd) {
8350
8353
  const duplexAsk = async (call) => {
8351
8354
  if (args.voice && dx) {
8352
8355
  const hint = summarizeCall(call.name, call.args).slice(0, 80);
8353
- if (cfg.voiceAskUi === "menu") {
8356
+ if (cfg.voiceAskUi !== "relay") {
8354
8357
  editorRef?.suspend();
8355
- const v = await selectMenu(process.stderr, { title: `? background worker asks to run ${call.name} ${hint}`, items: [{ label: "Allow", value: "y" }, { label: "Deny", value: "n" }], current: "n" });
8358
+ const v = await selectMenu(process.stderr, {
8359
+ title: `? background worker asks to run ${call.name} ${hint}`,
8360
+ items: [{ label: "Allow once", value: "y" }, { label: "Allow always", value: "a" }, { label: "Deny", value: "n" }],
8361
+ current: "y"
8362
+ });
8356
8363
  editorRef?.resume();
8357
8364
  editorRef?.redrawNow();
8358
- return { decision: v === "y" ? "allow" : "deny" };
8365
+ if (v === "a") {
8366
+ const cmd = typeof call.args?.command === "string" ? call.args.command : null;
8367
+ work.permissions?.options.rules.unshift(cmd ? { tool: call.name, pathGlob: cmd, decision: "allow" } : { tool: call.name, decision: "allow" });
8368
+ persistRule(cwd, "allow", cmd ? `${call.name}(${cmd})` : call.name);
8369
+ }
8370
+ return { decision: v === "y" || v === "a" ? "allow" : "deny" };
8359
8371
  }
8360
8372
  const id = `perm-${++permSeq}`;
8361
8373
  const a = await dx.parkQuestion(id, `Permission: may the background worker run ${call.name}${hint ? ` (${hint})` : ""}? Answer yes or no (you can also type it).`);
@@ -9704,7 +9716,32 @@ ${extra}` : body);
9704
9716
  } else if (running.length) {
9705
9717
  err(dim(` \u2026 waiting for ${running.length} background task(s) (Ctrl-C to force quit)
9706
9718
  `));
9707
- await dx.idle();
9719
+ let forced = false;
9720
+ let onCtrlC = () => {
9721
+ };
9722
+ const onByte = (b) => {
9723
+ if (!b.includes(3)) return;
9724
+ forced = true;
9725
+ for (const t of running) {
9726
+ t.status = "cancelled";
9727
+ t.controller.abort();
9728
+ }
9729
+ err(dim(`
9730
+ \u2026 force-quit \u2014 cancelled ${running.length} background task(s)
9731
+ `));
9732
+ onCtrlC();
9733
+ };
9734
+ process.stdin.on("data", onByte);
9735
+ await Promise.race([dx.idle(), new Promise((res) => {
9736
+ onCtrlC = res;
9737
+ })]);
9738
+ process.stdin.off("data", onByte);
9739
+ if (forced) {
9740
+ voiceIO?.stop();
9741
+ releaseStdin();
9742
+ await closeMcp(mounted);
9743
+ process.exit(130);
9744
+ }
9708
9745
  face.options.host?.flushText?.();
9709
9746
  duplexPersist();
9710
9747
  }