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

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