@livx.cc/agentx 0.99.25 → 0.99.26

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/index.d.ts CHANGED
@@ -879,6 +879,10 @@ interface TaskRecord {
879
879
  /** Accumulated `<spoken>` delivery text (every segment, whether or not it was actually voiced). Used to
880
880
  * RESUME a parked delivery after a trivial barge — see isTrivialBarge / redeliverParked. */
881
881
  spokenText?: string;
882
+ /** The user-turn counter (DuplexAgent.turn) at dispatch time. parkInFlightDeliveries() parks only tasks
883
+ * from a PRIOR turn (a superseded topic) — never the task the current turn just launched, so a barge on
884
+ * stale audio (or a substantive redirect) can't collateral-silence the work the user actually wants. */
885
+ createdTurn: number;
882
886
  }
883
887
  type WorkerTier = 'act' | 'think';
884
888
  declare class DuplexAgentOptions {
@@ -953,6 +957,9 @@ declare class DuplexAgent {
953
957
  readonly tasks: Map<string, TaskRecord>;
954
958
  private queue;
955
959
  private seq;
960
+ /** Monotonic user-turn counter (bumped once per SUBSTANTIVE turn). Stamped onto each task at dispatch
961
+ * (TaskRecord.createdTurn) so parking can tell a prior-topic task from the current turn's fresh one. */
962
+ private turn;
956
963
  private pendingEvents;
957
964
  /** Spoken text of parked deliveries that have SETTLED, awaiting a trivial-barge resume decision on the
958
965
  * next turn (a substantive turn clears it — the result stays in the transcript, recoverable by asking). */
@@ -1073,11 +1080,17 @@ declare class DuplexAgent {
1073
1080
  abortSpeculation(): void;
1074
1081
  /** Cancel a running background task — shared by the CancelTask tool and the CLI /tasks picker. */
1075
1082
  cancelTask(id: string): string;
1076
- /** Barge-in: the user took the floor while task(s) were running. Suppress those tasks' remaining SPOKEN
1077
- * delivery so a superseded topic never talks over the new one (the debt-after-jokes regression). The
1078
- * tasks keep running and still fold their result into the transcript — recoverable, just not spoken.
1079
- * Returns the parked ids (for logging). Does NOT cancel: that's a deliberate reflex/user action. */
1080
- parkInFlightDeliveries(): string[];
1083
+ /** Barge-in (or a dispatch-driven supersede): suppress running tasks' remaining SPOKEN delivery so a
1084
+ * superseded topic never talks over the new one (the debt-after-jokes regression). Tasks keep running and
1085
+ * still fold their result into the transcript — recoverable, just not spoken. Returns parked ids (logging).
1086
+ * Does NOT cancel. Two callers, two shapes:
1087
+ * - BARGE (default): the user interrupted AUDIO. Park EVERY running task (including this turn's) — the
1088
+ * worker they cut off must stop, whatever turn launched it — and re-arm trivial-barge auto-resume so a
1089
+ * following "go on" can bring the parked delivery back.
1090
+ * - SUPERSEDE (`priorTurnsOnly`, `reenableResume:false`, from dispatch()): a new dispatch redirected to
1091
+ * fresh work. Park only PRIOR-turn tasks so parallel siblings dispatched in the SAME turn don't silence
1092
+ * each other, and keep them suppressed (the user moved on — don't resurface on a stray "go on"). */
1093
+ parkInFlightDeliveries(reenableResume?: boolean, priorTurnsOnly?: boolean): string[];
1081
1094
  /** True while there is a parked delivery to potentially resume: either one already settled (queued) or
1082
1095
  * one still running that was parked by a barge. */
1083
1096
  private hasParkedDelivery;
@@ -1142,6 +1155,13 @@ declare class DuplexAgent {
1142
1155
  private integrationPrompt;
1143
1156
  private onWorkerSettled;
1144
1157
  private onWorkerFailed;
1158
+ /** A delivery the user has SUPERSEDED — parked AND moved on (suppressParkedResume, set by a redirect) —
1159
+ * must not hijack the voice when it settles late (partial or failed): that would talk over the new topic
1160
+ * and, worse, auto-escalate work the user already abandoned. Fold a recoverable note into the transcript
1161
+ * instead (the reflex can surface it if asked) and report true so the caller skips queueRevoice. A merely
1162
+ * barge-parked task (suppressParkedResume false — a "go on" may still resume it) is NOT superseded: it
1163
+ * keeps the normal surfacing path so a failure the user still cares about is not swallowed. */
1164
+ private foldIfSuperseded;
1145
1165
  private failTask;
1146
1166
  /** Live-switch the think tier: `false` disables (removes the Think tool from the voice agent),
1147
1167
  * a model id enables (adds the tool if missing). The system-prompt THINK_SLOT text is frozen at
package/dist/index.js CHANGED
@@ -6734,6 +6734,9 @@ var DuplexAgent = class _DuplexAgent {
6734
6734
  tasks = /* @__PURE__ */ new Map();
6735
6735
  queue = Promise.resolve();
6736
6736
  seq = 0;
6737
+ /** Monotonic user-turn counter (bumped once per SUBSTANTIVE turn). Stamped onto each task at dispatch
6738
+ * (TaskRecord.createdTurn) so parking can tell a prior-topic task from the current turn's fresh one. */
6739
+ turn = 0;
6737
6740
  pendingEvents = [];
6738
6741
  /** Spoken text of parked deliveries that have SETTLED, awaiting a trivial-barge resume decision on the
6739
6742
  * next turn (a substantive turn clears it — the result stays in the transcript, recoverable by asking). */
@@ -7027,6 +7030,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7027
7030
  return this.enqueue(async () => {
7028
7031
  await this.initMemory();
7029
7032
  this.resetTurn();
7033
+ this.turn++;
7030
7034
  this.awaitTrivialRedeliver = false;
7031
7035
  this.parkedRedeliver = [];
7032
7036
  this.suppressParkedResume = true;
@@ -7125,15 +7129,21 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7125
7129
  rec.controller.abort();
7126
7130
  return `Task ${rec.id} (${rec.label}) cancelled.`;
7127
7131
  }
7128
- /** Barge-in: the user took the floor while task(s) were running. Suppress those tasks' remaining SPOKEN
7129
- * delivery so a superseded topic never talks over the new one (the debt-after-jokes regression). The
7130
- * tasks keep running and still fold their result into the transcript — recoverable, just not spoken.
7131
- * Returns the parked ids (for logging). Does NOT cancel: that's a deliberate reflex/user action. */
7132
- parkInFlightDeliveries() {
7133
- this.suppressParkedResume = false;
7132
+ /** Barge-in (or a dispatch-driven supersede): suppress running tasks' remaining SPOKEN delivery so a
7133
+ * superseded topic never talks over the new one (the debt-after-jokes regression). Tasks keep running and
7134
+ * still fold their result into the transcript — recoverable, just not spoken. Returns parked ids (logging).
7135
+ * Does NOT cancel. Two callers, two shapes:
7136
+ * - BARGE (default): the user interrupted AUDIO. Park EVERY running task (including this turn's) — the
7137
+ * worker they cut off must stop, whatever turn launched it — and re-arm trivial-barge auto-resume so a
7138
+ * following "go on" can bring the parked delivery back.
7139
+ * - SUPERSEDE (`priorTurnsOnly`, `reenableResume:false`, from dispatch()): a new dispatch redirected to
7140
+ * fresh work. Park only PRIOR-turn tasks so parallel siblings dispatched in the SAME turn don't silence
7141
+ * each other, and keep them suppressed (the user moved on — don't resurface on a stray "go on"). */
7142
+ parkInFlightDeliveries(reenableResume = true, priorTurnsOnly = false) {
7143
+ if (reenableResume) this.suppressParkedResume = false;
7134
7144
  const parked = [];
7135
7145
  for (const rec of this.tasks.values())
7136
- if (rec.status === "running" && !rec.deliveryParked) {
7146
+ if (rec.status === "running" && !rec.deliveryParked && (!priorTurnsOnly || rec.createdTurn < this.turn)) {
7137
7147
  rec.deliveryParked = true;
7138
7148
  parked.push(rec.id);
7139
7149
  }
@@ -7319,7 +7329,7 @@ ${recent}` : brief) + verify + deliverContract;
7319
7329
  flushSpoken();
7320
7330
  return res;
7321
7331
  }).then((res) => this.maybeVerify(id, brief, res, tier, agentOpts, askBridge)).then((res) => this.onWorkerSettled(id, res)).catch((err) => this.onWorkerFailed(id, err));
7322
- this.tasks.set(id, { id, label, status: "running", controller, promise, tail, brief, followUp, splitter });
7332
+ this.tasks.set(id, { id, label, status: "running", controller, promise, tail, brief, followUp, splitter, createdTurn: this.turn });
7323
7333
  if (this.tasks.size > this.options.maxTaskRecords)
7324
7334
  for (const [tid, rec] of this.tasks) {
7325
7335
  if (this.tasks.size <= this.options.maxTaskRecords) break;
@@ -7493,6 +7503,7 @@ Another agent just implemented the above. Independently check the CURRENT state
7493
7503
  toolCalls: res.messages.filter((m) => m.role === "tool").length
7494
7504
  });
7495
7505
  if (incomplete) {
7506
+ if (this.foldIfSuperseded(rec, res.text)) return;
7496
7507
  return this.queueRevoice(this.integrationPrompt(rec, "incomplete", res.text, res.finishReason), true);
7497
7508
  }
7498
7509
  const tail = rec.splitter?.flush();
@@ -7514,12 +7525,25 @@ Another agent just implemented the above. Independently check the CURRENT state
7514
7525
  onWorkerFailed(id, err) {
7515
7526
  this.failTask(this.tasks.get(id), err instanceof Error ? err.message : String(err));
7516
7527
  }
7528
+ /** A delivery the user has SUPERSEDED — parked AND moved on (suppressParkedResume, set by a redirect) —
7529
+ * must not hijack the voice when it settles late (partial or failed): that would talk over the new topic
7530
+ * and, worse, auto-escalate work the user already abandoned. Fold a recoverable note into the transcript
7531
+ * instead (the reflex can surface it if asked) and report true so the caller skips queueRevoice. A merely
7532
+ * barge-parked task (suppressParkedResume false — a "go on" may still resume it) is NOT superseded: it
7533
+ * keeps the normal surfacing path so a failure the user still cares about is not swallowed. */
7534
+ foldIfSuperseded(rec, note) {
7535
+ if (!(rec.deliveryParked && this.suppressParkedResume)) return false;
7536
+ if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
7537
+ log13.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
7538
+ return true;
7539
+ }
7517
7540
  failTask(rec, msg) {
7518
7541
  this.dropAsk(rec.id);
7519
7542
  rec.status = "error";
7520
7543
  rec.result = msg;
7521
7544
  log13.warn(`task ${rec.id} failed: ${msg}`);
7522
7545
  this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
7546
+ if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
7523
7547
  this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
7524
7548
  }
7525
7549
  // --- voice tools (closures over this instance) ---
@@ -7540,6 +7564,7 @@ Another agent just implemented the above. Independently check the CURRENT state
7540
7564
  async dispatch(brief, tier = "act", label, followUp = false) {
7541
7565
  if (tier === "think" && this.options.thinkModel === false) tier = "act";
7542
7566
  if (followUp) this.autoEscalations++;
7567
+ this.parkInFlightDeliveries(false, true);
7543
7568
  const id = `t${++this.seq}`;
7544
7569
  const lbl = label ?? tier;
7545
7570
  await this.options.onTaskStart?.(id, lbl);