@bivy/bivy 0.20.7-staging.2 → 0.20.7-staging.3
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/bin/bivy.mjs +39 -5
- package/bin/codex-app-server-shim.mjs +78 -27
- package/dist/agent-instructions.js +2 -1
- package/dist/agents/claude-code/runtime.js +2 -1
- package/dist/agents/profiles.js +9 -6
- package/dist/apps/gateway.js +91 -3
- package/dist/apps/inspector.js +56 -1
- package/dist/apps/preview-shell.js +197 -5
- package/dist/apps/registry.js +4 -1
- package/dist/apps/scenario-proxy.js +65 -0
- package/dist/apps/scenarios.js +307 -0
- package/dist/apps/service.js +111 -7
- package/dist/apps/styles.css +4 -0
- package/dist/apps/types.js +2 -1
- package/dist/controllers/app-commands.js +3 -2
- package/dist/exec.js +4 -1
- package/dist/node-env.js +6 -0
- package/dist/protocol/client-command-routes.js +1 -1
- package/dist/protocol/client-command-schemas.js +3 -2
- package/dist/runtime/codex-sessions.js +158 -6
- package/dist/runtime/credential-provisioning.js +5 -0
- package/dist/runtime/grok-sessions.js +4 -2
- package/dist/runtime/index.js +26 -19
- package/dist/runtime/inherited-session-env.js +54 -0
- package/dist/runtime/model-ref.js +14 -1
- package/dist/runtime/opencode-auth.js +67 -0
- package/dist/runtime/opencode-preflight.js +10 -1
- package/dist/runtime/protocol.js +35 -1
- package/dist/runtime/tool-call-map.js +67 -3
- package/dist/server.js +49 -15
- package/dist/session/event-log.js +76 -5
- package/dist/session/fork-standup.js +1 -1
- package/dist/session/fork.js +6 -4
- package/dist/session/transcript-normal.js +52 -0
- package/dist/session/transcript-persistence.js +20 -3
- package/dist/session-ref.js +5 -2
- package/dist/worktree.js +25 -3
- package/package.json +1 -1
- package/web/assets/{AutomationsView-CJSYk0ja.js → AutomationsView-DuWxbo5B.js} +1 -1
- package/web/assets/{ChangesCard-Bp88fY6W.js → ChangesCard-BnHllUb0.js} +1 -1
- package/web/assets/{LibraryView-of9wmuzN.js → LibraryView-DpNuD-T7.js} +1 -1
- package/web/assets/{ProviderConnect-DG7SP4l2.js → ProviderConnect-4vjf567A.js} +1 -1
- package/web/assets/{Settings-DGH1U7wj.js → Settings-CHofnxyn.js} +3 -3
- package/web/assets/{Sheet-D-EHCMlj.js → Sheet-DwouI-Tt.js} +1 -1
- package/web/assets/{Terminal-qqysrwl_.js → Terminal-3mNEW7UT.js} +1 -1
- package/web/assets/{VoiceSettings-aeqQkwcZ.js → VoiceSettings-BI4B-zS1.js} +1 -1
- package/web/assets/{abnfDiagram-VCTEODGH-Ct-lmBn5.js → abnfDiagram-VCTEODGH-DPP1TRyh.js} +1 -1
- package/web/assets/architecture-7GRP2DOG-DUHnGffN.js +1 -0
- package/web/assets/{architectureDiagram-5GKGNRK7-CCq716Da.js → architectureDiagram-5GKGNRK7-CvgInp30.js} +1 -1
- package/web/assets/{blockDiagram-I7D4REHJ-Lka9F6ZB.js → blockDiagram-I7D4REHJ-CZAAYK-I.js} +1 -1
- package/web/assets/{c4Diagram-7LVT6UL2-nvhhyzri.js → c4Diagram-7LVT6UL2-BYJlvZiZ.js} +1 -1
- package/web/assets/channel-vvvNMjoB.js +1 -0
- package/web/assets/{chunk-4HAMMTFA-TZ5hhHBz.js → chunk-4HAMMTFA-Cj4-BAMD.js} +1 -1
- package/web/assets/{chunk-75Z2AOVW-IahKw8kM.js → chunk-75Z2AOVW-3eT-aS0t.js} +1 -1
- package/web/assets/{chunk-DU6HZSFF-BlcZUVOy.js → chunk-DU6HZSFF-D_4JI7OY.js} +1 -1
- package/web/assets/{chunk-F27PBJKO-3MGfEgKu.js → chunk-F27PBJKO-5wTG02yk.js} +1 -1
- package/web/assets/{chunk-GMAD6QVW-Be72n678.js → chunk-GMAD6QVW-IliCLU5B.js} +1 -1
- package/web/assets/{chunk-GVQU2GXP-GOVLT6xm.js → chunk-GVQU2GXP-CB3DCzUu.js} +1 -1
- package/web/assets/{chunk-IMKFNOWR-ByOzewrT.js → chunk-IMKFNOWR-Bg3eY-xc.js} +1 -1
- package/web/assets/{chunk-L3NEJ4N5-BCGhHmnW.js → chunk-L3NEJ4N5-BEMaPYT9.js} +1 -1
- package/web/assets/{chunk-OSK3NFVY-CAdliJE_.js → chunk-OSK3NFVY-DMc-FLYk.js} +1 -1
- package/web/assets/{chunk-P2QGCYS3-CM8cpgHa.js → chunk-P2QGCYS3-CaqPruPs.js} +1 -1
- package/web/assets/{chunk-POPQ4Y6H-CXLPhaI1.js → chunk-POPQ4Y6H-3h-I9lV1.js} +1 -1
- package/web/assets/{chunk-PWAF6VOD-BegFrwDe.js → chunk-PWAF6VOD-CPLNMq3b.js} +1 -1
- package/web/assets/{chunk-SHT3W25Y-iXj7Y5RI.js → chunk-SHT3W25Y-ErtwLBXR.js} +1 -1
- package/web/assets/{chunk-SVP7TREG-73pyTBZg.js → chunk-SVP7TREG-GNUjH0zF.js} +1 -1
- package/web/assets/{chunk-TICWLB2K-D5zwuuGI.js → chunk-TICWLB2K-DtbyVpEE.js} +1 -1
- package/web/assets/classDiagram-ZZMXUADV-C3eKmWom.js +1 -0
- package/web/assets/classDiagram-v2-VYDZK3BY-C3eKmWom.js +1 -0
- package/web/assets/{cynefin-OW5HDTMX-D3nRr9S_.js → cynefin-OW5HDTMX-CQt68k-b.js} +1 -1
- package/web/assets/{cynefinDiagram-5FMLGOSQ-C4CKMYMZ.js → cynefinDiagram-5FMLGOSQ-BeEQcUmX.js} +1 -1
- package/web/assets/{dagre-GXQ25YYZ-D-grkZ8X.js → dagre-GXQ25YYZ-CoekQT_h.js} +1 -1
- package/web/assets/{diagram-S7CK7UJ4-BNPS3YQX.js → diagram-S7CK7UJ4-DQBF8H5Z.js} +1 -1
- package/web/assets/{diagram-UQ7AKVKN-Bkp_TyRg.js → diagram-UQ7AKVKN-2Tw1WGQF.js} +1 -1
- package/web/assets/{diagram-VSXAHHWV-Dpxe_6MM.js → diagram-VSXAHHWV-C4Ns08EL.js} +1 -1
- package/web/assets/{diagram-VX7I27RA-DwGBvGn-.js → diagram-VX7I27RA-CMUQSfon.js} +1 -1
- package/web/assets/{diagram-Z3DM3KII-D-KvONMO.js → diagram-Z3DM3KII-Lqvr8Yf6.js} +1 -1
- package/web/assets/{ebnfDiagram-PWID7BFC-Di_D-a-0.js → ebnfDiagram-PWID7BFC-BAYJ-dgh.js} +1 -1
- package/web/assets/{erDiagram-RLTQ6QDP-2pnBfTU9.js → erDiagram-RLTQ6QDP-Dg8VYnq4.js} +1 -1
- package/web/assets/eventmodeling-NTZA5JFV-ChDRNWM1.js +1 -0
- package/web/assets/flowDiagram-HODETNUW-PojVb4l3.js +1 -0
- package/web/assets/{ganttDiagram-EL5Y4UJY-DAkhMTlk.js → ganttDiagram-EL5Y4UJY-ywnauJ7D.js} +1 -1
- package/web/assets/{gitGraph-4MIJSDKK-BQvemX2W.js → gitGraph-4MIJSDKK-bYI8lc-g.js} +1 -1
- package/web/assets/{gitGraphDiagram-WWUBYQGX-Dx7ya_2q.js → gitGraphDiagram-WWUBYQGX-erBDwxX8.js} +1 -1
- package/web/assets/{index-BXPRG7Pj.css → index-Ch1zstZB.css} +1 -1
- package/web/assets/{index-CFX8FhqJ.js → index-DzT1Tnf2.js} +2 -2
- package/web/assets/{info-A6RAGUB7-DaDps2sq.js → info-A6RAGUB7-BZbsPdZJ.js} +1 -1
- package/web/assets/{infoDiagram-27XIBGKW-DBqMvPr2.js → infoDiagram-27XIBGKW-qqTv-8cL.js} +1 -1
- package/web/assets/{ishikawaDiagram-5VMMS53U-DzciF3O_.js → ishikawaDiagram-5VMMS53U-v-e3YR9X.js} +1 -1
- package/web/assets/{journeyDiagram-3NMN7TZE-D9WHgGe3.js → journeyDiagram-3NMN7TZE-D61ZGndw.js} +1 -1
- package/web/assets/{kanban-definition-UXKFOSKX-Diokj8By.js → kanban-definition-UXKFOSKX-NeYWFrdx.js} +1 -1
- package/web/assets/{line-DmBeTXFn.js → line-BNxdvAy9.js} +1 -1
- package/web/assets/{mermaid-parser.core-H_O1CweI.js → mermaid-parser.core-BjJ-qoss.js} +3 -3
- package/web/assets/{mermaid.core-BAni15Ed.js → mermaid.core-DT4QwB_n.js} +4 -4
- package/web/assets/{mindmap-definition-YA3MSWOX-B_tXdzEa.js → mindmap-definition-YA3MSWOX-BxuVgGhr.js} +1 -1
- package/web/assets/{mount-Fdy9Xoxs.js → mount-CK5h6xJ4.js} +7 -7
- package/web/assets/{packet-AYTQ26CC-Dt4bPH7I.js → packet-AYTQ26CC-DIff_vXS.js} +1 -1
- package/web/assets/{pegDiagram-XKGWAZYB-loAbaKUZ.js → pegDiagram-XKGWAZYB-oDNo32c7.js} +1 -1
- package/web/assets/{pie-WAS4IAKB-DEmhJN7M.js → pie-WAS4IAKB-C0cdo_NU.js} +1 -1
- package/web/assets/{pieDiagram-E7YTZNPT-DfQvD2QY.js → pieDiagram-E7YTZNPT-BbThivUY.js} +1 -1
- package/web/assets/{quadrantDiagram-AXDQQJYC-Bhqo2rT-.js → quadrantDiagram-AXDQQJYC-CONfSoMt.js} +1 -1
- package/web/assets/{radar-RG4KPBEZ-BmV5tXie.js → radar-RG4KPBEZ-teYstrbs.js} +1 -1
- package/web/assets/{railroad-74A4TZTK-DoebyExe.js → railroad-74A4TZTK-B4VusJHl.js} +1 -1
- package/web/assets/railroad-abnf-HS5TGJTU-DIF_YU01.js +1 -0
- package/web/assets/railroad-ebnf-LZEXJU2U-BoHyJK5s.js +1 -0
- package/web/assets/railroad-peg-WCYAUIDC-uG1Bcrgs.js +1 -0
- package/web/assets/{railroadDiagram-O6MQD6OU-BhlfIpCW.js → railroadDiagram-O6MQD6OU-CKrYutQ_.js} +1 -1
- package/web/assets/{requirementDiagram-BXWQKSXE-vkxH8U-2.js → requirementDiagram-BXWQKSXE-COt2S2Y4.js} +1 -1
- package/web/assets/{sankeyDiagram-P5KCCOFB-D2iFl0bc.js → sankeyDiagram-P5KCCOFB-D72oeNbf.js} +1 -1
- package/web/assets/{sequenceDiagram-WJ2MYXX4-Ll_m9w0-.js → sequenceDiagram-WJ2MYXX4-DFpVcNjb.js} +1 -1
- package/web/assets/{settingsRoute-C8p10bxH.js → settingsRoute-Clztmzl_.js} +1 -1
- package/web/assets/{stateDiagram-D77RDMKH-B4KWuRz2.js → stateDiagram-D77RDMKH-BKW8l6Ru.js} +1 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-C1t9bUgR.js +1 -0
- package/web/assets/{swimlanes-42K2YHIH-CVXwQL4b.js → swimlanes-42K2YHIH-GRXgsapd.js} +1 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-Dp35pflJ.js +8 -0
- package/web/assets/{timeline-definition-24CTP7MA-C0XuGVkJ.js → timeline-definition-24CTP7MA-C8DELzYs.js} +1 -1
- package/web/assets/{treeView-Q6P3EWNA-BS4edB5m.js → treeView-Q6P3EWNA-C1MDeOPi.js} +1 -1
- package/web/assets/{treemap-WGGIJYW6-BZ54DPA4.js → treemap-WGGIJYW6-CcwgMCe9.js} +1 -1
- package/web/assets/{useStore-DEfLrrMh.js → useStore-CaQamKgO.js} +7 -5
- package/web/assets/{vennDiagram-4TSXK5OY-CAKXCk3I.js → vennDiagram-4TSXK5OY-Co-Nw6M-.js} +1 -1
- package/web/assets/{wardley-WFR3VGLG-DM5IZXxI.js → wardley-WFR3VGLG-FbKVF2Pu.js} +1 -1
- package/web/assets/{wardleyDiagram-VM6X3IG4--WWmycX2.js → wardleyDiagram-VM6X3IG4-Bt7NGXJX.js} +1 -1
- package/web/assets/{xychartDiagram-S5SC5T6Z-D-_SgAQy.js → xychartDiagram-S5SC5T6Z-BsgyT9WB.js} +1 -1
- package/web/index.html +2 -2
- package/web/sw.js +1 -1
- package/web/assets/architecture-7GRP2DOG-CD4t1j2T.js +0 -1
- package/web/assets/channel-Dl0Aa963.js +0 -1
- package/web/assets/classDiagram-ZZMXUADV-BvMwIvP0.js +0 -1
- package/web/assets/classDiagram-v2-VYDZK3BY-BvMwIvP0.js +0 -1
- package/web/assets/eventmodeling-NTZA5JFV-CF677pnK.js +0 -1
- package/web/assets/flowDiagram-HODETNUW-8SiMcM5n.js +0 -1
- package/web/assets/railroad-abnf-HS5TGJTU-C_F3QWV5.js +0 -1
- package/web/assets/railroad-ebnf-LZEXJU2U-BaNfyzXp.js +0 -1
- package/web/assets/railroad-peg-WCYAUIDC-BWyMWwxG.js +0 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-B_Xx-QG6.js +0 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-XYt-UA-z.js +0 -8
|
@@ -190,18 +190,57 @@ export function mapToolCall(toolName, input, context = {}) {
|
|
|
190
190
|
return decorate({ kind: "plan", ...(str(o, "plan", "text", "content", "message") ? { text: str(o, "plan", "text", "content", "message") } : {}) }, toolName, input, context);
|
|
191
191
|
}
|
|
192
192
|
if (inToolSet(DELEGATE, toolName, key)) {
|
|
193
|
-
const label = str(o, "subagent_type", "subagentType", "agent", "agentType", "role", "name");
|
|
193
|
+
const label = str(o, "subagent_type", "subagentType", "agent", "agentType", "role", "name", "task_name", "taskName");
|
|
194
194
|
const description = str(o, "description", "task", "prompt", "instructions", "goal", "message");
|
|
195
195
|
return decorate({ kind: "delegation", ...(label ? { label } : {}), ...(description ? { description } : {}) }, toolName, input, context);
|
|
196
196
|
}
|
|
197
197
|
return undefined;
|
|
198
198
|
}
|
|
199
|
+
/** The text of MCP-style content blocks (`[{type:"text",text}]`), the shape
|
|
200
|
+
* Claude, Pi and most MCP tools return. Undefined when there is no text block,
|
|
201
|
+
* so image-only or unknown payloads keep their JSON form. */
|
|
202
|
+
function blockText(content) {
|
|
203
|
+
if (!Array.isArray(content))
|
|
204
|
+
return undefined;
|
|
205
|
+
const texts = content
|
|
206
|
+
.map((block) => asRecord(block))
|
|
207
|
+
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
208
|
+
.map((block) => block.text);
|
|
209
|
+
return texts.length ? texts.join("\n") : undefined;
|
|
210
|
+
}
|
|
211
|
+
/** Harness frames an agent wraps around a sub-agent's report before handing it
|
|
212
|
+
* to the model. The user should read the report, not the frame. Each row
|
|
213
|
+
* matches the frame's opening (and closing, if any) and how the report inside
|
|
214
|
+
* is indented. */
|
|
215
|
+
const SUBAGENT_REPORT_FRAMES = [
|
|
216
|
+
// Claude Code 2.1.28x: "[Subagent hand-back] … The report follows:\n <report>"
|
|
217
|
+
{ header: /^\[Subagent hand-back\][^\n]*The report follows:\n/, indent: " " },
|
|
218
|
+
// OpenCode's task tool: <task id=… state=…>\n<task_result>\n<report>\n</task_result>\n</task>
|
|
219
|
+
{ header: /^<task\b[^>\n]*>\s*<task_result>\n?/, footer: /\n?<\/task_result>\s*<\/task>\s*$/, indent: "" },
|
|
220
|
+
];
|
|
221
|
+
function unwrapSubagentReport(text) {
|
|
222
|
+
for (const frame of SUBAGENT_REPORT_FRAMES) {
|
|
223
|
+
const match = frame.header.exec(text);
|
|
224
|
+
if (!match)
|
|
225
|
+
continue;
|
|
226
|
+
const inner = text.slice(match[0].length);
|
|
227
|
+
const body = frame.footer ? inner.replace(frame.footer, "") : inner;
|
|
228
|
+
if (!frame.indent)
|
|
229
|
+
return body;
|
|
230
|
+
return body
|
|
231
|
+
.split("\n")
|
|
232
|
+
.map((line) => (line.startsWith(frame.indent) ? line.slice(frame.indent.length) : line))
|
|
233
|
+
.join("\n");
|
|
234
|
+
}
|
|
235
|
+
return text;
|
|
236
|
+
}
|
|
199
237
|
export function mapToolResult(result, isError = false) {
|
|
200
238
|
const o = asRecord(result);
|
|
239
|
+
const structured = asRecord(o.structuredContent ?? o.details);
|
|
201
240
|
const content = o.content ?? o.output ?? o.text ?? o.aggregated_output ?? result;
|
|
202
|
-
const rawText = typeof content === "string" ? content : content == null ? "" : JSON.stringify(boundedToolPayload(content));
|
|
241
|
+
const rawText = unwrapSubagentReport(typeof content === "string" ? content : content == null ? "" : blockText(content) ?? JSON.stringify(boundedToolPayload(content)));
|
|
203
242
|
const text = rawText.length > RAW_LIMIT ? rawText.slice(0, RAW_LIMIT) : rawText;
|
|
204
|
-
const exit = o.exitCode ?? o.exit_code ?? o.code;
|
|
243
|
+
const exit = o.exitCode ?? o.exit_code ?? o.code ?? structured.exitCode ?? structured.exit_code;
|
|
205
244
|
return {
|
|
206
245
|
...(text ? { text } : {}),
|
|
207
246
|
...(typeof exit === "number" ? { exitCode: exit } : {}),
|
|
@@ -209,3 +248,28 @@ export function mapToolResult(result, isError = false) {
|
|
|
209
248
|
...(rawText.length > RAW_LIMIT ? { truncated: true } : {}),
|
|
210
249
|
};
|
|
211
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Fill in `detail` on tool_use blocks that lack it. Live tool events carry a
|
|
253
|
+
* detail, but a transcript reloaded from an agent's own store (a native session
|
|
254
|
+
* taken over, a reopened rollout) has bare blocks, which the client can only
|
|
255
|
+
* label generically. Messages without such blocks are returned as they were.
|
|
256
|
+
*/
|
|
257
|
+
export function withToolDetails(messages, provider = "unknown") {
|
|
258
|
+
return messages.map((message) => {
|
|
259
|
+
const content = message?.content;
|
|
260
|
+
if (!Array.isArray(content))
|
|
261
|
+
return message;
|
|
262
|
+
let changed = false;
|
|
263
|
+
const next = content.map((block) => {
|
|
264
|
+
const b = block;
|
|
265
|
+
if (b?.type !== "tool_use" || b.detail || typeof b.name !== "string")
|
|
266
|
+
return block;
|
|
267
|
+
const detail = mapToolCall(b.name, b.input, { provider, protocol: "unknown" });
|
|
268
|
+
if (!detail)
|
|
269
|
+
return block;
|
|
270
|
+
changed = true;
|
|
271
|
+
return { ...b, detail };
|
|
272
|
+
});
|
|
273
|
+
return changed ? { ...message, content: next } : message;
|
|
274
|
+
});
|
|
275
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
// First: drop agent-session markers the node inherited before anything reads them.
|
|
4
|
+
import "./node-env.js";
|
|
3
5
|
import fs from "node:fs";
|
|
4
6
|
import { createNodeUpdateChecker, managedInstall, updateRegistryUrl } from "./node-update.js";
|
|
5
7
|
import { createRemoteSessionAdmission, RemoteSessionAdmissionError } from "./session/remote-session-admission.js";
|
|
@@ -69,7 +71,7 @@ import { isNativeOAuthProvider, loginModelOAuth } from "./runtime/oauth/model-oa
|
|
|
69
71
|
import { decideOAuthLoginSweep } from "./runtime/oauth/oauth-login-sweep.js";
|
|
70
72
|
import { listCodexSessions, loadCodexTranscript } from "./runtime/codex-sessions.js";
|
|
71
73
|
import { dedupeSessionSummaries } from "./session-identity.js";
|
|
72
|
-
import { listNativePiSessions } from "./agents/pi/integration.js";
|
|
74
|
+
import { listNativePiSessions, piAgentDir } from "./agents/pi/integration.js";
|
|
73
75
|
import { deriveSessionState } from "./session/session-state.js";
|
|
74
76
|
import { resolveStreamingBehavior } from "./session/record.js";
|
|
75
77
|
import { createSessionEngine } from "./session/engine.js";
|
|
@@ -149,7 +151,7 @@ import { MetadataStore } from "./metadata.js";
|
|
|
149
151
|
import { resolveResumeRef, resumeRefFor, storedResumeRef } from "./session-ref.js";
|
|
150
152
|
import { materializeFork } from "./session/fork.js";
|
|
151
153
|
import { applyDirtyPatch } from "./session/fork-dirty.js";
|
|
152
|
-
import { normalizeMessages } from "./session/transcript-normal.js";
|
|
154
|
+
import { lastTranscriptModel, normalizeMessages } from "./session/transcript-normal.js";
|
|
153
155
|
import { buildNativeImportSeedPrompt } from "./session/native-import.js";
|
|
154
156
|
import { EventLog } from "./session/event-log.js";
|
|
155
157
|
import { SessionEventSequencer } from "./session/event-sequencer.js";
|
|
@@ -281,6 +283,9 @@ const piDir = path.join(appDir, "pi");
|
|
|
281
283
|
// the plaintext auth.json its native TUI reads, and its sessions.
|
|
282
284
|
const credsDir = path.join(appDir, "credentials");
|
|
283
285
|
const sessionsDir = path.join(piDir, "sessions");
|
|
286
|
+
// Where a path-based session may be resumed from: Bivy's own Pi sessions, and
|
|
287
|
+
// the native Pi store `bivy run pi` writes to (taken over or opened as chat).
|
|
288
|
+
const resumeRoots = [sessionsDir, path.join(piAgentDir(), "sessions")];
|
|
284
289
|
const intermediateMessagesDir = path.join(appDir, "intermediate-messages");
|
|
285
290
|
const toolActivitiesDir = path.join(appDir, "tool-activities");
|
|
286
291
|
// One append-only per-session log.
|
|
@@ -5189,6 +5194,13 @@ async function reconcileInterruptedSessions() {
|
|
|
5189
5194
|
const record = await resolveOrResumeSession(id, meta.path);
|
|
5190
5195
|
if (!record)
|
|
5191
5196
|
continue; // transcript gone / unresolvable — nothing to resume
|
|
5197
|
+
// "Pick up where you left off" needs a conversation to pick up. A native
|
|
5198
|
+
// terminal session whose agent kept no transcript reopens empty, and the
|
|
5199
|
+
// prompt would only start a confused turn with no context.
|
|
5200
|
+
if (!transcripts.conversationMessages(record).length) {
|
|
5201
|
+
console.log(`[resume] skipped ${id}: no conversation to resume`);
|
|
5202
|
+
continue;
|
|
5203
|
+
}
|
|
5192
5204
|
await runSessionTurn(record, buildInteractiveResumePrompt());
|
|
5193
5205
|
}
|
|
5194
5206
|
catch (error) {
|
|
@@ -6446,9 +6458,14 @@ async function applyRequestedModel(record, model) {
|
|
|
6446
6458
|
if (!model)
|
|
6447
6459
|
return;
|
|
6448
6460
|
try {
|
|
6461
|
+
// Protocol agents only publish their catalog once running. A fork or a new
|
|
6462
|
+
// session asks for its model before that, so warm the catalog first or the
|
|
6463
|
+
// choice is dropped and the agent's default model answers instead.
|
|
6464
|
+
if (!(await record.session.getModels()).length && record.session.warmModels) {
|
|
6465
|
+
await record.session.warmModels().catch(() => { });
|
|
6466
|
+
}
|
|
6449
6467
|
// A provider-less id (CLI --model, `bivy-model:`) binds against the
|
|
6450
|
-
// session's catalog
|
|
6451
|
-
// warm it when the first lookup can't place the id.
|
|
6468
|
+
// session's catalog; warm it when the first lookup can't place the id.
|
|
6452
6469
|
if (!model.provider) {
|
|
6453
6470
|
const resolve = async () => resolveModelRef(await record.session.getModels(), model, record.session.getCurrentModel());
|
|
6454
6471
|
model = await resolve();
|
|
@@ -8399,7 +8416,7 @@ function fastHistoryEvent(msg) {
|
|
|
8399
8416
|
if (!ref)
|
|
8400
8417
|
return null;
|
|
8401
8418
|
const runtimeId = agentFrom(msg) ?? meta?.runtimeId;
|
|
8402
|
-
const file = resolveResumeRef({ ref, resumesByPath: runtimeResumesByPath(runtimeId), sessionsDir });
|
|
8419
|
+
const file = resolveResumeRef({ ref, resumesByPath: runtimeResumesByPath(runtimeId), sessionsDir: resumeRoots });
|
|
8403
8420
|
const rt = getRuntime(runtimeId);
|
|
8404
8421
|
// Prefer the runtime's build-free read (pi/claude-code); otherwise use the
|
|
8405
8422
|
// transcript Bivy persisted itself, so process agents (Codex, generic CLIs)
|
|
@@ -8619,7 +8636,7 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
|
|
|
8619
8636
|
// refs could likewise resume the wrong id. Explicit path refs remain unchanged.
|
|
8620
8637
|
const durableRef = requestedRef ? storedResumeRef(requestedRef, requestedMeta) : undefined;
|
|
8621
8638
|
const requestedSessionFile = durableRef
|
|
8622
|
-
? resolveResumeRef({ ref: durableRef, resumesByPath: runtimeResumesByPath(refRuntimeId), sessionsDir })
|
|
8639
|
+
? resolveResumeRef({ ref: durableRef, resumesByPath: runtimeResumesByPath(refRuntimeId), sessionsDir: resumeRoots })
|
|
8623
8640
|
: undefined;
|
|
8624
8641
|
const storedMeta = requestedSessionFile ? metadata.getSession(requestedSessionFile) : undefined;
|
|
8625
8642
|
const restoredWorktree = requestedSessionFile ? restoredWorktreeFromMetadata(storedMeta) : undefined;
|
|
@@ -8860,6 +8877,13 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
|
|
|
8860
8877
|
// Best-effort; never block session creation.
|
|
8861
8878
|
}
|
|
8862
8879
|
}
|
|
8880
|
+
// A resumed session with no model of its own keeps the one its transcript
|
|
8881
|
+
// last answered with, instead of silently switching to the agent's default.
|
|
8882
|
+
if (requestedSessionFile && !record.session.getCurrentModel()) {
|
|
8883
|
+
const model = lastTranscriptModel(record.session.getMessages());
|
|
8884
|
+
if (model)
|
|
8885
|
+
await record.session.setModel(model.provider, model.id).catch(() => undefined);
|
|
8886
|
+
}
|
|
8863
8887
|
rememberSession(record);
|
|
8864
8888
|
rememberWorkspace(sessionWorkspace);
|
|
8865
8889
|
attachSessionListeners(record);
|
|
@@ -8938,7 +8962,7 @@ async function resolveOrResumeSession(sessionId, sessionPath) {
|
|
|
8938
8962
|
const resumesByPath = runtimeResumesByPath(runtimeId);
|
|
8939
8963
|
let key;
|
|
8940
8964
|
try {
|
|
8941
|
-
key = resolveResumeRef({ ref, resumesByPath, sessionsDir });
|
|
8965
|
+
key = resolveResumeRef({ ref, resumesByPath, sessionsDir: resumeRoots });
|
|
8942
8966
|
}
|
|
8943
8967
|
catch (error) {
|
|
8944
8968
|
// The sessions-dir path-traversal guard rejected this ref (path-based
|
|
@@ -9293,13 +9317,23 @@ const forkStandUp = createForkStandUp({
|
|
|
9293
9317
|
applyDirtyPatch,
|
|
9294
9318
|
gitRepoRoot,
|
|
9295
9319
|
materializeFork,
|
|
9296
|
-
saveForkTranscript: (markdown) => {
|
|
9297
|
-
//
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
const
|
|
9301
|
-
|
|
9302
|
-
|
|
9320
|
+
saveForkTranscript: (markdown, cwd) => {
|
|
9321
|
+
// In the fork's own `.bivy/` (git-excluded, like its worktrees), so the
|
|
9322
|
+
// agent can read it even when sandboxed to its workspace, and it never
|
|
9323
|
+
// shows up as a repo change. The node's data dir is the fallback.
|
|
9324
|
+
const name = `fork-transcript-${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}.md`;
|
|
9325
|
+
for (const dir of [path.join(cwd, ".bivy"), path.join(appDir, "fork-transcripts")]) {
|
|
9326
|
+
try {
|
|
9327
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
9328
|
+
const file = path.join(dir, name);
|
|
9329
|
+
fs.writeFileSync(file, markdown, { mode: 0o600 });
|
|
9330
|
+
return file;
|
|
9331
|
+
}
|
|
9332
|
+
catch {
|
|
9333
|
+
// try the next place
|
|
9334
|
+
}
|
|
9335
|
+
}
|
|
9336
|
+
return undefined;
|
|
9303
9337
|
},
|
|
9304
9338
|
getRuntime,
|
|
9305
9339
|
listRuntimes,
|
|
@@ -10709,7 +10743,7 @@ app.get("/api/multiplexers", async (_req, res) => {
|
|
|
10709
10743
|
// runtimes (`codex` or governed `codex-approvals`), while this endpoint stays a
|
|
10710
10744
|
// safe read-side view plus a `codex resume <id>` terminal handoff.
|
|
10711
10745
|
app.get("/api/codex/sessions", (_req, res) => {
|
|
10712
|
-
const sessions = listCodexSessions().map((s) => ({
|
|
10746
|
+
const sessions = listCodexSessions().filter((s) => !s.parentThreadId).map((s) => ({
|
|
10713
10747
|
id: s.id,
|
|
10714
10748
|
cwd: s.cwd,
|
|
10715
10749
|
createdAt: s.createdAt,
|
|
@@ -180,20 +180,91 @@ export function mergeBases(logged, runtime) {
|
|
|
180
180
|
loggedTexts.set(text, (loggedTexts.get(text) ?? 0) - n);
|
|
181
181
|
return true;
|
|
182
182
|
};
|
|
183
|
+
// The runtime's order, as either a message the merged list already has (its
|
|
184
|
+
// anchor) or one only the runtime had (fresh). Placement below uses it.
|
|
185
|
+
const sequence = [];
|
|
186
|
+
let byAtom; // first logged message holding each atom, built on first use
|
|
187
|
+
const anchorOf = (m) => {
|
|
188
|
+
if (!byAtom) {
|
|
189
|
+
byAtom = new Map();
|
|
190
|
+
for (const other of merged) {
|
|
191
|
+
const theirs = messageContentAtoms(other);
|
|
192
|
+
for (const atom of [...theirs.toolIds, ...theirs.texts])
|
|
193
|
+
if (!byAtom.has(atom))
|
|
194
|
+
byAtom.set(atom, other);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const atoms = messageContentAtoms(m);
|
|
198
|
+
for (const atom of [...atoms.toolIds, ...atoms.texts]) {
|
|
199
|
+
const found = byAtom.get(atom);
|
|
200
|
+
if (found)
|
|
201
|
+
return found;
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
};
|
|
183
205
|
for (const m of runtime) {
|
|
184
206
|
const key = JSON.stringify(m);
|
|
185
|
-
if (known.has(key))
|
|
207
|
+
if (known.has(key)) {
|
|
208
|
+
sequence.push({ anchor: merged[keys.indexOf(key)] });
|
|
186
209
|
continue;
|
|
210
|
+
}
|
|
187
211
|
const identity = identityOf(m);
|
|
188
212
|
if (identity && identities.has(identity)) {
|
|
189
213
|
add(m);
|
|
214
|
+
sequence.push({ anchor: m });
|
|
190
215
|
continue;
|
|
191
216
|
} // streaming refinement of a known turn
|
|
192
|
-
if (coveredByLogged(m))
|
|
193
|
-
|
|
194
|
-
|
|
217
|
+
if (coveredByLogged(m)) { // reopen re-serialization already in the base
|
|
218
|
+
const anchor = anchorOf(m);
|
|
219
|
+
if (anchor)
|
|
220
|
+
sequence.push({ anchor });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (add(m))
|
|
224
|
+
sequence.push({ fresh: m }); // not in the logged base
|
|
225
|
+
}
|
|
226
|
+
return placeFresh(merged, sequence);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Put messages only the runtime had where they happened, not at the end. A
|
|
230
|
+
* reload can carry turns or detail the logged base never stored (a native run
|
|
231
|
+
* taken over as a chat; a Codex rollout's tool calls under a text-only base):
|
|
232
|
+
* appended, they land after turns that came later. Each goes right before the
|
|
233
|
+
* next message the runtime shares with the base, else right after the previous
|
|
234
|
+
* one. With nothing shared to anchor to, its timestamp places it, and a message
|
|
235
|
+
* with neither (or a genuinely new turn) goes last.
|
|
236
|
+
*/
|
|
237
|
+
function placeFresh(merged, sequence) {
|
|
238
|
+
const freshSet = new Set(sequence.flatMap((item) => ("fresh" in item ? [item.fresh] : [])));
|
|
239
|
+
if (!freshSet.size)
|
|
240
|
+
return merged;
|
|
241
|
+
const timeOf = (m) => {
|
|
242
|
+
const t = m.timestamp;
|
|
243
|
+
return typeof t === "number" && Number.isFinite(t) ? t : undefined;
|
|
244
|
+
};
|
|
245
|
+
const placed = merged.filter((m) => !freshSet.has(m));
|
|
246
|
+
let previous; // last anchor seen, or last fresh placed after it
|
|
247
|
+
for (let i = 0; i < sequence.length; i++) {
|
|
248
|
+
const item = sequence[i];
|
|
249
|
+
if ("anchor" in item) {
|
|
250
|
+
previous = item.anchor;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const next = sequence.slice(i + 1).find((later) => "anchor" in later)?.anchor;
|
|
254
|
+
let at;
|
|
255
|
+
if (previous && placed.includes(previous))
|
|
256
|
+
at = placed.indexOf(previous) + 1;
|
|
257
|
+
else if (next && placed.includes(next))
|
|
258
|
+
at = placed.indexOf(next);
|
|
259
|
+
else {
|
|
260
|
+
const t = timeOf(item.fresh);
|
|
261
|
+
const later = t === undefined ? -1 : placed.findIndex((other) => (timeOf(other) ?? -Infinity) > t);
|
|
262
|
+
at = later < 0 ? placed.length : later;
|
|
263
|
+
}
|
|
264
|
+
placed.splice(at, 0, item.fresh);
|
|
265
|
+
previous = item.fresh;
|
|
195
266
|
}
|
|
196
|
-
return
|
|
267
|
+
return placed;
|
|
197
268
|
}
|
|
198
269
|
/** Content-block type carried by a folded outbound attachment. MUST match
|
|
199
270
|
* `AGENT_ATTACHMENT_BLOCK` in packages/core/src/store-render.ts — the client's
|
package/dist/session/fork.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeMessages, buildSeedPrompt, buildForkHistory, renderForkTranscript, } from "./transcript-normal.js";
|
|
1
|
+
import { normalizeMessages, buildSeedPrompt, buildForkHistory, relocateTranscript, renderForkTranscript, } from "./transcript-normal.js";
|
|
2
2
|
/** Cap on the display transcript a bundle carries; larger sessions fall back to
|
|
3
3
|
* showing the destination agent's own (text) replay. */
|
|
4
4
|
export const FORK_DISPLAY_MAX_BYTES = 2 * 1024 * 1024;
|
|
@@ -91,6 +91,8 @@ export function resolveForkFidelity(bundle, targetRuntime) {
|
|
|
91
91
|
export async function materializeFork(opts) {
|
|
92
92
|
const { bundle, targetRuntime, ctx } = opts;
|
|
93
93
|
const intended = resolveForkFidelity(bundle, targetRuntime);
|
|
94
|
+
// The portable history as it reads from the fork's own working directory.
|
|
95
|
+
const normalized = relocateTranscript(bundle.normalized, bundle.record.cwd, ctx.cwd);
|
|
94
96
|
const validImport = (result) => typeof result?.sessionFile === "string" && result.sessionFile.trim().length > 0 &&
|
|
95
97
|
typeof result.id === "string" && result.id.trim().length > 0;
|
|
96
98
|
// Native transport is the preferred same-runtime route, but never a single
|
|
@@ -113,7 +115,7 @@ export async function materializeFork(opts) {
|
|
|
113
115
|
typeof targetRuntime.importHistoryForFork === "function";
|
|
114
116
|
if (canReplay) {
|
|
115
117
|
try {
|
|
116
|
-
const history = buildForkHistory(
|
|
118
|
+
const history = buildForkHistory(normalized);
|
|
117
119
|
if (history.length > 0) {
|
|
118
120
|
const imported = await targetRuntime.importHistoryForFork(history, ctx);
|
|
119
121
|
if (validImport(imported))
|
|
@@ -128,12 +130,12 @@ export async function materializeFork(opts) {
|
|
|
128
130
|
// its tools can read — an app URL is a web page it can't fetch.
|
|
129
131
|
let transcriptFile;
|
|
130
132
|
try {
|
|
131
|
-
transcriptFile = opts.saveTranscript?.(renderForkTranscript(
|
|
133
|
+
transcriptFile = opts.saveTranscript?.(renderForkTranscript(normalized), ctx.cwd);
|
|
132
134
|
}
|
|
133
135
|
catch {
|
|
134
136
|
transcriptFile = undefined;
|
|
135
137
|
}
|
|
136
|
-
const seedPrompt = buildSeedPrompt(
|
|
138
|
+
const seedPrompt = buildSeedPrompt(normalized, {
|
|
137
139
|
targetAgent: targetRuntime.displayName,
|
|
138
140
|
context: { repoSlug: bundle.record.repoSlug, branch: bundle.record.branch, prUrl: bundle.record.prUrl },
|
|
139
141
|
...(transcriptFile ? { transcriptFile } : {}),
|
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import { handoffSeedOf } from "./handoff-seed.js";
|
|
2
2
|
import { applyPlanUpdate, planChecklist, planUpdateOf } from "./plan.js";
|
|
3
|
+
function escapeRegExp(text) {
|
|
4
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The transcript as it reads from a new working directory. A fork runs in its
|
|
8
|
+
* own copy of the workspace, but the history names files at the source's
|
|
9
|
+
* paths, and an agent that follows them edits the original checkout. Rewrite
|
|
10
|
+
* the source directory to the fork's wherever it appears as a path.
|
|
11
|
+
*/
|
|
12
|
+
export function relocateTranscript(transcript, from, to) {
|
|
13
|
+
const source = from?.replace(/\/+$/, "");
|
|
14
|
+
const target = to?.replace(/\/+$/, "");
|
|
15
|
+
if (!source || !target || source === target)
|
|
16
|
+
return transcript;
|
|
17
|
+
const pattern = new RegExp(`${escapeRegExp(source)}(?=$|[/\\s"'\`)\\],:;])`, "g");
|
|
18
|
+
const move = (text) => text.replace(pattern, target);
|
|
19
|
+
return {
|
|
20
|
+
...transcript,
|
|
21
|
+
turns: transcript.turns.map((turn) => ({
|
|
22
|
+
...turn,
|
|
23
|
+
text: move(turn.text),
|
|
24
|
+
...(turn.toolSummary ? { toolSummary: move(turn.toolSummary) } : {}),
|
|
25
|
+
})),
|
|
26
|
+
...(transcript.plan ? { plan: transcript.plan.map((entry) => ({ ...entry, text: move(entry.text) })) } : {}),
|
|
27
|
+
workspace: { from: source, to: target },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** One line telling the new agent where the work lives now. */
|
|
31
|
+
function workspaceNote(workspace) {
|
|
32
|
+
return `This conversation now continues in ${workspace.to}, a separate copy of ${workspace.from} where the earlier work happened. Paths above already point at the copy; work there.`;
|
|
33
|
+
}
|
|
3
34
|
/** Cap a tool payload down to a short, log-safe one-liner. */
|
|
4
35
|
function compactValue(value, max = 200) {
|
|
5
36
|
let s;
|
|
@@ -209,6 +240,7 @@ export function buildSeedPrompt(transcript, opts = {}) {
|
|
|
209
240
|
opts.context?.repoSlug ? `Repository: ${opts.context.repoSlug}` : null,
|
|
210
241
|
opts.context?.branch ? `Branch: ${opts.context.branch}` : null,
|
|
211
242
|
opts.context?.prUrl ? `PR: ${opts.context.prUrl}` : null,
|
|
243
|
+
transcript.workspace ? workspaceNote(transcript.workspace) : null,
|
|
212
244
|
"",
|
|
213
245
|
omitted > 0
|
|
214
246
|
? `Recent conversation (most recent last; ${omitted} earlier turn${omitted === 1 ? "" : "s"} omitted — see ${where}):`
|
|
@@ -274,6 +306,9 @@ export function buildForkHistory(transcript) {
|
|
|
274
306
|
if (transcript.plan?.length && lastReply >= 0) {
|
|
275
307
|
history[lastReply].text += `\n\n[plan] Where the plan stands:\n${planChecklist(transcript.plan)}`;
|
|
276
308
|
}
|
|
309
|
+
if (transcript.workspace && lastReply >= 0) {
|
|
310
|
+
history[lastReply].text += `\n\n[workspace] ${workspaceNote(transcript.workspace)}`;
|
|
311
|
+
}
|
|
277
312
|
return history;
|
|
278
313
|
}
|
|
279
314
|
/**
|
|
@@ -291,3 +326,20 @@ export function renderForkTranscript(transcript) {
|
|
|
291
326
|
const turns = buildForkHistory(transcript).map((turn) => `## ${turn.role === "user" ? "User" : "Assistant"}\n\n${turn.text}`);
|
|
292
327
|
return [...header, "", ...turns].join("\n\n").replace(/\n{3,}/g, "\n\n") + "\n";
|
|
293
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* The model the agent last answered with, as its own transcript records it
|
|
331
|
+
* (`model`, and `provider` when known, on an assistant message). A session
|
|
332
|
+
* resumed from a native transcript (a `bivy run <agent> --model …` taken over
|
|
333
|
+
* in chat) should keep that model, not fall back to the agent's default.
|
|
334
|
+
*/
|
|
335
|
+
export function lastTranscriptModel(messages) {
|
|
336
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
337
|
+
const message = messages[i];
|
|
338
|
+
if (message?.role !== "assistant" || message.bivyKind)
|
|
339
|
+
continue;
|
|
340
|
+
if (typeof message.model === "string" && message.model.trim()) {
|
|
341
|
+
return { provider: typeof message.provider === "string" ? message.provider : "", id: message.model.trim() };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
@@ -19,6 +19,7 @@ import { mergeBases } from "./event-log.js";
|
|
|
19
19
|
import { thinkingTextFromContent } from "./transcript-merge.js";
|
|
20
20
|
import { extractInlineImageUrls, fetchInlineImage, isFetchImageError, inlineImageDisplayName, assistantTextForImageScan } from "./inline-image-fetch.js";
|
|
21
21
|
import { historyDelta } from "../history-sync.js";
|
|
22
|
+
import { withToolDetails } from "../runtime/tool-call-map.js";
|
|
22
23
|
// Display-only intermediate reasoning is persisted as a length-capped HEAD so a
|
|
23
24
|
// runaway/looping agent can't grow the append-only log without bound. HEAD (not
|
|
24
25
|
// tail): once capped, the persisted text stops changing, so the skip-when-
|
|
@@ -75,6 +76,11 @@ export function createTranscriptPersistence(deps) {
|
|
|
75
76
|
const { eventLog, attachmentStore } = deps;
|
|
76
77
|
const project = createClientProjection(attachmentStore, eventLog);
|
|
77
78
|
const liveIntermediateBySession = new Map();
|
|
79
|
+
// `${sessionId}\0${callId}` -> what the call's first event recorded. Runtimes
|
|
80
|
+
// stream progress as updates that carry only partial output (Pi's bash), so
|
|
81
|
+
// an update without its own detail/parent keeps the call's, rather than
|
|
82
|
+
// overwriting the persisted card with a generic one.
|
|
83
|
+
const toolCallOverlay = new Map();
|
|
78
84
|
const lastPersistedIntermediateText = new Map();
|
|
79
85
|
// In-flight dedupe + failure cooldown so a repeated remote image URL only ever
|
|
80
86
|
// triggers one fetch and a broken URL isn't retried every message_end.
|
|
@@ -135,12 +141,19 @@ export function createTranscriptPersistence(deps) {
|
|
|
135
141
|
if (!["tool_call", "tool_execution_start", "tool_execution_update", "tool_execution_end", "tool_result", "function_call", "function_result"].includes(type))
|
|
136
142
|
return;
|
|
137
143
|
const callId = toolEventId(event);
|
|
144
|
+
// A turn-level failure some runtimes report on the tool channel (Claude's
|
|
145
|
+
// non-success `result`) names no tool and no call: it is not tool activity,
|
|
146
|
+
// and persisting it left a blank error card keyed "tool:".
|
|
147
|
+
if (callId === "tool:" && !event.toolName && !event.name)
|
|
148
|
+
return;
|
|
138
149
|
const toolCall = event.toolCall;
|
|
139
150
|
const input = (event.input || event.toolInput || event.args || toolCall?.input || {});
|
|
140
151
|
const name = String(event.toolName || event.name || toolCall?.name || "tool");
|
|
141
152
|
const now = Date.now();
|
|
142
153
|
const base = { role: "assistant", bivyKind: "tool", afterMessageCount: record.session.getMessages().length, createdAt: now };
|
|
154
|
+
const overlayKey = `${record.id}\0${callId}`;
|
|
143
155
|
if (type === "tool_result" || type === "tool_execution_end" || type === "function_result") {
|
|
156
|
+
toolCallOverlay.delete(overlayKey);
|
|
144
157
|
eventLog.append(record.id, { ...base, id: `bivy-tool-result-${callId}`, content: [{ type: "tool_result", toolUseId: callId, tool_use_id: callId, content: event.message ?? event.result ?? event.output ?? input.output ?? "", isError: Boolean(event.error || event.errorMessage), ...(event.detail ? { detail: event.detail } : {}) }] });
|
|
145
158
|
}
|
|
146
159
|
else if (isProgressOnlyPing(type, event.detail, input)) {
|
|
@@ -163,8 +176,11 @@ export function createTranscriptPersistence(deps) {
|
|
|
163
176
|
// `parentToolUseId` (Claude, protocol agents) or `parentToolCallId` (Pi's
|
|
164
177
|
// nested calls from codemode / ctx.executeTool).
|
|
165
178
|
const rawParent = event.parentToolUseId ?? event.parentToolCallId;
|
|
166
|
-
const
|
|
167
|
-
|
|
179
|
+
const prior = toolCallOverlay.get(overlayKey);
|
|
180
|
+
const parentToolUseId = typeof rawParent === "string" && rawParent ? rawParent : prior?.parentToolUseId;
|
|
181
|
+
const detail = event.detail ?? prior?.detail;
|
|
182
|
+
toolCallOverlay.set(overlayKey, { detail, parentToolUseId });
|
|
183
|
+
eventLog.append(record.id, { ...base, id: `bivy-tool-call-${callId}`, content: [{ type: "tool_use", id: callId, name, input, ...(parentToolUseId ? { parentToolUseId } : {}), ...(detail ? { detail } : {}) }] });
|
|
168
184
|
}
|
|
169
185
|
}
|
|
170
186
|
function persistIntermediateFromEvent(record, event, final = false) {
|
|
@@ -212,7 +228,8 @@ export function createTranscriptPersistence(deps) {
|
|
|
212
228
|
return eventLog.deriveBase(record.id, record.session.getMessages());
|
|
213
229
|
}
|
|
214
230
|
function buildHistoryEvent(opts) {
|
|
215
|
-
const
|
|
231
|
+
const described = withToolDetails(opts.messages, opts.runtimeId);
|
|
232
|
+
const messages = opts.sessionId ? project(opts.sessionId, described) : described;
|
|
216
233
|
const delta = historyDelta(messages, opts.cursor);
|
|
217
234
|
const record = opts.sessionId ? deps.getOpenSession(opts.sessionId) : undefined;
|
|
218
235
|
const bSess = record ? deps.bivySessionEnvelope(record) : undefined;
|
package/dist/session-ref.js
CHANGED
|
@@ -31,11 +31,14 @@ export function sessionIdFromRef(ref) {
|
|
|
31
31
|
* Throws for a path-based ref that escapes `sessionsDir`.
|
|
32
32
|
*/
|
|
33
33
|
export function resolveResumeRef(opts) {
|
|
34
|
-
const { ref, resumesByPath
|
|
34
|
+
const { ref, resumesByPath } = opts;
|
|
35
35
|
if (!resumesByPath)
|
|
36
36
|
return sessionIdFromRef(ref);
|
|
37
37
|
const resolved = path.resolve(ref);
|
|
38
|
-
|
|
38
|
+
// Every directory the node keeps or reads path-based transcripts in: its own,
|
|
39
|
+
// and the agent's native store (a `bivy run pi` session lives in Pi's).
|
|
40
|
+
const roots = typeof opts.sessionsDir === "string" ? [opts.sessionsDir] : opts.sessionsDir;
|
|
41
|
+
if (!roots.some((root) => resolved.startsWith(path.resolve(root) + path.sep))) {
|
|
39
42
|
throw new Error("Session file is outside the sessions directory");
|
|
40
43
|
}
|
|
41
44
|
return resolved;
|
package/dist/worktree.js
CHANGED
|
@@ -29,6 +29,22 @@ export async function gitRepoRoot(dir) {
|
|
|
29
29
|
return undefined;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The main working tree of the repo containing `dir`. From inside a linked
|
|
34
|
+
* worktree (a session's or a fork's), `--show-toplevel` names that worktree;
|
|
35
|
+
* new worktrees and the `.bivy/` exclude rule belong to the main one, or a fork
|
|
36
|
+
* of a fork nests its checkout inside its parent's tree.
|
|
37
|
+
*/
|
|
38
|
+
async function gitMainRoot(dir) {
|
|
39
|
+
try {
|
|
40
|
+
const { stdout } = await exec("git", ["-C", dir, "rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
41
|
+
const common = stdout.trim();
|
|
42
|
+
return common && path.basename(common) === ".git" ? path.dirname(common) : undefined;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
32
48
|
async function currentRef(repoRoot) {
|
|
33
49
|
try {
|
|
34
50
|
const { stdout } = await exec("git", ["-C", repoRoot, "rev-parse", "--abbrev-ref", "HEAD"]);
|
|
@@ -57,14 +73,20 @@ function excludeMeshDir(repoRoot) {
|
|
|
57
73
|
* `<repoRoot>/.bivy/worktrees/<slug>` by default (excluded from git).
|
|
58
74
|
*/
|
|
59
75
|
export async function createWorktree(opts) {
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
76
|
+
const sourceRoot = await gitRepoRoot(opts.repoDir);
|
|
77
|
+
if (!sourceRoot)
|
|
62
78
|
throw new Error(`Not a git repository: ${opts.repoDir}`);
|
|
79
|
+
const repoRoot = (await gitMainRoot(sourceRoot)) ?? sourceRoot;
|
|
63
80
|
const slug = branchSlug(opts.id);
|
|
64
81
|
const branch = opts.branch ?? `bivy/${slug}`;
|
|
65
82
|
const root = opts.root ?? path.join(repoRoot, ".bivy", "worktrees");
|
|
66
83
|
const wtPath = path.join(root, slug);
|
|
67
|
-
|
|
84
|
+
// Branch from the checkout asked for: a fork of a fork starts at its parent's HEAD.
|
|
85
|
+
let base = opts.base ?? (await currentRef(sourceRoot));
|
|
86
|
+
// A detached "HEAD" means the source's commit, not the main tree's HEAD.
|
|
87
|
+
if (base === "HEAD" && sourceRoot !== repoRoot) {
|
|
88
|
+
base = await exec("git", ["-C", sourceRoot, "rev-parse", "HEAD"]).then(({ stdout }) => stdout.trim() || "HEAD", () => "HEAD");
|
|
89
|
+
}
|
|
68
90
|
excludeMeshDir(repoRoot);
|
|
69
91
|
fs.mkdirSync(root, { recursive: true });
|
|
70
92
|
// Reuse the registered checkout in place. Recovery must not force-remove
|
package/package.json
CHANGED