@bivy/bivy 0.16.8-staging.1 → 0.16.8-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.
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// server injects its own session lookup, runtime access, event emit, and the fork
|
|
9
9
|
// helpers as composition deps, so this stays free of daemon state and unit-testable.
|
|
10
10
|
import { buildForkBundle } from "../session/fork.js";
|
|
11
|
-
import { captureDirtyPatch, captureWorkspaceDirtyPatch } from "../session/fork-dirty.js";
|
|
11
|
+
import { captureDirtyPatch, captureWorkspaceDirtyPatch, captureWorkspaceSnapshot } from "../session/fork-dirty.js";
|
|
12
12
|
export function createForkCommands(deps) {
|
|
13
13
|
return {
|
|
14
14
|
async "session.fork.retire-source"(msg) {
|
|
@@ -48,9 +48,15 @@ export function createForkCommands(deps) {
|
|
|
48
48
|
// A git workspace must be captured successfully, including sessions that
|
|
49
49
|
// were started in an unmanaged checkout rather than a Bivy worktree.
|
|
50
50
|
// Treating a git/read failure as "clean" could retire the only WIP copy.
|
|
51
|
+
const sourceCwd = rec.session.cwd || rec.workspace;
|
|
51
52
|
const dirtyPatch = rec.worktree
|
|
52
53
|
? captureDirtyPatch(rec.worktree.path)
|
|
53
|
-
: captureWorkspaceDirtyPatch(
|
|
54
|
+
: captureWorkspaceDirtyPatch(sourceCwd);
|
|
55
|
+
// Plain workspaces have no remote clone source, so carry their files in
|
|
56
|
+
// the E2E bundle instead of silently substituting the destination cwd.
|
|
57
|
+
const workspaceSnapshot = !forkRecord.repoSlug && (msg.crossNode === true || dirtyPatch === undefined)
|
|
58
|
+
? captureWorkspaceSnapshot(sourceCwd, { maxBytes: deps.forkWorkspaceMaxBytes() })
|
|
59
|
+
: undefined;
|
|
54
60
|
// Publish the source branch so a cross-node fork's COMMITTED work travels
|
|
55
61
|
// via origin (the destination adopts `origin/<branch>`; see
|
|
56
62
|
// resolveAdoptBaseRef). Uncommitted work rides the dirtyPatch above. Only
|
|
@@ -66,7 +72,7 @@ export function createForkCommands(deps) {
|
|
|
66
72
|
// When the client has already picked a target agent, pass it so the
|
|
67
73
|
// bundle omits the native payload for a cross-runtime fork (it could
|
|
68
74
|
// never be replayed there — see buildForkBundle). Unset => keep it.
|
|
69
|
-
const bundle = buildForkBundle({ runtime: deps.getRuntime(rec.runtimeId), sessionFile: rec.sessionFile, record: forkRecord, dirtyPatch, targetRuntimeId: deps.agentFrom(msg), liveMessages: rec.session.getMessages(), state: deps.forkInFlightState(rec) });
|
|
75
|
+
const bundle = buildForkBundle({ runtime: deps.getRuntime(rec.runtimeId), sessionFile: rec.sessionFile, record: forkRecord, dirtyPatch, workspaceSnapshot, targetRuntimeId: deps.agentFrom(msg), liveMessages: rec.session.getMessages(), state: deps.forkInFlightState(rec) });
|
|
70
76
|
deps.sendEvent({ type: "session.fork.bundle", requestId, bundle });
|
|
71
77
|
}
|
|
72
78
|
catch (error) {
|
|
@@ -136,11 +142,15 @@ export function createForkCommands(deps) {
|
|
|
136
142
|
// re-applies it into the fork's fresh worktree. Local git ops only.
|
|
137
143
|
// Include unmanaged git workspaces too. standUpFork will cut an isolated
|
|
138
144
|
// worktree for them, so their uncommitted state must ride with it.
|
|
145
|
+
const sourceCwd = rec.session.cwd || rec.workspace;
|
|
139
146
|
const dirtyPatch = rec.worktree
|
|
140
147
|
? captureDirtyPatch(rec.worktree.path)
|
|
141
|
-
: captureWorkspaceDirtyPatch(
|
|
148
|
+
: captureWorkspaceDirtyPatch(sourceCwd);
|
|
149
|
+
const workspaceSnapshot = dirtyPatch === undefined
|
|
150
|
+
? captureWorkspaceSnapshot(sourceCwd, { maxBytes: deps.forkWorkspaceMaxBytes() })
|
|
151
|
+
: undefined;
|
|
142
152
|
// Same runtime → the bundle carries the native payload → full fidelity.
|
|
143
|
-
const bundle = buildForkBundle({ runtime, sessionFile: rec.sessionFile, record: forkRecord, dirtyPatch, targetRuntimeId: rec.runtimeId, liveMessages: rec.session.getMessages(), state: deps.forkInFlightState(rec) });
|
|
153
|
+
const bundle = buildForkBundle({ runtime, sessionFile: rec.sessionFile, record: forkRecord, dirtyPatch, workspaceSnapshot, targetRuntimeId: rec.runtimeId, liveMessages: rec.session.getMessages(), state: deps.forkInFlightState(rec) });
|
|
144
154
|
// Cut a fresh fork branch (the source still holds its own); skip prereq
|
|
145
155
|
// detection (same node + same runtime ⇒ agent and repo are present).
|
|
146
156
|
const outcome = await deps.standUpFork({
|
package/dist/node-config.js
CHANGED
|
@@ -74,7 +74,7 @@ export function validateNodeConfig(value) {
|
|
|
74
74
|
const node = section(root, "node", ["workspace", "port", "maxConcurrentAutomations", "capabilities"], errors);
|
|
75
75
|
const defaults = section(root, "defaults", ["agent", "model", "sandbox", "approval"], errors);
|
|
76
76
|
const safety = section(root, "safety", ["maxSandbox", "approvalFloor"], errors);
|
|
77
|
-
const sessions = section(root, "sessions", ["sync", "worktreeSync", "standbyNodeId", "resume", "autoAttachToolImages", "wedgedTurnMinutes"], errors);
|
|
77
|
+
const sessions = section(root, "sessions", ["sync", "worktreeSync", "standbyNodeId", "resume", "autoAttachToolImages", "forkWorkspaceMaxBytes", "wedgedTurnMinutes"], errors);
|
|
78
78
|
const github = section(root, "github", ["issuePrompt"], errors);
|
|
79
79
|
const automation = section(root, "automation", ["checks", "checkTimeoutMinutes"], errors);
|
|
80
80
|
const agentsRaw = section(root, "agents", Object.keys(record(root.agents) ?? {}), errors);
|
|
@@ -133,6 +133,7 @@ export function validateNodeConfig(value) {
|
|
|
133
133
|
if (resume !== undefined && resume !== "auto" && resume !== "manual")
|
|
134
134
|
errors.push("sessions.resume must be auto or manual");
|
|
135
135
|
const autoAttachToolImages = optionalBoolean(sessions.autoAttachToolImages, "sessions.autoAttachToolImages", errors);
|
|
136
|
+
const forkWorkspaceMaxBytes = optionalInteger(sessions.forkWorkspaceMaxBytes, "sessions.forkWorkspaceMaxBytes", errors, 1_048_576, 1_073_741_824);
|
|
136
137
|
// Upper bound is the wall-clock turn cap (60 min): a wedged window at/above it
|
|
137
138
|
// would never fire before the cap. 0 disables the band.
|
|
138
139
|
const wedgedTurnMinutes = optionalInteger(sessions.wedgedTurnMinutes, "sessions.wedgedTurnMinutes", errors, 0, 60);
|
|
@@ -215,7 +216,7 @@ export function validateNodeConfig(value) {
|
|
|
215
216
|
...(Object.keys(node).length ? { node: { workspace, port, maxConcurrentAutomations, capabilities } } : {}),
|
|
216
217
|
...(Object.keys(defaults).length ? { defaults: { agent, model, sandbox, approval } } : {}),
|
|
217
218
|
...(Object.keys(safety).length ? { safety: { maxSandbox, approvalFloor } } : {}),
|
|
218
|
-
...(Object.keys(sessions).length ? { sessions: { sync, worktreeSync, standbyNodeId, resume, autoAttachToolImages, wedgedTurnMinutes } } : {}),
|
|
219
|
+
...(Object.keys(sessions).length ? { sessions: { sync, worktreeSync, standbyNodeId, resume, autoAttachToolImages, forkWorkspaceMaxBytes, wedgedTurnMinutes } } : {}),
|
|
219
220
|
...(Object.keys(github).length ? { github: { issuePrompt } } : {}),
|
|
220
221
|
...(Object.keys(automation).length ? { automation: { checks, checkTimeoutMinutes } } : {}),
|
|
221
222
|
...(Object.keys(agents).length ? { agents } : {}),
|
|
@@ -271,6 +272,7 @@ export function configToLegacySettings(config) {
|
|
|
271
272
|
...(config.sessions?.standbyNodeId ? { syncStandbyNodeId: config.sessions.standbyNodeId } : {}),
|
|
272
273
|
...(config.sessions?.resume ? { sessionResumeMode: config.sessions.resume } : {}),
|
|
273
274
|
...(config.sessions?.autoAttachToolImages !== undefined ? { autoAttachToolImages: config.sessions.autoAttachToolImages } : {}),
|
|
275
|
+
...(config.sessions?.forkWorkspaceMaxBytes !== undefined ? { forkWorkspaceMaxBytes: config.sessions.forkWorkspaceMaxBytes } : {}),
|
|
274
276
|
};
|
|
275
277
|
}
|
|
276
278
|
export function mergeLegacyIntoNodeConfig(cli, settings) {
|
|
@@ -333,6 +335,7 @@ export function mergeLegacyIntoNodeConfig(cli, settings) {
|
|
|
333
335
|
standbyNodeId: typeof settings.syncStandbyNodeId === "string" ? settings.syncStandbyNodeId : undefined,
|
|
334
336
|
resume: settings.sessionResumeMode === "manual" ? "manual" : "auto",
|
|
335
337
|
autoAttachToolImages: settings.autoAttachToolImages === true,
|
|
338
|
+
forkWorkspaceMaxBytes: Number.isInteger(settings.forkWorkspaceMaxBytes) ? Number(settings.forkWorkspaceMaxBytes) : undefined,
|
|
336
339
|
},
|
|
337
340
|
github: { issuePrompt: typeof settings.githubIssuePrompt === "string" ? settings.githubIssuePrompt : undefined },
|
|
338
341
|
automation: {
|
|
@@ -358,7 +361,7 @@ export function setConfigValue(config, dotted, value) {
|
|
|
358
361
|
"node.workspace", "node.port", "node.maxConcurrentAutomations", "node.capabilities",
|
|
359
362
|
"defaults.agent", "defaults.model", "defaults.sandbox", "defaults.approval",
|
|
360
363
|
"safety.maxSandbox", "safety.approvalFloor",
|
|
361
|
-
"sessions.sync", "sessions.worktreeSync", "sessions.standbyNodeId", "sessions.resume", "sessions.autoAttachToolImages", "sessions.wedgedTurnMinutes",
|
|
364
|
+
"sessions.sync", "sessions.worktreeSync", "sessions.standbyNodeId", "sessions.resume", "sessions.autoAttachToolImages", "sessions.forkWorkspaceMaxBytes", "sessions.wedgedTurnMinutes",
|
|
362
365
|
"github.issuePrompt", "automation.checks", "automation.checkTimeoutMinutes",
|
|
363
366
|
]);
|
|
364
367
|
if (!allowed.has(dotted) && !/^agents\.[a-z][a-z0-9-]{1,47}$/.test(dotted) && !/^environment\.[A-Z][A-Z0-9_]+$/.test(dotted))
|
package/dist/server.js
CHANGED
|
@@ -1345,6 +1345,7 @@ function writeSettings(settings) {
|
|
|
1345
1345
|
...("syncStandbyNodeId" in settings ? { standbyNodeId: typeof settings.syncStandbyNodeId === "string" ? settings.syncStandbyNodeId || undefined : undefined } : {}),
|
|
1346
1346
|
...(settings.sessionResumeMode === "auto" || settings.sessionResumeMode === "manual" ? { resume: settings.sessionResumeMode } : {}),
|
|
1347
1347
|
...(typeof settings.autoAttachToolImages === "boolean" ? { autoAttachToolImages: settings.autoAttachToolImages } : {}),
|
|
1348
|
+
...(Number.isInteger(settings.forkWorkspaceMaxBytes) ? { forkWorkspaceMaxBytes: Number(settings.forkWorkspaceMaxBytes) } : {}),
|
|
1348
1349
|
};
|
|
1349
1350
|
next.github = {
|
|
1350
1351
|
...next.github,
|
|
@@ -1463,6 +1464,7 @@ function nodeSettingsSnapshot() {
|
|
|
1463
1464
|
})(),
|
|
1464
1465
|
sessionResumeMode: nodeSessionResumeMode(),
|
|
1465
1466
|
autoAttachToolImages: readSettings().autoAttachToolImages === true,
|
|
1467
|
+
forkWorkspaceMaxBytes: Number.isInteger(readSettings().forkWorkspaceMaxBytes) ? Number(readSettings().forkWorkspaceMaxBytes) : 50 * 1024 * 1024,
|
|
1466
1468
|
};
|
|
1467
1469
|
}
|
|
1468
1470
|
async function applyNodeSettings(patch) {
|
|
@@ -1962,6 +1964,7 @@ const RELAY_COMMANDS = {
|
|
|
1962
1964
|
modelFrom,
|
|
1963
1965
|
pushModelAuthToControlPlane,
|
|
1964
1966
|
pushForkSourceBranch: (rec) => branchPublish.pushForkSourceBranch(rec),
|
|
1967
|
+
forkWorkspaceMaxBytes: () => nodeSettingsSnapshot().forkWorkspaceMaxBytes,
|
|
1965
1968
|
standUpFork: (opts) => forkStandUp.standUpFork(opts),
|
|
1966
1969
|
retireSource: (input) => forkRetire.retireSource(input),
|
|
1967
1970
|
}),
|
|
@@ -14,12 +14,27 @@ import path from "node:path";
|
|
|
14
14
|
* the marker as success would lose work-in-progress.
|
|
15
15
|
*/
|
|
16
16
|
const DEFAULT_MAX_BYTES = 5 * 1024 * 1024; // 5 MiB of patch text
|
|
17
|
+
const DEFAULT_WORKSPACE_MAX_BYTES = 50 * 1024 * 1024; // 50 MiB of files
|
|
18
|
+
function workspaceMaxBytes(configured) {
|
|
19
|
+
return configured !== undefined ? configured : DEFAULT_WORKSPACE_MAX_BYTES;
|
|
20
|
+
}
|
|
17
21
|
/** Find the containing git checkout without throwing for ordinary non-git
|
|
18
22
|
* workspaces. A discovered checkout is captured fail-closed by the caller. */
|
|
19
23
|
export function captureWorkspaceDirtyPatch(cwd, opts = {}) {
|
|
20
24
|
const probe = spawnSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], { encoding: "utf8", timeout: 10_000 });
|
|
21
|
-
if (probe.
|
|
22
|
-
|
|
25
|
+
if (probe.error)
|
|
26
|
+
throw probe.error;
|
|
27
|
+
if (probe.signal === "SIGTERM")
|
|
28
|
+
throw new Error("Timed out while inspecting the session workspace for uncommitted changes.");
|
|
29
|
+
if (probe.status !== 0) {
|
|
30
|
+
// A plain directory is a supported workspace. Do not, however, turn a
|
|
31
|
+
// broken/permission-denied git invocation into an apparent clean tree: for
|
|
32
|
+
// a MOVE that could discard the only copy of the user's edits.
|
|
33
|
+
const detail = String(probe.stderr || "").toLowerCase();
|
|
34
|
+
if (/not a git repository|outside a git work tree/.test(detail))
|
|
35
|
+
return undefined;
|
|
36
|
+
throw new Error(`Could not inspect the session workspace with git${probe.stderr ? `: ${String(probe.stderr).trim()}` : ""}`);
|
|
37
|
+
}
|
|
23
38
|
const root = String(probe.stdout || "").trim();
|
|
24
39
|
return root ? captureDirtyPatch(root, opts) : undefined;
|
|
25
40
|
}
|
|
@@ -58,6 +73,60 @@ export function captureDirtyPatch(repoDir, opts = {}) {
|
|
|
58
73
|
}
|
|
59
74
|
return { patch, untracked };
|
|
60
75
|
}
|
|
76
|
+
/** Capture a non-git workspace for a fork. Symlinks are recorded, never followed,
|
|
77
|
+
* and .git is excluded as it is a repository boundary rather than workspace data. */
|
|
78
|
+
export function captureWorkspaceSnapshot(root, opts = {}) {
|
|
79
|
+
const maxBytes = workspaceMaxBytes(opts.maxBytes);
|
|
80
|
+
const entries = [];
|
|
81
|
+
let byteLength = 0;
|
|
82
|
+
const walk = (dir, prefix) => {
|
|
83
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
84
|
+
if (!prefix && (entry.name === ".git" || entry.name === ".bivy"))
|
|
85
|
+
continue;
|
|
86
|
+
const rel = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
87
|
+
const abs = path.join(dir, entry.name);
|
|
88
|
+
const stat = fs.lstatSync(abs);
|
|
89
|
+
if (stat.isDirectory()) {
|
|
90
|
+
walk(abs, rel);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (stat.isSymbolicLink()) {
|
|
94
|
+
const target = fs.readlinkSync(abs);
|
|
95
|
+
byteLength += Buffer.byteLength(target);
|
|
96
|
+
entries.push({ path: rel, kind: "symlink", target });
|
|
97
|
+
}
|
|
98
|
+
else if (stat.isFile()) {
|
|
99
|
+
byteLength += stat.size;
|
|
100
|
+
entries.push({ path: rel, kind: "file", data: fs.readFileSync(abs).toString("base64"), mode: stat.mode & 0o777 });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
walk(path.resolve(root), "");
|
|
105
|
+
if (byteLength > maxBytes)
|
|
106
|
+
return { entries: [], byteLength, maxBytes, oversized: true };
|
|
107
|
+
return { entries, byteLength, maxBytes };
|
|
108
|
+
}
|
|
109
|
+
/** Materialise a captured workspace into a fresh destination directory. */
|
|
110
|
+
export function applyWorkspaceSnapshot(root, snapshot) {
|
|
111
|
+
if (!snapshot)
|
|
112
|
+
return;
|
|
113
|
+
if (snapshot.oversized)
|
|
114
|
+
throw new Error(`The workspace snapshot is ${snapshot.byteLength} bytes, above the ${snapshot.maxBytes}-byte transfer limit.`);
|
|
115
|
+
const destination = path.resolve(root);
|
|
116
|
+
for (const entry of snapshot.entries) {
|
|
117
|
+
const target = path.resolve(destination, entry.path);
|
|
118
|
+
if (target !== destination && !target.startsWith(`${destination}${path.sep}`))
|
|
119
|
+
throw new Error("Workspace snapshot contains an invalid path.");
|
|
120
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
121
|
+
if (entry.kind === "symlink")
|
|
122
|
+
fs.symlinkSync(entry.target ?? "", target);
|
|
123
|
+
else {
|
|
124
|
+
fs.writeFileSync(target, Buffer.from(entry.data ?? "", "base64"));
|
|
125
|
+
if (entry.mode !== undefined)
|
|
126
|
+
fs.chmodSync(target, entry.mode);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
61
130
|
/**
|
|
62
131
|
* Re-apply a captured patch onto a fresh checkout at `repoDir`. An oversized
|
|
63
132
|
* marker produces a warning as a final defence; normal fork stand-up rejects it
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
// Generic over the record type R (server passes SessionRecord) so the outcome it
|
|
17
17
|
// returns is the caller's own record type, not a narrowed copy.
|
|
18
18
|
import { randomBytes } from "node:crypto";
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
19
21
|
import { normalizeSandboxTier } from "../harness/sandbox.js";
|
|
20
22
|
import { parseRepo } from "../repo-workspace.js";
|
|
21
23
|
import { evaluateForkPrereqs, blockingForkPrereqs, missingForkPrereqs } from "../session/fork-prereqs.js";
|
|
24
|
+
import { applyWorkspaceSnapshot } from "./fork-dirty.js";
|
|
22
25
|
/**
|
|
23
26
|
* A one-line, user-facing disclosure of the source's in-flight state, or
|
|
24
27
|
* undefined when there is nothing to disclose. Pure so it is unit-testable.
|
|
@@ -57,6 +60,13 @@ export function createForkStandUp(deps) {
|
|
|
57
60
|
missing: [],
|
|
58
61
|
};
|
|
59
62
|
}
|
|
63
|
+
if (bundle.workspaceSnapshot?.oversized) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: `The source workspace is too large to transfer safely (${bundle.workspaceSnapshot.byteLength} bytes; limit ${bundle.workspaceSnapshot.maxBytes}). Reduce it and retry.`,
|
|
67
|
+
missing: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
60
70
|
// Carry the source's sandbox tier so a sandboxed session forks into a
|
|
61
71
|
// sandboxed one, rather than defaulting to this node's tier (fork.ts).
|
|
62
72
|
const forkSandbox = normalizeSandboxTier(bundle.record.sandbox);
|
|
@@ -159,12 +169,26 @@ export function createForkStandUp(deps) {
|
|
|
159
169
|
worktree = wt;
|
|
160
170
|
}
|
|
161
171
|
else {
|
|
172
|
+
// Non-repo-backed source. Materialise the complete workspace into an
|
|
173
|
+
// isolated directory on the destination; this also prevents same-node
|
|
174
|
+
// plain-directory forks from running two agents in one cwd.
|
|
175
|
+
if (bundle.workspaceSnapshot) {
|
|
176
|
+
const root = path.resolve(deps.defaultWorkspace);
|
|
177
|
+
fs.mkdirSync(root, { recursive: true });
|
|
178
|
+
const isolated = fs.mkdtempSync(path.join(root, ".bivy-fork-"));
|
|
179
|
+
applyWorkspaceSnapshot(isolated, bundle.workspaceSnapshot);
|
|
180
|
+
workspace = isolated;
|
|
181
|
+
cwd = isolated;
|
|
182
|
+
}
|
|
162
183
|
// Non-repo-backed source. The fork would otherwise reuse the PARENT's cwd,
|
|
163
184
|
// putting two sessions in one working tree — so when that cwd is itself a git
|
|
164
185
|
// checkout (a local repo without a GitHub origin), cut the fork its own
|
|
165
186
|
// worktree on a fresh branch. Best-effort: a non-git workspace has no tree to
|
|
166
187
|
// isolate, so the fork keeps the fallback cwd (no git collisions possible).
|
|
167
|
-
|
|
188
|
+
// A snapshot is authoritative for a machine-local source. Do not inspect
|
|
189
|
+
// the destination fallback cwd and accidentally turn it into a worktree
|
|
190
|
+
// fork of an unrelated repository.
|
|
191
|
+
const forkRepoRoot = bundle.workspaceSnapshot ? undefined : await deps.gitRepoRoot(cwd);
|
|
168
192
|
if (forkRepoRoot) {
|
|
169
193
|
const forkBranch = `bivy/fork-${randomBytes(6).toString("hex")}`;
|
|
170
194
|
const wt = await deps.withRepoLock(forkRepoRoot, () => deps.createWorktree({ repoDir: forkRepoRoot, id: forkBranch, branch: forkBranch }));
|
package/dist/session/fork.js
CHANGED
|
@@ -33,7 +33,14 @@ export function buildForkBundle(opts) {
|
|
|
33
33
|
// Only worth capturing when the target is the same runtime (or not yet known).
|
|
34
34
|
const nativeCouldReplay = !opts.targetRuntimeId || opts.targetRuntimeId === runtime.id;
|
|
35
35
|
let native;
|
|
36
|
-
|
|
36
|
+
// A native export reads the runtime's persisted store, which may lag the
|
|
37
|
+
// live session while a turn is streaming. Never prefer that stale snapshot
|
|
38
|
+
// for an in-flight fork: the normalized live transcript below is the only
|
|
39
|
+
// representation that includes the current turn. This is especially
|
|
40
|
+
// important for same-runtime forks, where a successful native import would
|
|
41
|
+
// otherwise prevent the portable fallback from being used.
|
|
42
|
+
const nativeSafe = !opts.state?.working;
|
|
43
|
+
if (sessionFile && nativeSafe && nativeCouldReplay && runtime.capabilities.forkTransport && runtime.exportForFork) {
|
|
37
44
|
try {
|
|
38
45
|
native = runtime.exportForFork(sessionFile);
|
|
39
46
|
}
|
|
@@ -42,7 +49,7 @@ export function buildForkBundle(opts) {
|
|
|
42
49
|
// transcript below still supports replay or a seeded continuation.
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
|
-
return { record, normalized, ...(native ? { native } : {}), ...(opts.dirtyPatch ? { dirtyPatch: opts.dirtyPatch } : {}), ...(opts.state ? { state: opts.state } : {}) };
|
|
52
|
+
return { record, normalized, ...(native ? { native } : {}), ...(opts.dirtyPatch ? { dirtyPatch: opts.dirtyPatch } : {}), ...(opts.workspaceSnapshot ? { workspaceSnapshot: opts.workspaceSnapshot } : {}), ...(opts.state ? { state: opts.state } : {}) };
|
|
46
53
|
}
|
|
47
54
|
/**
|
|
48
55
|
* Decide the best fidelity a fork of `bundle` into `targetRuntime` can achieve:
|
package/package.json
CHANGED