@koda-sl/baker-bridge 0.152.0-dev.1a19b1876 → 0.153.0-dev.ad2740332

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 (64) hide show
  1. package/README.md +41 -7
  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 +0 -7
  5. package/dist/hono/agent-session.js.map +1 -1
  6. package/dist/hono/agent-session.test.js +1 -7
  7. package/dist/hono/agent-session.test.js.map +1 -1
  8. package/dist/hono/agent.d.ts.map +1 -1
  9. package/dist/hono/agent.js +7 -9
  10. package/dist/hono/agent.js.map +1 -1
  11. package/dist/hono/bridge-job.d.ts +17 -1
  12. package/dist/hono/bridge-job.d.ts.map +1 -1
  13. package/dist/hono/bridge-job.js +158 -20
  14. package/dist/hono/bridge-job.js.map +1 -1
  15. package/dist/hono/bridge-job.test.js +250 -2
  16. package/dist/hono/bridge-job.test.js.map +1 -1
  17. package/dist/hono/convex.d.ts +2 -1
  18. package/dist/hono/convex.d.ts.map +1 -1
  19. package/dist/hono/convex.js +4 -0
  20. package/dist/hono/convex.js.map +1 -1
  21. package/dist/hono/main-sync.d.ts +7 -0
  22. package/dist/hono/main-sync.d.ts.map +1 -1
  23. package/dist/hono/main-sync.js +57 -5
  24. package/dist/hono/main-sync.js.map +1 -1
  25. package/dist/hono/main-sync.test.js +41 -1
  26. package/dist/hono/main-sync.test.js.map +1 -1
  27. package/dist/hono/relay.d.ts +10 -1
  28. package/dist/hono/relay.d.ts.map +1 -1
  29. package/dist/hono/relay.js +4 -0
  30. package/dist/hono/relay.js.map +1 -1
  31. package/dist/hono/relay.test.js +11 -0
  32. package/dist/hono/relay.test.js.map +1 -1
  33. package/dist/hono/served-model.d.ts +23 -0
  34. package/dist/hono/served-model.d.ts.map +1 -0
  35. package/dist/hono/served-model.js +82 -0
  36. package/dist/hono/served-model.js.map +1 -0
  37. package/dist/hono/served-model.test.d.ts +2 -0
  38. package/dist/hono/served-model.test.d.ts.map +1 -0
  39. package/dist/hono/served-model.test.js +29 -0
  40. package/dist/hono/served-model.test.js.map +1 -0
  41. package/dist/hono/session-continuity.d.ts +30 -0
  42. package/dist/hono/session-continuity.d.ts.map +1 -1
  43. package/dist/hono/session-continuity.integration.test.js +201 -5
  44. package/dist/hono/session-continuity.integration.test.js.map +1 -1
  45. package/dist/hono/session-continuity.js +99 -8
  46. package/dist/hono/session-continuity.js.map +1 -1
  47. package/dist/hono/session-store.d.ts +2 -0
  48. package/dist/hono/session-store.d.ts.map +1 -1
  49. package/dist/hono/session-store.js +6 -0
  50. package/dist/hono/session-store.js.map +1 -1
  51. package/dist/hono/session-usage-baseline.d.ts +1 -0
  52. package/dist/hono/session-usage-baseline.d.ts.map +1 -1
  53. package/dist/hono/session-usage-baseline.js +17 -4
  54. package/dist/hono/session-usage-baseline.js.map +1 -1
  55. package/dist/hono/turn-events.d.ts +1 -1
  56. package/dist/hono/turn-events.d.ts.map +1 -1
  57. package/dist/hono/turn-events.js.map +1 -1
  58. package/dist/review/digest.d.ts +35 -0
  59. package/dist/review/digest.d.ts.map +1 -1
  60. package/dist/review/digest.js +171 -27
  61. package/dist/review/digest.js.map +1 -1
  62. package/dist/review/digest.test.js +130 -1
  63. package/dist/review/digest.test.js.map +1 -1
  64. package/package.json +1 -1
@@ -69,6 +69,7 @@ function recordingRelay() {
69
69
  const compactionUpdates = [];
70
70
  const reviewDigests = [];
71
71
  const retractedEvents = [];
72
+ const toolOutcomeBatches = [];
72
73
  const contextUsageUpdates = [];
73
74
  return {
74
75
  assistantEvents,
@@ -78,6 +79,7 @@ function recordingRelay() {
78
79
  contextUsageUpdates,
79
80
  reviewDigests,
80
81
  retractedEvents,
82
+ toolOutcomeBatches,
81
83
  startPreview: () => undefined,
82
84
  previewText: () => undefined,
83
85
  previewTools: () => undefined,
@@ -99,6 +101,10 @@ function recordingRelay() {
99
101
  retractedEvents.push({ target, eventIds });
100
102
  return Promise.resolve();
101
103
  },
104
+ toolOutcomes: (_target, outcomes) => {
105
+ toolOutcomeBatches.push(outcomes);
106
+ return Promise.resolve();
107
+ },
102
108
  complete: () => Promise.resolve(),
103
109
  inputRequest: () => Promise.resolve(true),
104
110
  inputResolved: () => Promise.resolve(),
@@ -243,6 +249,7 @@ function createExecutor(options) {
243
249
  ...(options.inFlightTurns ? { inFlightTurns: options.inFlightTurns } : {}),
244
250
  ...(options.catchUpWithMain ? { catchUpWithMain: options.catchUpWithMain } : {}),
245
251
  ...(options.keepCliInStep ? { keepCliInStep: options.keepCliInStep } : {}),
252
+ ...(options.holdsNoWork ? { holdsNoWork: options.holdsNoWork } : {}),
246
253
  });
247
254
  }
248
255
  function memoryInFlightTurns(initial) {
@@ -262,6 +269,24 @@ function memoryInFlightTurns(initial) {
262
269
  },
263
270
  };
264
271
  }
272
+ // The CLI's own report of a failed model request, written as an assistant
273
+ // message — the shape production stores (`model: "<synthetic>"`, `error`).
274
+ function apiErrorMessage(uuid, text, error) {
275
+ return {
276
+ type: "assistant",
277
+ uuid,
278
+ session_id: "session_api_error",
279
+ parent_tool_use_id: null,
280
+ error,
281
+ is_api_error_message: true,
282
+ message: {
283
+ role: "assistant",
284
+ model: "<synthetic>",
285
+ content: [{ type: "text", text }],
286
+ stop_reason: "stop_sequence",
287
+ },
288
+ };
289
+ }
265
290
  function resultMessage(costUsd, sessionId, modelUsage) {
266
291
  return {
267
292
  type: "result",
@@ -939,6 +964,40 @@ describe("BridgeJobExecutor", () => {
939
964
  ],
940
965
  });
941
966
  });
967
+ // OpenRouter answers under its own slug. A CLI resuming a transcript that names
968
+ // the slug drops every earlier thinking block from its first request, which
969
+ // breaks the prompt cache right after the system prompt (BAKER-331).
970
+ it("hands the CLI a transcript that names the model the conversation asked for", async () => {
971
+ const configDir = await mkdtemp(join(tmpdir(), "bridge-served-model-"));
972
+ try {
973
+ const transcriptPath = join(configDir, "projects", "-home-user-repo", "session_resume.jsonl");
974
+ await mkdir(join(configDir, "projects", "-home-user-repo"), { recursive: true });
975
+ await writeFile(transcriptPath, `${JSON.stringify({
976
+ type: "assistant",
977
+ requestedModel: "claude-opus-5-5",
978
+ message: { id: "msg_1", model: "anthropic/claude-opus-5.5", content: [{ type: "thinking", thinking: "t" }] },
979
+ })}\n`);
980
+ vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
981
+ const resumedFrom = [];
982
+ const executor = createExecutor({
983
+ loadedBoundary: cleanBoundary("session_resume"),
984
+ queryResults: [
985
+ {
986
+ async *[Symbol.asyncIterator]() {
987
+ await Promise.resolve();
988
+ resumedFrom.push(readFileSync(transcriptPath, "utf8"));
989
+ yield resultMessage(0.2, "session_next");
990
+ },
991
+ },
992
+ ],
993
+ });
994
+ await executor.execute({ threadId: "thread_resume", turnId: "turn_resume", prompt: "Continue", history: history([]) }, activeControls());
995
+ expect(JSON.parse(resumedFrom[0] ?? "{}").message.model).toBe("claude-opus-5-5");
996
+ }
997
+ finally {
998
+ await rm(configDir, { recursive: true, force: true });
999
+ }
1000
+ });
942
1001
  it("publishes without the conversation, and without summarizing it first", async () => {
943
1002
  const queryInputs = [];
944
1003
  const savedBoundaries = [];
@@ -1739,6 +1798,28 @@ describe("BridgeJobExecutor", () => {
1739
1798
  expect(queryInputs).toHaveLength(0);
1740
1799
  expect(told).toBe(0);
1741
1800
  });
1801
+ it("keeps the note for the next turn when the user stops this one before its prompt goes out", async () => {
1802
+ const queryInputs = [];
1803
+ const controls = activeControls();
1804
+ let told = 0;
1805
+ const executor = createExecutor({
1806
+ queryInputs,
1807
+ mcpServersProvider: () => {
1808
+ controls.deactivate();
1809
+ return Promise.resolve(undefined);
1810
+ },
1811
+ catchUpWithMain: () => Promise.resolve({
1812
+ inWorkspace: true,
1813
+ changed: ["src/pages/home/hero.md"],
1814
+ told: () => {
1815
+ told += 1;
1816
+ return Promise.resolve();
1817
+ },
1818
+ }),
1819
+ });
1820
+ await executor.execute({ threadId: "thread_stopped_late", turnId: "turn_stopped_late", prompt: "Cambia el hero", history: history([]) }, controls);
1821
+ expect(told).toBe(0);
1822
+ });
1742
1823
  it("leaves the workspace of a finished Session alone while it is reviewed", async () => {
1743
1824
  let caughtUp = false;
1744
1825
  const executor = createExecutor({
@@ -1891,6 +1972,36 @@ describe("BridgeJobExecutor", () => {
1891
1972
  expect(queryInputs[0]?.options).toMatchObject({ resume: "session_interrupted", forkSession: true });
1892
1973
  expect(relay.contextSources.map((entry) => entry.source)).toEqual(["interrupted_resume"]);
1893
1974
  });
1975
+ it("tells a turn sent again after a restart what another chat published while it was down", async () => {
1976
+ const queryInputs = [];
1977
+ let told = 0;
1978
+ const executor = createExecutor({
1979
+ loadedBoundary: cleanBoundary("session_clean"),
1980
+ queryInputs,
1981
+ inFlightTurns: memoryInFlightTurns({
1982
+ version: 1,
1983
+ turnId: "turn_interrupted_sync",
1984
+ sessionId: "session_interrupted",
1985
+ updatedAtMs: 1,
1986
+ }),
1987
+ catchUpWithMain: () => Promise.resolve({
1988
+ inWorkspace: false,
1989
+ changed: ["src/pages/home/hero.md"],
1990
+ told: () => {
1991
+ told += 1;
1992
+ return Promise.resolve();
1993
+ },
1994
+ }),
1995
+ });
1996
+ await executor.execute({
1997
+ threadId: "thread_interrupted_sync",
1998
+ turnId: "turn_interrupted_sync",
1999
+ prompt: "Rebuild the five pages",
2000
+ }, activeControls());
2001
+ expect(queryInputs[0]?.prompt).toBe(INTERRUPTED_RESUME_PROMPT);
2002
+ expect(queryInputs[0]?.reminder).toContain("- src/pages/home/hero.md");
2003
+ expect(told).toBe(1);
2004
+ });
1894
2005
  it("runs the turn the ordinary way when the interrupted session cannot be resumed", async () => {
1895
2006
  const relay = recordingRelay();
1896
2007
  const queryInputs = [];
@@ -1919,6 +2030,39 @@ describe("BridgeJobExecutor", () => {
1919
2030
  expect(clearCalls).toEqual([]);
1920
2031
  expect(relay.contextSources.map((entry) => entry.source)).toEqual(["interrupted_resume", "local_resume"]);
1921
2032
  });
2033
+ it("keeps the chat's conversation when an interrupted publish that cannot be resumed is run again", async () => {
2034
+ const executor = createExecutor({
2035
+ loadedBoundary: cleanBoundary("session_clean"),
2036
+ inFlightTurns: memoryInFlightTurns({
2037
+ version: 1,
2038
+ turnId: "turn_publish",
2039
+ sessionId: "session_never_written",
2040
+ updatedAtMs: 1,
2041
+ }),
2042
+ queryResults: [
2043
+ scriptedError(new Error("No conversation found with session ID: session_never_written")),
2044
+ scriptedMessages(assistantMessage("assistant_published", "Published", "session_publish_only"), resultMessage(0.2, "session_publish_only")),
2045
+ ],
2046
+ });
2047
+ const outcome = await executor.execute({ threadId: "thread_publish", turnId: "turn_publish", prompt: "/publish" }, activeControls());
2048
+ expect(outcome).toMatchObject({ kind: "completed", cleanBoundary: undefined });
2049
+ });
2050
+ it("tells Convex a turn that left the workspace with no work of its own", async () => {
2051
+ const executor = createExecutor({ holdsNoWork: async () => true });
2052
+ const outcome = await executor.execute({ threadId: "thread_run", turnId: "turn_run", prompt: "Post the daily report to Slack. Change nothing." }, activeControls());
2053
+ expect(outcome).toMatchObject({ kind: "completed", workspaceUnchanged: true });
2054
+ });
2055
+ it("claims nothing about the workspace after a publish turn or one that left work", async () => {
2056
+ const publish = createExecutor({
2057
+ holdsNoWork: async () => true,
2058
+ queryResults: [scriptedMessages(resultMessage(0.1, "session_publish_only"))],
2059
+ });
2060
+ const edited = createExecutor({ holdsNoWork: async () => false });
2061
+ const published = await publish.execute({ threadId: "thread_publish", turnId: "turn_publish", prompt: "/publish" }, activeControls());
2062
+ const worked = await edited.execute({ threadId: "thread_run", turnId: "turn_run", prompt: "Rewrite the hero" }, activeControls());
2063
+ expect(published).not.toHaveProperty("workspaceUnchanged");
2064
+ expect(worked).not.toHaveProperty("workspaceUnchanged");
2065
+ });
1922
2066
  it("keeps the running turn's session on record until the turn ends", async () => {
1923
2067
  const recorded = [];
1924
2068
  const inFlightTurns = memoryInFlightTurns();
@@ -2199,7 +2343,7 @@ describe("BridgeJobExecutor", () => {
2199
2343
  expect(savedBoundaries).toEqual([]);
2200
2344
  expect(clearCalls).toContainEqual({ threadId: "thread_result_error", reason: "result_error" });
2201
2345
  });
2202
- it("preserves provider status details when an SDK result error omits errors", async () => {
2346
+ it("reports a provider's server error in product language, not its status line", async () => {
2203
2347
  const clearCalls = [];
2204
2348
  const executor = createExecutor({
2205
2349
  clearCalls,
@@ -2225,10 +2369,86 @@ describe("BridgeJobExecutor", () => {
2225
2369
  kind: "errored",
2226
2370
  costUsd: 0.17,
2227
2371
  model: "claude-test",
2228
- errors: ["Claude Code returned an error result: API Error: 500 Internal Server Error; apiErrorStatus 500"],
2372
+ errors: [expect.stringContaining("on our side")],
2229
2373
  });
2230
2374
  expect(clearCalls).toContainEqual({ threadId: "thread_provider_result_error", reason: "result_error" });
2231
2375
  });
2376
+ it("never shows the marketer a model provider's billing error, as a reply or as the turn's error", async () => {
2377
+ const providerError = "API Error: 402 Insufficient credits. Add more using https://openrouter.ai/settings/credits";
2378
+ const relay = recordingRelay();
2379
+ const executor = createExecutor({
2380
+ relay,
2381
+ queryResults: [
2382
+ scriptedMessages(apiErrorMessage("assistant_402", providerError, "unknown"), {
2383
+ type: "result",
2384
+ subtype: "success",
2385
+ is_error: true,
2386
+ result: providerError,
2387
+ api_error_status: 402,
2388
+ total_cost_usd: 0,
2389
+ session_id: "session_402",
2390
+ }),
2391
+ ],
2392
+ });
2393
+ const outcome = await executor.execute({ threadId: "thread_402", turnId: "turn_402", prompt: "Build the creatives", history: history([]) }, activeControls());
2394
+ const shown = JSON.stringify([relay.assistantEvents, outcome.kind === "errored" ? outcome.errors : []]);
2395
+ expect(outcome.kind).toBe("errored");
2396
+ expect(shown).not.toMatch(/API Error|openrouter|402/i);
2397
+ });
2398
+ it("never shows the marketer the provider refusing Baker's own model key", async () => {
2399
+ const executor = createExecutor({
2400
+ queryResults: [
2401
+ scriptedMessages({
2402
+ type: "result",
2403
+ subtype: "success",
2404
+ is_error: true,
2405
+ result: 'API Error: 401 {"error":{"message":"User not found.","code":401}}',
2406
+ api_error_status: 401,
2407
+ total_cost_usd: 0,
2408
+ session_id: "session_revoked_key",
2409
+ }),
2410
+ ],
2411
+ });
2412
+ const outcome = await executor.execute({ threadId: "thread_revoked_key", turnId: "turn_revoked_key", prompt: "Write headlines", history: history([]) }, activeControls());
2413
+ expect(outcome.kind === "errored" ? outcome.errors : []).toEqual([expect.stringContaining("on our side")]);
2414
+ });
2415
+ it("tells the marketer how to fix an image the provider could not read, not to send the same message again", async () => {
2416
+ const imageError = 'API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"Could not process image"}}';
2417
+ const executor = createExecutor({
2418
+ queryResults: [
2419
+ scriptedMessages({
2420
+ type: "result",
2421
+ subtype: "success",
2422
+ is_error: true,
2423
+ result: imageError,
2424
+ api_error_status: 400,
2425
+ total_cost_usd: 0,
2426
+ session_id: "session_bad_image",
2427
+ }),
2428
+ ],
2429
+ });
2430
+ const outcome = await executor.execute({ threadId: "thread_bad_image", turnId: "turn_bad_image", prompt: "Use this photo", history: history([]) }, activeControls());
2431
+ expect(outcome.kind === "errored" ? outcome.errors : []).toEqual([
2432
+ expect.stringContaining("extension lies about its format"),
2433
+ ]);
2434
+ });
2435
+ it("keeps a server error the turn recovered from out of the chat, and in the turn events", async () => {
2436
+ const providerError = "API Error: Server error mid-response. The response above may be incomplete.";
2437
+ const relay = recordingRelay();
2438
+ const turnEvents = [];
2439
+ const executor = createExecutor({
2440
+ relay,
2441
+ turnEvents,
2442
+ queryResults: [
2443
+ scriptedMessages(apiErrorMessage("assistant_server_error", providerError, "server_error"), assistantMessage("assistant_recovered", "Your three headlines are ready.", "session_recovered"), resultMessage(0.3, "session_recovered")),
2444
+ ],
2445
+ });
2446
+ const outcome = await executor.execute({ threadId: "thread_recovered", turnId: "turn_recovered", prompt: "Write headlines", history: history([]) }, activeControls());
2447
+ expect(outcome.kind).toBe("completed");
2448
+ expect(JSON.stringify(relay.assistantEvents)).not.toContain("API Error");
2449
+ expect(JSON.stringify(relay.assistantEvents)).toContain("Your three headlines are ready.");
2450
+ expect(turnEvents).toContainEqual(expect.objectContaining({ event: "api_error", detail: { error: "server_error", text: providerError } }));
2451
+ });
2232
2452
  it("closes SDK queries on error and inactive exits but not on natural clean completion", async () => {
2233
2453
  const naturalCloseCalls = [];
2234
2454
  const naturalExecutor = createExecutor({
@@ -4495,6 +4715,34 @@ describe("BridgeJobExecutor model refusals", () => {
4495
4715
  errors: ["I couldn't help with that request. Try rephrasing what you need."],
4496
4716
  });
4497
4717
  });
4718
+ // Tool results come back in user events, which are not stored as messages, so
4719
+ // their outcome is relayed on its own: a Session rebuilt from history needs it.
4720
+ it("relays how each tool call came out, bounded", async () => {
4721
+ const relay = recordingRelay();
4722
+ const toolResult = {
4723
+ type: "user",
4724
+ message: {
4725
+ role: "user",
4726
+ content: [
4727
+ { type: "tool_result", tool_use_id: "toolu_1", is_error: true, content: "Meta refused the rule" },
4728
+ { type: "tool_result", tool_use_id: "toolu_2", content: [{ type: "text", text: "x".repeat(5000) }] },
4729
+ ],
4730
+ },
4731
+ parent_tool_use_id: null,
4732
+ session_id: "session_tools",
4733
+ };
4734
+ const executor = createExecutor({
4735
+ relay,
4736
+ queryResults: [scriptedMessages(toolResult, resultMessage(0.01, "session_tools"))],
4737
+ });
4738
+ await executor.execute({ threadId: "thread_tools", turnId: "turn_tools", prompt: "Hi", history: history([]) }, activeControls());
4739
+ expect(relay.toolOutcomeBatches).toEqual([
4740
+ [
4741
+ { toolUseId: "toolu_1", isError: true, text: "Meta refused the rule" },
4742
+ { toolUseId: "toolu_2", isError: false, text: "x".repeat(2000) },
4743
+ ],
4744
+ ]);
4745
+ });
4498
4746
  // A fallback model retried the refused request. What the refused leg already
4499
4747
  // posted is superseded, so it leaves the chat instead of sitting above the
4500
4748
  // answer that replaced it.