@neta-art/cohub-cli 8.0.0 → 8.0.1
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/commands/runtime.js +53 -53
- package/dist/commands/sandboxd-binary.d.ts +1 -1
- package/dist/commands/sandboxd-binary.js +7 -1
- package/dist/runtime/archive-store.d.ts +3 -1
- package/dist/runtime/archive-store.js +2 -0
- package/dist/runtime/connection.js +34 -22
- package/dist/runtime/harness.d.ts +4 -0
- package/dist/runtime/harness.js +29 -16
- package/dist/runtime/instance.js +5 -5
- package/dist/runtime/json-rpc.d.ts +2 -0
- package/dist/runtime/json-rpc.js +2 -0
- package/dist/runtime/launch.js +22 -22
- package/dist/runtime/native-codex-hook.js +3 -3
- package/dist/runtime/native-install.js +9 -9
- package/dist/runtime/native-ipc.js +4 -4
- package/dist/runtime/native-pi-extension.js +1 -1
- package/dist/runtime/native-sync-store.js +19 -19
- package/dist/runtime/native-sync.js +5 -5
- package/dist/runtime/native-transcript.js +13 -13
- package/dist/runtime/presentation.js +31 -29
- package/dist/runtime/process-group.d.ts +2 -0
- package/dist/runtime/process-group.js +124 -31
- package/dist/runtime/session-store.d.ts +15 -2
- package/dist/runtime/session-store.js +79 -5
- package/dist/runtime/space-binding.js +3 -3
- package/dist/runtime/supervisor.js +4 -4
- package/package.json +1 -1
package/dist/runtime/launch.js
CHANGED
|
@@ -16,13 +16,13 @@ export const resolveLocalSpaceName = (root, name) => name?.trim() || basename(ro
|
|
|
16
16
|
export function parseRuntimeHarnesses(values) {
|
|
17
17
|
const names = values.flatMap((value) => value.split(",")).map((name) => name.trim()).filter(Boolean);
|
|
18
18
|
if (names.some((name) => !isLocalHarness(name)))
|
|
19
|
-
throw new Error("Harness must be pi or codex
|
|
19
|
+
throw new Error("Harness must be pi or codex");
|
|
20
20
|
return [...new Set(names.length ? names : ["pi"])];
|
|
21
21
|
}
|
|
22
22
|
export async function resolveRuntimeTarget(program, target) {
|
|
23
23
|
const spaceId = target?.trim() || explicitSpace(program) || (await getRuntimeSpaceBinding(process.cwd(), currentIdentityKey()))?.spaceId;
|
|
24
24
|
if (!spaceId)
|
|
25
|
-
throw new Error("No directory binding. Use --space <id> or runtime up
|
|
25
|
+
throw new Error("No directory binding. Use --space <id> or runtime up");
|
|
26
26
|
return spaceId;
|
|
27
27
|
}
|
|
28
28
|
export async function startBackgroundRuntime(config) {
|
|
@@ -47,11 +47,11 @@ export async function startBackgroundRuntime(config) {
|
|
|
47
47
|
else if (last)
|
|
48
48
|
resolve(last);
|
|
49
49
|
else
|
|
50
|
-
reject(new Error("Runtime did not start
|
|
50
|
+
reject(new Error("Runtime did not start"));
|
|
51
51
|
};
|
|
52
|
-
const timeout = setTimeout(() => finish(last ? undefined : new Error("Runtime startup timed out
|
|
52
|
+
const timeout = setTimeout(() => finish(last ? undefined : new Error("Runtime startup timed out")), 30_000);
|
|
53
53
|
child.on("error", (error) => finish(error));
|
|
54
|
-
child.on("exit", (code) => finish(new Error(`Runtime exited (${code})
|
|
54
|
+
child.on("exit", (code) => finish(new Error(`Runtime exited (${code})`)));
|
|
55
55
|
child.on("message", (message) => {
|
|
56
56
|
if (message.type === "status" && message.status) {
|
|
57
57
|
last = message.status;
|
|
@@ -69,17 +69,17 @@ export async function startBackgroundRuntime(config) {
|
|
|
69
69
|
export async function runtimeUp(program, dir, options) {
|
|
70
70
|
const requestedRoot = resolve(dir ?? process.cwd());
|
|
71
71
|
if (!(await stat(requestedRoot)).isDirectory())
|
|
72
|
-
throw new Error("Workspace is not a directory
|
|
72
|
+
throw new Error("Workspace is not a directory");
|
|
73
73
|
const root = await canonicalRuntimeRoot(requestedRoot);
|
|
74
74
|
const requested = options.space?.trim() || explicitSpace(program);
|
|
75
75
|
if (options.new && requested)
|
|
76
|
-
throw new Error("--new cannot be combined with --space or COHUB_SPACE_ID
|
|
76
|
+
throw new Error("--new cannot be combined with --space or COHUB_SPACE_ID");
|
|
77
77
|
if (options.name && requested)
|
|
78
|
-
throw new Error("--name only applies to a new Space
|
|
78
|
+
throw new Error("--name only applies to a new Space");
|
|
79
79
|
const identity = currentIdentityKey();
|
|
80
80
|
if (!identity) {
|
|
81
81
|
await requireAccessToken();
|
|
82
|
-
throw new Error("Cannot identify the signed-in account
|
|
82
|
+
throw new Error("Cannot identify the signed-in account");
|
|
83
83
|
}
|
|
84
84
|
const binding = await getRuntimeSpaceBinding(root, identity);
|
|
85
85
|
const existingId = requested || binding?.spaceId;
|
|
@@ -88,7 +88,7 @@ export async function runtimeUp(program, dir, options) {
|
|
|
88
88
|
const existing = await requestRuntimeInstance(runtimeInstanceDirectory(identity, existingId));
|
|
89
89
|
if (existing) {
|
|
90
90
|
if (existing.root !== root || options.harness.length && [...existing.harnesses].sort().join() !== [...harnesses].sort().join() || options.pi || options.codex) {
|
|
91
|
-
throw new Error("Runtime is running with a different configuration. Use down first
|
|
91
|
+
throw new Error("Runtime is running with a different configuration. Use down first");
|
|
92
92
|
}
|
|
93
93
|
printRuntimeSummary(existing, options.json, true);
|
|
94
94
|
return;
|
|
@@ -97,32 +97,32 @@ export async function runtimeUp(program, dir, options) {
|
|
|
97
97
|
if (!options.harness.length)
|
|
98
98
|
harnesses = await installedHarnesses(root, options);
|
|
99
99
|
if (!harnesses.length)
|
|
100
|
-
throw new Error("Install and sign in to Pi or Codex, or pass --harness
|
|
100
|
+
throw new Error("Install and sign in to Pi or Codex, or pass --harness");
|
|
101
101
|
let createNew = Boolean(options.new);
|
|
102
102
|
let name = resolveLocalSpaceName(root, options.name);
|
|
103
103
|
if (!options.yes) {
|
|
104
104
|
if (!process.stdin.isTTY)
|
|
105
|
-
throw new Error("Use --yes to authorize local execution
|
|
105
|
+
throw new Error("Use --yes to authorize local execution");
|
|
106
106
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
107
107
|
try {
|
|
108
108
|
if (!requested && binding) {
|
|
109
|
-
process.stderr.write(`\nLinked Space
|
|
110
|
-
const answer = (await rl.question("Reuse this Space? [Y/n, q to cancel]
|
|
109
|
+
process.stderr.write(`\nLinked Space\n ${runtimeWebUrl(binding.spaceId)}\n`);
|
|
110
|
+
const answer = (await rl.question("Reuse this Space? [Y/n, q to cancel] ")).trim().toLowerCase();
|
|
111
111
|
if (answer === "q")
|
|
112
112
|
return;
|
|
113
113
|
createNew = answer === "n" || answer === "no";
|
|
114
114
|
}
|
|
115
115
|
else if (!requested) {
|
|
116
|
-
const answer = (await rl.question("Create a new Space? [Y/n]
|
|
116
|
+
const answer = (await rl.question("Create a new Space? [Y/n] ")).trim().toLowerCase();
|
|
117
117
|
if (answer && answer !== "y" && answer !== "yes")
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
if (binding && createNew && !options.name)
|
|
121
121
|
name = `${name}-${randomUUID().slice(0, 6)}`;
|
|
122
122
|
if (!requested && (!binding || createNew))
|
|
123
|
-
name = (await rl.question(`Space name
|
|
124
|
-
process.stderr.write(`\nDirectory
|
|
125
|
-
const answer = await rl.question("Collaborators can execute as your OS user, beyond this folder. Allow? [y/N]
|
|
123
|
+
name = (await rl.question(`Space name [${name}]: `)).trim() || name;
|
|
124
|
+
process.stderr.write(`\nDirectory ${root}\n${requested ? `Space ${runtimeWebUrl(requested)}\n` : ""}`);
|
|
125
|
+
const answer = await rl.question("Collaborators can execute as your OS user, beyond this folder. Allow? [y/N] ");
|
|
126
126
|
if (!/^y(es)?$/i.test(answer.trim()))
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
@@ -133,7 +133,7 @@ export async function runtimeUp(program, dir, options) {
|
|
|
133
133
|
if (options.yes && createNew && binding && !options.name)
|
|
134
134
|
name = `${name}-${randomUUID().slice(0, 6)}`;
|
|
135
135
|
if (createNew && binding && await requestRuntimeInstance(runtimeInstanceDirectory(identity, binding.spaceId))) {
|
|
136
|
-
throw new Error("Stop the existing Runtime before rebinding this directory
|
|
136
|
+
throw new Error("Stop the existing Runtime before rebinding this directory");
|
|
137
137
|
}
|
|
138
138
|
// Fail local preflight before creating remote state; the worker reuses this catalog.
|
|
139
139
|
const capabilities = await discoverHarnesses(harnesses, options, root);
|
|
@@ -145,14 +145,14 @@ export async function runtimeUp(program, dir, options) {
|
|
|
145
145
|
validateSpace: async (id) => {
|
|
146
146
|
const sandbox = (await client.space(id).sandbox.get()).sandbox;
|
|
147
147
|
if (sandbox?.provider !== "local")
|
|
148
|
-
throw new Error("Space does not have a local Runtime
|
|
148
|
+
throw new Error("Space does not have a local Runtime");
|
|
149
149
|
},
|
|
150
150
|
});
|
|
151
151
|
const config = { spaceId, root, identity, harnesses, capabilities, executables: { pi: options.pi, codex: options.codex }, background: Boolean(options.detach), verbose: options.verbose };
|
|
152
152
|
const existing = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
|
|
153
153
|
if (existing) {
|
|
154
154
|
if (existing.root !== root)
|
|
155
|
-
throw new Error("This Space is running in another directory
|
|
155
|
+
throw new Error("This Space is running in another directory");
|
|
156
156
|
printRuntimeSummary(existing, options.json, true);
|
|
157
157
|
return;
|
|
158
158
|
}
|
|
@@ -170,7 +170,7 @@ export async function runtimeUp(program, dir, options) {
|
|
|
170
170
|
printRuntimeSummary(status, options.json, source === "binding");
|
|
171
171
|
}
|
|
172
172
|
else if (!announced && status.state === "starting" && !options.json)
|
|
173
|
-
process.stderr.write(`Connecting
|
|
173
|
+
process.stderr.write(`Connecting\n ${runtimeWebUrl(spaceId)}\n Logs ${status.diagnosticsPath}\n`);
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
176
|
}
|
|
@@ -5,7 +5,7 @@ export async function runCodexNativeHook(payload) {
|
|
|
5
5
|
return;
|
|
6
6
|
const value = payload;
|
|
7
7
|
if (!value || typeof value.cwd !== "string" || typeof value.session_id !== "string")
|
|
8
|
-
throw new Error("Invalid Codex hook identity
|
|
8
|
+
throw new Error("Invalid Codex hook identity");
|
|
9
9
|
if (typeof value.transcript_path !== "string" || !value.transcript_path)
|
|
10
10
|
return;
|
|
11
11
|
const result = await requestNativeDaemon({ harness: "codex", cwd: value.cwd, path: value.transcript_path, nativeSessionId: value.session_id });
|
|
@@ -17,12 +17,12 @@ async function main() {
|
|
|
17
17
|
for await (const chunk of process.stdin) {
|
|
18
18
|
input += chunk.toString();
|
|
19
19
|
if (Buffer.byteLength(input) > 4 * 1024 * 1024)
|
|
20
|
-
throw new Error("Codex hook input is too large
|
|
20
|
+
throw new Error("Codex hook input is too large");
|
|
21
21
|
}
|
|
22
22
|
await runCodexNativeHook(JSON.parse(input));
|
|
23
23
|
}
|
|
24
24
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
25
25
|
void main().catch((error) => {
|
|
26
|
-
process.stderr.write(`Cohub sync pending; native execution continues
|
|
26
|
+
process.stderr.write(`Cohub sync pending; native execution continues: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
27
27
|
});
|
|
28
28
|
}
|
|
@@ -21,7 +21,7 @@ async function installText(path, update) {
|
|
|
21
21
|
const info = await lstat(path).catch((error) => { if (missing(error))
|
|
22
22
|
return null; throw error; });
|
|
23
23
|
if (info && (!info.isFile() || info.isSymbolicLink()))
|
|
24
|
-
throw new Error(`Refusing to replace a non-regular file
|
|
24
|
+
throw new Error(`Refusing to replace a non-regular file: ${path}`);
|
|
25
25
|
const original = info ? await readFile(path, "utf8") : null;
|
|
26
26
|
const next = update(original);
|
|
27
27
|
if (next === original)
|
|
@@ -54,7 +54,7 @@ async function installText(path, update) {
|
|
|
54
54
|
const current = await readFile(path, "utf8").catch((error) => { if (missing(error))
|
|
55
55
|
return null; throw error; });
|
|
56
56
|
if (current !== original)
|
|
57
|
-
throw new Error(`Configuration changed during installation
|
|
57
|
+
throw new Error(`Configuration changed during installation: ${path}`);
|
|
58
58
|
await rename(temporary, path);
|
|
59
59
|
const directory = await open(dirname(path), "r");
|
|
60
60
|
try {
|
|
@@ -75,10 +75,10 @@ export async function verifyNativeSyncSupport(harnesses, cwd, executables = {})
|
|
|
75
75
|
if (harness === "pi") {
|
|
76
76
|
const version = /\b(\d+)\.(\d+)\.(\d+)\b/.exec(stdout);
|
|
77
77
|
if (!version || Number(version[1]) === 0 && (Number(version[2]) < 85 || Number(version[2]) === 85 && Number(version[3]) < 1))
|
|
78
|
-
throw new Error("Native sync requires Pi 0.85.1+
|
|
78
|
+
throw new Error("Native sync requires Pi 0.85.1+");
|
|
79
79
|
}
|
|
80
80
|
else if (!/^hooks\s+stable\s+true\s*$/m.test(stdout))
|
|
81
|
-
throw new Error("Install a Codex version with stable Hooks and enable hooks first
|
|
81
|
+
throw new Error("Install a Codex version with stable Hooks and enable hooks first");
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
/** Install once in the user's native configuration; data collection remains explicitly project-scoped. */
|
|
@@ -92,10 +92,10 @@ export async function installNativeSync(input) {
|
|
|
92
92
|
if (!input.disabled)
|
|
93
93
|
for (const harness of input.harnesses) {
|
|
94
94
|
if (harness === "pi") {
|
|
95
|
-
const content = `// Cohub native Turn sync
|
|
95
|
+
const content = `// Cohub native Turn sync\nexport { default } from ${JSON.stringify(extension.href)};\n`;
|
|
96
96
|
await installText(join(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent"), "extensions", "cohub.ts"), (existing) => {
|
|
97
97
|
if (existing !== null && existing !== content)
|
|
98
|
-
throw new Error("Pi Cohub extension already exists; preserve it and review manually
|
|
98
|
+
throw new Error("Pi Cohub extension already exists; preserve it and review manually");
|
|
99
99
|
return content;
|
|
100
100
|
});
|
|
101
101
|
}
|
|
@@ -105,9 +105,9 @@ export async function installNativeSync(input) {
|
|
|
105
105
|
if (existing?.includes(block))
|
|
106
106
|
return existing;
|
|
107
107
|
if (existing?.includes(START) || existing?.includes(END))
|
|
108
|
-
throw new Error("Codex Cohub hook block differs; preserve it and review manually
|
|
108
|
+
throw new Error("Codex Cohub hook block differs; preserve it and review manually");
|
|
109
109
|
if (existing && /^\s*hooks\s*=/m.test(existing))
|
|
110
|
-
throw new Error("Inline Codex hooks require manual merging
|
|
110
|
+
throw new Error("Inline Codex hooks require manual merging");
|
|
111
111
|
return `${existing ?? ""}${existing?.endsWith("\n") ? "\n" : "\n\n"}${block}`;
|
|
112
112
|
});
|
|
113
113
|
}
|
|
@@ -115,7 +115,7 @@ export async function installNativeSync(input) {
|
|
|
115
115
|
await withRuntimeSpaceBindingsLock(async () => {
|
|
116
116
|
const previous = await readNativeSyncConfig(runtimeRoot, input.identity);
|
|
117
117
|
if (previous && previous.root !== input.root)
|
|
118
|
-
throw new Error("Space native sync belongs to another directory
|
|
118
|
+
throw new Error("Space native sync belongs to another directory");
|
|
119
119
|
const harnesses = new Set(previous?.harnesses ?? []);
|
|
120
120
|
for (const harness of input.harnesses) {
|
|
121
121
|
if (input.disabled)
|
|
@@ -10,7 +10,7 @@ const parse = (raw) => {
|
|
|
10
10
|
const value = JSON.parse(raw);
|
|
11
11
|
if (value?.type !== "native.capture" || !["pi", "codex"].includes(value.harness)
|
|
12
12
|
|| typeof value.cwd !== "string" || typeof value.path !== "string" || typeof value.nativeSessionId !== "string") {
|
|
13
|
-
throw new Error("Invalid native daemon request
|
|
13
|
+
throw new Error("Invalid native daemon request");
|
|
14
14
|
}
|
|
15
15
|
return value;
|
|
16
16
|
};
|
|
@@ -25,18 +25,18 @@ export async function nativeDaemonSocketFor(cwd) {
|
|
|
25
25
|
export async function requestNativeDaemon(input) {
|
|
26
26
|
const path = await nativeDaemonSocketFor(input.cwd);
|
|
27
27
|
if (!path)
|
|
28
|
-
return { ok: false, message: "Native Runtime is not bound
|
|
28
|
+
return { ok: false, message: "Native Runtime is not bound" };
|
|
29
29
|
return new Promise((resolve, reject) => {
|
|
30
30
|
const socket = createConnection(path);
|
|
31
31
|
let buffer = "";
|
|
32
|
-
const timer = setTimeout(() => { socket.destroy(); reject(new Error("Native Runtime daemon timed out
|
|
32
|
+
const timer = setTimeout(() => { socket.destroy(); reject(new Error("Native Runtime daemon timed out")); }, 15_000);
|
|
33
33
|
const finish = (error, result) => {
|
|
34
34
|
clearTimeout(timer);
|
|
35
35
|
socket.destroy();
|
|
36
36
|
if (error)
|
|
37
37
|
reject(error);
|
|
38
38
|
else
|
|
39
|
-
resolve(result ?? { ok: false, message: "Empty daemon response
|
|
39
|
+
resolve(result ?? { ok: false, message: "Empty daemon response" });
|
|
40
40
|
};
|
|
41
41
|
socket.once("error", (error) => finish(error));
|
|
42
42
|
socket.on("data", (chunk) => {
|
|
@@ -10,7 +10,7 @@ export default function cohubNativeExtension(pi) {
|
|
|
10
10
|
const report = (error) => {
|
|
11
11
|
const message = error instanceof Error ? error.message : String(error);
|
|
12
12
|
if (message !== lastError)
|
|
13
|
-
context?.ui.notify(`Cohub sync pending
|
|
13
|
+
context?.ui.notify(`Cohub sync pending: ${message}`, "warning");
|
|
14
14
|
lastError = message;
|
|
15
15
|
};
|
|
16
16
|
const capture = (ctx, settled = false) => {
|
|
@@ -54,7 +54,7 @@ export class NativeSyncStore {
|
|
|
54
54
|
async binding() {
|
|
55
55
|
const value = await readJson(this.bindingPath());
|
|
56
56
|
if (value?.version !== 1 || value.identity !== this.options.identity || value.spaceId !== this.options.spaceId || value.nativeSessionId !== this.options.nativeSessionId || value.instanceKey !== this.options.instanceKey || value.harness !== this.options.harness)
|
|
57
|
-
throw new Error("Native binding mismatch
|
|
57
|
+
throw new Error("Native binding mismatch");
|
|
58
58
|
return value;
|
|
59
59
|
}
|
|
60
60
|
async initialize(path, transcript) {
|
|
@@ -62,7 +62,7 @@ export class NativeSyncStore {
|
|
|
62
62
|
if (existing) {
|
|
63
63
|
const binding = await this.binding();
|
|
64
64
|
if (binding.path !== path)
|
|
65
|
-
throw new Error("Native path changed; original binding retained
|
|
65
|
+
throw new Error("Native path changed; original binding retained");
|
|
66
66
|
return binding;
|
|
67
67
|
}
|
|
68
68
|
const managed = await findRuntimeNativeSession(this.options.runtimeRoot, this.options.harness, transcript.nativeSessionId, path);
|
|
@@ -70,7 +70,7 @@ export class NativeSyncStore {
|
|
|
70
70
|
const anchors = [];
|
|
71
71
|
if (managed) {
|
|
72
72
|
if (managed.pendingTurnId)
|
|
73
|
-
throw new Error("Reconcile the managed Turn before native continuation
|
|
73
|
+
throw new Error("Reconcile the managed Turn before native continuation");
|
|
74
74
|
if (managed.throughTurnId) {
|
|
75
75
|
const index = await readJson(join(this.options.runtimeRoot, "archives", "versions", `${managed.throughTurnId}.json`));
|
|
76
76
|
if (index) {
|
|
@@ -79,7 +79,7 @@ export class NativeSyncStore {
|
|
|
79
79
|
for await (const bytes of createReadStream(path, { end: parsed.sizeBytes - 1 }))
|
|
80
80
|
checksum.update(bytes);
|
|
81
81
|
if (checksum.digest("hex") !== parsed.sha256)
|
|
82
|
-
throw new Error("Runtime history prefix changed
|
|
82
|
+
throw new Error("Runtime history prefix changed");
|
|
83
83
|
throughBytes = parsed.sizeBytes;
|
|
84
84
|
}
|
|
85
85
|
else {
|
|
@@ -87,7 +87,7 @@ export class NativeSyncStore {
|
|
|
87
87
|
for await (const bytes of createReadStream(path))
|
|
88
88
|
checksum.update(bytes);
|
|
89
89
|
if (checksum.digest("hex") !== managed.checksum)
|
|
90
|
-
throw new Error("Cannot identify the last complete Runtime Turn
|
|
90
|
+
throw new Error("Cannot identify the last complete Runtime Turn");
|
|
91
91
|
throughBytes = (await stat(path)).size;
|
|
92
92
|
anchors.push({ turnId: managed.throughTurnId, sizeBytes: throughBytes, sha256: managed.checksum });
|
|
93
93
|
}
|
|
@@ -114,13 +114,13 @@ export class NativeSyncStore {
|
|
|
114
114
|
}
|
|
115
115
|
async capture(path, transcript) {
|
|
116
116
|
if (transcript.nativeSessionId !== this.options.nativeSessionId)
|
|
117
|
-
throw new Error("Native session identity mismatch
|
|
117
|
+
throw new Error("Native session identity mismatch");
|
|
118
118
|
await withRuntimeSpaceBindingsLock(async () => {
|
|
119
119
|
const binding = await this.initialize(path, transcript);
|
|
120
120
|
if (binding.throughBytes > 0) {
|
|
121
121
|
const anchor = binding.anchors.find((entry) => entry.sizeBytes === binding.throughBytes);
|
|
122
122
|
if (!anchor || transcript.prefixes.get(binding.throughBytes) !== anchor.sha256)
|
|
123
|
-
throw new Error("Runtime history prefix changed; original binding retained
|
|
123
|
+
throw new Error("Runtime history prefix changed; original binding retained");
|
|
124
124
|
}
|
|
125
125
|
let parentKey = null;
|
|
126
126
|
let parentCloudTurnId = binding.throughTurnId;
|
|
@@ -130,7 +130,7 @@ export class NativeSyncStore {
|
|
|
130
130
|
// Native offsets only validate whole-Turn archive checkpoints; they never become cloud fork anchors.
|
|
131
131
|
const matches = (binding.anchors ?? []).filter((anchor) => anchor.sizeBytes >= turn.contentEndBytes && turn.boundaries[anchor.sizeBytes] === anchor.sha256);
|
|
132
132
|
if (new Set(matches.map((anchor) => anchor.turnId)).size > 1)
|
|
133
|
-
throw new Error("Ambiguous Runtime Turn boundary
|
|
133
|
+
throw new Error("Ambiguous Runtime Turn boundary");
|
|
134
134
|
parentCloudTurnId = matches[0]?.turnId ?? null;
|
|
135
135
|
knownBoundary = matches.length > 0;
|
|
136
136
|
parentKey = null;
|
|
@@ -143,12 +143,12 @@ export class NativeSyncStore {
|
|
|
143
143
|
continue;
|
|
144
144
|
}
|
|
145
145
|
if (!parentKey && !knownBoundary)
|
|
146
|
-
throw new Error("Native continuation is not at a complete Runtime Turn boundary
|
|
146
|
+
throw new Error("Native continuation is not at a complete Runtime Turn boundary");
|
|
147
147
|
const turnId = this.turnId(turn.key);
|
|
148
148
|
const old = await readJson(this.receiptPath(turnId));
|
|
149
149
|
if (old?.result) {
|
|
150
150
|
if (turn.contentEndBytes < (old.contentEndBytes ?? old.endBytes) || JSON.stringify(turn.userContent) !== JSON.stringify(old.userContent) || turn.result && JSON.stringify(nativeTurnCompleteSchema.parse(turn.result)) !== JSON.stringify(old.result)) {
|
|
151
|
-
throw new Error("Native branch is inside a settled Turn; only whole-Turn forks are supported
|
|
151
|
+
throw new Error("Native branch is inside a settled Turn; only whole-Turn forks are supported");
|
|
152
152
|
}
|
|
153
153
|
parentKey = turn.key;
|
|
154
154
|
continue;
|
|
@@ -158,7 +158,7 @@ export class NativeSyncStore {
|
|
|
158
158
|
userContent: turn.userContent, startedAt: turn.startedAt, endBytes: turn.endBytes, contentEndBytes: turn.contentEndBytes, result,
|
|
159
159
|
...(!result ? { progress: nativeTurnProgressSchema.parse({ revision: turn.endBytes, messages: turn.messages }) } : {}) };
|
|
160
160
|
if (old && (JSON.stringify(old.userContent) !== JSON.stringify(receipt.userContent) || old.parentKey !== receipt.parentKey))
|
|
161
|
-
throw new Error("Native Turn changed; original receipt retained
|
|
161
|
+
throw new Error("Native Turn changed; original receipt retained");
|
|
162
162
|
// Capture immutable native bytes before publishing the completed receipt. Subsequent Turns may change the source.
|
|
163
163
|
if (result)
|
|
164
164
|
await this.archives.stage({ sessionId: binding.originSessionId, harness: binding.harness, nativeSessionId: binding.nativeSessionId, path, sizeBytes: turn.endBytes, expectedChecksum: turn.sha256 }, turnId);
|
|
@@ -186,7 +186,7 @@ export class NativeSyncStore {
|
|
|
186
186
|
continue;
|
|
187
187
|
}
|
|
188
188
|
if (receipt?.version !== 1 || receipt.turnId !== this.turnId(receipt.key))
|
|
189
|
-
throw new Error("Native receipt is corrupt; original retained
|
|
189
|
+
throw new Error("Native receipt is corrupt; original retained");
|
|
190
190
|
receipts.push(receipt);
|
|
191
191
|
}
|
|
192
192
|
return receipts.sort((a, b) => a.endBytes - b.endBytes || a.turnId.localeCompare(b.turnId));
|
|
@@ -233,7 +233,7 @@ export class NativeSyncStore {
|
|
|
233
233
|
parent = await readJson(this.acknowledgementPath(parentId));
|
|
234
234
|
}
|
|
235
235
|
if (!parent)
|
|
236
|
-
throw new Error("Parent binding is missing
|
|
236
|
+
throw new Error("Parent binding is missing");
|
|
237
237
|
}
|
|
238
238
|
let request = await readJson(this.requestPath(receipt.turnId));
|
|
239
239
|
if (!request) {
|
|
@@ -244,10 +244,10 @@ export class NativeSyncStore {
|
|
|
244
244
|
let remote = await readJson(this.cloudBindingPath(receipt.turnId));
|
|
245
245
|
if (!remote) {
|
|
246
246
|
if (!transport.startNativeTurn)
|
|
247
|
-
throw new Error("Native Runtime WS is unavailable
|
|
247
|
+
throw new Error("Native Runtime WS is unavailable");
|
|
248
248
|
remote = await transport.startNativeTurn(request, { signal });
|
|
249
249
|
if (remote.turnId !== receipt.turnId)
|
|
250
|
-
throw new Error("Server Turn identity mismatch
|
|
250
|
+
throw new Error("Server Turn identity mismatch");
|
|
251
251
|
await atomicRuntimeJson(this.cloudBindingPath(receipt.turnId), remote);
|
|
252
252
|
}
|
|
253
253
|
if (!receipt.result) {
|
|
@@ -267,7 +267,7 @@ export class NativeSyncStore {
|
|
|
267
267
|
return false;
|
|
268
268
|
}
|
|
269
269
|
if (!transport.completeNativeTurn)
|
|
270
|
-
throw new Error("Native Runtime WS is unavailable
|
|
270
|
+
throw new Error("Native Runtime WS is unavailable");
|
|
271
271
|
// Artifact retries back off: the terminal state is durable, so hammering the completion
|
|
272
272
|
// endpoint every flush cycle (5s) while object storage is down only adds load.
|
|
273
273
|
const backoffPath = join(this.root, "backoff", `${receipt.turnId}.json`);
|
|
@@ -279,7 +279,7 @@ export class NativeSyncStore {
|
|
|
279
279
|
// Terminal state is durable; only the artifact snapshot is missing. Keep the receipt pending
|
|
280
280
|
// and retry on the next flush cycle (>= 30s) until artifacts persist.
|
|
281
281
|
await atomicRuntimeJson(backoffPath, { at: Date.now() });
|
|
282
|
-
throw new Error("Native artifacts are pending; completion replays later
|
|
282
|
+
throw new Error("Native artifacts are pending; completion replays later");
|
|
283
283
|
}
|
|
284
284
|
await rm(backoffPath, { force: true });
|
|
285
285
|
await atomicRuntimeJson(this.acknowledgementPath(receipt.turnId), remote);
|
|
@@ -327,7 +327,7 @@ export class NativeSyncStore {
|
|
|
327
327
|
async cloudArchive(index) {
|
|
328
328
|
const binding = await readJson(this.acknowledgementPath(index.turnId));
|
|
329
329
|
if (!binding)
|
|
330
|
-
throw new Error("Native Turn result is not confirmed
|
|
330
|
+
throw new Error("Native Turn result is not confirmed");
|
|
331
331
|
if (index.parentTurnId) {
|
|
332
332
|
const parent = await readJson(this.acknowledgementPath(index.parentTurnId));
|
|
333
333
|
if (parent?.sessionId === binding.sessionId)
|
|
@@ -338,7 +338,7 @@ export class NativeSyncStore {
|
|
|
338
338
|
let parentId = index.parentTurnId;
|
|
339
339
|
while (parentId) {
|
|
340
340
|
if (visited.has(parentId))
|
|
341
|
-
throw new Error("Cyclic native archive
|
|
341
|
+
throw new Error("Cyclic native archive");
|
|
342
342
|
visited.add(parentId);
|
|
343
343
|
const previous = harnessArchiveIndexSchema.parse(await readJson(join(this.archives.root, "versions", `${parentId}.json`)));
|
|
344
344
|
segments.unshift(...previous.segments);
|
|
@@ -13,10 +13,10 @@ export function nativeArchiveTransport(spaceId, identity) {
|
|
|
13
13
|
const client = createClient().space(spaceId);
|
|
14
14
|
const guard = async (task) => {
|
|
15
15
|
if (currentIdentityKey() !== identity)
|
|
16
|
-
throw new Error("Native sync account changed
|
|
16
|
+
throw new Error("Native sync account changed");
|
|
17
17
|
const result = await task();
|
|
18
18
|
if (currentIdentityKey() !== identity)
|
|
19
|
-
throw new Error("Native sync account changed
|
|
19
|
+
throw new Error("Native sync account changed");
|
|
20
20
|
return result;
|
|
21
21
|
};
|
|
22
22
|
return {
|
|
@@ -29,7 +29,7 @@ export async function readNativeSyncConfig(runtimeRoot, identity) {
|
|
|
29
29
|
try {
|
|
30
30
|
const config = JSON.parse(await readFile(nativeSyncConfigPath(runtimeRoot, identity), "utf8"));
|
|
31
31
|
if (config.version !== 1 || config.identity !== identity || !Array.isArray(config.harnesses))
|
|
32
|
-
throw new Error("Invalid native sync configuration
|
|
32
|
+
throw new Error("Invalid native sync configuration");
|
|
33
33
|
return config;
|
|
34
34
|
}
|
|
35
35
|
catch (error) {
|
|
@@ -57,7 +57,7 @@ export async function captureNativeSession(input) {
|
|
|
57
57
|
const path = await canonicalRuntimeRoot(input.path);
|
|
58
58
|
const transcript = await readNativeTranscript(path, input.harness, input);
|
|
59
59
|
if (await canonicalRuntimeRoot(transcript.cwd) !== root || input.nativeSessionId && transcript.nativeSessionId !== input.nativeSessionId)
|
|
60
|
-
throw new Error("Native transcript belongs to another project or Session
|
|
60
|
+
throw new Error("Native transcript belongs to another project or Session");
|
|
61
61
|
const key = JSON.stringify([identity, space.spaceId, input.harness, transcript.nativeSessionId, path]);
|
|
62
62
|
let store = nativeStores.get(key);
|
|
63
63
|
if (!store) {
|
|
@@ -74,7 +74,7 @@ export async function captureNativeSession(input) {
|
|
|
74
74
|
const managedPath = managed ? await canonicalRuntimeRoot(managed.path).catch((error) => { if (error.code === "ENOENT")
|
|
75
75
|
return null; throw error; }) : null;
|
|
76
76
|
if (candidates.length && managedPath !== path)
|
|
77
|
-
throw new Error("Native path changed; original bindings retained
|
|
77
|
+
throw new Error("Native path changed; original bindings retained");
|
|
78
78
|
// Restored Pi working copies can share a native ID. Existing Cohub sidecars disambiguate them.
|
|
79
79
|
store = new NativeSyncStore({ runtimeRoot, spaceId: space.spaceId, identity, harness: input.harness, nativeSessionId: transcript.nativeSessionId,
|
|
80
80
|
instanceKey: managedPath === path ? path : undefined, transport });
|
|
@@ -8,7 +8,7 @@ const list = (value) => Array.isArray(value) ? value : [];
|
|
|
8
8
|
const iso = (value) => {
|
|
9
9
|
const date = new Date(typeof value === "number" || typeof value === "string" ? value : 0);
|
|
10
10
|
if (!Number.isFinite(date.getTime()))
|
|
11
|
-
throw new Error("Invalid native timestamp
|
|
11
|
+
throw new Error("Invalid native timestamp");
|
|
12
12
|
return date.toISOString();
|
|
13
13
|
};
|
|
14
14
|
/** Partial trailing records are retried, never parsed or acknowledged as complete. */
|
|
@@ -24,9 +24,9 @@ export async function readNativeTranscript(path, harness, options = {}) {
|
|
|
24
24
|
pendingBytes += bytes.length;
|
|
25
25
|
checksum.update(bytes);
|
|
26
26
|
if (pendingBytes > 32 * 1024 * 1024)
|
|
27
|
-
throw new Error("Native record is too large
|
|
27
|
+
throw new Error("Native record is too large");
|
|
28
28
|
if (offset + pendingBytes > 128 * 1024 * 1024)
|
|
29
|
-
throw new Error("Native transcript exceeds the capture limit; original retained
|
|
29
|
+
throw new Error("Native transcript exceeds the capture limit; original retained");
|
|
30
30
|
};
|
|
31
31
|
for await (const chunk of createReadStream(path)) {
|
|
32
32
|
let start = 0;
|
|
@@ -47,7 +47,7 @@ export async function readNativeTranscript(path, harness, options = {}) {
|
|
|
47
47
|
value = record(JSON.parse(line.toString("utf8")));
|
|
48
48
|
}
|
|
49
49
|
catch {
|
|
50
|
-
throw new Error("Invalid native JSON record; original retained
|
|
50
|
+
throw new Error("Invalid native JSON record; original retained");
|
|
51
51
|
}
|
|
52
52
|
lines.push({ value, startBytes, endBytes: offset, sha256 });
|
|
53
53
|
}
|
|
@@ -56,24 +56,24 @@ export async function readNativeTranscript(path, harness, options = {}) {
|
|
|
56
56
|
append(chunk.subarray(start));
|
|
57
57
|
}
|
|
58
58
|
if (!lines.length)
|
|
59
|
-
throw new Error("Native transcript is empty
|
|
59
|
+
throw new Error("Native transcript is empty");
|
|
60
60
|
return { ...(harness === "pi" ? parsePiTranscript(lines, options) : parseCodexTranscript(lines)), prefixes };
|
|
61
61
|
}
|
|
62
62
|
function parsePiTranscript(lines, options) {
|
|
63
63
|
const header = lines[0]?.value ?? {};
|
|
64
64
|
if (header.type !== "session" || !text(header.id))
|
|
65
|
-
throw new Error("Invalid Pi session
|
|
65
|
+
throw new Error("Invalid Pi session");
|
|
66
66
|
const entries = new Map(lines.slice(1).filter((line) => text(line.value.id)).map((line) => [text(line.value.id), line]));
|
|
67
67
|
const branch = [];
|
|
68
68
|
let leaf = options.leafId ?? (lines.length > 1 ? text(lines.at(-1)?.value.id) : "");
|
|
69
69
|
const visited = new Set();
|
|
70
70
|
while (leaf) {
|
|
71
71
|
if (visited.has(leaf))
|
|
72
|
-
throw new Error("Cyclic Pi history
|
|
72
|
+
throw new Error("Cyclic Pi history");
|
|
73
73
|
visited.add(leaf);
|
|
74
74
|
const entry = entries.get(leaf);
|
|
75
75
|
if (!entry)
|
|
76
|
-
throw new Error("Pi parent history is missing
|
|
76
|
+
throw new Error("Pi parent history is missing");
|
|
77
77
|
branch.push(entry);
|
|
78
78
|
leaf = text(entry.value.parentId);
|
|
79
79
|
}
|
|
@@ -128,7 +128,7 @@ function parsePiTranscript(lines, options) {
|
|
|
128
128
|
else if (current && message.role === "toolResult") {
|
|
129
129
|
const assistant = messages.at(-1);
|
|
130
130
|
if (!assistant)
|
|
131
|
-
throw new Error("Pi tool result has no assistant Turn
|
|
131
|
+
throw new Error("Pi tool result has no assistant Turn");
|
|
132
132
|
assistant.content.push({ type: "tool_result", tool_use_id: text(message.toolCallId), content: typeof message.content === "string" ? message.content : piContent(message.content), is_error: Boolean(message.isError) });
|
|
133
133
|
}
|
|
134
134
|
if (current) {
|
|
@@ -158,9 +158,9 @@ function parseCodexTranscript(lines) {
|
|
|
158
158
|
const header = lines[0]?.value ?? {};
|
|
159
159
|
const metadata = record(header.payload);
|
|
160
160
|
if (header.type !== "session_meta" || !text(metadata.id))
|
|
161
|
-
throw new Error("Invalid Codex session
|
|
161
|
+
throw new Error("Invalid Codex session");
|
|
162
162
|
if (metadata.history_base || metadata.fork_source)
|
|
163
|
-
throw new Error("Codex history references another rollout; retain the original and materialize its full history first
|
|
163
|
+
throw new Error("Codex history references another rollout; retain the original and materialize its full history first");
|
|
164
164
|
const turns = [];
|
|
165
165
|
let current = null;
|
|
166
166
|
let messages = [];
|
|
@@ -188,7 +188,7 @@ function parseCodexTranscript(lines) {
|
|
|
188
188
|
turns.push(current);
|
|
189
189
|
current = { key: text(payload.turn_id), parentKey: turns.at(-1)?.key ?? null, userContent: [], messages: [], startedAt: iso(entry.timestamp), startBytes: line.startBytes, endBytes: line.endBytes, contentEndBytes: line.endBytes, boundaries: {}, sha256: line.sha256, result: null };
|
|
190
190
|
if (!current.key)
|
|
191
|
-
throw new Error("Codex Turn identity is missing
|
|
191
|
+
throw new Error("Codex Turn identity is missing");
|
|
192
192
|
messages = current.messages;
|
|
193
193
|
userFromResponse = false;
|
|
194
194
|
}
|
|
@@ -255,7 +255,7 @@ function parseCodexTranscript(lines) {
|
|
|
255
255
|
}
|
|
256
256
|
if (entry.type === "event_msg" && ["turn_complete", "task_complete", "turn_aborted"].includes(text(payload.type))) {
|
|
257
257
|
if (payload.turn_id && payload.turn_id !== current.key)
|
|
258
|
-
throw new Error("Codex Turn boundary mismatch
|
|
258
|
+
throw new Error("Codex Turn boundary mismatch");
|
|
259
259
|
if (!messages.length && text(payload.last_agent_message))
|
|
260
260
|
messages.push({ content: [{ type: "text", text: text(payload.last_agent_message) }], model, provider });
|
|
261
261
|
const errorMessage = text(record(payload.error).message);
|