@koda-sl/baker-bridge 0.108.0 → 0.109.0-dev.fa751dbf1
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/README.md +14 -1
- package/dist/hono/agent.d.ts.map +1 -1
- package/dist/hono/agent.js +9 -4
- package/dist/hono/agent.js.map +1 -1
- package/dist/hono/attachments.d.ts +1 -6
- package/dist/hono/attachments.d.ts.map +1 -1
- package/dist/hono/attachments.js +4 -9
- package/dist/hono/attachments.js.map +1 -1
- package/dist/hono/attachments.test.js +0 -5
- package/dist/hono/attachments.test.js.map +1 -1
- package/dist/hono/bridge-job.d.ts +11 -10
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +45 -30
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +164 -82
- package/dist/hono/bridge-job.test.js.map +1 -1
- package/dist/hono/main-sync.d.ts +46 -0
- package/dist/hono/main-sync.d.ts.map +1 -0
- package/dist/hono/main-sync.js +409 -0
- package/dist/hono/main-sync.js.map +1 -0
- package/dist/hono/main-sync.test.d.ts +2 -0
- package/dist/hono/main-sync.test.d.ts.map +1 -0
- package/dist/hono/main-sync.test.js +276 -0
- package/dist/hono/main-sync.test.js.map +1 -0
- package/dist/hono/session-continuity.d.ts +1 -0
- package/dist/hono/session-continuity.d.ts.map +1 -1
- package/dist/hono/session-continuity.js +5 -2
- package/dist/hono/session-continuity.js.map +1 -1
- package/dist/hono/types.d.ts +6 -7
- package/dist/hono/types.d.ts.map +1 -1
- package/dist/review/digest.d.ts +1 -7
- package/dist/review/digest.d.ts.map +1 -1
- package/dist/review/digest.js +16 -80
- package/dist/review/digest.js.map +1 -1
- package/dist/review/digest.test.js +0 -83
- package/dist/review/digest.test.js.map +1 -1
- package/package.json +1 -2
|
@@ -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
|
-
|
|
232
|
+
...(options.mainSync ? { mainSync: options.mainSync } : {}),
|
|
233
233
|
});
|
|
234
234
|
}
|
|
235
235
|
function resultMessage(costUsd, sessionId, modelUsage) {
|
|
@@ -1473,87 +1473,198 @@ describe("BridgeJobExecutor", () => {
|
|
|
1473
1473
|
expect(queryInputs[0]?.prompt).toBe("/publish");
|
|
1474
1474
|
expect(queryInputs[0]?.reminder).toBeUndefined();
|
|
1475
1475
|
});
|
|
1476
|
-
it("
|
|
1476
|
+
it("brings the workspace level with main before the turn and tells the agent what changed, beside the viewing context", async () => {
|
|
1477
1477
|
const queryInputs = [];
|
|
1478
|
+
const syncedThreads = [];
|
|
1479
|
+
const syncedPublishes = [];
|
|
1478
1480
|
const executor = createExecutor({
|
|
1479
|
-
loadedBoundary: cleanBoundary("session_stale_viewing"),
|
|
1480
1481
|
queryInputs,
|
|
1481
|
-
queryResults: [
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
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
|
+
afterTurn: () => Promise.resolve(),
|
|
1493
|
+
},
|
|
1485
1494
|
});
|
|
1486
1495
|
await executor.execute({
|
|
1487
|
-
threadId: "
|
|
1488
|
-
turnId: "
|
|
1489
|
-
prompt: "
|
|
1490
|
-
history: history([
|
|
1496
|
+
threadId: "thread_main_moved",
|
|
1497
|
+
turnId: "turn_main_moved",
|
|
1498
|
+
prompt: "change the hero title",
|
|
1499
|
+
history: history([]),
|
|
1491
1500
|
currentReference: { type: "landing", slug: "homepage", title: "Homepage" },
|
|
1501
|
+
publishedElsewhere: [
|
|
1502
|
+
{
|
|
1503
|
+
sha: "abc123",
|
|
1504
|
+
chatId: "sibling",
|
|
1505
|
+
title: "New hero",
|
|
1506
|
+
publishedAt: 1,
|
|
1507
|
+
paths: ["src/pages/hero/index.astro"],
|
|
1508
|
+
},
|
|
1509
|
+
],
|
|
1492
1510
|
}, activeControls());
|
|
1493
|
-
expect(
|
|
1494
|
-
expect(
|
|
1495
|
-
expect(queryInputs[
|
|
1511
|
+
expect(syncedThreads).toEqual(["thread_main_moved"]);
|
|
1512
|
+
expect(syncedPublishes).toEqual(["abc123"]);
|
|
1513
|
+
expect(queryInputs[0]?.prompt).toBe("change the hero title");
|
|
1514
|
+
expect(queryInputs[0]?.reminder).toContain("Baker: this workspace now includes work published");
|
|
1515
|
+
expect(queryInputs[0]?.reminder).toContain('landing "Homepage" (slug: homepage)');
|
|
1496
1516
|
});
|
|
1497
|
-
|
|
1498
|
-
// Session received with it. A rebuilt Session only has that request's history,
|
|
1499
|
-
// which names the files but cannot open them, so they are handed over again.
|
|
1500
|
-
it("hands over the continued request's files when the Session is rebuilt", async () => {
|
|
1517
|
+
it("realigns the workspace once the turn ends, after whatever the agent did to it", async () => {
|
|
1501
1518
|
const queryInputs = [];
|
|
1519
|
+
const realignedAfter = [];
|
|
1502
1520
|
const executor = createExecutor({
|
|
1503
1521
|
queryInputs,
|
|
1504
|
-
|
|
1522
|
+
queryResults: [scriptedMessages(resultMessage(0, "session_after_turn"))],
|
|
1523
|
+
mainSync: {
|
|
1524
|
+
beforeTurn: () => Promise.resolve({}),
|
|
1525
|
+
refuseWrite: () => null,
|
|
1526
|
+
afterTurn: () => {
|
|
1527
|
+
realignedAfter.push(queryInputs.length);
|
|
1528
|
+
return Promise.resolve();
|
|
1529
|
+
},
|
|
1530
|
+
},
|
|
1505
1531
|
});
|
|
1506
|
-
await executor.execute({
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1532
|
+
await executor.execute({ threadId: "thread_after_turn", turnId: "turn_after_turn", prompt: "edit the hero", history: history([]) }, activeControls());
|
|
1533
|
+
expect(realignedAfter).toEqual([1]);
|
|
1534
|
+
});
|
|
1535
|
+
it("does not move the workspace under a publish command", async () => {
|
|
1536
|
+
const queryInputs = [];
|
|
1537
|
+
const syncedThreads = [];
|
|
1538
|
+
const executor = createExecutor({
|
|
1539
|
+
queryInputs,
|
|
1540
|
+
queryResults: [scriptedMessages(resultMessage(0, "session_publish_main"))],
|
|
1541
|
+
mainSync: {
|
|
1542
|
+
beforeTurn: (threadId) => {
|
|
1543
|
+
syncedThreads.push(threadId);
|
|
1544
|
+
return Promise.resolve({ resumed: "<system-reminder>\nBaker: moved\n</system-reminder>" });
|
|
1545
|
+
},
|
|
1546
|
+
refuseWrite: () => null,
|
|
1547
|
+
afterTurn: () => Promise.resolve(),
|
|
1548
|
+
},
|
|
1549
|
+
});
|
|
1550
|
+
await executor.execute({ threadId: "thread_publish_main", turnId: "turn_publish_main", prompt: "/publish", history: history([]) }, activeControls());
|
|
1551
|
+
expect(syncedThreads).toEqual([]);
|
|
1552
|
+
expect(queryInputs[0]?.reminder).toBeUndefined();
|
|
1516
1553
|
});
|
|
1517
|
-
it("
|
|
1554
|
+
it("tells a resumed session only what is new, since it remembers every earlier note", async () => {
|
|
1518
1555
|
const queryInputs = [];
|
|
1519
1556
|
const executor = createExecutor({
|
|
1520
|
-
loadedBoundary: cleanBoundary("
|
|
1557
|
+
loadedBoundary: cleanBoundary("session_resumed_main"),
|
|
1521
1558
|
queryInputs,
|
|
1522
|
-
|
|
1559
|
+
queryResults: [scriptedMessages(resultMessage(0, "session_resumed_main_next"))],
|
|
1560
|
+
mainSync: {
|
|
1561
|
+
beforeTurn: () => Promise.resolve({ recovered: "<system-reminder>\nBaker: standing\n</system-reminder>" }),
|
|
1562
|
+
refuseWrite: () => null,
|
|
1563
|
+
afterTurn: () => Promise.resolve(),
|
|
1564
|
+
},
|
|
1523
1565
|
});
|
|
1524
|
-
await executor.execute({
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1566
|
+
await executor.execute({ threadId: "thread_resumed_main", turnId: "turn_resumed_main", prompt: "hola", history: history([]) }, activeControls());
|
|
1567
|
+
expect(queryInputs[0]?.reminder).toBeUndefined();
|
|
1568
|
+
});
|
|
1569
|
+
it("repeats the standing workspace notice when a stale session falls back to rehydration", async () => {
|
|
1570
|
+
const queryInputs = [];
|
|
1571
|
+
const executor = createExecutor({
|
|
1572
|
+
loadedBoundary: cleanBoundary("session_stale_main"),
|
|
1573
|
+
queryInputs,
|
|
1574
|
+
queryResults: [
|
|
1575
|
+
scriptedError(new Error("Cannot resume: session not found")),
|
|
1576
|
+
scriptedMessages(resultMessage(0, "session_stale_main_next")),
|
|
1530
1577
|
],
|
|
1531
|
-
|
|
1578
|
+
mainSync: {
|
|
1579
|
+
beforeTurn: () => Promise.resolve({ recovered: "<system-reminder>\nBaker: standing\n</system-reminder>" }),
|
|
1580
|
+
refuseWrite: () => null,
|
|
1581
|
+
afterTurn: () => Promise.resolve(),
|
|
1582
|
+
},
|
|
1583
|
+
});
|
|
1584
|
+
await executor.execute({
|
|
1585
|
+
threadId: "thread_stale_main",
|
|
1586
|
+
turnId: "turn_stale_main",
|
|
1587
|
+
prompt: "quita el pricing",
|
|
1588
|
+
history: history([{ role: "user", text: "Persisted fact", createdAt: 1 }]),
|
|
1532
1589
|
}, activeControls());
|
|
1533
|
-
expect(queryInputs[0]?.
|
|
1590
|
+
expect(queryInputs[0]?.reminder).toBeUndefined();
|
|
1591
|
+
expect(queryInputs[1]?.reminder).toContain("Baker: standing");
|
|
1534
1592
|
});
|
|
1535
|
-
it("
|
|
1593
|
+
it("brings the workspace level with main before a skill command without putting a note in front of it", async () => {
|
|
1594
|
+
const workspace = await mkdtemp(join(tmpdir(), "baker-job-skill-main-"));
|
|
1595
|
+
await mkdir(join(workspace, ".claude", "skills", "landing"), { recursive: true });
|
|
1596
|
+
projectRootMock.value = workspace;
|
|
1536
1597
|
const queryInputs = [];
|
|
1598
|
+
const syncs = [];
|
|
1537
1599
|
const executor = createExecutor({
|
|
1538
|
-
|
|
1600
|
+
queryInputs,
|
|
1601
|
+
queryResults: [scriptedMessages(resultMessage(0, "session_skill_main"))],
|
|
1602
|
+
mainSync: {
|
|
1603
|
+
beforeTurn: (threadId, _published, turn) => {
|
|
1604
|
+
syncs.push({ threadId, ...turn });
|
|
1605
|
+
return Promise.resolve({});
|
|
1606
|
+
},
|
|
1607
|
+
refuseWrite: () => null,
|
|
1608
|
+
afterTurn: () => Promise.resolve(),
|
|
1609
|
+
},
|
|
1610
|
+
});
|
|
1611
|
+
await executor.execute({ threadId: "thread_skill_main", turnId: "turn_skill_main", prompt: "/landing pricing", history: history([]) }, activeControls());
|
|
1612
|
+
expect(syncs).toEqual([{ threadId: "thread_skill_main", withNote: false }]);
|
|
1613
|
+
expect(queryInputs[0]?.prompt).toBe("/landing pricing");
|
|
1614
|
+
expect(queryInputs[0]?.reminder).toBeUndefined();
|
|
1615
|
+
});
|
|
1616
|
+
it("refuses a write that would create a page another chat already published, with the reason for the agent", async () => {
|
|
1617
|
+
let guardResult;
|
|
1618
|
+
const executor = new BridgeJobExecutor({
|
|
1619
|
+
sdk: {
|
|
1620
|
+
query: (input) => ({
|
|
1621
|
+
async *[Symbol.asyncIterator]() {
|
|
1622
|
+
guardResult = await input.options.canUseTool?.("Write", { file_path: "/workspace/src/pages/sales-v2/index.astro", content: "v2" }, {
|
|
1623
|
+
toolUseID: "tool_write",
|
|
1624
|
+
requestId: "req_write",
|
|
1625
|
+
signal: input.options.abortController?.signal ?? new AbortController().signal,
|
|
1626
|
+
});
|
|
1627
|
+
yield resultMessage(0, "session_page_clash");
|
|
1628
|
+
},
|
|
1629
|
+
}),
|
|
1630
|
+
},
|
|
1631
|
+
model: "claude-test",
|
|
1632
|
+
sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
|
|
1633
|
+
relay: recordingRelay(),
|
|
1634
|
+
mainSync: {
|
|
1635
|
+
beforeTurn: () => Promise.resolve({}),
|
|
1636
|
+
refuseWrite: (threadId, filePath) => threadId === "thread_page_clash" && filePath.endsWith("src/pages/sales-v2/index.astro")
|
|
1637
|
+
? "`src/pages/sales-v2` was already published by another chat"
|
|
1638
|
+
: null,
|
|
1639
|
+
afterTurn: () => Promise.resolve(),
|
|
1640
|
+
},
|
|
1641
|
+
});
|
|
1642
|
+
await executor.execute({ threadId: "thread_page_clash", turnId: "turn_page_clash", prompt: "build sales v2" }, activeControls());
|
|
1643
|
+
expect(guardResult).toEqual({
|
|
1644
|
+
behavior: "deny",
|
|
1645
|
+
message: "`src/pages/sales-v2` was already published by another chat",
|
|
1646
|
+
});
|
|
1647
|
+
});
|
|
1648
|
+
it("keeps the viewing-reference context beside the message when a stale session falls back to rehydration", async () => {
|
|
1649
|
+
const queryInputs = [];
|
|
1650
|
+
const executor = createExecutor({
|
|
1651
|
+
loadedBoundary: cleanBoundary("session_stale_viewing"),
|
|
1539
1652
|
queryInputs,
|
|
1540
1653
|
queryResults: [
|
|
1541
1654
|
scriptedError(new Error("Cannot resume: session not found")),
|
|
1542
|
-
scriptedMessages(resultMessage(0, "
|
|
1655
|
+
scriptedMessages(resultMessage(0, "session_next_viewing")),
|
|
1543
1656
|
],
|
|
1544
|
-
prepareAttachments: async (attachments) => `\n\nFILES: ${attachments.map((attachment) => attachment.filename).join(", ")}`,
|
|
1545
1657
|
});
|
|
1546
1658
|
await executor.execute({
|
|
1547
|
-
threadId: "
|
|
1548
|
-
turnId: "
|
|
1549
|
-
prompt: "
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
],
|
|
1553
|
-
history: history([{ role: "user", text: "Build a page with my logo", createdAt: 1 }]),
|
|
1659
|
+
threadId: "thread_stale_viewing",
|
|
1660
|
+
turnId: "turn_stale_viewing",
|
|
1661
|
+
prompt: "quita el pricing",
|
|
1662
|
+
history: history([{ role: "user", text: "Persisted fact", createdAt: 1 }]),
|
|
1663
|
+
currentReference: { type: "landing", slug: "homepage", title: "Homepage" },
|
|
1554
1664
|
}, activeControls());
|
|
1555
|
-
expect(queryInputs[
|
|
1556
|
-
expect(queryInputs[1]?.prompt).toContain("
|
|
1665
|
+
expect(queryInputs[1]?.prompt).toContain("Current user request\nquita el pricing");
|
|
1666
|
+
expect(queryInputs[1]?.prompt).not.toContain("preview pane");
|
|
1667
|
+
expect(queryInputs[1]?.reminder).toContain('landing "Homepage" (slug: homepage)');
|
|
1557
1668
|
});
|
|
1558
1669
|
it("clears an invalid clean boundary and retries once with rehydrated history", async () => {
|
|
1559
1670
|
const relay = recordingRelay();
|
|
@@ -3236,35 +3347,6 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
|
|
|
3236
3347
|
expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.1 });
|
|
3237
3348
|
expect(endInputAt.count).toBe(3);
|
|
3238
3349
|
});
|
|
3239
|
-
// The production shape: the model refused, the CLI answered with an error
|
|
3240
|
-
// result and then waited on stdin like after any other result. The error was
|
|
3241
|
-
// no longer thrown, and only a clean result ended input, so the turn sat idle
|
|
3242
|
-
// until the 30-minute session watchdog reported a lost connection.
|
|
3243
|
-
it("ends input after the error result of a refused turn", async () => {
|
|
3244
|
-
const endInputAt = { count: -1 };
|
|
3245
|
-
const refusal = {
|
|
3246
|
-
type: "system",
|
|
3247
|
-
subtype: "model_refusal_no_fallback",
|
|
3248
|
-
original_model: "claude-opus-5-5",
|
|
3249
|
-
api_refusal_category: "reasoning_extraction",
|
|
3250
|
-
session_id: "session_refused",
|
|
3251
|
-
};
|
|
3252
|
-
const erroredResult = {
|
|
3253
|
-
type: "result",
|
|
3254
|
-
subtype: "error_during_execution",
|
|
3255
|
-
is_error: true,
|
|
3256
|
-
errors: ["API Error: Claude Code is unable to respond to this request."],
|
|
3257
|
-
total_cost_usd: 0.01,
|
|
3258
|
-
session_id: "session_refused",
|
|
3259
|
-
};
|
|
3260
|
-
const executor = createExecutor({ queryResults: [cliLikeQuery(endInputAt, refusal, erroredResult)] });
|
|
3261
|
-
const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
|
|
3262
|
-
expect(outcome).toMatchObject({
|
|
3263
|
-
kind: "errored",
|
|
3264
|
-
errors: ["I couldn't help with that request. Try rephrasing what you need."],
|
|
3265
|
-
});
|
|
3266
|
-
expect(endInputAt.count).toBe(2);
|
|
3267
|
-
});
|
|
3268
3350
|
it("does not hold the turn open for ambient skip_transcript tasks", async () => {
|
|
3269
3351
|
const endInputAt = { count: -1 };
|
|
3270
3352
|
const executor = createExecutor({
|