@bivy/bivy 0.9.1-staging.128 → 0.9.1-staging.129
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/dist/runtime/claude-code.js +32 -3
- package/dist/runtime/pi.js +43 -4
- package/dist/server.js +47 -38
- package/dist/session/fork.js +47 -16
- package/package.json +1 -1
|
@@ -1457,17 +1457,46 @@ export class ClaudeCodeRuntime {
|
|
|
1457
1457
|
const projectDir = path.join(root, "projects", projectSlug);
|
|
1458
1458
|
fs.mkdirSync(projectDir, { recursive: true });
|
|
1459
1459
|
let parentUuid = null;
|
|
1460
|
-
const
|
|
1460
|
+
const model = ctx.model?.provider === "anthropic"
|
|
1461
|
+
? ctx.model.id
|
|
1462
|
+
: this.options.defaultModel ?? FALLBACK_MODELS.find((candidate) => candidate.id.includes("sonnet")).id;
|
|
1463
|
+
const timestamp = Date.now();
|
|
1464
|
+
const lines = history.map((turn, index) => {
|
|
1461
1465
|
const uuid = randomUUID();
|
|
1466
|
+
// Claude's store is permissive when read for display, but `--resume` / the
|
|
1467
|
+
// Agent SDK expects provider-native assistant envelopes. Keep the portable
|
|
1468
|
+
// history generic until this final serializer, then emit the same minimum
|
|
1469
|
+
// shape as a real Claude transcript (content blocks, model/id, stop reason,
|
|
1470
|
+
// usage and timestamps). This is the Claude counterpart to pi's native
|
|
1471
|
+
// serializer and prevents a replay that paints correctly but fails on the
|
|
1472
|
+
// first continued prompt.
|
|
1473
|
+
const message = turn.role === "user"
|
|
1474
|
+
? { role: "user", content: turn.text }
|
|
1475
|
+
: {
|
|
1476
|
+
model,
|
|
1477
|
+
id: `msg_bivy_${uuid.replace(/-/g, "")}`,
|
|
1478
|
+
type: "message",
|
|
1479
|
+
role: "assistant",
|
|
1480
|
+
content: [{ type: "text", text: turn.text }],
|
|
1481
|
+
stop_reason: "end_turn",
|
|
1482
|
+
stop_sequence: null,
|
|
1483
|
+
usage: {
|
|
1484
|
+
input_tokens: 0,
|
|
1485
|
+
cache_creation_input_tokens: 0,
|
|
1486
|
+
cache_read_input_tokens: 0,
|
|
1487
|
+
output_tokens: 0,
|
|
1488
|
+
},
|
|
1489
|
+
};
|
|
1462
1490
|
const entry = {
|
|
1463
1491
|
parentUuid,
|
|
1464
1492
|
isSidechain: false,
|
|
1465
1493
|
userType: "external",
|
|
1466
1494
|
cwd,
|
|
1467
1495
|
sessionId: newId,
|
|
1468
|
-
type:
|
|
1469
|
-
message
|
|
1496
|
+
type: turn.role,
|
|
1497
|
+
message,
|
|
1470
1498
|
uuid,
|
|
1499
|
+
timestamp: new Date(timestamp + index).toISOString(),
|
|
1471
1500
|
};
|
|
1472
1501
|
parentUuid = uuid;
|
|
1473
1502
|
return JSON.stringify(entry);
|
package/dist/runtime/pi.js
CHANGED
|
@@ -12,7 +12,7 @@ import fs from "node:fs";
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
15
|
-
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { createPiModelRuntime } from "./pi-oauth.js";
|
|
17
17
|
import { toModelInfo as sharedToModelInfo } from "./normalize.js";
|
|
18
18
|
import { provisionPiAuthJson } from "./credential-provisioning.js";
|
|
@@ -539,9 +539,48 @@ export class PiRuntime {
|
|
|
539
539
|
* reconstruct provider-specific tool blocks. Never touches the source.
|
|
540
540
|
*/
|
|
541
541
|
async importHistoryForFork(history, ctx) {
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
542
|
+
const cwd = ctx.cwd || ctx.workspace;
|
|
543
|
+
const sessionManager = SessionManager.create(cwd, this.options.sessionsDir);
|
|
544
|
+
const settings = SettingsManager.create(cwd, this.options.piDir);
|
|
545
|
+
const defaultProvider = settings.getDefaultProvider();
|
|
546
|
+
const defaultModel = settings.getDefaultModel();
|
|
547
|
+
const targetModel = ctx.model ?? (defaultProvider && defaultModel ? { provider: defaultProvider, id: defaultModel } : undefined);
|
|
548
|
+
const timestamp = Date.now();
|
|
549
|
+
for (const [index, message] of history.entries()) {
|
|
550
|
+
// SessionManager deliberately persists unvalidated objects. A bare
|
|
551
|
+
// `{role, content: string}` looks readable in the UI, but is NOT a valid
|
|
552
|
+
// pi AssistantMessage: the first real prompt later reaches provider and
|
|
553
|
+
// usage code that expects block-array content plus model/usage metadata.
|
|
554
|
+
// That was the codex→pi fork crash ("reading 'length'"). Materialise the
|
|
555
|
+
// portable prose as complete pi-ai messages instead.
|
|
556
|
+
const imported = message.role === "user"
|
|
557
|
+
? {
|
|
558
|
+
role: "user",
|
|
559
|
+
content: [{ type: "text", text: message.text }],
|
|
560
|
+
timestamp: timestamp + index,
|
|
561
|
+
}
|
|
562
|
+
: {
|
|
563
|
+
role: "assistant",
|
|
564
|
+
content: [{ type: "text", text: message.text }],
|
|
565
|
+
// The target model is enough for pi to restore the requested model
|
|
566
|
+
// before Bivy applies it again after stand-up. The synthetic API id
|
|
567
|
+
// intentionally prevents provider-specific reasoning replay rules;
|
|
568
|
+
// this history contains portable plain text only.
|
|
569
|
+
api: "bivy-fork",
|
|
570
|
+
provider: targetModel?.provider ?? "bivy-fork",
|
|
571
|
+
model: targetModel?.id ?? "portable-history",
|
|
572
|
+
usage: {
|
|
573
|
+
input: 0,
|
|
574
|
+
output: 0,
|
|
575
|
+
cacheRead: 0,
|
|
576
|
+
cacheWrite: 0,
|
|
577
|
+
totalTokens: 0,
|
|
578
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
579
|
+
},
|
|
580
|
+
stopReason: "stop",
|
|
581
|
+
timestamp: timestamp + index,
|
|
582
|
+
};
|
|
583
|
+
sessionManager.appendMessage(imported);
|
|
545
584
|
}
|
|
546
585
|
const sessionFile = sessionManager.getSessionFile();
|
|
547
586
|
if (!sessionFile)
|
package/dist/server.js
CHANGED
|
@@ -3598,27 +3598,12 @@ const RELAY_COMMANDS = {
|
|
|
3598
3598
|
// changes into an E2E bundle the client carries to the destination node.
|
|
3599
3599
|
const requestId = typeof msg.requestId === "string" ? msg.requestId : undefined;
|
|
3600
3600
|
const rec = resolveSession(msg.sessionId);
|
|
3601
|
-
if (!rec
|
|
3602
|
-
relay?.sendEvent({ type: "session.fork.error", requestId, error: "Session not found
|
|
3601
|
+
if (!rec) {
|
|
3602
|
+
relay?.sendEvent({ type: "session.fork.error", requestId, error: "Session not found on this node." });
|
|
3603
3603
|
return;
|
|
3604
3604
|
}
|
|
3605
3605
|
try {
|
|
3606
|
-
const
|
|
3607
|
-
const repoSlug = src.startsWith("repo:") ? src.slice(5) : src.startsWith("issue:") ? src.slice(6).replace(/#\d+$/, "") : undefined;
|
|
3608
|
-
const forkRecord = {
|
|
3609
|
-
sourceSessionId: rec.id,
|
|
3610
|
-
runtimeId: rec.runtimeId,
|
|
3611
|
-
workspace: rec.workspace,
|
|
3612
|
-
cwd: rec.session.cwd || rec.worktree?.path || rec.workspace,
|
|
3613
|
-
worktree: rec.worktree?.path,
|
|
3614
|
-
branch: rec.worktree?.branch,
|
|
3615
|
-
repoSlug,
|
|
3616
|
-
prUrl: rec.prUrl,
|
|
3617
|
-
source: rec.source,
|
|
3618
|
-
title: rec.session.getName(),
|
|
3619
|
-
model: rec.session.getCurrentModel()?.name,
|
|
3620
|
-
sandbox: rec.sandbox,
|
|
3621
|
-
};
|
|
3606
|
+
const forkRecord = forkRecordFor(rec);
|
|
3622
3607
|
let dirtyPatch;
|
|
3623
3608
|
if (rec.worktree) {
|
|
3624
3609
|
try {
|
|
@@ -3673,6 +3658,12 @@ const RELAY_COMMANDS = {
|
|
|
3673
3658
|
// so cut an independent fork branch instead of trying to adopt it.
|
|
3674
3659
|
worktree: msg.sameNode === true ? "fresh" : "adopt",
|
|
3675
3660
|
detectPrereqs: true,
|
|
3661
|
+
// A same-node cross-agent fork can safely retain a non-repo workspace.
|
|
3662
|
+
// A cross-node path belongs to the source machine, so standUpFork keeps
|
|
3663
|
+
// its destination default unless repo metadata lets it reconstruct one.
|
|
3664
|
+
...(msg.sameNode === true
|
|
3665
|
+
? { fallback: { workspace: bundle.record.workspace, cwd: bundle.record.cwd } }
|
|
3666
|
+
: {}),
|
|
3676
3667
|
});
|
|
3677
3668
|
if (!outcome.ok) {
|
|
3678
3669
|
relay?.sendEvent({ type: "session.fork.error", requestId, error: outcome.error, missing: outcome.missing });
|
|
@@ -3694,28 +3685,13 @@ const RELAY_COMMANDS = {
|
|
|
3694
3685
|
// (model may still change); any node/agent change goes through import above.
|
|
3695
3686
|
const requestId = typeof msg.requestId === "string" ? msg.requestId : undefined;
|
|
3696
3687
|
const rec = resolveSession(msg.sessionId);
|
|
3697
|
-
if (!rec
|
|
3698
|
-
relay?.sendEvent({ type: "session.fork.error", requestId, error: "Session not found
|
|
3688
|
+
if (!rec) {
|
|
3689
|
+
relay?.sendEvent({ type: "session.fork.error", requestId, error: "Session not found on this node." });
|
|
3699
3690
|
return;
|
|
3700
3691
|
}
|
|
3701
3692
|
try {
|
|
3702
3693
|
const runtime = getRuntime(rec.runtimeId);
|
|
3703
|
-
const
|
|
3704
|
-
const repoSlug = src.startsWith("repo:") ? src.slice(5) : src.startsWith("issue:") ? src.slice(6).replace(/#\d+$/, "") : undefined;
|
|
3705
|
-
const forkRecord = {
|
|
3706
|
-
sourceSessionId: rec.id,
|
|
3707
|
-
runtimeId: rec.runtimeId,
|
|
3708
|
-
workspace: rec.workspace,
|
|
3709
|
-
cwd: rec.session.cwd || rec.worktree?.path || rec.workspace,
|
|
3710
|
-
worktree: rec.worktree?.path,
|
|
3711
|
-
branch: rec.worktree?.branch,
|
|
3712
|
-
repoSlug,
|
|
3713
|
-
prUrl: rec.prUrl,
|
|
3714
|
-
source: rec.source,
|
|
3715
|
-
title: rec.session.getName(),
|
|
3716
|
-
model: rec.session.getCurrentModel()?.name,
|
|
3717
|
-
sandbox: rec.sandbox,
|
|
3718
|
-
};
|
|
3694
|
+
const forkRecord = forkRecordFor(rec);
|
|
3719
3695
|
// Carry uncommitted work: capture from the SOURCE worktree; standUpFork
|
|
3720
3696
|
// re-applies it into the fork's fresh worktree. Local git ops only.
|
|
3721
3697
|
let dirtyPatch;
|
|
@@ -6066,6 +6042,33 @@ async function withRepoLock(key, fn) {
|
|
|
6066
6042
|
repoWorktreeLocks.set(key, run.then(() => { }, () => { }));
|
|
6067
6043
|
return run;
|
|
6068
6044
|
}
|
|
6045
|
+
/** Build the runtime-neutral metadata carried by both local and remote forks. */
|
|
6046
|
+
function forkRecordFor(rec) {
|
|
6047
|
+
const src = rec.source ?? "";
|
|
6048
|
+
const repoSlug = src.startsWith("repo:")
|
|
6049
|
+
? src.slice(5)
|
|
6050
|
+
: src.startsWith("issue:")
|
|
6051
|
+
? src.slice(6).replace(/#\d+$/, "")
|
|
6052
|
+
: undefined;
|
|
6053
|
+
const currentModel = rec.session.getCurrentModel();
|
|
6054
|
+
return {
|
|
6055
|
+
sourceSessionId: rec.id,
|
|
6056
|
+
runtimeId: rec.runtimeId,
|
|
6057
|
+
workspace: rec.workspace,
|
|
6058
|
+
cwd: rec.session.cwd || rec.worktree?.path || rec.workspace,
|
|
6059
|
+
worktree: rec.worktree?.path,
|
|
6060
|
+
branch: rec.worktree?.branch,
|
|
6061
|
+
repoSlug,
|
|
6062
|
+
prUrl: rec.prUrl,
|
|
6063
|
+
source: rec.source,
|
|
6064
|
+
title: rec.session.getName(),
|
|
6065
|
+
model: currentModel?.name,
|
|
6066
|
+
...(currentModel?.provider && currentModel.id
|
|
6067
|
+
? { modelRef: { provider: String(currentModel.provider), id: String(currentModel.id) } }
|
|
6068
|
+
: {}),
|
|
6069
|
+
sandbox: rec.sandbox,
|
|
6070
|
+
};
|
|
6071
|
+
}
|
|
6069
6072
|
/**
|
|
6070
6073
|
* Best-effort push of a fork SOURCE's branch to origin before the bundle leaves
|
|
6071
6074
|
* the node, so a cross-node fork's committed work travels via origin (the
|
|
@@ -6198,7 +6201,13 @@ async function standUpFork(opts) {
|
|
|
6198
6201
|
}
|
|
6199
6202
|
// Materialise the transcript, then stand the session up — resume the imported
|
|
6200
6203
|
// transcript (full) or a fresh session the caller seeds with plan.seedPrompt.
|
|
6201
|
-
|
|
6204
|
+
// Preserve the source model only when the runtime itself is unchanged. A
|
|
6205
|
+
// cross-agent fork must let the destination choose its own default unless the
|
|
6206
|
+
// caller explicitly supplied a model valid for that target. Thread the final
|
|
6207
|
+
// choice through the generic import context: adapters that need native model
|
|
6208
|
+
// metadata (Pi/Claude) can serialize it; the others simply ignore it.
|
|
6209
|
+
const targetModel = opts.model ?? (targetRuntimeId === bundle.record.runtimeId ? bundle.record.modelRef : undefined);
|
|
6210
|
+
const plan = await materializeFork({ bundle, targetRuntime, ctx: { workspace, cwd, model: targetModel }, seed: { transcriptUrl: opts.transcriptUrl } });
|
|
6202
6211
|
const record = plan.kind === "resume"
|
|
6203
6212
|
? await createSession(cwd, plan.sessionFile, { runtimeId: targetRuntimeId, source: bundle.record.source, sandbox: forkSandbox, makeActive: false })
|
|
6204
6213
|
: await createSession(cwd, undefined, { runtimeId: targetRuntimeId, source: bundle.record.source, sandbox: forkSandbox, makeActive: false });
|
|
@@ -6224,7 +6233,7 @@ async function standUpFork(opts) {
|
|
|
6224
6233
|
// cleanly, so the fork isn't silently missing work-in-progress.
|
|
6225
6234
|
if (dirtyWarning)
|
|
6226
6235
|
broadcast({ type: "session.notice", sessionId: record.id, message: dirtyWarning });
|
|
6227
|
-
await applyRequestedModel(record,
|
|
6236
|
+
await applyRequestedModel(record, targetModel);
|
|
6228
6237
|
persistSessionMetadata(record);
|
|
6229
6238
|
scheduleAdvertise();
|
|
6230
6239
|
const missing = opts.detectPrereqs
|
package/dist/session/fork.js
CHANGED
|
@@ -12,7 +12,17 @@ export function buildForkBundle(opts) {
|
|
|
12
12
|
// Prefer the build-free readMessages fast path (pi/Claude); fall back to the
|
|
13
13
|
// live session's transcript for runtimes without one (the generic CLI runtime),
|
|
14
14
|
// so a fork *from* any agent still carries its real history.
|
|
15
|
-
|
|
15
|
+
let messages = opts.liveMessages;
|
|
16
|
+
if (sessionFile && runtime.readMessages) {
|
|
17
|
+
try {
|
|
18
|
+
messages = runtime.readMessages(sessionFile) ?? messages;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Runtime-native readers are best-effort. The live transcript is the
|
|
22
|
+
// universal source fallback and keeps one broken adapter from blocking a
|
|
23
|
+
// fork out to every other agent.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
16
26
|
const normalized = normalizeMessages(messages, {
|
|
17
27
|
sourceRuntimeId: runtime.id,
|
|
18
28
|
model: record.model,
|
|
@@ -21,9 +31,16 @@ export function buildForkBundle(opts) {
|
|
|
21
31
|
});
|
|
22
32
|
// Only worth capturing when the target is the same runtime (or not yet known).
|
|
23
33
|
const nativeCouldReplay = !opts.targetRuntimeId || opts.targetRuntimeId === runtime.id;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
let native;
|
|
35
|
+
if (sessionFile && nativeCouldReplay && runtime.capabilities.forkTransport && runtime.exportForFork) {
|
|
36
|
+
try {
|
|
37
|
+
native = runtime.exportForFork(sessionFile);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// A native export is an optimization, never the only route. The portable
|
|
41
|
+
// transcript below still supports replay or a seeded continuation.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
27
44
|
return { record, normalized, ...(native ? { native } : {}), ...(opts.dirtyPatch ? { dirtyPatch: opts.dirtyPatch } : {}) };
|
|
28
45
|
}
|
|
29
46
|
/**
|
|
@@ -47,6 +64,7 @@ export function resolveForkFidelity(bundle, targetRuntime) {
|
|
|
47
64
|
return "full";
|
|
48
65
|
const canReplayHistory = !!targetRuntime.capabilities.forkHistoryImport &&
|
|
49
66
|
typeof targetRuntime.importHistoryForFork === "function" &&
|
|
67
|
+
Array.isArray(bundle.normalized?.turns) &&
|
|
50
68
|
bundle.normalized.turns.length > 0;
|
|
51
69
|
return canReplayHistory ? "replayed" : "seeded";
|
|
52
70
|
}
|
|
@@ -60,25 +78,38 @@ export function resolveForkFidelity(bundle, targetRuntime) {
|
|
|
60
78
|
*/
|
|
61
79
|
export async function materializeFork(opts) {
|
|
62
80
|
const { bundle, targetRuntime, ctx } = opts;
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
81
|
+
const intended = resolveForkFidelity(bundle, targetRuntime);
|
|
82
|
+
const validImport = (result) => typeof result?.sessionFile === "string" && result.sessionFile.trim().length > 0 &&
|
|
83
|
+
typeof result.id === "string" && result.id.trim().length > 0;
|
|
84
|
+
// Native transport is the preferred same-runtime route, but never a single
|
|
85
|
+
// point of failure. An adapter upgrade, stale native store, or malformed
|
|
86
|
+
// payload degrades through the exact same portable path all agents share.
|
|
87
|
+
if (intended === "full" && bundle.native && targetRuntime.importForFork) {
|
|
88
|
+
try {
|
|
89
|
+
const imported = await targetRuntime.importForFork(bundle.native, ctx);
|
|
90
|
+
if (validImport(imported))
|
|
91
|
+
return { kind: "resume", fidelity: "full", ...imported };
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Continue to portable replay (if supported), then the universal seed.
|
|
95
|
+
}
|
|
67
96
|
}
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
97
|
+
// Write the runtime-neutral conversation into any destination that advertises
|
|
98
|
+
// a native history serializer. This path also rescues a failed same-runtime
|
|
99
|
+
// native import, so Pi/Claude/Codex all share one degradation ladder.
|
|
100
|
+
const canReplay = !!targetRuntime.capabilities.forkHistoryImport &&
|
|
101
|
+
typeof targetRuntime.importHistoryForFork === "function";
|
|
102
|
+
if (canReplay) {
|
|
73
103
|
try {
|
|
74
104
|
const history = buildForkHistory(bundle.normalized);
|
|
75
105
|
if (history.length > 0) {
|
|
76
|
-
const
|
|
77
|
-
|
|
106
|
+
const imported = await targetRuntime.importHistoryForFork(history, ctx);
|
|
107
|
+
if (validImport(imported))
|
|
108
|
+
return { kind: "resume", fidelity: "replayed", ...imported };
|
|
78
109
|
}
|
|
79
110
|
}
|
|
80
111
|
catch {
|
|
81
|
-
//
|
|
112
|
+
// Fall through to the agent-independent seeded continuation below.
|
|
82
113
|
}
|
|
83
114
|
}
|
|
84
115
|
const seedPrompt = buildSeedPrompt(bundle.normalized, {
|
package/package.json
CHANGED