@koda-sl/baker-bridge 0.108.0-dev.0306d9ad9 → 0.108.0-dev.92a938f8d

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.
Files changed (59) hide show
  1. package/README.md +30 -32
  2. package/dist/hono/agent-session.d.ts +0 -1
  3. package/dist/hono/agent-session.d.ts.map +1 -1
  4. package/dist/hono/agent-session.js +2 -13
  5. package/dist/hono/agent-session.js.map +1 -1
  6. package/dist/hono/agent-session.test.js +6 -43
  7. package/dist/hono/agent-session.test.js.map +1 -1
  8. package/dist/hono/agent-tasks.d.ts.map +1 -1
  9. package/dist/hono/agent-tasks.js +3 -7
  10. package/dist/hono/agent-tasks.js.map +1 -1
  11. package/dist/hono/agent-tasks.test.js +0 -15
  12. package/dist/hono/agent-tasks.test.js.map +1 -1
  13. package/dist/hono/agent.d.ts.map +1 -1
  14. package/dist/hono/agent.js +4 -9
  15. package/dist/hono/agent.js.map +1 -1
  16. package/dist/hono/agent.test.js +9 -9
  17. package/dist/hono/avatar-casting.js +2 -2
  18. package/dist/hono/avatar-casting.js.map +1 -1
  19. package/dist/hono/bridge-job.d.ts +15 -20
  20. package/dist/hono/bridge-job.d.ts.map +1 -1
  21. package/dist/hono/bridge-job.js +138 -110
  22. package/dist/hono/bridge-job.js.map +1 -1
  23. package/dist/hono/bridge-job.test.js +175 -76
  24. package/dist/hono/bridge-job.test.js.map +1 -1
  25. package/dist/hono/connection-input.d.ts +8 -1
  26. package/dist/hono/connection-input.d.ts.map +1 -1
  27. package/dist/hono/connection-input.js +4 -2
  28. package/dist/hono/connection-input.js.map +1 -1
  29. package/dist/hono/convex.d.ts +1 -0
  30. package/dist/hono/convex.d.ts.map +1 -1
  31. package/dist/hono/convex.js +4 -0
  32. package/dist/hono/convex.js.map +1 -1
  33. package/dist/hono/convex.test.js +3 -3
  34. package/dist/hono/mcp-servers.d.ts +0 -2
  35. package/dist/hono/mcp-servers.d.ts.map +1 -1
  36. package/dist/hono/mcp-servers.js +16 -17
  37. package/dist/hono/mcp-servers.js.map +1 -1
  38. package/dist/hono/mcp-servers.test.js +38 -10
  39. package/dist/hono/mcp-servers.test.js.map +1 -1
  40. package/dist/hono/model-capabilities.d.ts.map +1 -1
  41. package/dist/hono/model-capabilities.js +19 -28
  42. package/dist/hono/model-capabilities.js.map +1 -1
  43. package/dist/hono/model-capabilities.test.js +15 -37
  44. package/dist/hono/model-capabilities.test.js.map +1 -1
  45. package/dist/hono/relay.d.ts +2 -1
  46. package/dist/hono/relay.d.ts.map +1 -1
  47. package/dist/hono/relay.js +3 -0
  48. package/dist/hono/relay.js.map +1 -1
  49. package/dist/hono/relay.test.js +22 -6
  50. package/dist/hono/relay.test.js.map +1 -1
  51. package/dist/hono/session-continuity.integration.test.js +4 -4
  52. package/dist/hono/tag-fields.js +1 -1
  53. package/dist/hono/tag-fields.js.map +1 -1
  54. package/dist/hono/tag-input.js +1 -1
  55. package/dist/hono/tag-input.js.map +1 -1
  56. package/dist/hono/turn-events.d.ts +1 -1
  57. package/dist/hono/turn-events.d.ts.map +1 -1
  58. package/dist/hono/turn-events.js.map +1 -1
  59. package/package.json +1 -1
@@ -16,7 +16,7 @@ vi.mock("./runtime-files.ts", async (importOriginal) => {
16
16
  };
17
17
  });
18
18
  import { AVATAR_CASTING_TOOL_NAME } from "./avatar-casting.js";
19
- import { BridgeJobExecutor, createStallDetector, isContextOverflowRefusal, isControlChannelDeny, OUTPUT_STYLE, parsePlanDecision, remapAnswersToQuestionText, } from "./bridge-job.js";
19
+ import { BridgeJobExecutor, createStallDetector, isContextOverflowRefusal, isControlChannelDeny, parsePlanDecision, remapAnswersToQuestionText, } from "./bridge-job.js";
20
20
  import { CONNECTION_INPUT_TOOL_NAME } from "./connection-input.js";
21
21
  import { FLOW_INPUT_TOOL_NAME } from "./flow-input.js";
22
22
  import { bridgeRuntimeFiles } from "./runtime-files.js";
@@ -63,6 +63,7 @@ function recordingRelay() {
63
63
  const agentActivityUpdates = [];
64
64
  const compactionUpdates = [];
65
65
  const reviewDigests = [];
66
+ const retractedEvents = [];
66
67
  const contextUsageUpdates = [];
67
68
  return {
68
69
  assistantEvents,
@@ -71,6 +72,7 @@ function recordingRelay() {
71
72
  compactionUpdates,
72
73
  contextUsageUpdates,
73
74
  reviewDigests,
75
+ retractedEvents,
74
76
  startPreview: () => undefined,
75
77
  previewText: () => undefined,
76
78
  previewTools: () => undefined,
@@ -86,6 +88,10 @@ function recordingRelay() {
86
88
  assistantEvents.push({ target, event, model });
87
89
  return Promise.resolve();
88
90
  },
91
+ retractEvents: (target, eventIds) => {
92
+ retractedEvents.push({ target, eventIds });
93
+ return Promise.resolve();
94
+ },
89
95
  complete: () => Promise.resolve(),
90
96
  inputRequest: () => Promise.resolve(true),
91
97
  inputResolved: () => Promise.resolve(),
@@ -308,8 +314,8 @@ describe("isControlChannelDeny", () => {
308
314
  })).toBe(false);
309
315
  });
310
316
  it("does not match a PreToolUse hook block that quotes the envelope in its explanation", () => {
311
- // Live-run finding: the scaffold's guard-background-commands hook, when it
312
- // still blocked backgrounded subagents, explained why by quoting the failure.
317
+ // Live-run finding: the scaffold's guard-background-commands hook explains
318
+ // WHY it blocks a backgrounded subagent by quoting the failure it prevents.
313
319
  // Its block arrives as an is_error tool_result, so an envelope-substring
314
320
  // match counted it as a deny — three blocked spawns in one turn would have
315
321
  // tripped the breaker and errored a perfectly healthy turn.
@@ -745,13 +751,13 @@ describe("BridgeJobExecutor", () => {
745
751
  const relay = recordingRelay();
746
752
  const executor = createExecutor({
747
753
  relay,
748
- model: "claude-opus-4-6",
754
+ model: "claude-opus-5-5",
749
755
  queryResults: [
750
756
  scriptedMessages(assistantMessage("assistant_1", "Done.", "session_next", {
751
757
  usage: { input_tokens: 4000, cache_read_input_tokens: 96000 },
752
758
  }),
753
759
  // The runtime reports Opus's catalog window as 200k through the
754
- // OpenRouter passthrough — wrong. Opus 4.6 is 1M on OpenRouter.
760
+ // OpenRouter passthrough — wrong. Opus 5.5 is 1M on OpenRouter.
755
761
  resultMessage(0.02, "session_next", {
756
762
  "claude-main": {
757
763
  inputTokens: 4000,
@@ -1163,7 +1169,8 @@ describe("BridgeJobExecutor", () => {
1163
1169
  // Read off a session's own `init` tool list, the CLI offers these to every
1164
1170
  // Session. Each one reaches past what a marketer's chat is for: the worktree
1165
1171
  // pair rearranges the branches Publish owns, `Workflow` fans out a fleet on
1166
- // the organization's OpenRouter key.
1172
+ // the organization's OpenRouter key, and `SendMessage` addresses peers a
1173
+ // single-session Runtime does not have.
1167
1174
  it("removes the tools that reach outside what a Session is", async () => {
1168
1175
  const queryInputs = [];
1169
1176
  const executor = createExecutor({
@@ -1172,12 +1179,10 @@ describe("BridgeJobExecutor", () => {
1172
1179
  });
1173
1180
  await executor.execute({ threadId: "thread_reach", turnId: "turn_reach", prompt: "Build a landing", history: history([]) }, activeControls());
1174
1181
  const disallowed = queryInputs[0]?.options.disallowedTools ?? [];
1175
- expect(disallowed).toEqual(expect.arrayContaining(["EnterWorktree", "ExitWorktree", "Workflow"]));
1182
+ expect(disallowed).toEqual(expect.arrayContaining(["EnterWorktree", "ExitWorktree", "Workflow", "SendMessage"]));
1176
1183
  // `Task` is the subagent spawn itself — the scaffold's own fan-out workers
1177
- // ride it, so blocking multi-agent orchestration must not catch it. And
1178
- // `SendMessage` is how the agent follows up with a subagent it already ran.
1184
+ // ride it, so blocking multi-agent orchestration must not catch it.
1179
1185
  expect(disallowed).not.toContain("Task");
1180
- expect(disallowed).not.toContain("SendMessage");
1181
1186
  });
1182
1187
  it("allowlists the workspace's own skills so the CLI's built-in developer skills stay out of the session", async () => {
1183
1188
  const workspace = await mkdtemp(join(tmpdir(), "baker-job-skills-"));
@@ -1365,42 +1370,31 @@ describe("BridgeJobExecutor", () => {
1365
1370
  await executor.execute({ threadId: "thread_uw", turnId: "turn_uw", prompt: "Hi", history: history([]) }, activeControls());
1366
1371
  expect((queryInputs[0]?.options.settings ?? {})).not.toHaveProperty("autoCompactWindow");
1367
1372
  });
1368
- // The output style is pinned on every turn, so the settings tier now always has
1369
- // something in it — even for a model whose window we cannot resolve and with no
1370
- // chat id to enable the memory directory, which used to leave it empty.
1371
- it("always sends the settings tier, because the output style is pinned every turn", async () => {
1372
- vi.stubEnv("BAKER_CHAT_ID", "");
1373
+ // No output style is sent. `Concise` was pinned against Opus 5's preamble; on
1374
+ // Opus 5.5 it suppressed the one-line progress notes a marketer reads while a
1375
+ // long turn works. The voice rules live in the scaffold CLAUDE.md instead.
1376
+ it("sends no output style, so the default voice and its progress notes survive", async () => {
1373
1377
  const queryInputs = [];
1374
1378
  const executor = createExecutor({
1375
1379
  queryInputs,
1376
- model: "claude-unknown",
1377
- queryResults: [scriptedMessages(resultMessage(0, "session_no_settings"))],
1380
+ queryResults: [scriptedMessages(resultMessage(0, "session_style"))],
1378
1381
  });
1379
- await executor.execute({ threadId: "thread_ns", turnId: "turn_ns", prompt: "Hi", history: history([]) }, activeControls());
1380
- expect(queryInputs[0]?.options.settings).toEqual({ outputStyle: OUTPUT_STYLE });
1381
- });
1382
- // The CLI accepts an unrecognized output-style name silently — a misspelling
1383
- // returns a clean 200 on the default voice, with nothing at runtime to notice
1384
- // it. Verified against the CLI: `{"outputStyle":"NotARealStyle"}` succeeded.
1385
- // So the literal is pinned here rather than left to review.
1386
- it("names the built-in Concise style exactly", () => {
1387
- expect(OUTPUT_STYLE).toBe("Concise");
1382
+ await executor.execute({ threadId: "thread_style", turnId: "turn_style_1", prompt: "Hi", history: history([]) }, activeControls());
1383
+ expect((queryInputs[0]?.options.settings ?? {})).not.toHaveProperty("outputStyle");
1388
1384
  });
1389
- it("runs every turn under the pinned output style", async () => {
1385
+ // Nothing to put in the settings tier (unknown model window, no chat id for the
1386
+ // memory directory) means no `settings` key at all: the SDK forwards even `{}`
1387
+ // to the CLI as a `--settings` flag tier.
1388
+ it("omits the settings tier when nothing in it applies", async () => {
1389
+ vi.stubEnv("BAKER_CHAT_ID", "");
1390
1390
  const queryInputs = [];
1391
1391
  const executor = createExecutor({
1392
1392
  queryInputs,
1393
- queryResults: [
1394
- scriptedMessages(resultMessage(0, "session_style")),
1395
- scriptedMessages(resultMessage(0, "session_style")),
1396
- ],
1393
+ model: "claude-unknown",
1394
+ queryResults: [scriptedMessages(resultMessage(0, "session_no_settings"))],
1397
1395
  });
1398
- await executor.execute({ threadId: "thread_style", turnId: "turn_style_1", prompt: "Hi", history: history([]) }, activeControls());
1399
- await executor.execute({ threadId: "thread_style", turnId: "turn_style_2", prompt: "Again", history: history([]) }, activeControls());
1400
- expect(queryInputs[0]?.options.settings).toMatchObject({ outputStyle: "Concise" });
1401
- // Constant across turns — the style is part of the system prompt, so a
1402
- // per-turn value would re-write the cache prefix on every request.
1403
- expect(queryInputs[1]?.options.settings).toMatchObject({ outputStyle: "Concise" });
1396
+ await executor.execute({ threadId: "thread_ns", turnId: "turn_ns", prompt: "Hi", history: history([]) }, activeControls());
1397
+ expect(queryInputs[0]?.options).not.toHaveProperty("settings");
1404
1398
  });
1405
1399
  it("runs exact publish commands with the prompt passed through verbatim", async () => {
1406
1400
  const queryInputs = [];
@@ -1929,8 +1923,8 @@ describe("BridgeJobExecutor", () => {
1929
1923
  expect(castingRequests[0]).toMatchObject({ avatarHandle: "elena" });
1930
1924
  });
1931
1925
  it("hands the agent's own casting call the face the user picked, instead of a second picker", async () => {
1932
- // The tool tells the agent to call it IMMEDIATELY after `baker avatars create`, so
1933
- // this is the documented path, not an edge. The auto-cast owed for that create used
1926
+ // An agent may still call it right after `baker avatars create` (the tool now says the
1927
+ // picker opens by itself, but the call is tolerated). The auto-cast owed for that create used
1934
1928
  // to run under its own tool-use id and then let the explicit call through unhandled —
1935
1929
  // which returns the tool's `{ status: "declined" }` default. The agent was told the
1936
1930
  // user had rejected all four faces moments after they picked one, and the remedy in
@@ -3096,29 +3090,6 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
3096
3090
  expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.2 });
3097
3091
  expect(endInputAt.count).toBe(4);
3098
3092
  });
3099
- it("tells the session watchdog while a tool call or background task is running", async () => {
3100
- const endInputAt = { count: -1 };
3101
- const executor = createExecutor({
3102
- turnEndSettleMs: 20,
3103
- queryResults: [
3104
- cliLikeQuery(endInputAt, toolUseAssistantMessage("Bash", { command: "baker canvas run x.canvas.json" }), {
3105
- type: "user",
3106
- session_id: "session_bg",
3107
- message: { role: "user", content: [{ type: "tool_result", tool_use_id: "toolu_stream", content: "ok" }] },
3108
- }, taskStarted("task_render"), resultMessage(0.1, "session_bg"), taskSettled("task_render"), resultMessage(0.2, "session_bg")),
3109
- ],
3110
- });
3111
- const reported = [];
3112
- const controls = {
3113
- ...activeControls(),
3114
- setWorkInFlight: (inFlight) => {
3115
- if (reported.at(-1) !== inFlight)
3116
- reported.push(inFlight);
3117
- },
3118
- };
3119
- await executor.execute({ threadId: "thread_bg", turnId: "turn_bg", prompt: "Render", history: history([]) }, controls);
3120
- expect(reported).toEqual([true, false, true, false]);
3121
- });
3122
3093
  it("ends the turn when the last task settles without waking the model into a second result", async () => {
3123
3094
  // A settle is not guaranteed to produce another result. If only a result
3124
3095
  // could arm the end, this shape would strand in `for await` until the
@@ -3134,20 +3105,6 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
3134
3105
  expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.1 });
3135
3106
  expect(endInputAt.count).toBe(3);
3136
3107
  });
3137
- it("bills what a task spent after the last result when it settles without waking the model", async () => {
3138
- // The last result reported $0.10; the task kept working after it. With no
3139
- // second result, only the CLI's own running total can say what it cost.
3140
- const endInputAt = { count: -1 };
3141
- const query = cliLikeQuery(endInputAt, taskStarted("task_silent"), resultMessage(0.1, "session_bg"), taskSettled("task_silent"));
3142
- const executor = createExecutor({
3143
- turnEndSettleMs: 20,
3144
- queryResults: [
3145
- { ...query, usage: () => Promise.resolve({ totalCostUsd: 0.35, modelUsage: {} }) },
3146
- ],
3147
- });
3148
- const outcome = await executor.execute({ threadId: "thread_silent", turnId: "turn_silent", prompt: "Fan out one producer", history: history([]) }, activeControls());
3149
- expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.35 });
3150
- });
3151
3108
  it("re-arms the settle window when a passive system event trails the result", async () => {
3152
3109
  // Passive trailing events (files persisted, session state) must not cancel
3153
3110
  // a scheduled end — only genuine model activity does. A cancel with no
@@ -3217,6 +3174,35 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
3217
3174
  expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.1 });
3218
3175
  expect(endInputAt.count).toBe(3);
3219
3176
  });
3177
+ // The production shape: the model refused, the CLI answered with an error
3178
+ // result and then waited on stdin like after any other result. The error was
3179
+ // no longer thrown, and only a clean result ended input, so the turn sat idle
3180
+ // until the 30-minute session watchdog reported a lost connection.
3181
+ it("ends input after the error result of a refused turn", async () => {
3182
+ const endInputAt = { count: -1 };
3183
+ const refusal = {
3184
+ type: "system",
3185
+ subtype: "model_refusal_no_fallback",
3186
+ original_model: "claude-opus-5-5",
3187
+ api_refusal_category: "reasoning_extraction",
3188
+ session_id: "session_refused",
3189
+ };
3190
+ const erroredResult = {
3191
+ type: "result",
3192
+ subtype: "error_during_execution",
3193
+ is_error: true,
3194
+ errors: ["API Error: Claude Code is unable to respond to this request."],
3195
+ total_cost_usd: 0.01,
3196
+ session_id: "session_refused",
3197
+ };
3198
+ const executor = createExecutor({ queryResults: [cliLikeQuery(endInputAt, refusal, erroredResult)] });
3199
+ const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
3200
+ expect(outcome).toMatchObject({
3201
+ kind: "errored",
3202
+ errors: ["I couldn't help with that request. Try rephrasing what you need."],
3203
+ });
3204
+ expect(endInputAt.count).toBe(2);
3205
+ });
3220
3206
  it("does not hold the turn open for ambient skip_transcript tasks", async () => {
3221
3207
  const endInputAt = { count: -1 };
3222
3208
  const executor = createExecutor({
@@ -3415,4 +3401,117 @@ describe("BridgeJobExecutor review mode", () => {
3415
3401
  expect(outcome).toMatchObject({ kind: "completed", structuredOutput: findings });
3416
3402
  });
3417
3403
  });
3404
+ describe("BridgeJobExecutor model refusals", () => {
3405
+ function refusalNoFallback() {
3406
+ return {
3407
+ type: "system",
3408
+ subtype: "model_refusal_no_fallback",
3409
+ original_model: "claude-opus-5-5",
3410
+ request_id: "req_1",
3411
+ api_refusal_category: "cyber",
3412
+ content: "Claude Code is unable to respond to this request.",
3413
+ uuid: "refusal_notice",
3414
+ session_id: "session_refused",
3415
+ };
3416
+ }
3417
+ it("ends a refused turn with a product-language error instead of a success", async () => {
3418
+ const executor = createExecutor({
3419
+ queryResults: [scriptedMessages(refusalNoFallback(), resultMessage(0.01, "session_refused"))],
3420
+ });
3421
+ const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
3422
+ expect(outcome).toMatchObject({
3423
+ kind: "errored",
3424
+ errors: ["I couldn't help with that request. Try rephrasing what you need."],
3425
+ });
3426
+ });
3427
+ it("records the refusal category for whoever reads the turn afterwards", async () => {
3428
+ const turnEvents = [];
3429
+ const executor = createExecutor({
3430
+ turnEvents,
3431
+ queryResults: [scriptedMessages(refusalNoFallback(), resultMessage(0.01, "session_refused"))],
3432
+ });
3433
+ await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
3434
+ expect(turnEvents).toContainEqual(expect.objectContaining({ event: "model_refusal", detail: expect.objectContaining({ category: "cyber" }) }));
3435
+ });
3436
+ // The CLI renders the refusal as an assistant error frame written for a
3437
+ // terminal user. The turn already ends on a product-language error, so that
3438
+ // frame never reaches the chat.
3439
+ it("does not relay the CLI's refusal frame to the chat", async () => {
3440
+ const relay = recordingRelay();
3441
+ const refusalFrame = {
3442
+ type: "assistant",
3443
+ uuid: "refusal_frame",
3444
+ message: {
3445
+ role: "assistant",
3446
+ content: [{ type: "text", text: "API Error: Claude Code is unable to respond to this request." }],
3447
+ stop_reason: "refusal",
3448
+ },
3449
+ };
3450
+ const executor = createExecutor({
3451
+ relay,
3452
+ queryResults: [scriptedMessages(refusalFrame, refusalNoFallback(), resultMessage(0.01, "session_refused"))],
3453
+ });
3454
+ await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
3455
+ expect(relay.assistantEvents).toEqual([]);
3456
+ });
3457
+ // A refused subagent is reported the same way, but the main agent gets the
3458
+ // failed task back and carries on — the turn is still its to finish.
3459
+ it("lets the turn complete when the main conversation carries on after a refusal", async () => {
3460
+ const executor = createExecutor({
3461
+ queryResults: [
3462
+ scriptedMessages(refusalNoFallback(), assistantMessage("assistant_after", "Done another way.", "session_refused"), resultMessage(0.01, "session_refused")),
3463
+ ],
3464
+ });
3465
+ const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
3466
+ expect(outcome).toMatchObject({ kind: "completed" });
3467
+ });
3468
+ it("keeps the product-language error when the refused turn's result is an error", async () => {
3469
+ const erroredResult = {
3470
+ type: "result",
3471
+ subtype: "error_during_execution",
3472
+ is_error: true,
3473
+ errors: ["API Error: Claude Code is unable to respond to this request."],
3474
+ total_cost_usd: 0.01,
3475
+ session_id: "session_refused",
3476
+ };
3477
+ const executor = createExecutor({ queryResults: [scriptedMessages(refusalNoFallback(), erroredResult)] });
3478
+ const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
3479
+ expect(outcome).toMatchObject({
3480
+ kind: "errored",
3481
+ errors: ["I couldn't help with that request. Try rephrasing what you need."],
3482
+ });
3483
+ });
3484
+ // A fallback model retried the refused request. What the refused leg already
3485
+ // posted is superseded, so it leaves the chat instead of sitting above the
3486
+ // answer that replaced it.
3487
+ it("retracts the messages a refusal fallback superseded", async () => {
3488
+ const relay = recordingRelay();
3489
+ const fallbackNotice = {
3490
+ type: "system",
3491
+ subtype: "model_refusal_fallback",
3492
+ trigger: "refusal",
3493
+ direction: "retry",
3494
+ scope: "session",
3495
+ original_model: "claude-opus-5-5",
3496
+ fallback_model: "claude-sonnet-5",
3497
+ request_id: "req_1",
3498
+ api_refusal_category: "cyber",
3499
+ retracted_message_uuids: ["refused_partial_1", "refused_partial_2"],
3500
+ content: "Retried on a different model.",
3501
+ uuid: "fallback_notice",
3502
+ session_id: "session_fallback",
3503
+ };
3504
+ const executor = createExecutor({
3505
+ relay,
3506
+ queryResults: [
3507
+ scriptedMessages(assistantMessage("refused_partial_1", "Sure, here is", "session_fallback"), fallbackNotice, assistantMessage("fallback_answer", "Here is the plan.", "session_fallback"), resultMessage(0.01, "session_fallback")),
3508
+ ],
3509
+ });
3510
+ const outcome = await executor.execute({ threadId: "thread_fb", turnId: "turn_fb", prompt: "Hi", history: history([]) }, activeControls());
3511
+ expect(relay.retractedEvents).toEqual([
3512
+ { target: { threadId: "thread_fb", turnId: "turn_fb" }, eventIds: ["refused_partial_1", "refused_partial_2"] },
3513
+ ]);
3514
+ expect(outcome).toMatchObject({ kind: "completed" });
3515
+ });
3516
+ });
3418
3517
  //# sourceMappingURL=bridge-job.test.js.map