@neta-art/cohub-cli 8.1.1 → 8.3.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/dist/commands/runtime.d.ts +0 -12
- package/dist/commands/runtime.js +129 -119
- package/dist/commands/search.js +3 -9
- package/dist/commands/spaces.js +5 -2
- package/dist/output.d.ts +6 -0
- package/dist/output.js +11 -0
- package/dist/runtime/archive-store.d.ts +4 -0
- package/dist/runtime/archive-store.js +10 -6
- package/dist/runtime/codex-usage.d.ts +0 -2
- package/dist/runtime/codex-usage.js +0 -12
- package/dist/runtime/connection.d.ts +5 -5
- package/dist/runtime/connection.js +27 -16
- package/dist/runtime/harness.d.ts +8 -16
- package/dist/runtime/harness.js +1 -366
- package/dist/runtime/instance.d.ts +8 -1
- package/dist/runtime/instance.js +15 -8
- package/dist/runtime/launch.js +10 -5
- package/dist/runtime/native/adapters.d.ts +32 -0
- package/dist/runtime/native/adapters.js +79 -0
- package/dist/runtime/{native-archive.d.ts → native/archive.d.ts} +4 -3
- package/dist/runtime/{native-archive.js → native/archive.js} +7 -14
- package/dist/runtime/native/attach.d.ts +33 -0
- package/dist/runtime/native/attach.js +103 -0
- package/dist/runtime/native/codex-link.d.ts +40 -0
- package/dist/runtime/native/codex-link.js +249 -0
- package/dist/runtime/native/config.d.ts +16 -0
- package/dist/runtime/native/config.js +24 -0
- package/dist/runtime/native/daemon.d.ts +88 -0
- package/dist/runtime/native/daemon.js +367 -0
- package/dist/runtime/native/execution.d.ts +45 -0
- package/dist/runtime/native/execution.js +337 -0
- package/dist/runtime/native/identity.d.ts +9 -0
- package/dist/runtime/native/identity.js +15 -0
- package/dist/runtime/native/ingest.d.ts +241 -0
- package/dist/runtime/native/ingest.js +823 -0
- package/dist/runtime/native/install.d.ts +9 -0
- package/dist/runtime/native/install.js +105 -0
- package/dist/runtime/native/pi-extension.d.ts +35 -0
- package/dist/runtime/native/pi-extension.js +160 -0
- package/dist/runtime/native/pi-link.d.ts +67 -0
- package/dist/runtime/native/pi-link.js +238 -0
- package/dist/runtime/native/results.d.ts +41 -0
- package/dist/runtime/native/results.js +128 -0
- package/dist/runtime/native/sessions.d.ts +45 -0
- package/dist/runtime/native/sessions.js +92 -0
- package/dist/runtime/native/transcript.d.ts +63 -0
- package/dist/runtime/native/transcript.js +594 -0
- package/dist/runtime/native/translate.d.ts +44 -0
- package/dist/runtime/native/translate.js +263 -0
- package/dist/runtime/native/transport.d.ts +7 -0
- package/dist/runtime/native/transport.js +16 -0
- package/dist/runtime/native/version.d.ts +5 -0
- package/dist/runtime/native/version.js +18 -0
- package/dist/runtime/presentation.d.ts +7 -10
- package/dist/runtime/presentation.js +35 -10
- package/dist/runtime/projection-store.d.ts +6 -13
- package/dist/runtime/projection-store.js +11 -74
- package/dist/runtime/supervisor.d.ts +2 -0
- package/dist/runtime/supervisor.js +36 -86
- package/dist/runtime/turn-projection.d.ts +1 -3
- package/dist/runtime/turn-projection.js +3 -10
- package/package.json +4 -5
- package/dist/runtime/native-attach.d.ts +0 -28
- package/dist/runtime/native-attach.js +0 -47
- package/dist/runtime/native-codex-hook.d.ts +0 -1
- package/dist/runtime/native-codex-hook.js +0 -28
- package/dist/runtime/native-install.d.ts +0 -21
- package/dist/runtime/native-install.js +0 -130
- package/dist/runtime/native-ipc.d.ts +0 -37
- package/dist/runtime/native-ipc.js +0 -118
- package/dist/runtime/native-pi-extension.d.ts +0 -20
- package/dist/runtime/native-pi-extension.js +0 -47
- package/dist/runtime/native-sync-store.d.ts +0 -102
- package/dist/runtime/native-sync-store.js +0 -368
- package/dist/runtime/native-sync.d.ts +0 -50
- package/dist/runtime/native-sync.js +0 -221
- package/dist/runtime/native-transcript.d.ts +0 -35
- package/dist/runtime/native-transcript.js +0 -310
- package/dist/runtime/session-store.d.ts +0 -84
- package/dist/runtime/session-store.js +0 -587
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { NativeSyncStore, type NativeSyncTransport } from "./native-sync-store.js";
|
|
2
|
-
import type { NativeRuntimeEvent } from "@neta-art/cohub";
|
|
3
|
-
export type NativeSyncConfig = {
|
|
4
|
-
version: 1;
|
|
5
|
-
identity: string;
|
|
6
|
-
spaceId: string;
|
|
7
|
-
root: string;
|
|
8
|
-
harnesses: ("pi" | "codex")[];
|
|
9
|
-
};
|
|
10
|
-
export declare const nativeRuntimeRoot: (spaceId: string) => string;
|
|
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
|
-
}>;
|
|
35
|
-
export declare function nativeArchiveTransport(spaceId: string, identity: string): Pick<NativeSyncTransport, "prepareRuntimeArchive" | "commitRuntimeArchive" | "getRuntimeArchive">;
|
|
36
|
-
export declare function readNativeSyncConfig(runtimeRoot: string, identity: string): Promise<NativeSyncConfig | null>;
|
|
37
|
-
/** Local capture only. Neither Pi callbacks nor Codex hooks wait for Cohub's network. */
|
|
38
|
-
export declare function captureNativeSession(input: {
|
|
39
|
-
harness: "pi" | "codex";
|
|
40
|
-
cwd: string;
|
|
41
|
-
path: string;
|
|
42
|
-
nativeSessionId?: string;
|
|
43
|
-
settled?: boolean;
|
|
44
|
-
leafId?: string | null;
|
|
45
|
-
sessionStartedAt?: string;
|
|
46
|
-
origin?: "local_import";
|
|
47
|
-
}): Promise<NativeSyncStore | null>;
|
|
48
|
-
/** The existing Runtime supervisor retries receipts even after the original terminal has exited. */
|
|
49
|
-
export declare function nativeWebSocketTransport(spaceId: string, identity: string, send: (event: NativeRuntimeEvent) => Promise<unknown>): NativeSyncTransport;
|
|
50
|
-
export declare function flushNativeSessions(spaceId: string, identity: string, signal: AbortSignal, report: (error: unknown) => void, transportOverride?: NativeSyncTransport): Promise<boolean>;
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { createClient } from "../client.js";
|
|
5
|
-
import { currentIdentityKey } from "../space.js";
|
|
6
|
-
import { canonicalRuntimeRoot, getRuntimeSpaceBinding } from "./space-binding.js";
|
|
7
|
-
import { readNativeTranscript, readNativeTranscriptHeader } from "./native-transcript.js";
|
|
8
|
-
import { findRuntimeNativeSession } from "./session-store.js";
|
|
9
|
-
import { listNativeSyncStores, nativeIdentityHash, NativeSyncStore } from "./native-sync-store.js";
|
|
10
|
-
export const nativeRuntimeRoot = (spaceId) => join(homedir(), ".local", "state", "cohub", "runtime", spaceId);
|
|
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
|
-
}
|
|
93
|
-
export function nativeArchiveTransport(spaceId, identity) {
|
|
94
|
-
const client = createClient().space(spaceId);
|
|
95
|
-
const guard = async (task) => {
|
|
96
|
-
if (currentIdentityKey() !== identity)
|
|
97
|
-
throw new Error("Native sync account changed");
|
|
98
|
-
const result = await task();
|
|
99
|
-
if (currentIdentityKey() !== identity)
|
|
100
|
-
throw new Error("Native sync account changed");
|
|
101
|
-
return result;
|
|
102
|
-
};
|
|
103
|
-
return {
|
|
104
|
-
prepareRuntimeArchive: (...args) => guard(() => client.prepareRuntimeArchive(...args)),
|
|
105
|
-
commitRuntimeArchive: (...args) => guard(() => client.commitRuntimeArchive(...args)),
|
|
106
|
-
getRuntimeArchive: (...args) => guard(() => client.getRuntimeArchive(...args)),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
export async function readNativeSyncConfig(runtimeRoot, identity) {
|
|
110
|
-
try {
|
|
111
|
-
const config = JSON.parse(await readFile(nativeSyncConfigPath(runtimeRoot, identity), "utf8"));
|
|
112
|
-
if (config.version !== 1 || config.identity !== identity || !Array.isArray(config.harnesses))
|
|
113
|
-
throw new Error("Invalid native sync configuration");
|
|
114
|
-
return config;
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
if (error.code === "ENOENT")
|
|
118
|
-
return null;
|
|
119
|
-
throw error;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const nativeStores = new Map();
|
|
123
|
-
const missing = (error) => error.code === "ENOENT";
|
|
124
|
-
/** Canonical alias, but a missing target reports itself instead of failing capture. */
|
|
125
|
-
const canonicalPathOrNull = async (path) => canonicalRuntimeRoot(path).catch((error) => { if (missing(error))
|
|
126
|
-
return null; throw error; });
|
|
127
|
-
/** Local capture only. Neither Pi callbacks nor Codex hooks wait for Cohub's network. */
|
|
128
|
-
export async function captureNativeSession(input) {
|
|
129
|
-
if (process.env.COHUB_TURN_ID || process.env.COHUB_EXECUTION_TOKEN)
|
|
130
|
-
return null;
|
|
131
|
-
const identity = currentIdentityKey();
|
|
132
|
-
if (!identity)
|
|
133
|
-
return null;
|
|
134
|
-
// Pi and Codex create their transcripts lazily, so a pending capture for a file that does
|
|
135
|
-
// not exist yet (or a workspace that has since been removed) is not a sync failure.
|
|
136
|
-
const root = await canonicalPathOrNull(input.cwd);
|
|
137
|
-
if (!root)
|
|
138
|
-
return null;
|
|
139
|
-
const space = await getRuntimeSpaceBinding(root, identity);
|
|
140
|
-
if (!space)
|
|
141
|
-
return null;
|
|
142
|
-
const runtimeRoot = nativeRuntimeRoot(space.spaceId);
|
|
143
|
-
const config = await readNativeSyncConfig(runtimeRoot, identity);
|
|
144
|
-
if (!config || config.root !== root || config.spaceId !== space.spaceId || !config.harnesses.includes(input.harness))
|
|
145
|
-
return null;
|
|
146
|
-
const path = await canonicalPathOrNull(input.path);
|
|
147
|
-
if (!path)
|
|
148
|
-
return null;
|
|
149
|
-
const transcript = await readNativeTranscript(path, input.harness, input);
|
|
150
|
-
if (await canonicalPathOrNull(transcript.cwd) !== root || input.nativeSessionId && transcript.nativeSessionId !== input.nativeSessionId)
|
|
151
|
-
throw new Error("Native transcript belongs to another project or Session");
|
|
152
|
-
const key = JSON.stringify([identity, space.spaceId, input.harness, transcript.nativeSessionId, path]);
|
|
153
|
-
let store = nativeStores.get(key);
|
|
154
|
-
if (!store) {
|
|
155
|
-
const transport = nativeArchiveTransport(space.spaceId, identity);
|
|
156
|
-
const candidates = (await listNativeSyncStores(runtimeRoot, space.spaceId, identity, transport))
|
|
157
|
-
.filter((candidate) => candidate.options.harness === input.harness && candidate.options.nativeSessionId === transcript.nativeSessionId);
|
|
158
|
-
for (const candidate of candidates)
|
|
159
|
-
if ((await candidate.binding()).path === path) {
|
|
160
|
-
store = candidate;
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
if (!store) {
|
|
164
|
-
const managed = await findRuntimeNativeSession(runtimeRoot, input.harness, transcript.nativeSessionId, path);
|
|
165
|
-
const managedPath = managed ? await canonicalPathOrNull(managed.path) : null;
|
|
166
|
-
if (candidates.length && managedPath !== path)
|
|
167
|
-
throw new Error("Native path changed; original bindings retained");
|
|
168
|
-
// Restored Pi working copies can share a native ID. Existing Cohub sidecars disambiguate them.
|
|
169
|
-
store = new NativeSyncStore({ runtimeRoot, spaceId: space.spaceId, identity, harness: input.harness, nativeSessionId: transcript.nativeSessionId,
|
|
170
|
-
instanceKey: managedPath === path ? path : undefined, transport });
|
|
171
|
-
}
|
|
172
|
-
if (nativeStores.size >= 256)
|
|
173
|
-
nativeStores.delete(nativeStores.keys().next().value ?? "");
|
|
174
|
-
nativeStores.set(key, store);
|
|
175
|
-
}
|
|
176
|
-
await store.capture(path, transcript, { sessionStartedAt: input.sessionStartedAt, origin: input.origin });
|
|
177
|
-
return store;
|
|
178
|
-
}
|
|
179
|
-
/** The existing Runtime supervisor retries receipts even after the original terminal has exited. */
|
|
180
|
-
export function nativeWebSocketTransport(spaceId, identity, send) {
|
|
181
|
-
const archive = nativeArchiveTransport(spaceId, identity);
|
|
182
|
-
return {
|
|
183
|
-
...archive,
|
|
184
|
-
startNativeTurn: async (input) => await send({ type: "start", input }),
|
|
185
|
-
completeNativeTurn: async (sessionId, turnId, result) => await send({ type: "complete", sessionId, turnId, result }),
|
|
186
|
-
updateNativeTurn: async (sessionId, turnId, progress) => await send({ type: "progress", sessionId, turnId, progress }),
|
|
187
|
-
heartbeatNativeTurn: async (sessionId, turnId) => await send({ type: "heartbeat", sessionId, turnId }),
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
export async function flushNativeSessions(spaceId, identity, signal, report, transportOverride) {
|
|
191
|
-
if (currentIdentityKey() !== identity)
|
|
192
|
-
return false;
|
|
193
|
-
const runtimeRoot = nativeRuntimeRoot(spaceId);
|
|
194
|
-
const config = await readNativeSyncConfig(runtimeRoot, identity);
|
|
195
|
-
if (!config || config.spaceId !== spaceId)
|
|
196
|
-
return false;
|
|
197
|
-
const transport = transportOverride;
|
|
198
|
-
if (!transport)
|
|
199
|
-
return false;
|
|
200
|
-
const stores = await listNativeSyncStores(runtimeRoot, spaceId, identity, transport);
|
|
201
|
-
let failed = false;
|
|
202
|
-
for (const store of stores) {
|
|
203
|
-
signal.throwIfAborted();
|
|
204
|
-
const binding = await store.binding();
|
|
205
|
-
if (!config.harnesses.includes(binding.harness))
|
|
206
|
-
continue;
|
|
207
|
-
try {
|
|
208
|
-
// Codex hooks trigger captures; re-reading here also closes gaps after reconnects or missed hooks.
|
|
209
|
-
if (binding.harness === "codex")
|
|
210
|
-
await store.capture(binding.path, await readNativeTranscript(binding.path, "codex"));
|
|
211
|
-
await store.flush(AbortSignal.any([signal, AbortSignal.timeout(30_000)]));
|
|
212
|
-
}
|
|
213
|
-
catch (error) {
|
|
214
|
-
if (!signal.aborted) {
|
|
215
|
-
failed = true;
|
|
216
|
-
report(error);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return failed;
|
|
221
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { ContentBlock, NativeTurnComplete, NativeTurnMessage } from "@neta-art/cohub";
|
|
2
|
-
export type NativeTranscriptTurn = {
|
|
3
|
-
key: string;
|
|
4
|
-
parentKey: string | null;
|
|
5
|
-
cloudTurnId?: string;
|
|
6
|
-
userContent: ContentBlock[];
|
|
7
|
-
messages: NativeTurnMessage[];
|
|
8
|
-
startedAt: string;
|
|
9
|
-
startBytes: number;
|
|
10
|
-
endBytes: number;
|
|
11
|
-
contentEndBytes: number;
|
|
12
|
-
boundaries: Record<number, string>;
|
|
13
|
-
sha256: string;
|
|
14
|
-
result: NativeTurnComplete | null;
|
|
15
|
-
};
|
|
16
|
-
export type NativeTranscript = {
|
|
17
|
-
nativeSessionId: string;
|
|
18
|
-
cwd: string;
|
|
19
|
-
cloudSessionId?: string;
|
|
20
|
-
turns: NativeTranscriptTurn[];
|
|
21
|
-
prefixes: ReadonlyMap<number, string>;
|
|
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
|
-
}>;
|
|
30
|
-
/** Partial trailing records are retried, never parsed or acknowledged as complete. */
|
|
31
|
-
export declare function readNativeTranscript(path: string, harness: "pi" | "codex", options?: {
|
|
32
|
-
settled?: boolean;
|
|
33
|
-
leafId?: string | null;
|
|
34
|
-
signal?: AbortSignal;
|
|
35
|
-
}): Promise<NativeTranscript>;
|
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
import { createReadStream } from "node:fs";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { piContent } from "./harness.js";
|
|
4
|
-
import { codexTokenTotals, codexUsage } from "./codex-usage.js";
|
|
5
|
-
import { record } from "./json-rpc.js";
|
|
6
|
-
const text = (value) => typeof value === "string" ? value : "";
|
|
7
|
-
const list = (value) => Array.isArray(value) ? value : [];
|
|
8
|
-
const iso = (value) => {
|
|
9
|
-
const date = new Date(typeof value === "number" || typeof value === "string" ? value : 0);
|
|
10
|
-
if (!Number.isFinite(date.getTime()))
|
|
11
|
-
throw new Error("Invalid native timestamp");
|
|
12
|
-
return date.toISOString();
|
|
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
|
-
}
|
|
43
|
-
/** Partial trailing records are retried, never parsed or acknowledged as complete. */
|
|
44
|
-
export async function readNativeTranscript(path, harness, options = {}) {
|
|
45
|
-
const lines = [];
|
|
46
|
-
const prefixes = new Map();
|
|
47
|
-
let fragments = [], pendingBytes = 0, offset = 0;
|
|
48
|
-
const checksum = createHash("sha256");
|
|
49
|
-
const append = (bytes) => {
|
|
50
|
-
if (!bytes.length)
|
|
51
|
-
return;
|
|
52
|
-
fragments.push(bytes);
|
|
53
|
-
pendingBytes += bytes.length;
|
|
54
|
-
checksum.update(bytes);
|
|
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");
|
|
59
|
-
};
|
|
60
|
-
for await (const chunk of createReadStream(path, { signal: options.signal })) {
|
|
61
|
-
let start = 0;
|
|
62
|
-
for (let end = chunk.indexOf(10); end >= 0; end = chunk.indexOf(10, start)) {
|
|
63
|
-
append(chunk.subarray(start, end + 1));
|
|
64
|
-
// Concatenate once per record, not once per read chunk (large base64 images stay linear).
|
|
65
|
-
const bytes = fragments.length === 1 ? fragments[0] : Buffer.concat(fragments, pendingBytes);
|
|
66
|
-
const line = bytes.subarray(0, bytes.length - 1);
|
|
67
|
-
const startBytes = offset;
|
|
68
|
-
offset += pendingBytes;
|
|
69
|
-
fragments = [];
|
|
70
|
-
pendingBytes = 0;
|
|
71
|
-
const sha256 = checksum.copy().digest("hex");
|
|
72
|
-
prefixes.set(offset, sha256);
|
|
73
|
-
if (line.length) {
|
|
74
|
-
let value;
|
|
75
|
-
try {
|
|
76
|
-
value = record(JSON.parse(line.toString("utf8")));
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
throw new Error("Invalid native JSON record; original retained");
|
|
80
|
-
}
|
|
81
|
-
lines.push({ value, startBytes, endBytes: offset, sha256 });
|
|
82
|
-
}
|
|
83
|
-
start = end + 1;
|
|
84
|
-
}
|
|
85
|
-
append(chunk.subarray(start));
|
|
86
|
-
}
|
|
87
|
-
if (!lines.length)
|
|
88
|
-
throw new Error("Native transcript is empty");
|
|
89
|
-
return { ...(harness === "pi" ? parsePiTranscript(lines, options) : parseCodexTranscript(lines)), prefixes };
|
|
90
|
-
}
|
|
91
|
-
function parsePiTranscript(lines, options) {
|
|
92
|
-
const header = lines[0]?.value ?? {};
|
|
93
|
-
if (header.type !== "session" || !text(header.id))
|
|
94
|
-
throw new Error("Invalid Pi session");
|
|
95
|
-
const entries = new Map(lines.slice(1).filter((line) => text(line.value.id)).map((line) => [text(line.value.id), line]));
|
|
96
|
-
const branch = [];
|
|
97
|
-
let leaf = options.leafId ?? (lines.length > 1 ? text(lines.at(-1)?.value.id) : "");
|
|
98
|
-
const visited = new Set();
|
|
99
|
-
while (leaf) {
|
|
100
|
-
if (visited.has(leaf))
|
|
101
|
-
throw new Error("Cyclic Pi history");
|
|
102
|
-
visited.add(leaf);
|
|
103
|
-
const entry = entries.get(leaf);
|
|
104
|
-
if (!entry)
|
|
105
|
-
throw new Error("Pi parent history is missing");
|
|
106
|
-
branch.push(entry);
|
|
107
|
-
leaf = text(entry.value.parentId);
|
|
108
|
-
}
|
|
109
|
-
branch.reverse();
|
|
110
|
-
const turns = [];
|
|
111
|
-
let cloudSessionId = text(record(header.cohub).sessionId) || text(record(header.affinity).sessionId);
|
|
112
|
-
let current = null;
|
|
113
|
-
let messages = [];
|
|
114
|
-
let completedAt = iso(header.timestamp);
|
|
115
|
-
const finish = (settled) => {
|
|
116
|
-
if (!current)
|
|
117
|
-
return;
|
|
118
|
-
const last = messages.at(-1);
|
|
119
|
-
if (settled)
|
|
120
|
-
current.result = { messages, completedAt, status: !last || ["aborted", "pending", "toolUse"].includes(last.stopReason ?? "") ? "interrupted" : last?.errorMessage || last?.stopReason === "error" ? "failed" : "completed" };
|
|
121
|
-
turns.push(current);
|
|
122
|
-
};
|
|
123
|
-
for (const line of branch) {
|
|
124
|
-
const entry = line.value;
|
|
125
|
-
if (entry.type !== "message") {
|
|
126
|
-
if (current) {
|
|
127
|
-
current.endBytes = line.endBytes;
|
|
128
|
-
current.sha256 = line.sha256;
|
|
129
|
-
current.boundaries[line.endBytes] = line.sha256;
|
|
130
|
-
}
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
const message = record(entry.message);
|
|
134
|
-
if (!["user", "assistant", "toolResult"].includes(text(message.role))) {
|
|
135
|
-
// Native-only UI / shell records remain in raw archives; they do not rewrite a settled agent Turn.
|
|
136
|
-
if (current) {
|
|
137
|
-
current.endBytes = line.endBytes;
|
|
138
|
-
current.sha256 = line.sha256;
|
|
139
|
-
current.boundaries[line.endBytes] = line.sha256;
|
|
140
|
-
}
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
const timestamp = iso(entry.timestamp ?? message.timestamp);
|
|
144
|
-
if (message.role === "user") {
|
|
145
|
-
finish(true);
|
|
146
|
-
completedAt = timestamp;
|
|
147
|
-
const cloudTurnId = text(record(message.meta).turnId);
|
|
148
|
-
if (cloudTurnId && !text(record(header.cohub).sessionId) && !text(record(header.affinity).sessionId))
|
|
149
|
-
cloudSessionId = text(record(message.meta).sourceSessionId) || cloudSessionId;
|
|
150
|
-
current = { key: text(entry.id), parentKey: turns.at(-1)?.key ?? null, ...(cloudTurnId ? { cloudTurnId } : {}), userContent: typeof message.content === "string" ? [{ type: "text", text: message.content }] : piContent(message.content), messages: [], startedAt: completedAt, startBytes: line.startBytes, endBytes: line.endBytes, contentEndBytes: line.endBytes, boundaries: {}, sha256: line.sha256, result: null };
|
|
151
|
-
messages = current.messages;
|
|
152
|
-
}
|
|
153
|
-
else if (current && message.role === "assistant") {
|
|
154
|
-
messages.push({ content: piContent(message.content), provider: text(message.provider) || null, model: text(message.model) || null,
|
|
155
|
-
usage: record(message.usage), stopReason: text(message.stopReason) || null, errorMessage: text(message.errorMessage) || null });
|
|
156
|
-
}
|
|
157
|
-
else if (current && message.role === "toolResult") {
|
|
158
|
-
const assistant = messages.at(-1);
|
|
159
|
-
if (!assistant)
|
|
160
|
-
throw new Error("Pi tool result has no assistant Turn");
|
|
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) });
|
|
162
|
-
}
|
|
163
|
-
if (current) {
|
|
164
|
-
current.endBytes = line.endBytes;
|
|
165
|
-
current.contentEndBytes = line.endBytes;
|
|
166
|
-
current.sha256 = line.sha256;
|
|
167
|
-
current.boundaries[line.endBytes] = line.sha256;
|
|
168
|
-
}
|
|
169
|
-
completedAt = timestamp;
|
|
170
|
-
}
|
|
171
|
-
finish(Boolean(options.settled));
|
|
172
|
-
return { nativeSessionId: text(header.id), cwd: text(header.cwd), cloudSessionId: cloudSessionId || undefined, turns };
|
|
173
|
-
}
|
|
174
|
-
function codexContent(value) {
|
|
175
|
-
return list(value).flatMap((item) => {
|
|
176
|
-
const block = record(item);
|
|
177
|
-
if (["input_text", "output_text", "text"].includes(text(block.type)))
|
|
178
|
-
return [{ type: "text", text: text(block.text) }];
|
|
179
|
-
if (block.type === "input_image" && text(block.image_url)) {
|
|
180
|
-
const data = /^data:([^;]+);base64,(.*)$/s.exec(text(block.image_url));
|
|
181
|
-
return [{ type: "image", source: data ? { type: "base64", media_type: data[1] ?? "image/png", data: data[2] ?? "" } : { type: "url", url: text(block.image_url) } }];
|
|
182
|
-
}
|
|
183
|
-
return [];
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
function parseCodexTranscript(lines) {
|
|
187
|
-
const header = lines[0]?.value ?? {};
|
|
188
|
-
const metadata = record(header.payload);
|
|
189
|
-
if (header.type !== "session_meta" || !text(metadata.id))
|
|
190
|
-
throw new Error("Invalid Codex session");
|
|
191
|
-
if (metadata.history_base || metadata.fork_source)
|
|
192
|
-
throw new Error("Codex history references another rollout; retain the original and materialize its full history first");
|
|
193
|
-
const turns = [];
|
|
194
|
-
let current = null;
|
|
195
|
-
let messages = [];
|
|
196
|
-
let model = null;
|
|
197
|
-
let cloudSessionId = text(record(metadata.cohub).sessionId);
|
|
198
|
-
let provider = text(metadata.model_provider) || null;
|
|
199
|
-
let userFromResponse = false;
|
|
200
|
-
const usageByTurn = new Map();
|
|
201
|
-
const assistant = () => {
|
|
202
|
-
let last = messages.at(-1);
|
|
203
|
-
if (!last) {
|
|
204
|
-
last = { content: [], model, provider };
|
|
205
|
-
messages.push(last);
|
|
206
|
-
}
|
|
207
|
-
return last;
|
|
208
|
-
};
|
|
209
|
-
for (const line of lines.slice(1)) {
|
|
210
|
-
const entry = line.value, payload = record(entry.payload);
|
|
211
|
-
if (entry.type === "turn_context") {
|
|
212
|
-
model = text(payload.model) || model;
|
|
213
|
-
provider = text(payload.model_provider) || provider;
|
|
214
|
-
}
|
|
215
|
-
if (entry.type === "event_msg" && ["turn_started", "task_started"].includes(text(payload.type))) {
|
|
216
|
-
if (current)
|
|
217
|
-
turns.push(current);
|
|
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 };
|
|
219
|
-
if (!current.key)
|
|
220
|
-
throw new Error("Codex Turn identity is missing");
|
|
221
|
-
messages = current.messages;
|
|
222
|
-
userFromResponse = false;
|
|
223
|
-
}
|
|
224
|
-
if (entry.type === "token_usage_record") {
|
|
225
|
-
const usage = record(payload.turn_token_usage);
|
|
226
|
-
if (typeof usage.total_tokens === "number")
|
|
227
|
-
usageByTurn.set(text(payload.turn_id), codexUsage(codexTokenTotals({ inputTokens: usage.input_tokens, outputTokens: usage.output_tokens, cachedInputTokens: usage.cached_input_tokens, cacheWriteInputTokens: usage.cache_write_input_tokens, totalTokens: usage.total_tokens })));
|
|
228
|
-
}
|
|
229
|
-
if (!current) {
|
|
230
|
-
const previous = turns.at(-1);
|
|
231
|
-
if (previous) {
|
|
232
|
-
previous.endBytes = line.endBytes;
|
|
233
|
-
previous.sha256 = line.sha256;
|
|
234
|
-
previous.boundaries[line.endBytes] = line.sha256;
|
|
235
|
-
}
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
current.endBytes = line.endBytes;
|
|
239
|
-
current.sha256 = line.sha256;
|
|
240
|
-
current.boundaries[line.endBytes] = line.sha256;
|
|
241
|
-
current.contentEndBytes = line.endBytes;
|
|
242
|
-
if (entry.type === "response_item") {
|
|
243
|
-
const cohub = record(record(entry.metadata).cohub);
|
|
244
|
-
if (typeof cohub.turnId === "string") {
|
|
245
|
-
current.cloudTurnId = cohub.turnId;
|
|
246
|
-
if (!text(record(metadata.cohub).sessionId))
|
|
247
|
-
cloudSessionId = text(cohub.sessionId) || cloudSessionId;
|
|
248
|
-
}
|
|
249
|
-
if (payload.type === "message" && payload.role === "user") {
|
|
250
|
-
if (!userFromResponse)
|
|
251
|
-
current.userContent = [];
|
|
252
|
-
current.userContent.push(...codexContent(payload.content));
|
|
253
|
-
userFromResponse = true;
|
|
254
|
-
}
|
|
255
|
-
else if (payload.type === "message" && payload.role === "assistant") {
|
|
256
|
-
const content = codexContent(payload.content);
|
|
257
|
-
if (!messages.length || messages.at(-1)?.content.length)
|
|
258
|
-
messages.push({ content, model, provider });
|
|
259
|
-
else
|
|
260
|
-
assistant().content.push(...content);
|
|
261
|
-
}
|
|
262
|
-
else if (payload.type === "reasoning") {
|
|
263
|
-
const thinking = list(payload.summary).map((item) => text(record(item).text)).join("\n");
|
|
264
|
-
if (thinking)
|
|
265
|
-
assistant().content.push({ type: "thinking", thinking });
|
|
266
|
-
}
|
|
267
|
-
else if (["function_call", "custom_tool_call"].includes(text(payload.type))) {
|
|
268
|
-
let input;
|
|
269
|
-
try {
|
|
270
|
-
input = payload.type === "custom_tool_call" ? { input: payload.input } : record(JSON.parse(text(payload.arguments)));
|
|
271
|
-
}
|
|
272
|
-
catch {
|
|
273
|
-
input = { raw: payload.arguments };
|
|
274
|
-
}
|
|
275
|
-
assistant().content.push({ type: "tool_use", id: text(payload.call_id), name: text(payload.name), input });
|
|
276
|
-
}
|
|
277
|
-
else if (["function_call_output", "custom_tool_call_output"].includes(text(payload.type))) {
|
|
278
|
-
assistant().content.push({ type: "tool_result", tool_use_id: text(payload.call_id), content: typeof payload.output === "string" ? payload.output : JSON.stringify(payload.output ?? null) });
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
if (entry.type === "event_msg" && payload.type === "user_message" && !userFromResponse) {
|
|
282
|
-
current.userContent = [{ type: "text", text: text(payload.message) }];
|
|
283
|
-
userFromResponse = true;
|
|
284
|
-
}
|
|
285
|
-
if (entry.type === "event_msg" && ["turn_complete", "task_complete", "turn_aborted"].includes(text(payload.type))) {
|
|
286
|
-
if (payload.turn_id && payload.turn_id !== current.key)
|
|
287
|
-
throw new Error("Codex Turn boundary mismatch");
|
|
288
|
-
if (!messages.length && text(payload.last_agent_message))
|
|
289
|
-
messages.push({ content: [{ type: "text", text: text(payload.last_agent_message) }], model, provider });
|
|
290
|
-
const errorMessage = text(record(payload.error).message);
|
|
291
|
-
const status = payload.type === "turn_aborted" ? "interrupted" : errorMessage ? "failed" : "completed";
|
|
292
|
-
const final = assistant();
|
|
293
|
-
final.stopReason = status === "interrupted" ? "aborted" : status === "failed" ? "error" : "stop";
|
|
294
|
-
if (errorMessage)
|
|
295
|
-
final.errorMessage = errorMessage;
|
|
296
|
-
current.result = { messages, completedAt: iso(entry.timestamp), status };
|
|
297
|
-
turns.push(current);
|
|
298
|
-
current = null;
|
|
299
|
-
messages = [];
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
if (current && userFromResponse)
|
|
303
|
-
turns.push(current);
|
|
304
|
-
for (const turn of turns) {
|
|
305
|
-
const last = turn.result?.messages.at(-1);
|
|
306
|
-
if (last && usageByTurn.has(turn.key))
|
|
307
|
-
last.usage = usageByTurn.get(turn.key);
|
|
308
|
-
}
|
|
309
|
-
return { nativeSessionId: text(metadata.id), cwd: text(metadata.cwd), cloudSessionId: cloudSessionId || undefined, turns };
|
|
310
|
-
}
|