@vellumai/assistant 0.11.10-dev.202609091714.3d3ebb3 → 0.11.10-dev.202609091820.7951ec9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.10-dev.202609091714.3d3ebb3",
3
+ "version": "0.11.10-dev.202609091820.7951ec9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -12,6 +12,7 @@
12
12
  * covered by `src/__tests__/conversation-wait-for-idle.test.ts`.
13
13
  */
14
14
  import {
15
+ afterEach,
15
16
  beforeEach,
16
17
  describe,
17
18
  expect,
@@ -35,8 +36,18 @@ mock.module("../../daemon/conversation-store.js", () => ({
35
36
  // production (a BYO provider resolves the profile key through its own column
36
37
  // of the intent matrix), so it is scripted rather than read from a catalog.
37
38
  let pinProfileSupportsVision = true;
39
+ // Vision capability of the conversation's own profile, which an escalated
40
+ // leg is pinned to. Scripted for the same reason.
41
+ let conversationProfileSupportsVision = true;
42
+ // Per-profile answers that outrank the two switches above, for a mix whose
43
+ // arms differ.
44
+ const visionByProfile = new Map<string, boolean>();
38
45
  mock.module("../../plugin-api/vision-support.js", () => ({
39
- doesSupportVision: () => pinProfileSupportsVision,
46
+ doesSupportVision: (profile: string) =>
47
+ visionByProfile.get(profile) ??
48
+ (profile === "latency-optimized"
49
+ ? pinProfileSupportsVision
50
+ : conversationProfileSupportsVision),
40
51
  }));
41
52
 
42
53
  // Attachment hydration for the parked-camera-frame path. Only `att-frame-*`
@@ -126,6 +137,8 @@ mock.module("../../persistence/conversation-crud.js", () => ({
126
137
  }));
127
138
 
128
139
  import { setConfig } from "../../__tests__/helpers/set-config.js";
140
+ import { selectWinningProfile } from "../../config/llm-resolver.js";
141
+ import { getConfig } from "../../config/loader.js";
129
142
  import { ABORT_WATCHDOG_MS } from "../../daemon/abort-watchdog.js";
130
143
  import { VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND } from "../../plugin-api/constants.js";
131
144
  import { assistantEventHub } from "../../runtime/assistant-event-hub.js";
@@ -162,6 +175,9 @@ interface WaitForIdleCall {
162
175
 
163
176
  interface FakeConversation {
164
177
  conversationId: string;
178
+ /** Per-conversation profile pin an escalated leg follows; absent = none. */
179
+ inferenceProfile?: string | null;
180
+ inferenceProfileExpiresAt?: number | null;
165
181
  callSessionId: string | undefined;
166
182
  forcePromptSideEffects: boolean;
167
183
  currentRequestId: string | undefined;
@@ -208,6 +224,10 @@ function makeFakeConversation(opts: {
208
224
  workingDir?: string;
209
225
  /** In-memory history the profile pin reads; undefined models a text-only call. */
210
226
  messages?: Array<{ role: string; content: unknown[] }>;
227
+ /** Per-conversation profile pin; undefined models an unpinned conversation. */
228
+ inferenceProfile?: string;
229
+ /** Expiry of that pin; a past stamp models a lapsed session pin. */
230
+ inferenceProfileExpiresAt?: number;
211
231
  }) {
212
232
  const waitForIdleCalls: WaitForIdleCall[] = [];
213
233
  const confirmationDecisions: Array<{ requestId: string; decision: string }> =
@@ -224,6 +244,12 @@ function makeFakeConversation(opts: {
224
244
  | undefined;
225
245
  const conversation: FakeConversation = {
226
246
  conversationId: "conv-voice-bridge-test",
247
+ ...(opts.inferenceProfile !== undefined
248
+ ? { inferenceProfile: opts.inferenceProfile }
249
+ : {}),
250
+ ...(opts.inferenceProfileExpiresAt !== undefined
251
+ ? { inferenceProfileExpiresAt: opts.inferenceProfileExpiresAt }
252
+ : {}),
227
253
  // The workspace boundary the reach check compares paths against. A real
228
254
  // conversation always has one; without it the approval gate fails closed.
229
255
  workingDir: opts.workingDir ?? "/tmp/workspace-voice-bridge-test",
@@ -2581,19 +2607,19 @@ describe("transcript hygiene (teardown pass)", () => {
2581
2607
  });
2582
2608
  });
2583
2609
 
2584
- describe("startVoiceTurn image-bearing profile pin", () => {
2585
- /** A persisted user message carrying a photo taken mid-call. */
2586
- const PHOTO_HISTORY = [
2587
- { role: "user", content: [{ type: "text", text: "here's a photo:" }] },
2588
- {
2589
- role: "user",
2590
- content: [
2591
- { type: "text", text: "here's a photo:" },
2592
- { type: "image", source: { type: "base64", data: "abc" } },
2593
- ],
2594
- },
2595
- ];
2610
+ /** A persisted user message carrying a photo taken mid-call. */
2611
+ const PHOTO_HISTORY = [
2612
+ { role: "user", content: [{ type: "text", text: "here's a photo:" }] },
2613
+ {
2614
+ role: "user",
2615
+ content: [
2616
+ { type: "text", text: "here's a photo:" },
2617
+ { type: "image", source: { type: "base64", data: "abc" } },
2618
+ ],
2619
+ },
2620
+ ];
2596
2621
 
2622
+ describe("startVoiceTurn image-bearing profile pin", () => {
2597
2623
  beforeEach(() => {
2598
2624
  pinProfileSupportsVision = true;
2599
2625
  });
@@ -2682,3 +2708,188 @@ describe("startVoiceTurn image-bearing profile pin", () => {
2682
2708
  expect(runOptions.overrideProfile).toBe("quality-optimized");
2683
2709
  });
2684
2710
  });
2711
+
2712
+ describe("startVoiceTurn escalated-leg profile pin", () => {
2713
+ beforeEach(() => {
2714
+ pinProfileSupportsVision = true;
2715
+ conversationProfileSupportsVision = true;
2716
+ });
2717
+
2718
+ afterEach(() => {
2719
+ setConfig("llm", {});
2720
+ });
2721
+
2722
+ async function runOptionsFor(opts: {
2723
+ messages?: Array<{ role: string; content: unknown[] }>;
2724
+ inferenceProfile?: string;
2725
+ inferenceProfileExpiresAt?: number;
2726
+ turn?: Record<string, unknown>;
2727
+ }): Promise<Record<string, unknown>> {
2728
+ const fake = makeFakeConversation({
2729
+ processing: false,
2730
+ ...(opts.messages ? { messages: opts.messages } : {}),
2731
+ ...(opts.inferenceProfile
2732
+ ? { inferenceProfile: opts.inferenceProfile }
2733
+ : {}),
2734
+ ...(opts.inferenceProfileExpiresAt !== undefined
2735
+ ? { inferenceProfileExpiresAt: opts.inferenceProfileExpiresAt }
2736
+ : {}),
2737
+ });
2738
+ fakeConversation = fake.conversation;
2739
+ let runOptions: Record<string, unknown> = {};
2740
+ fake.conversation.runAgentLoop = async (...args: unknown[]) => {
2741
+ runOptions = args[2] as Record<string, unknown>;
2742
+ };
2743
+ await startVoiceTurn({
2744
+ ...makeTurnOptions(),
2745
+ routingLeg: "escalated",
2746
+ ...(opts.turn ?? {}),
2747
+ });
2748
+ return runOptions;
2749
+ }
2750
+
2751
+ test("with no chat-model selection the leg keeps the call-site profile", async () => {
2752
+ const runOptions = await runOptionsFor({});
2753
+
2754
+ expect(runOptions.callSite).toBe("callAgent");
2755
+ expect(runOptions.overrideProfile).toBeUndefined();
2756
+ expect(runOptions.forceOverrideProfile).toBeUndefined();
2757
+ });
2758
+
2759
+ test("the workspace chat-model selection pins the leg", async () => {
2760
+ // `callAgent`'s chain never consults `llm.activeProfile`, so without the
2761
+ // pin the hand-off would land on the site's shipped default while the
2762
+ // same conversation's typed turns run on the user's pick.
2763
+ setConfig("llm", { activeProfile: "quality-optimized" });
2764
+
2765
+ const runOptions = await runOptionsFor({});
2766
+
2767
+ expect(runOptions.callSite).toBe("callAgent");
2768
+ expect(runOptions.overrideProfile).toBe("quality-optimized");
2769
+ expect(runOptions.forceOverrideProfile).toBe(true);
2770
+ });
2771
+
2772
+ test("the conversation's own pin wins over the workspace selection", async () => {
2773
+ setConfig("llm", { activeProfile: "quality-optimized" });
2774
+
2775
+ const runOptions = await runOptionsFor({
2776
+ inferenceProfile: "cost-optimized",
2777
+ });
2778
+
2779
+ expect(runOptions.overrideProfile).toBe("cost-optimized");
2780
+ expect(runOptions.forceOverrideProfile).toBe(true);
2781
+ });
2782
+
2783
+ test("a lapsed conversation pin falls back to the workspace selection", async () => {
2784
+ setConfig("llm", { activeProfile: "quality-optimized" });
2785
+
2786
+ const runOptions = await runOptionsFor({
2787
+ inferenceProfile: "cost-optimized",
2788
+ inferenceProfileExpiresAt: Date.now() - 1,
2789
+ });
2790
+
2791
+ expect(runOptions.overrideProfile).toBe("quality-optimized");
2792
+ });
2793
+
2794
+ test("only the escalated leg follows the conversation", async () => {
2795
+ setConfig("llm", { activeProfile: "quality-optimized" });
2796
+
2797
+ const frontDoor = await runOptionsFor({
2798
+ turn: { routingLeg: "front-door" },
2799
+ });
2800
+ expect(frontDoor.callSite).toBe("voiceFrontDoor");
2801
+ expect(frontDoor.overrideProfile).toBeUndefined();
2802
+
2803
+ const unrouted = await runOptionsFor({ turn: { routingLeg: undefined } });
2804
+ expect(unrouted.callSite).toBe("callAgent");
2805
+ expect(unrouted.overrideProfile).toBeUndefined();
2806
+ });
2807
+
2808
+ test("an image stays on a conversation profile whose model takes it", async () => {
2809
+ setConfig("llm", { activeProfile: "quality-optimized" });
2810
+
2811
+ const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
2812
+
2813
+ expect(runOptions.overrideProfile).toBe("quality-optimized");
2814
+ });
2815
+
2816
+ test("an image hands a text-only conversation profile to the image pin", async () => {
2817
+ // A model that rejects the image fails the whole leg, so the image pin
2818
+ // outranks the conversation's choice for this one turn.
2819
+ setConfig("llm", { activeProfile: "quality-optimized" });
2820
+ conversationProfileSupportsVision = false;
2821
+
2822
+ const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
2823
+
2824
+ expect(runOptions.overrideProfile).toBe("latency-optimized");
2825
+ expect(runOptions.forceOverrideProfile).toBe(true);
2826
+ });
2827
+
2828
+ test("an image with no image-capable profile anywhere keeps the conversation profile", async () => {
2829
+ setConfig("llm", { activeProfile: "quality-optimized" });
2830
+ conversationProfileSupportsVision = false;
2831
+ pinProfileSupportsVision = false;
2832
+
2833
+ const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
2834
+
2835
+ expect(runOptions.overrideProfile).toBe("quality-optimized");
2836
+ });
2837
+
2838
+ test("an explicit routing pin wins over the conversation profile", async () => {
2839
+ setConfig("llm", { activeProfile: "quality-optimized" });
2840
+
2841
+ const runOptions = await runOptionsFor({
2842
+ turn: { overrideProfile: "balanced" },
2843
+ });
2844
+
2845
+ expect(runOptions.overrideProfile).toBe("balanced");
2846
+ });
2847
+
2848
+ test("a mix is judged by the arm serving this conversation, not by any arm", async () => {
2849
+ // A mix reads as vision-capable when any arm is, but dispatch expands it
2850
+ // to one arm from the conversation seed. Only that arm's model sees the
2851
+ // image, so only that arm's capability decides whether the image pin
2852
+ // takes over. The pin itself stays the mix's own name, so dispatch lands
2853
+ // on the same arm.
2854
+ const llm = {
2855
+ activeProfile: "voice-mix",
2856
+ profiles: {
2857
+ "voice-mix": {
2858
+ mix: [
2859
+ { profile: "quality-optimized", weight: 1 },
2860
+ { profile: "cost-optimized", weight: 1 },
2861
+ ],
2862
+ },
2863
+ },
2864
+ };
2865
+ setConfig("llm", llm);
2866
+ let chosenArm: string | undefined;
2867
+ selectWinningProfile("mainAgent", getConfig().llm, {
2868
+ selectionSeed: "conv-voice-bridge-test",
2869
+ onMixSelected: ({ chosenProfile }) => {
2870
+ chosenArm = chosenProfile;
2871
+ },
2872
+ });
2873
+ expect(chosenArm).toBeDefined();
2874
+ const otherArm =
2875
+ chosenArm === "quality-optimized"
2876
+ ? "cost-optimized"
2877
+ : "quality-optimized";
2878
+ try {
2879
+ // Only the unchosen arm takes images: judged as "any arm", the mix
2880
+ // would keep the pin off and the image would reach a text-only model.
2881
+ visionByProfile.set(chosenArm!, false);
2882
+ visionByProfile.set(otherArm, true);
2883
+ const textOnlyArm = await runOptionsFor({ messages: PHOTO_HISTORY });
2884
+ expect(textOnlyArm.overrideProfile).toBe("latency-optimized");
2885
+
2886
+ // Only the chosen arm takes images: no pin needed, the mix stands.
2887
+ visionByProfile.set(chosenArm!, true);
2888
+ visionByProfile.set(otherArm, false);
2889
+ const visionArm = await runOptionsFor({ messages: PHOTO_HISTORY });
2890
+ expect(visionArm.overrideProfile).toBe("voice-mix");
2891
+ } finally {
2892
+ visionByProfile.clear();
2893
+ }
2894
+ });
2895
+ });
@@ -22,6 +22,7 @@ import type {
22
22
  TurnChannelContext,
23
23
  TurnInterfaceContext,
24
24
  } from "../channels/types.js";
25
+ import { selectWinningProfile } from "../config/llm-resolver.js";
25
26
  import { getConfig } from "../config/loader.js";
26
27
  import { ABORT_WATCHDOG_MS } from "../daemon/abort-watchdog.js";
27
28
  import { CONVERSATION_BUSY_MESSAGE } from "../daemon/conversation-messaging.js";
@@ -37,7 +38,9 @@ import { resolveAttachmentsForPersist } from "../persistence/attachments-store.j
37
38
  import {
38
39
  deleteMessageById,
39
40
  getMessageById,
41
+ type OverrideProfileFields,
40
42
  recordConversationPersistedSeq,
43
+ resolveOverrideProfile,
41
44
  updateMessageContent,
42
45
  } from "../persistence/conversation-crud.js";
43
46
  import { VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND } from "../plugin-api/constants.js";
@@ -91,6 +94,50 @@ const log = getLogger("voice-session-bridge");
91
94
  */
92
95
  const VOICE_IMAGE_PROFILE = "latency-optimized";
93
96
 
97
+ /**
98
+ * The profile the conversation's own text turns run on, resolved the way a
99
+ * `mainAgent` turn resolves it: the conversation's pinned profile when it
100
+ * carries one, else the workspace chat-model selection (`llm.activeProfile`),
101
+ * else the main agent's call-site pin.
102
+ *
103
+ * The escalated voice leg runs through `callAgent`, whose chain never
104
+ * consults `llm.activeProfile`, so without this the hand-off lands on that
105
+ * site's shipped `balanced` default while the same conversation's typed
106
+ * turns run on whatever the user picked. Pinning the text-turn winner keeps
107
+ * the stronger model the front door escalates to the one the conversation is
108
+ * already using.
109
+ *
110
+ * `profile` is the name to pin (a mix's own name, so dispatch re-expands it
111
+ * to the same arm from the conversation seed); `modelProfile` is the concrete
112
+ * profile whose model actually runs (the chosen arm of a mix), which is what
113
+ * capability checks must judge: a mix reads as vision-capable when any arm
114
+ * is, but only one arm serves this conversation.
115
+ *
116
+ * Null when nothing above named a profile (the winner is the code-owned
117
+ * anchor): the leg then keeps its ordinary call-site resolution, which lands
118
+ * on the same anchor intent and still honors a `callAgent` site pin.
119
+ */
120
+ function conversationProfileForEscalation(
121
+ conversation: OverrideProfileFields & { conversationId: string },
122
+ ): { profile: string; modelProfile: string } | null {
123
+ const overrideProfile = resolveOverrideProfile(conversation);
124
+ let chosenArm: string | undefined;
125
+ const selection = selectWinningProfile("mainAgent", getConfig().llm, {
126
+ ...(overrideProfile != null ? { overrideProfile } : {}),
127
+ selectionSeed: conversation.conversationId,
128
+ onMixSelected: ({ chosenProfile }) => {
129
+ chosenArm = chosenProfile;
130
+ },
131
+ });
132
+ if (selection.source === "default" || selection.profileName == null) {
133
+ return null;
134
+ }
135
+ return {
136
+ profile: selection.profileName,
137
+ modelProfile: chosenArm ?? selection.profileName,
138
+ };
139
+ }
140
+
94
141
  /**
95
142
  * Does this conversation's history carry an image?
96
143
  *
@@ -422,10 +469,10 @@ export interface VoiceTurnOptions {
422
469
  signal?: AbortSignal;
423
470
  /**
424
471
  * Ad-hoc inference-profile override applied to every LLM call this turn
425
- * issues (forwarded to `runAgentLoop` with `forceOverrideProfile`). Used by
426
- * triage-and-escalate voice routing to run the front-door leg on the fast
427
- * profile and the escalated leg on the quality profile. Undefined = the
428
- * call-site default (today's behavior).
472
+ * issues (forwarded to `runAgentLoop` with `forceOverrideProfile`). Wins
473
+ * over the bridge's own pins (the image pin, and the conversation's
474
+ * profile for an escalated leg). Undefined = those pins, else the
475
+ * call-site default.
429
476
  */
430
477
  overrideProfile?: string;
431
478
  /**
@@ -1902,23 +1949,47 @@ export async function startVoiceTurn(
1902
1949
  conversation.toolsDisabledDepth++;
1903
1950
  frontDoorToolsSuppressed = true;
1904
1951
  }
1952
+ // An escalated leg follows the conversation's own model: the front
1953
+ // door hands off to the profile the caller's typed turns already run
1954
+ // on, not to `callAgent`'s shipped default. Null keeps the ordinary
1955
+ // call-site resolution.
1956
+ const conversationProfile =
1957
+ opts.routingLeg === "escalated"
1958
+ ? conversationProfileForEscalation(conversation)
1959
+ : null;
1905
1960
  // Resolved once here rather than inside the options literal below, so
1906
1961
  // the history scan happens once per leg. A front-door leg is skipped:
1907
- // its own call site already resolves to the same profile. The
1908
- // capability check comes before the scan because it is the cheaper of
1909
- // the two and it decides whether the pin is worth anything at all.
1910
- const carriesImage =
1962
+ // its own call site already resolves to the same profile. A
1963
+ // conversation profile whose model takes images needs no image pin
1964
+ // either; one that does not yields to the image pin, since a model
1965
+ // that rejects an image fails the whole leg. The judged profile is the
1966
+ // concrete arm that serves this conversation, not a mix's name. The
1967
+ // capability checks come before the scan because they are the cheaper
1968
+ // of the two and they decide whether the pin is worth anything at all.
1969
+ const needsImagePin =
1911
1970
  opts.routingLeg !== "front-door" &&
1971
+ !(
1972
+ conversationProfile != null &&
1973
+ doesSupportVision(conversationProfile.modelProfile)
1974
+ ) &&
1912
1975
  doesSupportVision(VOICE_IMAGE_PROFILE) &&
1913
1976
  conversationCarriesImage(conversation.getMessages());
1914
- if (carriesImage) {
1977
+ if (needsImagePin) {
1915
1978
  log.info(
1916
1979
  { turnId, routingLeg: opts.routingLeg ?? null },
1917
1980
  "Voice leg carries an image; pinning the image-capable profile",
1918
1981
  );
1982
+ } else if (conversationProfile != null) {
1983
+ log.info(
1984
+ { turnId, profile: conversationProfile.profile },
1985
+ "Escalated voice leg pinned to the conversation's own profile",
1986
+ );
1919
1987
  }
1920
1988
  const profilePin =
1921
- opts.overrideProfile ?? (carriesImage ? VOICE_IMAGE_PROFILE : null);
1989
+ opts.overrideProfile ??
1990
+ (needsImagePin
1991
+ ? VOICE_IMAGE_PROFILE
1992
+ : (conversationProfile?.profile ?? null));
1922
1993
  await conversation.runAgentLoop(persistedContent, messageId, {
1923
1994
  onEvent: (msg: AssistantEvent) => {
1924
1995
  if (msg.type === "assistant_turn_start") {
@@ -1995,15 +2066,17 @@ export async function startVoiceTurn(
1995
2066
  ...(isEscalationContinuation
1996
2067
  ? { messageKind: VOICE_ESCALATION_CONTINUATION_MESSAGE_KIND }
1997
2068
  : {}),
1998
- // Triage-and-escalate routing pins this turn to the fast front-door or
1999
- // strong escalation profile. `forceOverrideProfile` floats it above the
2000
- // callAgent call-site layers (callAgent is not `mainAgent`, so the
2001
- // override would otherwise sit below the call-site profile).
2069
+ // Triage-and-escalate routing pins this turn to the fast front-door
2070
+ // profile or to the conversation's own profile for the escalated
2071
+ // leg. `forceOverrideProfile` floats it above the callAgent call-site
2072
+ // layers (callAgent is not `mainAgent`, so the override would
2073
+ // otherwise sit below the call-site profile).
2002
2074
  //
2003
2075
  // An explicit routing pin wins; failing that, a leg whose history
2004
- // carries an image is pinned to a profile whose model takes one. A
2005
- // front-door leg needs neither: its own call site already resolves
2006
- // there.
2076
+ // carries an image is pinned to a profile whose model takes one;
2077
+ // failing that, an escalated leg is pinned to the conversation's
2078
+ // profile. A front-door leg needs none of these: its own call site
2079
+ // already resolves there.
2007
2080
  ...(profilePin != null
2008
2081
  ? { overrideProfile: profilePin, forceOverrideProfile: true }
2009
2082
  : {}),
@@ -8,10 +8,10 @@
8
8
  * caller is mid-thought — the leg is discarded and listening continues.
9
9
  * - `[1]` ({@link ESCALATE_VERDICT_TOKEN}) followed by ONE short natural
10
10
  * holding phrase: the turn is too tricky — the phrase is spoken (capped
11
- * at a single sentence) while the turn re-runs on the call-site default
12
- * profile, the model an un-routed voice turn would have used. Because
13
- * the holding phrase is spoken, the caller never hears the stronger
14
- * model's think-time as silence.
11
+ * at a single sentence) while the turn re-runs on the conversation's
12
+ * own profile, the model the caller's typed turns already run on.
13
+ * Because the holding phrase is spoken, the caller never hears the
14
+ * stronger model's think-time as silence.
15
15
  * - Anything else: the output IS the answer, streamed straight to TTS
16
16
  * (low first-token latency -> the caller hears audio fast).
17
17
  *
@@ -38,11 +38,13 @@ export { ESCALATE_VERDICT_TOKEN, HOLD_VERDICT_TOKEN };
38
38
 
39
39
  // The fast model fronting every turn is pinned by the `voiceFrontDoor` call
40
40
  // site (see config/call-site-defaults.ts) — no per-turn profile override.
41
- // The escalated leg likewise carries NO override: it runs on the ordinary
42
- // call-agent resolution, i.e. exactly the profile an un-routed voice turn
43
- // would use (balanced for a fresh workspace, or whatever the user pinned).
44
- // That guarantees an escalated answer is never weaker OR stronger than the
45
- // pre-routing behavior, and honors per-user profile choices.
41
+ // The escalated leg is pinned by the bridge to the conversation's own
42
+ // profile: the conversation's pinned profile if it has one, else the
43
+ // workspace chat-model selection — the model the caller's typed turns in the
44
+ // same conversation already run on (see `conversationProfileForEscalation`
45
+ // in voice-session-bridge.ts). A conversation with no such selection keeps
46
+ // the ordinary call-agent resolution. That honors the user's model choice
47
+ // instead of dropping every hand-off onto the call site's shipped default.
46
48
 
47
49
  /**
48
50
  * Which leg of a triaged turn a `startVoiceTurn` call represents. Undefined
@@ -194,8 +194,9 @@ describe("live-voice triage-and-escalate routing", () => {
194
194
  const escalated = starter.mock.calls[1]?.[0];
195
195
  expect(frontDoor?.overrideProfile).toBeUndefined();
196
196
  expect(frontDoor?.routingLeg).toBe("front-door");
197
- // The escalated leg runs on the ordinary call-agent resolution: no
198
- // override either.
197
+ // The session passes no override either: the bridge itself pins the
198
+ // escalated leg to the conversation's own profile (covered in
199
+ // calls/__tests__/voice-session-bridge.test.ts).
199
200
  expect(escalated?.overrideProfile).toBeUndefined();
200
201
  expect(escalated?.routingLeg).toBe("escalated");
201
202
  expect(escalated?.content).toBe(ESCALATION_CONTINUATION_CONTENT);
@@ -5853,11 +5853,12 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
5853
5853
  }
5854
5854
  }
5855
5855
 
5856
- // No overrideProfile: the escalated leg runs on the call-site default —
5857
- // the exact profile an un-routed voice turn would use (see
5858
- // voice-triage-escalate.ts). The bridge phrase the caller just heard is
5859
- // handed along so the escalated continuation rule can quote it and ban
5860
- // a re-announcing echo ("Let me check…" twice in a row).
5856
+ // No overrideProfile here: the bridge pins the escalated leg to the
5857
+ // conversation's own profile, the model the caller's typed turns already
5858
+ // run on (see voice-triage-escalate.ts). The bridge phrase the caller
5859
+ // just heard is handed along so the escalated continuation rule can
5860
+ // quote it and ban a re-announcing echo ("Let me check…" twice in a
5861
+ // row).
5861
5862
  void this.startAssistantLeg(activeTurn, {
5862
5863
  content: ESCALATION_CONTINUATION_CONTENT,
5863
5864
  routingLeg: "escalated",