@livx.cc/agentx 0.99.118 → 0.99.119

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
@@ -1,5 +1,5 @@
1
- import { a as AgentOptions, H as Hooks, p as RunResult, f as Decider, A as Agent, i as PermissionPolicy, G as Gender, L as LineKind, C as ChannelMode, q as SecondChannelOptions, S as SecondChannel } from './secondChannel-BCEr_QyM.js';
2
- export { b as ChatFragment, c as Classification, d as Classifier, e as CompactResult, D as DEFAULT_MUTATING, g as Decision, M as ModelRouter, h as ModelRouterOptions, P as PermissionOptions, j as PermissionRule, k as PreToolUseDecision, R as ReasoningEffort, l as RecordingHooks, m as RecordingLifecycle, n as RouteDecision, o as RouterState, T as Tier, r as ToolUse, s as ToolUseMeta, t as claudeCodeDisallowedTools, u as composeHooks, v as estimateTokens, w as jevAvailable, x as jevClassifier, y as pickTier, z as planMode, B as reasoningToChatFragment } from './secondChannel-BCEr_QyM.js';
1
+ import { a as AgentOptions, H as Hooks, p as RunResult, f as Decider, A as Agent, i as PermissionPolicy, G as Gender, L as LineKind, C as ChannelMode, q as SecondChannelOptions, S as SecondChannel } from './secondChannel-DepBV0Zv.js';
2
+ export { b as ChatFragment, c as Classification, d as Classifier, e as CompactResult, D as DEFAULT_MUTATING, g as Decision, M as ModelRouter, h as ModelRouterOptions, P as PermissionOptions, j as PermissionRule, k as PreToolUseDecision, R as ReasoningEffort, l as RecordingHooks, m as RecordingLifecycle, n as RouteDecision, o as RouterState, T as Tier, r as ToolUse, s as ToolUseMeta, t as claudeCodeDisallowedTools, u as composeHooks, v as estimateTokens, w as jevAvailable, x as jevClassifier, y as pickTier, z as planMode, B as reasoningToChatFragment } from './secondChannel-DepBV0Zv.js';
3
3
  export { MODEL_ALIASES, ModelSwitch, ResolveModelSwitchOpts, latestClaude, modelShortLabel, resolveModelAlias, resolveModelSwitch } from './models.js';
4
4
  import { IFilesystem, FileMetadata } from '@livx.cc/wcli/core';
5
5
  export { CommandExecutor, FileMetadata, IFilesystem, IndexedDbFilesystem, MemFilesystem, registerHeadlessCommands } from '@livx.cc/wcli/core';
@@ -1666,15 +1666,40 @@ interface MainEvent {
1666
1666
  name?: string;
1667
1667
  args?: unknown;
1668
1668
  }
1669
+ /** One turn's timeline, ms from the user's last word (the STT's word clock when the host passes it, else the final's
1670
+ * arrival). A stage that did not happen is absent. */
1671
+ interface TurnTimeline {
1672
+ /** The STT's final reached the call (STT latency + silence endpoint + the engine's merge hold). */
1673
+ final?: number;
1674
+ /** TurnGate sent the turn to MAIN (its own hold for a mid-phrase line included). */
1675
+ sent?: number;
1676
+ /** The fill channel chose its first line for this turn (bridge/filler/progress) — handed to the host, not yet heard. */
1677
+ fill?: number;
1678
+ fillKind?: string;
1679
+ /** That line's audio started (hostAudio: the host's player; else = fill). */
1680
+ fillAudio?: number;
1681
+ /** MAIN's first event (text or tool). */
1682
+ mainFirst?: number;
1683
+ /** MAIN's first text token. */
1684
+ mainText?: number;
1685
+ /** MAIN's first audio started (the host's player). */
1686
+ mainAudio?: number;
1687
+ /** false: the host gave no last-word time — the anchor is the final's arrival. */
1688
+ lastWord: boolean;
1689
+ /** How the turn ended: answered = MAIN audio played · cut = barged · replaced = a fuller re-final took it · silent = no MAIN audio. */
1690
+ end?: 'answered' | 'cut' | 'replaced' | 'silent';
1691
+ }
1669
1692
  interface CallMetric {
1670
- name: 'first_sound' | 'first_word' | 'turn' | 'drop' | 'hold' | 'fill' | 'lang';
1693
+ name: 'first_sound' | 'first_word' | 'turn' | 'drop' | 'hold' | 'fill' | 'lang' | 'timeline';
1671
1694
  id?: string;
1672
- /** ms from the user's final (the first final of a held/merged turn) */
1695
+ /** ms from the user's last word (see TurnTimeline) */
1673
1696
  ms?: number;
1674
1697
  by?: 'fill' | 'progress' | 'main';
1675
1698
  kind?: string;
1676
1699
  text?: string;
1677
1700
  lang?: string | null;
1701
+ /** name 'timeline': the turn's stages. */
1702
+ timeline?: TurnTimeline;
1678
1703
  }
1679
1704
  declare class VoiceCallOptions {
1680
1705
  /** Host runs MAIN with exactly `text` (a replace: roll the `replaces` turn back first). */
@@ -1707,6 +1732,9 @@ declare class VoiceCallOptions {
1707
1732
  /** Extra SecondChannel options (tests, tuning). speak/show/cancel are always VoiceCall's own. */
1708
1733
  channel: Partial<SecondChannelOptions>;
1709
1734
  onMetric: (m: CallMetric) => void;
1735
+ /** The host reports when a fill line's audio REALLY starts (heard()). Off: a line handed to `speak` counts as heard —
1736
+ * right only for a host whose speak() plays at once (a relay host would count lines its player later dropped). */
1737
+ hostAudio: boolean;
1710
1738
  now: () => number;
1711
1739
  /** An aside (aside()) needs this long since the last one — a cue answered every time sounds scripted. */
1712
1740
  asideGapMs: number;
@@ -1722,6 +1750,8 @@ declare class VoiceCall {
1722
1750
  private lastAside;
1723
1751
  private lastAudibleAt;
1724
1752
  private firstFinalAt;
1753
+ private lastWordAt;
1754
+ private earlyHeard;
1725
1755
  private progressTimer;
1726
1756
  private ended;
1727
1757
  constructor(options?: Partial<VoiceCallOptions> | undefined);
@@ -1738,14 +1768,25 @@ declare class VoiceCall {
1738
1768
  setLanguage(tag: string): void;
1739
1769
  /** STT partial (the whole partial). */
1740
1770
  interim(text: string, _atMs?: number, aec?: boolean): void;
1741
- /** STT final → TurnGate. */
1742
- final(text: string, sttId?: string): void;
1771
+ /** STT final → TurnGate. `lastWordAgoMs`: how long ago the user's last word of it ended (the STT's word clock,
1772
+ * bod-voice UtteranceMeta.lastWordAgoMs) — the turn's latency anchor; absent = the final's arrival. `lang`: the
1773
+ * language the STT identified for it (UtteranceMeta.language) — what the turn's words follow when they alone can't tell
1774
+ * (a short English line after a Hebrew one left the fill channel on Hebrew: its bridge was refused, the turn silent). */
1775
+ final(text: string, sttId?: string, o?: {
1776
+ lastWordAgoMs?: number;
1777
+ lang?: string;
1778
+ }): void;
1779
+ private finalAt;
1780
+ private sttLang;
1781
+ /** The host's player started a line's audio now (or in `inMs`): `fill`/`progress` = the fill channel's, `main` = MAIN's
1782
+ * reply (as main({type:'audio'})). With hostAudio this is what first_sound counts. */
1783
+ heard(by: 'fill' | 'progress' | 'main', inMs?: number): void;
1743
1784
  /** A v2 host that decides turns itself: the user's line already went to MAIN (no TurnGate, no sendTurn) — the fill
1744
1785
  * channel follows its language and covers the wait. */
1745
1786
  hostTurn(text: string): void;
1746
1787
  pause(ms: number): void;
1747
- /** MAIN's stream. */
1748
- main(ev: MainEvent): void;
1788
+ /** MAIN's stream. (`inMs`: an audio event's start is that far ahead — the host player's queue.) */
1789
+ main(ev: MainEvent, inMs?: number): void;
1749
1790
  /** The user barged in (hard): cut fill + MAIN audio (the host cuts its own), abort a running MAIN turn. */
1750
1791
  barge(): void;
1751
1792
  /** Soft barge-in: keep (a listener cue — no turn) or cut (→ barge()). */
@@ -1768,6 +1809,9 @@ declare class VoiceCall {
1768
1809
  /** Every fill/progress line passes here: only in the turn's known language, never after the call ended. */
1769
1810
  private say;
1770
1811
  private sounded;
1812
+ /** The turn is over (answered + audible, replaced, the next turn, the call's end): its timeline, once. A turn whose MAIN
1813
+ * never became audible still reports first_word — with no ms and why (cut / replaced / silent) — so every turn has one. */
1814
+ private close;
1771
1815
  private onTool;
1772
1816
  private clearProgress;
1773
1817
  }
package/dist/index.js CHANGED
@@ -13104,11 +13104,26 @@ var SecondChannel = class {
13104
13104
  }
13105
13105
  /** Hard gates every line passes, whoever proposed it. */
13106
13106
  allowed(k) {
13107
- if (!this.o.kinds.includes(k)) return false;
13108
- 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;
13109
- if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle" && !this.capped();
13110
- if (k === "filler") return !this.userSpeaking && this.main === "idle";
13111
- return (this.main === "queued" || this.main === "thinking") && !this.bridged;
13107
+ return !this.blocked(k);
13108
+ }
13109
+ /** Why a line of kind `k` can't play now ('' = it can) — the call's ledger names a missing bridge's cause. */
13110
+ blocked(k) {
13111
+ if (!this.o.kinds.includes(k)) return "kind off";
13112
+ if (this.audioLocked) return `main audio (${this.main})`;
13113
+ if (this.inflight) return "a line still playing";
13114
+ if (this.o.now() - this.lastAt < this.o.minGapMs) return `${Math.round(this.o.now() - this.lastAt)}ms since the last line`;
13115
+ if (k === this.lastKind && (k !== "backchannel" || this.o.now() - this.lastAt < this.o.backchannelGapMs)) return "same kind as the last line";
13116
+ if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle" && !this.capped() ? "" : "not a backchannel moment";
13117
+ if (k === "filler") return !this.userSpeaking && this.main === "idle" ? "" : "not a filler moment";
13118
+ return this.bridged ? "bridged already" : this.main === "queued" || this.main === "thinking" ? "" : `main ${this.main}`;
13119
+ }
13120
+ /** Why this turn gets no bridge by rule ('' = it may) — language, a quick/phatic turn, or blocked(). */
13121
+ bridgeBlock() {
13122
+ const w = words2(this.heard);
13123
+ if (!this.lang) return "language unknown";
13124
+ if (w.length <= 3 || w.length <= 6 && isPhatic(this.heard, this.bank)) return w.length <= 3 ? "quick turn" : "phatic";
13125
+ if (w.length < this.o.bridgeMinWords) return `under ${this.o.bridgeMinWords} words`;
13126
+ return this.blocked("bridge");
13112
13127
  }
13113
13128
  bcTimes = [];
13114
13129
  capped() {
@@ -13155,7 +13170,10 @@ var SecondChannel = class {
13155
13170
  return breath || /[,;:…]$/.test(this.heard);
13156
13171
  }
13157
13172
  async consider(trigger, pauseMs, breath = false, cue = null) {
13158
- if (this.busy) return;
13173
+ if (this.busy) {
13174
+ if (trigger === "working") this.o.onEvent({ trigger, rule: "silence", ms: 0, acted: "silence", reason: "deciding another line" });
13175
+ return;
13176
+ }
13159
13177
  const n0 = words2(this.heard).length;
13160
13178
  this.decidedAt = n0;
13161
13179
  if (!(this.o.anticipate && this.plan && trigger === "interim")) cue = null;
@@ -13165,7 +13183,10 @@ var SecondChannel = class {
13165
13183
  log28.info("chatty mode without echo cancellation \u2014 normal timing (our lines would re-trigger the mic)");
13166
13184
  }
13167
13185
  const rule = this.rule(trigger, pauseMs, breath, cue);
13168
- if (rule === "silence") return;
13186
+ if (rule === "silence") {
13187
+ if (trigger === "working") this.o.onEvent({ trigger, rule, ms: 0, acted: "silence", reason: this.bridgeBlock() || "rule" });
13188
+ return;
13189
+ }
13169
13190
  this.busy = true;
13170
13191
  const gen = this.gen, t0 = this.o.now();
13171
13192
  const ev = (e) => this.o.onEvent({ trigger, rule, ms: Math.round(this.o.now() - t0), ...cue ? { cue: cue.line } : {}, ...e });
@@ -13359,6 +13380,9 @@ var VoiceCallOptions = class {
13359
13380
  channel = {};
13360
13381
  onMetric = () => {
13361
13382
  };
13383
+ /** The host reports when a fill line's audio REALLY starts (heard()). Off: a line handed to `speak` counts as heard —
13384
+ * right only for a host whose speak() plays at once (a relay host would count lines its player later dropped). */
13385
+ hostAudio = false;
13362
13386
  now = () => performance.now();
13363
13387
  /** An aside (aside()) needs this long since the last one — a cue answered every time sounds scripted. */
13364
13388
  asideGapMs = 8e3;
@@ -13378,11 +13402,17 @@ var VoiceCall = class {
13378
13402
  } : {},
13379
13403
  mode: o.mode,
13380
13404
  aec: o.aec,
13381
- bridgeDelayMs: 700,
13405
+ // v3 §3: first sound <1.2s p50 from the last word — the endpoint takes ~0.6s and the bridge's TTS ~0.2s, so the bridge
13406
+ // goes 300ms after the turn if MAIN is still silent (it always is that early: warm MAIN's first token is ≥1s), no veto
13407
+ bridgeDelayMs: 300,
13382
13408
  instantBridge: true,
13383
- // v3 §3: first sound <1.2s p50 — a bridge at 700ms if MAIN is still silent, no veto, no wait
13384
13409
  ...this.lang ? { lang: this.lang } : {},
13385
13410
  ...o.channel,
13411
+ // the ledger: a turn's bridge moment that stayed silent, and why (its wait then has no first sound until MAIN speaks)
13412
+ onEvent: (e) => {
13413
+ if (e.trigger === "working" && e.acted !== "spoken") o.onMetric({ name: "drop", ...this.cur ? { id: this.cur.id } : {}, kind: `no bridge: ${e.reason ?? e.acted}`, ...e.text ? { text: e.text } : {} });
13414
+ o.channel.onEvent?.(e);
13415
+ },
13386
13416
  speak: (text, kind) => this.say(text, kind),
13387
13417
  show: (text, kind) => o.show(text, kind),
13388
13418
  cancel: () => o.cancel(),
@@ -13409,6 +13439,10 @@ var VoiceCall = class {
13409
13439
  lastAudibleAt = -Infinity;
13410
13440
  firstFinalAt = null;
13411
13441
  // the first final of the turn being formed (hold/merge)
13442
+ lastWordAt = null;
13443
+ // the user's last word before the turn being formed (its latest final's)
13444
+ earlyHeard = null;
13445
+ // a fill line heard while that turn was still held
13412
13446
  progressTimer = null;
13413
13447
  ended = false;
13414
13448
  get o() {
@@ -13437,12 +13471,37 @@ var VoiceCall = class {
13437
13471
  this.follow(text);
13438
13472
  this.fill.onInterim(text);
13439
13473
  }
13440
- /** STT final → TurnGate. */
13441
- final(text, sttId) {
13474
+ /** STT final → TurnGate. `lastWordAgoMs`: how long ago the user's last word of it ended (the STT's word clock,
13475
+ * bod-voice UtteranceMeta.lastWordAgoMs) — the turn's latency anchor; absent = the final's arrival. `lang`: the
13476
+ * language the STT identified for it (UtteranceMeta.language) — what the turn's words follow when they alone can't tell
13477
+ * (a short English line after a Hebrew one left the fill channel on Hebrew: its bridge was refused, the turn silent). */
13478
+ final(text, sttId, o = {}) {
13442
13479
  if (this.ended) return;
13443
- this.firstFinalAt ??= this.o.now();
13480
+ const now3 = this.o.now(), { lastWordAgoMs } = o;
13481
+ this.firstFinalAt ??= now3;
13482
+ this.sttLang = o.lang ? primaryLang(o.lang) : null;
13483
+ this.lastWordAt = typeof lastWordAgoMs === "number" && lastWordAgoMs >= 0 ? now3 - lastWordAgoMs : null;
13484
+ this.finalAt = now3;
13444
13485
  this.gate.final(text, sttId);
13445
13486
  }
13487
+ finalAt = 0;
13488
+ sttLang = null;
13489
+ // the STT's language for the final being followed
13490
+ /** The host's player started a line's audio now (or in `inMs`): `fill`/`progress` = the fill channel's, `main` = MAIN's
13491
+ * reply (as main({type:'audio'})). With hostAudio this is what first_sound counts. */
13492
+ heard(by, inMs = 0) {
13493
+ if (this.ended) return;
13494
+ if (by === "main") return this.main({ type: "audio", ...this.cur ? { id: this.cur.id } : {} }, inMs);
13495
+ const at = this.o.now() + Math.max(0, inMs);
13496
+ if (this.gate.holding || !this.cur || this.cur.closed) {
13497
+ this.earlyHeard ??= { at, by };
13498
+ return;
13499
+ }
13500
+ this.lastAudibleAt = at;
13501
+ const c = this.cur;
13502
+ c.marks.fillAudio ??= at;
13503
+ this.sounded(c, by, at);
13504
+ }
13446
13505
  /** A v2 host that decides turns itself: the user's line already went to MAIN (no TurnGate, no sendTurn) — the fill
13447
13506
  * channel follows its language and covers the wait. */
13448
13507
  hostTurn(text) {
@@ -13453,8 +13512,8 @@ var VoiceCall = class {
13453
13512
  pause(ms) {
13454
13513
  if (!this.ended) this.fill.onPause(ms);
13455
13514
  }
13456
- /** MAIN's stream. */
13457
- main(ev) {
13515
+ /** MAIN's stream. (`inMs`: an audio event's start is that far ahead — the host player's queue.) */
13516
+ main(ev, inMs = 0) {
13458
13517
  const c = this.cur;
13459
13518
  if (!c || ev.id && ev.id !== c.id) return;
13460
13519
  switch (ev.type) {
@@ -13463,6 +13522,8 @@ var VoiceCall = class {
13463
13522
  return;
13464
13523
  case "text":
13465
13524
  if (!ev.text) return;
13525
+ c.marks.mainFirst ??= this.o.now();
13526
+ c.marks.mainText ??= this.o.now();
13466
13527
  if (!c.streamed) {
13467
13528
  c.streamed = true;
13468
13529
  this.gate.replyStarted(c.id);
@@ -13472,25 +13533,32 @@ var VoiceCall = class {
13472
13533
  this.fill.onMainTokens(ev.text);
13473
13534
  return;
13474
13535
  case "tool":
13536
+ c.marks.mainFirst ??= this.o.now();
13475
13537
  this.gate.replyStarted(c.id);
13476
13538
  void this.onTool(c, ev.name ?? "", ev.args);
13477
13539
  return;
13478
13540
  case "tool_done":
13479
13541
  return;
13480
- case "audio":
13481
- this.lastAudibleAt = this.o.now();
13542
+ case "audio": {
13543
+ const at = this.o.now() + Math.max(0, inMs);
13544
+ this.lastAudibleAt = at;
13482
13545
  this.fill.onAgentState("first_audio");
13483
- this.sounded(c, "main");
13546
+ this.sounded(c, "main", at);
13484
13547
  if (!c.worded) {
13485
13548
  c.worded = true;
13486
- this.o.onMetric({ name: "first_word", id: c.id, ms: Math.round(this.o.now() - c.at), by: "main" });
13549
+ c.marks.mainAudio = at;
13550
+ this.o.onMetric({ name: "first_word", id: c.id, ms: Math.round(at - c.at), by: "main" });
13487
13551
  }
13552
+ if (c.done) this.close(c, "answered");
13488
13553
  return;
13554
+ }
13489
13555
  case "done":
13490
13556
  case "error":
13491
13557
  c.running = false;
13558
+ c.done = true;
13492
13559
  this.clearProgress();
13493
13560
  this.fill.onAgentState("idle");
13561
+ if (c.worded) this.close(c, "answered");
13494
13562
  return;
13495
13563
  }
13496
13564
  }
@@ -13507,6 +13575,7 @@ var VoiceCall = class {
13507
13575
  this.cur.running = false;
13508
13576
  this.o.abortTurn(this.cur.id);
13509
13577
  }
13578
+ if (this.cur && !this.cur.worded) this.cur.end ??= "cut";
13510
13579
  }
13511
13580
  /** Soft barge-in: keep (a listener cue — no turn) or cut (→ barge()). */
13512
13581
  async overlap(text) {
@@ -13536,6 +13605,7 @@ var VoiceCall = class {
13536
13605
  return this.fill.drain();
13537
13606
  }
13538
13607
  end() {
13608
+ if (this.cur) this.close(this.cur);
13539
13609
  this.ended = true;
13540
13610
  this.gate.end();
13541
13611
  this.clearProgress();
@@ -13544,24 +13614,39 @@ var VoiceCall = class {
13544
13614
  /** Follow the user's language from their own words: a confident detection, else the current language if the words
13545
13615
  * fit it, else unknown (silent). Pinned into the channel so its own detection never diverges. */
13546
13616
  follow(text) {
13547
- const l = detectLanguage(text) ?? (this.lang && fitsLanguage(text, this.lang) ? this.lang : null);
13617
+ const fits = (l2) => l2 && fitsLanguage(text, l2) ? l2 : null;
13618
+ const l = detectLanguage(text) ?? fits(this.sttLang) ?? fits(this.lang);
13548
13619
  if (l !== this.lang) this.o.onMetric({ name: "lang", lang: l, text });
13549
13620
  this.lang = l;
13550
13621
  if (l) this.fill.setLanguage(l, true);
13551
13622
  }
13552
13623
  onTurn(t) {
13553
- const at = this.firstFinalAt ?? this.o.now();
13624
+ const now3 = this.o.now(), lastWord = this.lastWordAt !== null;
13625
+ const at = this.lastWordAt ?? this.firstFinalAt ?? now3;
13626
+ const finalAt = this.finalAt || now3;
13554
13627
  this.firstFinalAt = null;
13628
+ this.lastWordAt = null;
13629
+ if (this.cur) this.close(this.cur, t.replaces === this.cur.id ? "replaced" : void 0);
13555
13630
  this.follow(t.text);
13556
13631
  this.clearProgress();
13557
- this.cur = { id: t.id, text: t.text, at, running: true, streamed: false, sounded: false, worded: false };
13632
+ this.cur = { id: t.id, text: t.text, at, finalAt, lastWord, running: true, streamed: false, sounded: false, worded: false, done: false, marks: { sent: now3 }, closed: false };
13633
+ const early = this.earlyHeard;
13634
+ this.earlyHeard = null;
13635
+ if (early && early.at >= at) {
13636
+ this.cur.marks.fillAudio = early.at;
13637
+ this.sounded(this.cur, early.by, early.at);
13638
+ }
13558
13639
  this.progress.turn();
13559
13640
  this.o.onMetric({ name: "turn", id: t.id, kind: t.kind, text: t.text });
13560
13641
  this.o.sendTurn(t.text, { id: t.id, ...t.replaces ? { replaces: t.replaces } : {} });
13561
13642
  if (t.kind === "new") this.fill.onUserTurn(t.text);
13562
13643
  }
13563
13644
  onDrop(text, why) {
13564
- if (!this.gate.holding) this.firstFinalAt = null;
13645
+ if (!this.gate.holding) {
13646
+ this.firstFinalAt = null;
13647
+ this.lastWordAt = null;
13648
+ this.earlyHeard = null;
13649
+ }
13565
13650
  this.o.onMetric({ name: "drop", kind: why, text });
13566
13651
  }
13567
13652
  /** Every fill/progress line passes here: only in the turn's known language, never after the call ended. */
@@ -13575,16 +13660,48 @@ var VoiceCall = class {
13575
13660
  return false;
13576
13661
  }
13577
13662
  if (r === false) return false;
13578
- this.lastAudibleAt = this.o.now();
13579
13663
  this.o.onMetric({ name: "fill", kind, text, lang: this.lang });
13580
- if (this.cur && (kind === "bridge" || kind === "progress")) this.sounded(this.cur, kind === "progress" ? "progress" : "fill");
13581
- else if (this.cur && this.cur.running && !this.cur.sounded && kind === "filler") this.sounded(this.cur, "fill");
13664
+ const c = this.cur && !this.cur.closed ? this.cur : void 0;
13665
+ const forTurn = !!c && (kind === "bridge" || kind === "progress" || kind === "filler" && c.running && !c.sounded);
13666
+ if (c && forTurn && c.marks.fill === void 0) {
13667
+ c.marks.fill = this.o.now();
13668
+ c.fillKind = kind;
13669
+ }
13670
+ if (this.o.hostAudio) return r;
13671
+ this.lastAudibleAt = this.o.now();
13672
+ if (c && forTurn) {
13673
+ c.marks.fillAudio ??= this.o.now();
13674
+ this.sounded(c, kind === "progress" ? "progress" : "fill");
13675
+ }
13582
13676
  return r;
13583
13677
  }
13584
- sounded(c, by) {
13678
+ sounded(c, by, at = this.o.now()) {
13585
13679
  if (c.sounded) return;
13586
13680
  c.sounded = true;
13587
- this.o.onMetric({ name: "first_sound", id: c.id, ms: Math.round(this.o.now() - c.at), by });
13681
+ this.o.onMetric({ name: "first_sound", id: c.id, ms: Math.round(at - c.at), by });
13682
+ }
13683
+ /** The turn is over (answered + audible, replaced, the next turn, the call's end): its timeline, once. A turn whose MAIN
13684
+ * never became audible still reports first_word — with no ms and why (cut / replaced / silent) — so every turn has one. */
13685
+ close(c, end) {
13686
+ if (c.closed) return;
13687
+ c.closed = true;
13688
+ c.end = c.worded ? "answered" : end ?? c.end ?? "silent";
13689
+ if (!c.worded) this.o.onMetric({ name: "first_word", id: c.id, by: "main", kind: c.end });
13690
+ const rel = (t) => t === void 0 ? void 0 : Math.round(t - c.at);
13691
+ const m = c.marks, tl = {
13692
+ final: rel(c.finalAt),
13693
+ sent: rel(m.sent),
13694
+ fill: rel(m.fill),
13695
+ ...c.fillKind ? { fillKind: c.fillKind } : {},
13696
+ fillAudio: rel(m.fillAudio),
13697
+ mainFirst: rel(m.mainFirst),
13698
+ mainText: rel(m.mainText),
13699
+ mainAudio: rel(m.mainAudio),
13700
+ lastWord: c.lastWord,
13701
+ end: c.end
13702
+ };
13703
+ for (const k of Object.keys(tl)) if (tl[k] === void 0) delete tl[k];
13704
+ this.o.onMetric({ name: "timeline", id: c.id, text: c.text, timeline: tl });
13588
13705
  }
13589
13706
  async onTool(c, name, args) {
13590
13707
  const lang = this.lang;