@koda-sl/baker-bridge 0.108.0-dev.f7c15fa76 → 0.109.0-dev.f28c07bd2

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 (44) hide show
  1. package/README.md +1 -14
  2. package/dist/hono/agent.d.ts.map +1 -1
  3. package/dist/hono/agent.js +4 -9
  4. package/dist/hono/agent.js.map +1 -1
  5. package/dist/hono/attachments.d.ts +6 -1
  6. package/dist/hono/attachments.d.ts.map +1 -1
  7. package/dist/hono/attachments.js +9 -4
  8. package/dist/hono/attachments.js.map +1 -1
  9. package/dist/hono/attachments.test.js +5 -0
  10. package/dist/hono/attachments.test.js.map +1 -1
  11. package/dist/hono/baker-ui-server.test.d.ts +2 -0
  12. package/dist/hono/baker-ui-server.test.d.ts.map +1 -0
  13. package/dist/hono/baker-ui-server.test.js +46 -0
  14. package/dist/hono/baker-ui-server.test.js.map +1 -0
  15. package/dist/hono/bridge-job.d.ts +10 -11
  16. package/dist/hono/bridge-job.d.ts.map +1 -1
  17. package/dist/hono/bridge-job.js +38 -39
  18. package/dist/hono/bridge-job.js.map +1 -1
  19. package/dist/hono/bridge-job.test.js +94 -140
  20. package/dist/hono/bridge-job.test.js.map +1 -1
  21. package/dist/hono/session-continuity.d.ts +0 -1
  22. package/dist/hono/session-continuity.d.ts.map +1 -1
  23. package/dist/hono/session-continuity.js +2 -5
  24. package/dist/hono/session-continuity.js.map +1 -1
  25. package/dist/hono/tag-input.d.ts.map +1 -1
  26. package/dist/hono/tag-input.js +4 -0
  27. package/dist/hono/tag-input.js.map +1 -1
  28. package/dist/hono/types.d.ts +7 -6
  29. package/dist/hono/types.d.ts.map +1 -1
  30. package/dist/review/digest.d.ts +7 -1
  31. package/dist/review/digest.d.ts.map +1 -1
  32. package/dist/review/digest.js +80 -16
  33. package/dist/review/digest.js.map +1 -1
  34. package/dist/review/digest.test.js +83 -0
  35. package/dist/review/digest.test.js.map +1 -1
  36. package/package.json +2 -1
  37. package/dist/hono/main-sync.d.ts +0 -41
  38. package/dist/hono/main-sync.d.ts.map +0 -1
  39. package/dist/hono/main-sync.js +0 -390
  40. package/dist/hono/main-sync.js.map +0 -1
  41. package/dist/hono/main-sync.test.d.ts +0 -2
  42. package/dist/hono/main-sync.test.d.ts.map +0 -1
  43. package/dist/hono/main-sync.test.js +0 -266
  44. package/dist/hono/main-sync.test.js.map +0 -1
@@ -229,7 +229,7 @@ function createExecutor(options) {
229
229
  now: options.now,
230
230
  readUsageBaseline: async (sessionId) => options.usageBaselines?.[sessionId],
231
231
  stalledCliExitMs: 0,
232
- ...(options.mainSync ? { mainSync: options.mainSync } : {}),
232
+ prepareAttachments: options.prepareAttachments,
233
233
  });
234
234
  }
235
235
  function resultMessage(costUsd, sessionId, modelUsage) {
@@ -1112,6 +1112,18 @@ describe("BridgeJobExecutor", () => {
1112
1112
  await executor.execute({ threadId: "thread_bedrock", turnId: "turn_bedrock", prompt: "Build a landing", history: history([]) }, activeControls());
1113
1113
  expect(queryInputs[0]?.options.env).toMatchObject({ CLAUDE_CODE_MODEL_CAPABILITIES: "-per_turn_effort" });
1114
1114
  });
1115
+ // Behind the OpenRouter base URL the CLI loads every MCP tool schema up front:
1116
+ // a connected Make toolkit put ~180 schemas (~115k tokens) into every request of
1117
+ // a Session that called two of them. Tool search defers them to their names.
1118
+ it("defers MCP tool schemas behind tool search instead of loading them into every request", async () => {
1119
+ const queryInputs = [];
1120
+ const executor = createExecutor({
1121
+ queryInputs,
1122
+ queryResults: [scriptedMessages(resultMessage(0, "session_tool_search"))],
1123
+ });
1124
+ await executor.execute({ threadId: "thread_tool_search", turnId: "turn_tool_search", prompt: "Check Make", history: history([]) }, activeControls());
1125
+ expect(queryInputs[0]?.options.env).toMatchObject({ ENABLE_TOOL_SEARCH: "true" });
1126
+ });
1115
1127
  it("silences the CLI task reminders and ANSI colour in the agent subprocess", async () => {
1116
1128
  const queryInputs = [];
1117
1129
  const executor = createExecutor({
@@ -1473,174 +1485,87 @@ describe("BridgeJobExecutor", () => {
1473
1485
  expect(queryInputs[0]?.prompt).toBe("/publish");
1474
1486
  expect(queryInputs[0]?.reminder).toBeUndefined();
1475
1487
  });
1476
- it("brings the workspace level with main before the turn and tells the agent what changed, beside the viewing context", async () => {
1488
+ it("keeps the viewing-reference context beside the message when a stale session falls back to rehydration", async () => {
1477
1489
  const queryInputs = [];
1478
- const syncedThreads = [];
1479
- const syncedPublishes = [];
1480
1490
  const executor = createExecutor({
1491
+ loadedBoundary: cleanBoundary("session_stale_viewing"),
1481
1492
  queryInputs,
1482
- queryResults: [scriptedMessages(resultMessage(0, "session_main_moved"))],
1483
- mainSync: {
1484
- beforeTurn: (threadId, published) => {
1485
- syncedThreads.push(threadId);
1486
- syncedPublishes.push(...published.map((entry) => entry.sha));
1487
- return Promise.resolve({
1488
- recovered: "<system-reminder>\nBaker: this workspace now includes work published since your last look.\n</system-reminder>",
1489
- });
1490
- },
1491
- refuseWrite: () => null,
1492
- },
1493
+ queryResults: [
1494
+ scriptedError(new Error("Cannot resume: session not found")),
1495
+ scriptedMessages(resultMessage(0, "session_next_viewing")),
1496
+ ],
1493
1497
  });
1494
1498
  await executor.execute({
1495
- threadId: "thread_main_moved",
1496
- turnId: "turn_main_moved",
1497
- prompt: "change the hero title",
1498
- history: history([]),
1499
+ threadId: "thread_stale_viewing",
1500
+ turnId: "turn_stale_viewing",
1501
+ prompt: "quita el pricing",
1502
+ history: history([{ role: "user", text: "Persisted fact", createdAt: 1 }]),
1499
1503
  currentReference: { type: "landing", slug: "homepage", title: "Homepage" },
1500
- publishedElsewhere: [
1501
- {
1502
- sha: "abc123",
1503
- chatId: "sibling",
1504
- title: "New hero",
1505
- publishedAt: 1,
1506
- paths: ["src/pages/hero/index.astro"],
1507
- },
1508
- ],
1509
1504
  }, activeControls());
1510
- expect(syncedThreads).toEqual(["thread_main_moved"]);
1511
- expect(syncedPublishes).toEqual(["abc123"]);
1512
- expect(queryInputs[0]?.prompt).toBe("change the hero title");
1513
- expect(queryInputs[0]?.reminder).toContain("Baker: this workspace now includes work published");
1514
- expect(queryInputs[0]?.reminder).toContain('landing "Homepage" (slug: homepage)');
1515
- });
1516
- it("does not move the workspace under a publish command", async () => {
1517
- const queryInputs = [];
1518
- const syncedThreads = [];
1519
- const executor = createExecutor({
1520
- queryInputs,
1521
- queryResults: [scriptedMessages(resultMessage(0, "session_publish_main"))],
1522
- mainSync: {
1523
- beforeTurn: (threadId) => {
1524
- syncedThreads.push(threadId);
1525
- return Promise.resolve({ resumed: "<system-reminder>\nBaker: moved\n</system-reminder>" });
1526
- },
1527
- refuseWrite: () => null,
1528
- },
1529
- });
1530
- await executor.execute({ threadId: "thread_publish_main", turnId: "turn_publish_main", prompt: "/publish", history: history([]) }, activeControls());
1531
- expect(syncedThreads).toEqual([]);
1532
- expect(queryInputs[0]?.reminder).toBeUndefined();
1533
- });
1534
- it("tells a resumed session only what is new, since it remembers every earlier note", async () => {
1535
- const queryInputs = [];
1536
- const executor = createExecutor({
1537
- loadedBoundary: cleanBoundary("session_resumed_main"),
1538
- queryInputs,
1539
- queryResults: [scriptedMessages(resultMessage(0, "session_resumed_main_next"))],
1540
- mainSync: {
1541
- beforeTurn: () => Promise.resolve({ recovered: "<system-reminder>\nBaker: standing\n</system-reminder>" }),
1542
- refuseWrite: () => null,
1543
- },
1544
- });
1545
- await executor.execute({ threadId: "thread_resumed_main", turnId: "turn_resumed_main", prompt: "hola", history: history([]) }, activeControls());
1546
- expect(queryInputs[0]?.reminder).toBeUndefined();
1505
+ expect(queryInputs[1]?.prompt).toContain("Current user request\nquita el pricing");
1506
+ expect(queryInputs[1]?.prompt).not.toContain("preview pane");
1507
+ expect(queryInputs[1]?.reminder).toContain('landing "Homepage" (slug: homepage)');
1547
1508
  });
1548
- it("repeats the standing workspace notice when a stale session falls back to rehydration", async () => {
1509
+ // A late answer to a blocking prompt continues a request whose files the
1510
+ // Session received with it. A rebuilt Session only has that request's history,
1511
+ // which names the files but cannot open them, so they are handed over again.
1512
+ it("hands over the continued request's files when the Session is rebuilt", async () => {
1549
1513
  const queryInputs = [];
1550
1514
  const executor = createExecutor({
1551
- loadedBoundary: cleanBoundary("session_stale_main"),
1552
1515
  queryInputs,
1553
- queryResults: [
1554
- scriptedError(new Error("Cannot resume: session not found")),
1555
- scriptedMessages(resultMessage(0, "session_stale_main_next")),
1556
- ],
1557
- mainSync: {
1558
- beforeTurn: () => Promise.resolve({ recovered: "<system-reminder>\nBaker: standing\n</system-reminder>" }),
1559
- refuseWrite: () => null,
1560
- },
1516
+ prepareAttachments: async (attachments, attachedTo) => `\n\nFILES for ${attachedTo}: ${attachments.map((attachment) => attachment.filename).join(", ")}`,
1561
1517
  });
1562
1518
  await executor.execute({
1563
- threadId: "thread_stale_main",
1564
- turnId: "turn_stale_main",
1565
- prompt: "quita el pricing",
1566
- history: history([{ role: "user", text: "Persisted fact", createdAt: 1 }]),
1519
+ threadId: "thread_continued_files",
1520
+ turnId: "turn_continued_files",
1521
+ prompt: "They have now replied: HubSpot",
1522
+ continuedAttachments: [
1523
+ { url: "https://files.example/logo.png", contentType: "image/png", filename: "logo.png" },
1524
+ ],
1525
+ history: history([{ role: "user", text: "Build a page with my logo", createdAt: 1 }]),
1567
1526
  }, activeControls());
1568
- expect(queryInputs[0]?.reminder).toBeUndefined();
1569
- expect(queryInputs[1]?.reminder).toContain("Baker: standing");
1527
+ expect(queryInputs[0]?.prompt).toContain("FILES for continued_request: logo.png");
1570
1528
  });
1571
- it("brings the workspace level with main before a skill command without putting a note in front of it", async () => {
1572
- const workspace = await mkdtemp(join(tmpdir(), "baker-job-skill-main-"));
1573
- await mkdir(join(workspace, ".claude", "skills", "landing"), { recursive: true });
1574
- projectRootMock.value = workspace;
1529
+ it("does not hand the continued request's files over again to a resumed Session", async () => {
1575
1530
  const queryInputs = [];
1576
- const syncs = [];
1577
1531
  const executor = createExecutor({
1532
+ loadedBoundary: cleanBoundary("session_holds_files"),
1578
1533
  queryInputs,
1579
- queryResults: [scriptedMessages(resultMessage(0, "session_skill_main"))],
1580
- mainSync: {
1581
- beforeTurn: (threadId, _published, turn) => {
1582
- syncs.push({ threadId, ...turn });
1583
- return Promise.resolve({});
1584
- },
1585
- refuseWrite: () => null,
1586
- },
1587
- });
1588
- await executor.execute({ threadId: "thread_skill_main", turnId: "turn_skill_main", prompt: "/landing pricing", history: history([]) }, activeControls());
1589
- expect(syncs).toEqual([{ threadId: "thread_skill_main", withNote: false }]);
1590
- expect(queryInputs[0]?.prompt).toBe("/landing pricing");
1591
- expect(queryInputs[0]?.reminder).toBeUndefined();
1592
- });
1593
- it("refuses a write that would create a page another chat already published, with the reason for the agent", async () => {
1594
- let guardResult;
1595
- const executor = new BridgeJobExecutor({
1596
- sdk: {
1597
- query: (input) => ({
1598
- async *[Symbol.asyncIterator]() {
1599
- guardResult = await input.options.canUseTool?.("Write", { file_path: "/workspace/src/pages/sales-v2/index.astro", content: "v2" }, {
1600
- toolUseID: "tool_write",
1601
- requestId: "req_write",
1602
- signal: input.options.abortController?.signal ?? new AbortController().signal,
1603
- });
1604
- yield resultMessage(0, "session_page_clash");
1605
- },
1606
- }),
1607
- },
1608
- model: "claude-test",
1609
- sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
1610
- relay: recordingRelay(),
1611
- mainSync: {
1612
- beforeTurn: () => Promise.resolve({}),
1613
- refuseWrite: (threadId, filePath) => threadId === "thread_page_clash" && filePath.endsWith("src/pages/sales-v2/index.astro")
1614
- ? "`src/pages/sales-v2` was already published by another chat"
1615
- : null,
1616
- },
1617
- });
1618
- await executor.execute({ threadId: "thread_page_clash", turnId: "turn_page_clash", prompt: "build sales v2" }, activeControls());
1619
- expect(guardResult).toEqual({
1620
- behavior: "deny",
1621
- message: "`src/pages/sales-v2` was already published by another chat",
1534
+ prepareAttachments: async (attachments) => `\n\nFILES: ${attachments.map((attachment) => attachment.filename).join(", ")}`,
1622
1535
  });
1536
+ await executor.execute({
1537
+ threadId: "thread_resumed_files",
1538
+ turnId: "turn_resumed_files",
1539
+ prompt: "They have now replied: HubSpot",
1540
+ continuedAttachments: [
1541
+ { url: "https://files.example/logo.png", contentType: "image/png", filename: "logo.png" },
1542
+ ],
1543
+ history: history([{ role: "user", text: "Build a page with my logo", createdAt: 1 }]),
1544
+ }, activeControls());
1545
+ expect(queryInputs[0]?.prompt).toBe("They have now replied: HubSpot");
1623
1546
  });
1624
- it("keeps the viewing-reference context beside the message when a stale session falls back to rehydration", async () => {
1547
+ it("hands the continued request's files over when a stale Session falls back to history", async () => {
1625
1548
  const queryInputs = [];
1626
1549
  const executor = createExecutor({
1627
- loadedBoundary: cleanBoundary("session_stale_viewing"),
1550
+ loadedBoundary: cleanBoundary("session_stale_files"),
1628
1551
  queryInputs,
1629
1552
  queryResults: [
1630
1553
  scriptedError(new Error("Cannot resume: session not found")),
1631
- scriptedMessages(resultMessage(0, "session_next_viewing")),
1554
+ scriptedMessages(resultMessage(0, "session_rebuilt_files")),
1632
1555
  ],
1556
+ prepareAttachments: async (attachments) => `\n\nFILES: ${attachments.map((attachment) => attachment.filename).join(", ")}`,
1633
1557
  });
1634
1558
  await executor.execute({
1635
- threadId: "thread_stale_viewing",
1636
- turnId: "turn_stale_viewing",
1637
- prompt: "quita el pricing",
1638
- history: history([{ role: "user", text: "Persisted fact", createdAt: 1 }]),
1639
- currentReference: { type: "landing", slug: "homepage", title: "Homepage" },
1559
+ threadId: "thread_stale_files",
1560
+ turnId: "turn_stale_files",
1561
+ prompt: "They have now replied: HubSpot",
1562
+ continuedAttachments: [
1563
+ { url: "https://files.example/logo.png", contentType: "image/png", filename: "logo.png" },
1564
+ ],
1565
+ history: history([{ role: "user", text: "Build a page with my logo", createdAt: 1 }]),
1640
1566
  }, activeControls());
1641
- expect(queryInputs[1]?.prompt).toContain("Current user request\nquita el pricing");
1642
- expect(queryInputs[1]?.prompt).not.toContain("preview pane");
1643
- expect(queryInputs[1]?.reminder).toContain('landing "Homepage" (slug: homepage)');
1567
+ expect(queryInputs[0]?.prompt).not.toContain("FILES");
1568
+ expect(queryInputs[1]?.prompt).toContain("Current user request\nThey have now replied: HubSpot\n\nFILES: logo.png");
1644
1569
  });
1645
1570
  it("clears an invalid clean boundary and retries once with rehydrated history", async () => {
1646
1571
  const relay = recordingRelay();
@@ -3323,6 +3248,35 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
3323
3248
  expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.1 });
3324
3249
  expect(endInputAt.count).toBe(3);
3325
3250
  });
3251
+ // The production shape: the model refused, the CLI answered with an error
3252
+ // result and then waited on stdin like after any other result. The error was
3253
+ // no longer thrown, and only a clean result ended input, so the turn sat idle
3254
+ // until the 30-minute session watchdog reported a lost connection.
3255
+ it("ends input after the error result of a refused turn", async () => {
3256
+ const endInputAt = { count: -1 };
3257
+ const refusal = {
3258
+ type: "system",
3259
+ subtype: "model_refusal_no_fallback",
3260
+ original_model: "claude-opus-5-5",
3261
+ api_refusal_category: "reasoning_extraction",
3262
+ session_id: "session_refused",
3263
+ };
3264
+ const erroredResult = {
3265
+ type: "result",
3266
+ subtype: "error_during_execution",
3267
+ is_error: true,
3268
+ errors: ["API Error: Claude Code is unable to respond to this request."],
3269
+ total_cost_usd: 0.01,
3270
+ session_id: "session_refused",
3271
+ };
3272
+ const executor = createExecutor({ queryResults: [cliLikeQuery(endInputAt, refusal, erroredResult)] });
3273
+ const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
3274
+ expect(outcome).toMatchObject({
3275
+ kind: "errored",
3276
+ errors: ["I couldn't help with that request. Try rephrasing what you need."],
3277
+ });
3278
+ expect(endInputAt.count).toBe(2);
3279
+ });
3326
3280
  it("does not hold the turn open for ambient skip_transcript tasks", async () => {
3327
3281
  const endInputAt = { count: -1 };
3328
3282
  const executor = createExecutor({