@neta-art/cohub-cli 8.0.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -16
- package/dist/commands/runtime.js +190 -87
- package/dist/commands/sandboxd-binary.d.ts +1 -1
- package/dist/commands/sandboxd-binary.js +8 -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/diagnostics.js +8 -1
- 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 +29 -23
- package/dist/runtime/native-attach.d.ts +28 -0
- package/dist/runtime/native-attach.js +47 -0
- package/dist/runtime/native-codex-hook.js +3 -3
- package/dist/runtime/native-install.js +9 -9
- package/dist/runtime/native-ipc.d.ts +2 -0
- package/dist/runtime/native-ipc.js +7 -5
- package/dist/runtime/native-pi-extension.js +1 -1
- package/dist/runtime/native-sync-store.d.ts +6 -1
- package/dist/runtime/native-sync-store.js +24 -21
- package/dist/runtime/native-sync.d.ts +25 -0
- package/dist/runtime/native-sync.js +89 -8
- package/dist/runtime/native-transcript.d.ts +8 -0
- package/dist/runtime/native-transcript.js +45 -16
- package/dist/runtime/presentation.d.ts +10 -1
- package/dist/runtime/presentation.js +47 -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 -5
- package/package.json +2 -2
|
@@ -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
|
}
|
|
@@ -112,15 +112,15 @@ export class NativeSyncStore {
|
|
|
112
112
|
await atomicRuntimeJson(this.bindingPath(), binding);
|
|
113
113
|
return binding;
|
|
114
114
|
}
|
|
115
|
-
async capture(path, transcript) {
|
|
115
|
+
async capture(path, transcript, context = {}) {
|
|
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;
|
|
@@ -156,9 +156,11 @@ export class NativeSyncStore {
|
|
|
156
156
|
const result = turn.result ? nativeTurnCompleteSchema.parse(turn.result) : null;
|
|
157
157
|
const receipt = { version: 1, turnId, key: turn.key, parentKey, parentCloudTurnId: parentKey ? null : parentCloudTurnId,
|
|
158
158
|
userContent: turn.userContent, startedAt: turn.startedAt, endBytes: turn.endBytes, contentEndBytes: turn.contentEndBytes, result,
|
|
159
|
+
...(context.sessionStartedAt ? { sessionStartedAt: context.sessionStartedAt } : {}),
|
|
160
|
+
...(context.origin ? { origin: context.origin } : {}),
|
|
159
161
|
...(!result ? { progress: nativeTurnProgressSchema.parse({ revision: turn.endBytes, messages: turn.messages }) } : {}) };
|
|
160
162
|
if (old && (JSON.stringify(old.userContent) !== JSON.stringify(receipt.userContent) || old.parentKey !== receipt.parentKey))
|
|
161
|
-
throw new Error("Native Turn changed; original receipt retained
|
|
163
|
+
throw new Error("Native Turn changed; original receipt retained");
|
|
162
164
|
// Capture immutable native bytes before publishing the completed receipt. Subsequent Turns may change the source.
|
|
163
165
|
if (result)
|
|
164
166
|
await this.archives.stage({ sessionId: binding.originSessionId, harness: binding.harness, nativeSessionId: binding.nativeSessionId, path, sizeBytes: turn.endBytes, expectedChecksum: turn.sha256 }, turnId);
|
|
@@ -186,7 +188,7 @@ export class NativeSyncStore {
|
|
|
186
188
|
continue;
|
|
187
189
|
}
|
|
188
190
|
if (receipt?.version !== 1 || receipt.turnId !== this.turnId(receipt.key))
|
|
189
|
-
throw new Error("Native receipt is corrupt; original retained
|
|
191
|
+
throw new Error("Native receipt is corrupt; original retained");
|
|
190
192
|
receipts.push(receipt);
|
|
191
193
|
}
|
|
192
194
|
return receipts.sort((a, b) => a.endBytes - b.endBytes || a.turnId.localeCompare(b.turnId));
|
|
@@ -233,21 +235,22 @@ export class NativeSyncStore {
|
|
|
233
235
|
parent = await readJson(this.acknowledgementPath(parentId));
|
|
234
236
|
}
|
|
235
237
|
if (!parent)
|
|
236
|
-
throw new Error("Parent binding is missing
|
|
238
|
+
throw new Error("Parent binding is missing");
|
|
237
239
|
}
|
|
238
240
|
let request = await readJson(this.requestPath(receipt.turnId));
|
|
239
241
|
if (!request) {
|
|
240
242
|
request = nativeTurnStartSchema.parse({ turnId: receipt.turnId, sessionId: parent?.sessionId ?? binding.sessionId, parentTurnId: parent?.turnId ?? receipt.parentCloudTurnId,
|
|
241
|
-
branchSessionId: nativeStableId(`${receipt.turnId}:branch`), harness: binding.harness, nativeSessionId: binding.nativeSessionId, userContent: receipt.userContent, startedAt: receipt.startedAt
|
|
243
|
+
branchSessionId: nativeStableId(`${receipt.turnId}:branch`), harness: binding.harness, nativeSessionId: binding.nativeSessionId, userContent: receipt.userContent, startedAt: receipt.startedAt,
|
|
244
|
+
...(receipt.sessionStartedAt ? { sessionStartedAt: receipt.sessionStartedAt } : {}), ...(receipt.origin ? { origin: receipt.origin } : {}) });
|
|
242
245
|
await atomicRuntimeJson(this.requestPath(receipt.turnId), request);
|
|
243
246
|
}
|
|
244
247
|
let remote = await readJson(this.cloudBindingPath(receipt.turnId));
|
|
245
248
|
if (!remote) {
|
|
246
249
|
if (!transport.startNativeTurn)
|
|
247
|
-
throw new Error("Native Runtime WS is unavailable
|
|
250
|
+
throw new Error("Native Runtime WS is unavailable");
|
|
248
251
|
remote = await transport.startNativeTurn(request, { signal });
|
|
249
252
|
if (remote.turnId !== receipt.turnId)
|
|
250
|
-
throw new Error("Server Turn identity mismatch
|
|
253
|
+
throw new Error("Server Turn identity mismatch");
|
|
251
254
|
await atomicRuntimeJson(this.cloudBindingPath(receipt.turnId), remote);
|
|
252
255
|
}
|
|
253
256
|
if (!receipt.result) {
|
|
@@ -267,7 +270,7 @@ export class NativeSyncStore {
|
|
|
267
270
|
return false;
|
|
268
271
|
}
|
|
269
272
|
if (!transport.completeNativeTurn)
|
|
270
|
-
throw new Error("Native Runtime WS is unavailable
|
|
273
|
+
throw new Error("Native Runtime WS is unavailable");
|
|
271
274
|
// Artifact retries back off: the terminal state is durable, so hammering the completion
|
|
272
275
|
// endpoint every flush cycle (5s) while object storage is down only adds load.
|
|
273
276
|
const backoffPath = join(this.root, "backoff", `${receipt.turnId}.json`);
|
|
@@ -279,7 +282,7 @@ export class NativeSyncStore {
|
|
|
279
282
|
// Terminal state is durable; only the artifact snapshot is missing. Keep the receipt pending
|
|
280
283
|
// and retry on the next flush cycle (>= 30s) until artifacts persist.
|
|
281
284
|
await atomicRuntimeJson(backoffPath, { at: Date.now() });
|
|
282
|
-
throw new Error("Native artifacts are pending; completion replays later
|
|
285
|
+
throw new Error("Native artifacts are pending; completion replays later");
|
|
283
286
|
}
|
|
284
287
|
await rm(backoffPath, { force: true });
|
|
285
288
|
await atomicRuntimeJson(this.acknowledgementPath(receipt.turnId), remote);
|
|
@@ -327,7 +330,7 @@ export class NativeSyncStore {
|
|
|
327
330
|
async cloudArchive(index) {
|
|
328
331
|
const binding = await readJson(this.acknowledgementPath(index.turnId));
|
|
329
332
|
if (!binding)
|
|
330
|
-
throw new Error("Native Turn result is not confirmed
|
|
333
|
+
throw new Error("Native Turn result is not confirmed");
|
|
331
334
|
if (index.parentTurnId) {
|
|
332
335
|
const parent = await readJson(this.acknowledgementPath(index.parentTurnId));
|
|
333
336
|
if (parent?.sessionId === binding.sessionId)
|
|
@@ -338,7 +341,7 @@ export class NativeSyncStore {
|
|
|
338
341
|
let parentId = index.parentTurnId;
|
|
339
342
|
while (parentId) {
|
|
340
343
|
if (visited.has(parentId))
|
|
341
|
-
throw new Error("Cyclic native archive
|
|
344
|
+
throw new Error("Cyclic native archive");
|
|
342
345
|
visited.add(parentId);
|
|
343
346
|
const previous = harnessArchiveIndexSchema.parse(await readJson(join(this.archives.root, "versions", `${parentId}.json`)));
|
|
344
347
|
segments.unshift(...previous.segments);
|
|
@@ -9,6 +9,29 @@ export type NativeSyncConfig = {
|
|
|
9
9
|
};
|
|
10
10
|
export declare const nativeRuntimeRoot: (spaceId: string) => string;
|
|
11
11
|
export declare const nativeSyncConfigPath: (runtimeRoot: string, identity: string) => string;
|
|
12
|
+
export type NativeImportCandidate = {
|
|
13
|
+
harness: "pi" | "codex";
|
|
14
|
+
path: string;
|
|
15
|
+
nativeSessionId: string;
|
|
16
|
+
turnCount: number;
|
|
17
|
+
sessionStartedAt: string | undefined;
|
|
18
|
+
};
|
|
19
|
+
/** Find existing native transcripts for exactly one bound project. */
|
|
20
|
+
export declare function discoverNativeImportCandidates(root: string, harnesses: ("pi" | "codex")[], options?: {
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
onProgress?: (progress: {
|
|
23
|
+
harness: "pi" | "codex";
|
|
24
|
+
scanned: number;
|
|
25
|
+
candidates: number;
|
|
26
|
+
}) => void;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
candidates: NativeImportCandidate[];
|
|
29
|
+
errors: {
|
|
30
|
+
harness: "pi" | "codex";
|
|
31
|
+
path: string;
|
|
32
|
+
message: string;
|
|
33
|
+
}[];
|
|
34
|
+
}>;
|
|
12
35
|
export declare function nativeArchiveTransport(spaceId: string, identity: string): Pick<NativeSyncTransport, "prepareRuntimeArchive" | "commitRuntimeArchive" | "getRuntimeArchive">;
|
|
13
36
|
export declare function readNativeSyncConfig(runtimeRoot: string, identity: string): Promise<NativeSyncConfig | null>;
|
|
14
37
|
/** Local capture only. Neither Pi callbacks nor Codex hooks wait for Cohub's network. */
|
|
@@ -19,6 +42,8 @@ export declare function captureNativeSession(input: {
|
|
|
19
42
|
nativeSessionId?: string;
|
|
20
43
|
settled?: boolean;
|
|
21
44
|
leafId?: string | null;
|
|
45
|
+
sessionStartedAt?: string;
|
|
46
|
+
origin?: "local_import";
|
|
22
47
|
}): Promise<NativeSyncStore | null>;
|
|
23
48
|
/** The existing Runtime supervisor retries receipts even after the original terminal has exited. */
|
|
24
49
|
export declare function nativeWebSocketTransport(spaceId: string, identity: string, send: (event: NativeRuntimeEvent) => Promise<unknown>): NativeSyncTransport;
|
|
@@ -1,22 +1,103 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { createClient } from "../client.js";
|
|
5
5
|
import { currentIdentityKey } from "../space.js";
|
|
6
6
|
import { canonicalRuntimeRoot, getRuntimeSpaceBinding } from "./space-binding.js";
|
|
7
|
-
import { readNativeTranscript } from "./native-transcript.js";
|
|
7
|
+
import { readNativeTranscript, readNativeTranscriptHeader } from "./native-transcript.js";
|
|
8
8
|
import { findRuntimeNativeSession } from "./session-store.js";
|
|
9
9
|
import { listNativeSyncStores, nativeIdentityHash, NativeSyncStore } from "./native-sync-store.js";
|
|
10
10
|
export const nativeRuntimeRoot = (spaceId) => join(homedir(), ".local", "state", "cohub", "runtime", spaceId);
|
|
11
11
|
export const nativeSyncConfigPath = (runtimeRoot, identity) => join(runtimeRoot, "native", nativeIdentityHash(identity), "config.json");
|
|
12
|
+
const piSessionDirectory = (cwd) => {
|
|
13
|
+
const custom = process.env.PI_CODING_AGENT_SESSION_DIR?.trim();
|
|
14
|
+
if (custom)
|
|
15
|
+
return custom;
|
|
16
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent");
|
|
17
|
+
const safePath = `--${cwd.replace(/^[/\\\\]/, "").replace(/[/\\\\:]/g, "-")}--`;
|
|
18
|
+
return join(agentDir, "sessions", safePath);
|
|
19
|
+
};
|
|
20
|
+
const codexSessionDirectory = () => join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "sessions");
|
|
21
|
+
const LARGE_TRANSCRIPT_BYTES = 64 * 1024 * 1024;
|
|
22
|
+
async function nativeTranscriptPaths(root, harness, signal) {
|
|
23
|
+
const pending = [harness === "pi" ? piSessionDirectory(root) : codexSessionDirectory()];
|
|
24
|
+
const paths = [];
|
|
25
|
+
const errors = [];
|
|
26
|
+
while (pending.length) {
|
|
27
|
+
signal?.throwIfAborted();
|
|
28
|
+
const directory = pending.pop();
|
|
29
|
+
if (!directory)
|
|
30
|
+
continue;
|
|
31
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
|
|
32
|
+
if (error.code === "ENOENT")
|
|
33
|
+
return [];
|
|
34
|
+
errors.push({ harness, path: directory, message: error instanceof Error ? error.message : String(error) });
|
|
35
|
+
return [];
|
|
36
|
+
});
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const path = join(directory, entry.name);
|
|
39
|
+
if (entry.isDirectory())
|
|
40
|
+
pending.push(path);
|
|
41
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
42
|
+
paths.push(path);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { paths, errors };
|
|
46
|
+
}
|
|
47
|
+
/** Find existing native transcripts for exactly one bound project. */
|
|
48
|
+
export async function discoverNativeImportCandidates(root, harnesses, options = {}) {
|
|
49
|
+
const candidates = [];
|
|
50
|
+
const errors = [];
|
|
51
|
+
for (const harness of harnesses) {
|
|
52
|
+
options.signal?.throwIfAborted();
|
|
53
|
+
const scanned = await nativeTranscriptPaths(root, harness, options.signal);
|
|
54
|
+
errors.push(...scanned.errors);
|
|
55
|
+
let start = 0;
|
|
56
|
+
while (start < scanned.paths.length) {
|
|
57
|
+
options.signal?.throwIfAborted();
|
|
58
|
+
const paths = scanned.paths.slice(start, start + 2);
|
|
59
|
+
const sizes = await Promise.all(paths.map((path) => stat(path).then((value) => value.size).catch(() => 0)));
|
|
60
|
+
const concurrency = sizes.some((size) => size > LARGE_TRANSCRIPT_BYTES) ? 1 : 2;
|
|
61
|
+
const batch = await Promise.all(paths.slice(0, concurrency).map(async (path) => {
|
|
62
|
+
try {
|
|
63
|
+
const header = await readNativeTranscriptHeader(path, harness, options.signal);
|
|
64
|
+
if (await canonicalRuntimeRoot(header.cwd) !== root)
|
|
65
|
+
return null;
|
|
66
|
+
const transcript = await readNativeTranscript(path, harness, { settled: true, signal: options.signal });
|
|
67
|
+
if (transcript.turns.length === 0)
|
|
68
|
+
return { error: { harness, path, message: "Transcript contains no importable Turns" } };
|
|
69
|
+
return { harness, path, nativeSessionId: transcript.nativeSessionId, turnCount: transcript.turns.length,
|
|
70
|
+
sessionStartedAt: transcript.turns.map((turn) => turn.startedAt).sort()[0] };
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return { error: { harness, path, message: error instanceof Error ? error.message : String(error) } };
|
|
74
|
+
}
|
|
75
|
+
}));
|
|
76
|
+
for (const result of batch) {
|
|
77
|
+
options.signal?.throwIfAborted();
|
|
78
|
+
if (!result)
|
|
79
|
+
continue;
|
|
80
|
+
if ("error" in result) {
|
|
81
|
+
if (result.error)
|
|
82
|
+
errors.push(result.error);
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
candidates.push(result);
|
|
86
|
+
options.onProgress?.({ harness, scanned: start + batch.length, candidates: candidates.length });
|
|
87
|
+
}
|
|
88
|
+
start += concurrency;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { candidates, errors };
|
|
92
|
+
}
|
|
12
93
|
export function nativeArchiveTransport(spaceId, identity) {
|
|
13
94
|
const client = createClient().space(spaceId);
|
|
14
95
|
const guard = async (task) => {
|
|
15
96
|
if (currentIdentityKey() !== identity)
|
|
16
|
-
throw new Error("Native sync account changed
|
|
97
|
+
throw new Error("Native sync account changed");
|
|
17
98
|
const result = await task();
|
|
18
99
|
if (currentIdentityKey() !== identity)
|
|
19
|
-
throw new Error("Native sync account changed
|
|
100
|
+
throw new Error("Native sync account changed");
|
|
20
101
|
return result;
|
|
21
102
|
};
|
|
22
103
|
return {
|
|
@@ -29,7 +110,7 @@ export async function readNativeSyncConfig(runtimeRoot, identity) {
|
|
|
29
110
|
try {
|
|
30
111
|
const config = JSON.parse(await readFile(nativeSyncConfigPath(runtimeRoot, identity), "utf8"));
|
|
31
112
|
if (config.version !== 1 || config.identity !== identity || !Array.isArray(config.harnesses))
|
|
32
|
-
throw new Error("Invalid native sync configuration
|
|
113
|
+
throw new Error("Invalid native sync configuration");
|
|
33
114
|
return config;
|
|
34
115
|
}
|
|
35
116
|
catch (error) {
|
|
@@ -57,7 +138,7 @@ export async function captureNativeSession(input) {
|
|
|
57
138
|
const path = await canonicalRuntimeRoot(input.path);
|
|
58
139
|
const transcript = await readNativeTranscript(path, input.harness, input);
|
|
59
140
|
if (await canonicalRuntimeRoot(transcript.cwd) !== root || input.nativeSessionId && transcript.nativeSessionId !== input.nativeSessionId)
|
|
60
|
-
throw new Error("Native transcript belongs to another project or Session
|
|
141
|
+
throw new Error("Native transcript belongs to another project or Session");
|
|
61
142
|
const key = JSON.stringify([identity, space.spaceId, input.harness, transcript.nativeSessionId, path]);
|
|
62
143
|
let store = nativeStores.get(key);
|
|
63
144
|
if (!store) {
|
|
@@ -74,7 +155,7 @@ export async function captureNativeSession(input) {
|
|
|
74
155
|
const managedPath = managed ? await canonicalRuntimeRoot(managed.path).catch((error) => { if (error.code === "ENOENT")
|
|
75
156
|
return null; throw error; }) : null;
|
|
76
157
|
if (candidates.length && managedPath !== path)
|
|
77
|
-
throw new Error("Native path changed; original bindings retained
|
|
158
|
+
throw new Error("Native path changed; original bindings retained");
|
|
78
159
|
// Restored Pi working copies can share a native ID. Existing Cohub sidecars disambiguate them.
|
|
79
160
|
store = new NativeSyncStore({ runtimeRoot, spaceId: space.spaceId, identity, harness: input.harness, nativeSessionId: transcript.nativeSessionId,
|
|
80
161
|
instanceKey: managedPath === path ? path : undefined, transport });
|
|
@@ -83,7 +164,7 @@ export async function captureNativeSession(input) {
|
|
|
83
164
|
nativeStores.delete(nativeStores.keys().next().value ?? "");
|
|
84
165
|
nativeStores.set(key, store);
|
|
85
166
|
}
|
|
86
|
-
await store.capture(path, transcript);
|
|
167
|
+
await store.capture(path, transcript, { sessionStartedAt: input.sessionStartedAt, origin: input.origin });
|
|
87
168
|
return store;
|
|
88
169
|
}
|
|
89
170
|
/** The existing Runtime supervisor retries receipts even after the original terminal has exited. */
|
|
@@ -20,8 +20,16 @@ export type NativeTranscript = {
|
|
|
20
20
|
turns: NativeTranscriptTurn[];
|
|
21
21
|
prefixes: ReadonlyMap<number, string>;
|
|
22
22
|
};
|
|
23
|
+
export declare const NATIVE_MAX_RECORD_BYTES: number;
|
|
24
|
+
export declare const NATIVE_MAX_TRANSCRIPT_BYTES: number;
|
|
25
|
+
/** Read only the session header before deciding whether a global Codex file belongs to this project. */
|
|
26
|
+
export declare function readNativeTranscriptHeader(path: string, harness: "pi" | "codex", signal?: AbortSignal): Promise<{
|
|
27
|
+
nativeSessionId: string;
|
|
28
|
+
cwd: string;
|
|
29
|
+
}>;
|
|
23
30
|
/** Partial trailing records are retried, never parsed or acknowledged as complete. */
|
|
24
31
|
export declare function readNativeTranscript(path: string, harness: "pi" | "codex", options?: {
|
|
25
32
|
settled?: boolean;
|
|
26
33
|
leafId?: string | null;
|
|
34
|
+
signal?: AbortSignal;
|
|
27
35
|
}): Promise<NativeTranscript>;
|
|
@@ -8,9 +8,38 @@ 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
|
+
export const NATIVE_MAX_RECORD_BYTES = 256 * 1024 * 1024;
|
|
15
|
+
export const NATIVE_MAX_TRANSCRIPT_BYTES = 1024 * 1024 * 1024;
|
|
16
|
+
const NATIVE_HEADER_MAX_BYTES = 1024 * 1024;
|
|
17
|
+
/** Read only the session header before deciding whether a global Codex file belongs to this project. */
|
|
18
|
+
export async function readNativeTranscriptHeader(path, harness, signal) {
|
|
19
|
+
const stream = createReadStream(path, { signal });
|
|
20
|
+
let buffer = Buffer.alloc(0);
|
|
21
|
+
try {
|
|
22
|
+
for await (const chunk of stream) {
|
|
23
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
24
|
+
const newline = buffer.indexOf(10);
|
|
25
|
+
if (newline >= 0) {
|
|
26
|
+
const value = record(JSON.parse(buffer.subarray(0, newline).toString("utf8")));
|
|
27
|
+
const payload = record(value.payload);
|
|
28
|
+
const nativeSessionId = harness === "pi" ? text(value.id) : text(payload.id);
|
|
29
|
+
const cwd = harness === "pi" ? text(value.cwd) : text(payload.cwd);
|
|
30
|
+
if ((harness === "pi" && value.type !== "session") || (harness === "codex" && value.type !== "session_meta") || !nativeSessionId || !cwd)
|
|
31
|
+
throw new Error("Invalid native session header");
|
|
32
|
+
return { nativeSessionId, cwd };
|
|
33
|
+
}
|
|
34
|
+
if (buffer.length > NATIVE_HEADER_MAX_BYTES)
|
|
35
|
+
throw new Error("Native session header is too large");
|
|
36
|
+
}
|
|
37
|
+
throw new Error("Native transcript is empty");
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
stream.destroy();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
14
43
|
/** Partial trailing records are retried, never parsed or acknowledged as complete. */
|
|
15
44
|
export async function readNativeTranscript(path, harness, options = {}) {
|
|
16
45
|
const lines = [];
|
|
@@ -23,12 +52,12 @@ export async function readNativeTranscript(path, harness, options = {}) {
|
|
|
23
52
|
fragments.push(bytes);
|
|
24
53
|
pendingBytes += bytes.length;
|
|
25
54
|
checksum.update(bytes);
|
|
26
|
-
if (pendingBytes >
|
|
27
|
-
throw new Error("Native record
|
|
28
|
-
if (offset + pendingBytes >
|
|
29
|
-
throw new Error("Native transcript exceeds the
|
|
55
|
+
if (pendingBytes > NATIVE_MAX_RECORD_BYTES)
|
|
56
|
+
throw new Error("Native record exceeds the 256 MiB local parse limit; original retained");
|
|
57
|
+
if (offset + pendingBytes > NATIVE_MAX_TRANSCRIPT_BYTES)
|
|
58
|
+
throw new Error("Native transcript exceeds the 1 GiB local parse limit; original retained");
|
|
30
59
|
};
|
|
31
|
-
for await (const chunk of createReadStream(path)) {
|
|
60
|
+
for await (const chunk of createReadStream(path, { signal: options.signal })) {
|
|
32
61
|
let start = 0;
|
|
33
62
|
for (let end = chunk.indexOf(10); end >= 0; end = chunk.indexOf(10, start)) {
|
|
34
63
|
append(chunk.subarray(start, end + 1));
|
|
@@ -47,7 +76,7 @@ export async function readNativeTranscript(path, harness, options = {}) {
|
|
|
47
76
|
value = record(JSON.parse(line.toString("utf8")));
|
|
48
77
|
}
|
|
49
78
|
catch {
|
|
50
|
-
throw new Error("Invalid native JSON record; original retained
|
|
79
|
+
throw new Error("Invalid native JSON record; original retained");
|
|
51
80
|
}
|
|
52
81
|
lines.push({ value, startBytes, endBytes: offset, sha256 });
|
|
53
82
|
}
|
|
@@ -56,24 +85,24 @@ export async function readNativeTranscript(path, harness, options = {}) {
|
|
|
56
85
|
append(chunk.subarray(start));
|
|
57
86
|
}
|
|
58
87
|
if (!lines.length)
|
|
59
|
-
throw new Error("Native transcript is empty
|
|
88
|
+
throw new Error("Native transcript is empty");
|
|
60
89
|
return { ...(harness === "pi" ? parsePiTranscript(lines, options) : parseCodexTranscript(lines)), prefixes };
|
|
61
90
|
}
|
|
62
91
|
function parsePiTranscript(lines, options) {
|
|
63
92
|
const header = lines[0]?.value ?? {};
|
|
64
93
|
if (header.type !== "session" || !text(header.id))
|
|
65
|
-
throw new Error("Invalid Pi session
|
|
94
|
+
throw new Error("Invalid Pi session");
|
|
66
95
|
const entries = new Map(lines.slice(1).filter((line) => text(line.value.id)).map((line) => [text(line.value.id), line]));
|
|
67
96
|
const branch = [];
|
|
68
97
|
let leaf = options.leafId ?? (lines.length > 1 ? text(lines.at(-1)?.value.id) : "");
|
|
69
98
|
const visited = new Set();
|
|
70
99
|
while (leaf) {
|
|
71
100
|
if (visited.has(leaf))
|
|
72
|
-
throw new Error("Cyclic Pi history
|
|
101
|
+
throw new Error("Cyclic Pi history");
|
|
73
102
|
visited.add(leaf);
|
|
74
103
|
const entry = entries.get(leaf);
|
|
75
104
|
if (!entry)
|
|
76
|
-
throw new Error("Pi parent history is missing
|
|
105
|
+
throw new Error("Pi parent history is missing");
|
|
77
106
|
branch.push(entry);
|
|
78
107
|
leaf = text(entry.value.parentId);
|
|
79
108
|
}
|
|
@@ -128,7 +157,7 @@ function parsePiTranscript(lines, options) {
|
|
|
128
157
|
else if (current && message.role === "toolResult") {
|
|
129
158
|
const assistant = messages.at(-1);
|
|
130
159
|
if (!assistant)
|
|
131
|
-
throw new Error("Pi tool result has no assistant Turn
|
|
160
|
+
throw new Error("Pi tool result has no assistant Turn");
|
|
132
161
|
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
162
|
}
|
|
134
163
|
if (current) {
|
|
@@ -158,9 +187,9 @@ function parseCodexTranscript(lines) {
|
|
|
158
187
|
const header = lines[0]?.value ?? {};
|
|
159
188
|
const metadata = record(header.payload);
|
|
160
189
|
if (header.type !== "session_meta" || !text(metadata.id))
|
|
161
|
-
throw new Error("Invalid Codex session
|
|
190
|
+
throw new Error("Invalid Codex session");
|
|
162
191
|
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
|
|
192
|
+
throw new Error("Codex history references another rollout; retain the original and materialize its full history first");
|
|
164
193
|
const turns = [];
|
|
165
194
|
let current = null;
|
|
166
195
|
let messages = [];
|
|
@@ -188,7 +217,7 @@ function parseCodexTranscript(lines) {
|
|
|
188
217
|
turns.push(current);
|
|
189
218
|
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
219
|
if (!current.key)
|
|
191
|
-
throw new Error("Codex Turn identity is missing
|
|
220
|
+
throw new Error("Codex Turn identity is missing");
|
|
192
221
|
messages = current.messages;
|
|
193
222
|
userFromResponse = false;
|
|
194
223
|
}
|
|
@@ -255,7 +284,7 @@ function parseCodexTranscript(lines) {
|
|
|
255
284
|
}
|
|
256
285
|
if (entry.type === "event_msg" && ["turn_complete", "task_complete", "turn_aborted"].includes(text(payload.type))) {
|
|
257
286
|
if (payload.turn_id && payload.turn_id !== current.key)
|
|
258
|
-
throw new Error("Codex Turn boundary mismatch
|
|
287
|
+
throw new Error("Codex Turn boundary mismatch");
|
|
259
288
|
if (!messages.length && text(payload.last_agent_message))
|
|
260
289
|
messages.push({ content: [{ type: "text", text: text(payload.last_agent_message) }], model, provider });
|
|
261
290
|
const errorMessage = text(record(payload.error).message);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NativeSyncConfig } from "./native-sync.js";
|
|
1
2
|
import type { RuntimeDiagnostic, RuntimeDiagnosticLevel } from "./diagnostics.js";
|
|
2
3
|
export declare const diagnosticLevels: RuntimeDiagnosticLevel[];
|
|
3
4
|
export declare const atLeastLevel: (level: RuntimeDiagnosticLevel, minimum: RuntimeDiagnosticLevel) => boolean;
|
|
@@ -16,6 +17,14 @@ export type RuntimeSummary = {
|
|
|
16
17
|
workspaceConnected: boolean;
|
|
17
18
|
diagnosticsPath: string;
|
|
18
19
|
background: boolean;
|
|
19
|
-
nativeSync?: boolean;
|
|
20
20
|
};
|
|
21
21
|
export declare function printRuntimeSummary(summary: RuntimeSummary, json?: boolean, reused?: boolean): void;
|
|
22
|
+
export type NativeSessionStatus = {
|
|
23
|
+
harness: "pi" | "codex";
|
|
24
|
+
nativeSessionId: string;
|
|
25
|
+
sessionId: string | null;
|
|
26
|
+
pendingTurns: number;
|
|
27
|
+
pendingArchives: number;
|
|
28
|
+
};
|
|
29
|
+
/** `runtime status` native sync block: installation state plus per-Harness pending work. */
|
|
30
|
+
export declare function formatNativeSync(config: NativeSyncConfig | null, sessions: NativeSessionStatus[], error?: string | null): string;
|
|
@@ -3,26 +3,28 @@ export const diagnosticLevels = ["debug", "info", "warn", "error"];
|
|
|
3
3
|
export const atLeastLevel = (level, minimum) => diagnosticLevels.indexOf(level) >= diagnosticLevels.indexOf(minimum);
|
|
4
4
|
export const runtimeWebUrl = (spaceId) => `https://${resolveCohubEnvironment() === "prod" ? "" : "dev."}cohub.live/spaces/${spaceId}`;
|
|
5
5
|
const messages = {
|
|
6
|
-
"runtime.ready": "Harness connected
|
|
7
|
-
"runtime.available": "Runtime ready
|
|
8
|
-
"runtime.websocket.closed": "Connection lost; reconnecting
|
|
9
|
-
"runtime.heartbeat_timeout": "Connection timed out; reconnecting
|
|
10
|
-
"runtime.auth_token_failed": "Cannot obtain credentials; retrying
|
|
11
|
-
"runtime.auth_required": "Sign in with cohub auth login
|
|
12
|
-
"runtime.stopped": "Runtime stopped
|
|
13
|
-
"runtime.failed": "Runtime needs attention
|
|
14
|
-
"runtime.turn_failed": "Turn failed; local files retained
|
|
15
|
-
"runtime.
|
|
16
|
-
"runtime.
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"archive.
|
|
20
|
-
"
|
|
21
|
-
"archive.
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"sandboxd.
|
|
25
|
-
"sandboxd.
|
|
6
|
+
"runtime.ready": "Harness connected",
|
|
7
|
+
"runtime.available": "Runtime ready",
|
|
8
|
+
"runtime.websocket.closed": "Connection lost; reconnecting",
|
|
9
|
+
"runtime.heartbeat_timeout": "Connection timed out; reconnecting",
|
|
10
|
+
"runtime.auth_token_failed": "Cannot obtain credentials; retrying",
|
|
11
|
+
"runtime.auth_required": "Sign in with cohub auth login",
|
|
12
|
+
"runtime.stopped": "Runtime stopped",
|
|
13
|
+
"runtime.failed": "Runtime needs attention",
|
|
14
|
+
"runtime.turn_failed": "Turn failed; local files retained",
|
|
15
|
+
"runtime.turn_cleanup_pending": "Turn result saved; tool cleanup still unconfirmed",
|
|
16
|
+
"runtime.connection_failed": "Connection attempt failed; retrying",
|
|
17
|
+
"runtime.execution_transport_detached": "Execution continues locally; result replays after reconnect",
|
|
18
|
+
"runtime.execution_transport_invalidated": "Execution interrupted; outcome needs reconciliation",
|
|
19
|
+
"archive.upload_pending": "Archive upload pending; local data retained",
|
|
20
|
+
"native.sync_pending": "Native sync pending; local records retained",
|
|
21
|
+
"archive.capture_pending": "Archive capture pending",
|
|
22
|
+
"archive.capture_unavailable": "Archive unavailable; original receipt retained",
|
|
23
|
+
"archive.restore_failed": "Native restore unavailable; using saved history",
|
|
24
|
+
"sandboxd.process_exit": "File bridge stopped; restarting",
|
|
25
|
+
"sandboxd.download": "Preparing file bridge",
|
|
26
|
+
"sandboxd.connected": "File bridge connected",
|
|
27
|
+
"sandboxd.disconnected": "File bridge disconnected; reconnecting",
|
|
26
28
|
};
|
|
27
29
|
export function formatDiagnostic(event, verbose = false) {
|
|
28
30
|
const text = messages[event.event] ?? (typeof event.data?.message === "string" ? event.data.message : event.event);
|
|
@@ -46,7 +48,7 @@ export function createDiagnosticConsole(verbose = false, write = (line) => proce
|
|
|
46
48
|
if (last.size >= 256)
|
|
47
49
|
last.delete(last.keys().next().value ?? "");
|
|
48
50
|
last.set(key, { at: now, suppressed: 0 });
|
|
49
|
-
const repeated = previous?.suppressed ? ` (+${previous.suppressed} repeated
|
|
51
|
+
const repeated = previous?.suppressed ? ` (+${previous.suppressed} repeated)` : "";
|
|
50
52
|
write(`${formatDiagnostic(event, verbose).trimEnd()}${repeated}\n`);
|
|
51
53
|
};
|
|
52
54
|
}
|
|
@@ -56,21 +58,37 @@ export function printRuntimeSummary(summary, json = false, reused = false) {
|
|
|
56
58
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
57
59
|
return;
|
|
58
60
|
}
|
|
59
|
-
const label = summary.state === "ready" ? "Runtime ready
|
|
60
|
-
process.stdout.write(`\n${label}${reused ? " · reused
|
|
61
|
+
const label = summary.state === "ready" ? "Runtime ready" : `Runtime ${summary.state}`;
|
|
62
|
+
process.stdout.write(`\n${label}${reused ? " · reused" : ""}\n\n`);
|
|
61
63
|
const rows = [
|
|
62
|
-
["Space
|
|
63
|
-
["URL
|
|
64
|
-
["Directory
|
|
64
|
+
["Space", summary.spaceId],
|
|
65
|
+
["URL", value.url],
|
|
66
|
+
["Directory", summary.root],
|
|
65
67
|
["Harness", summary.harnesses.join(" · ")],
|
|
66
|
-
["Mode
|
|
68
|
+
["Mode", summary.background ? "Background" : "Foreground"],
|
|
67
69
|
["PID", String(summary.pid)],
|
|
68
|
-
["Logs
|
|
70
|
+
["Logs", summary.diagnosticsPath],
|
|
69
71
|
];
|
|
70
72
|
for (const [name, text] of rows)
|
|
71
73
|
process.stdout.write(` ${name} ${text}\n`);
|
|
72
74
|
process.stdout.write(`\n cohub runtime logs --space ${summary.spaceId} --follow\n cohub runtime down --space ${summary.spaceId}\n`);
|
|
73
75
|
if (!summary.background && !reused)
|
|
74
|
-
process.stdout.write(" Ctrl+C to stop
|
|
76
|
+
process.stdout.write(" Ctrl+C to stop\n");
|
|
75
77
|
process.stdout.write("\n");
|
|
76
78
|
}
|
|
79
|
+
/** `runtime status` native sync block: installation state plus per-Harness pending work. */
|
|
80
|
+
export function formatNativeSync(config, sessions, error = null) {
|
|
81
|
+
if (error)
|
|
82
|
+
return `Native sync unknown — ${error} · fix or remove the config, then runtime up\n`;
|
|
83
|
+
if (!config?.harnesses.length)
|
|
84
|
+
return "Native sync off · run cohub runtime up to enable\n";
|
|
85
|
+
const lines = [`Native sync enabled · ${config.harnesses.join(", ")}`];
|
|
86
|
+
for (const session of sessions) {
|
|
87
|
+
lines.push(` ${session.harness.padEnd(5)} ${session.nativeSessionId.slice(0, 8)} ${session.pendingTurns} Turns · ${session.pendingArchives} archives pending`);
|
|
88
|
+
}
|
|
89
|
+
if (!sessions.length)
|
|
90
|
+
lines.push(" No native chats captured yet");
|
|
91
|
+
else if (sessions.every((session) => !session.pendingTurns && !session.pendingArchives))
|
|
92
|
+
lines.push(" Up to date");
|
|
93
|
+
return `${lines.join("\n")}\n`;
|
|
94
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export declare class ProcessCleanupUncertainError extends Error {
|
|
2
2
|
}
|
|
3
|
+
/** True only when the process group is gone or holds nothing that can execute. */
|
|
4
|
+
export declare function confirmQuiescentProcessGroup(pid: number): Promise<boolean>;
|
|
3
5
|
export declare function stopProcessGroup(pid: number): Promise<void>;
|