@neta-art/cohub-cli 8.2.0 → 8.3.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.d.ts +0 -12
- package/dist/commands/runtime.js +129 -144
- 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/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 +11 -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} +0 -3
- package/dist/runtime/{native-archive.js → native/archive.js} +2 -17
- package/dist/runtime/native/attach.d.ts +38 -0
- package/dist/runtime/native/attach.js +113 -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 +113 -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 → native/transcript.d.ts} +5 -5
- package/dist/runtime/{native-transcript.js → native/transcript.js} +37 -11
- 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 -110
- 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 -116
- package/dist/runtime/native-sync-store.js +0 -495
- package/dist/runtime/native-sync.d.ts +0 -65
- package/dist/runtime/native-sync.js +0 -326
- package/dist/runtime/session-store.d.ts +0 -84
- package/dist/runtime/session-store.js +0 -601
package/dist/runtime/instance.js
CHANGED
|
@@ -32,10 +32,10 @@ async function readRecord(directory) {
|
|
|
32
32
|
throw error;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
function request(record, action,
|
|
35
|
+
function request(record, action, payload, timeoutMs) {
|
|
36
36
|
return new Promise((resolve, reject) => {
|
|
37
37
|
const socket = createConnection(record.socket);
|
|
38
|
-
const timer = setTimeout(() => { socket.destroy(); reject(new Error("Runtime control timed out")); },
|
|
38
|
+
const timer = setTimeout(() => { socket.destroy(); reject(new Error("Runtime control timed out")); }, timeoutMs);
|
|
39
39
|
let buffer = "";
|
|
40
40
|
let settled = false;
|
|
41
41
|
const finish = (error, value) => {
|
|
@@ -49,7 +49,7 @@ function request(record, action, force = false) {
|
|
|
49
49
|
else if (value)
|
|
50
50
|
resolve(value);
|
|
51
51
|
};
|
|
52
|
-
socket.on("connect", () => socket.write(`${JSON.stringify({ nonce: record.nonce, action
|
|
52
|
+
socket.on("connect", () => socket.write(`${JSON.stringify({ ...payload, nonce: record.nonce, action })}\n`));
|
|
53
53
|
socket.on("error", (error) => finish(error));
|
|
54
54
|
socket.on("close", () => finish(new Error("Runtime control closed")));
|
|
55
55
|
socket.on("data", (chunk) => {
|
|
@@ -67,7 +67,7 @@ function request(record, action, force = false) {
|
|
|
67
67
|
else if (response.nonce !== record.nonce || response.status?.pid !== record.pid)
|
|
68
68
|
finish(new Error("Runtime identity changed"));
|
|
69
69
|
else
|
|
70
|
-
finish(undefined, response.status);
|
|
70
|
+
finish(undefined, { status: response.status, result: response.result });
|
|
71
71
|
}
|
|
72
72
|
catch (error) {
|
|
73
73
|
finish(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -76,11 +76,15 @@ function request(record, action, force = false) {
|
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
export async function requestRuntimeInstance(directory, action = "status", force = false) {
|
|
79
|
+
return (await controlRuntimeInstance(directory, action, { force }))?.status ?? null;
|
|
80
|
+
}
|
|
81
|
+
/** Send a control action to the running Runtime; `null` when none runs. */
|
|
82
|
+
export async function controlRuntimeInstance(directory, action, payload = {}, timeoutMs = 3000) {
|
|
79
83
|
const record = await readRecord(directory);
|
|
80
84
|
if (!record || !alive(record.pid))
|
|
81
85
|
return null;
|
|
82
86
|
try {
|
|
83
|
-
return await request(record, action,
|
|
87
|
+
return await request(record, action, payload, timeoutMs);
|
|
84
88
|
}
|
|
85
89
|
catch (error) {
|
|
86
90
|
// A reused PID is not proof of ownership. Missing/refused endpoints cannot
|
|
@@ -91,7 +95,7 @@ export async function requestRuntimeInstance(directory, action = "status", force
|
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
/** Private local IPC is both the single-instance guard and the control surface. */
|
|
94
|
-
export async function ownRuntimeInstance(directory, status, stop) {
|
|
98
|
+
export async function ownRuntimeInstance(directory, status, stop, control = async () => { throw new Error("Unknown Runtime control request"); }) {
|
|
95
99
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
96
100
|
return withRuntimeSpaceBindingsLock(async () => {
|
|
97
101
|
if (await requestRuntimeInstance(directory))
|
|
@@ -118,17 +122,20 @@ export async function ownRuntimeInstance(directory, status, stop) {
|
|
|
118
122
|
if (!input.includes("\n"))
|
|
119
123
|
return;
|
|
120
124
|
socket.removeAllListeners("data");
|
|
125
|
+
// The request arrived; answering it may take longer than an idle client is allowed.
|
|
126
|
+
socket.setTimeout(0);
|
|
121
127
|
void (async () => {
|
|
122
128
|
const message = JSON.parse(input.slice(0, input.indexOf("\n")));
|
|
123
129
|
if (message.nonce !== record.nonce) {
|
|
124
130
|
socket.destroy();
|
|
125
131
|
return;
|
|
126
132
|
}
|
|
133
|
+
let result;
|
|
127
134
|
if (message.action === "stop")
|
|
128
135
|
await stop(message.force === true);
|
|
129
136
|
else if (message.action !== "status")
|
|
130
|
-
|
|
131
|
-
socket.end(`${JSON.stringify({ nonce: record.nonce, status: status() })}\n`);
|
|
137
|
+
result = await control(String(message.action), message);
|
|
138
|
+
socket.end(`${JSON.stringify({ nonce: record.nonce, status: status(), ...(result === undefined ? {} : { result }) })}\n`);
|
|
132
139
|
})().catch((error) => socket.end(`${JSON.stringify({ error: error instanceof Error ? error.message : String(error) })}\n`));
|
|
133
140
|
});
|
|
134
141
|
});
|
package/dist/runtime/launch.js
CHANGED
|
@@ -9,8 +9,8 @@ import { requireAccessToken } from "../auth.js";
|
|
|
9
9
|
import { createClient } from "../client.js";
|
|
10
10
|
import { currentIdentityKey, explicitSpace } from "../space.js";
|
|
11
11
|
import { canonicalRuntimeRoot, getRuntimeSpaceBinding, resolveRuntimeSpace } from "./space-binding.js";
|
|
12
|
-
import { requestRuntimeInstance, runtimeInstanceDirectory } from "./instance.js";
|
|
13
|
-
import { ensureNativeSync } from "./native
|
|
12
|
+
import { controlRuntimeInstance, requestRuntimeInstance, runtimeInstanceDirectory } from "./instance.js";
|
|
13
|
+
import { ensureNativeSync, repairIntegrations } from "./native/attach.js";
|
|
14
14
|
import { createDiagnosticConsole, printRuntimeSummary, runtimeWebUrl } from "./presentation.js";
|
|
15
15
|
import { runRuntime } from "./supervisor.js";
|
|
16
16
|
export const resolveLocalSpaceName = (root, name) => name?.trim() || basename(root) || "local-space";
|
|
@@ -93,7 +93,12 @@ export async function runtimeUp(program, dir, options) {
|
|
|
93
93
|
}
|
|
94
94
|
// `up` is the single idempotent entry: a reused instance adopts its running
|
|
95
95
|
// Harnesses (unless explicitly overridden) and completes native sync setup.
|
|
96
|
-
await ensureNativeSync({ root, spaceId: existing.spaceId, identity, harnesses: options.harness.length ? harnesses : parseRuntimeHarnesses(existing.harnesses), yes: options.yes, executables: { pi: options.pi, codex: options.codex } });
|
|
96
|
+
const native = await ensureNativeSync({ root, spaceId: existing.spaceId, identity, harnesses: options.harness.length ? harnesses : parseRuntimeHarnesses(existing.harnesses), yes: options.yes, executables: { pi: options.pi, codex: options.codex } });
|
|
97
|
+
// The running Runtime applies consent given now without restarting.
|
|
98
|
+
const directory = runtimeInstanceDirectory(identity, existing.spaceId);
|
|
99
|
+
await controlRuntimeInstance(directory, "reload").catch(() => undefined);
|
|
100
|
+
if (native.importHistory)
|
|
101
|
+
await controlRuntimeInstance(directory, "import", { command: "start" }, 60_000).catch(() => undefined);
|
|
97
102
|
printRuntimeSummary(existing, options.json, true);
|
|
98
103
|
return;
|
|
99
104
|
}
|
|
@@ -139,6 +144,7 @@ export async function runtimeUp(program, dir, options) {
|
|
|
139
144
|
if (createNew && binding && await requestRuntimeInstance(runtimeInstanceDirectory(identity, binding.spaceId))) {
|
|
140
145
|
throw new Error("Stop the existing Runtime before rebinding this directory");
|
|
141
146
|
}
|
|
147
|
+
await repairIntegrations(options.pi);
|
|
142
148
|
// Fail local preflight before creating remote state; the worker reuses this catalog.
|
|
143
149
|
const capabilities = await discoverHarnesses(harnesses, options, root);
|
|
144
150
|
const client = createClient();
|
|
@@ -153,8 +159,8 @@ export async function runtimeUp(program, dir, options) {
|
|
|
153
159
|
},
|
|
154
160
|
});
|
|
155
161
|
// Consent (or --yes) precedes any user-level integration install; failure never blocks startup.
|
|
156
|
-
await ensureNativeSync({ root, spaceId, identity, harnesses, yes: options.yes, executables: { pi: options.pi, codex: options.codex } });
|
|
157
|
-
const config = { spaceId, root, identity, harnesses, capabilities, executables: { pi: options.pi, codex: options.codex }, background: Boolean(options.detach), verbose: options.verbose };
|
|
162
|
+
const native = await ensureNativeSync({ root, spaceId, identity, harnesses, yes: options.yes, executables: { pi: options.pi, codex: options.codex } });
|
|
163
|
+
const config = { spaceId, root, identity, harnesses, capabilities, executables: { pi: options.pi, codex: options.codex }, background: Boolean(options.detach), verbose: options.verbose, importHistory: native.importHistory };
|
|
158
164
|
const existing = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
|
|
159
165
|
if (existing) {
|
|
160
166
|
if (existing.root !== root)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type NativeTranscript, type NativeTranscriptTurn } from "./transcript.js";
|
|
2
|
+
export type { NativeTranscript, NativeTranscriptTurn };
|
|
3
|
+
export type Harness = "pi" | "codex";
|
|
4
|
+
/**
|
|
5
|
+
* Why a Codex thread exists. Codex writes every thread it starts into one rollout tree: the user's
|
|
6
|
+
* conversations, its own background work (guardian reviews, compaction, memory consolidation) and
|
|
7
|
+
* agents spawned from a conversation.
|
|
8
|
+
*/
|
|
9
|
+
export type ThreadKind = "user" | "internal" | "spawned";
|
|
10
|
+
export type SessionHeader = {
|
|
11
|
+
harness: Harness;
|
|
12
|
+
nativeSessionId: string;
|
|
13
|
+
cwd: string;
|
|
14
|
+
thread: ThreadKind;
|
|
15
|
+
};
|
|
16
|
+
/** Directories holding this harness's transcripts; a transcript's header says which project it is. */
|
|
17
|
+
export declare function transcriptRoots(harness: Harness): string[];
|
|
18
|
+
/** Pi's encoding of a project directory into its session folder name. */
|
|
19
|
+
export declare const piSessionDirectoryName: (cwd: string) => string;
|
|
20
|
+
/** Whether a directory found under a root can hold transcripts of `cwd`. */
|
|
21
|
+
export declare function acceptsDirectory(path: string, root: string, harness: Harness, cwd: string): boolean;
|
|
22
|
+
/** Name-only check: can this path hold a transcript of this harness for `cwd`? */
|
|
23
|
+
export declare function acceptsTranscript(path: string, harness: Harness, cwd: string): boolean;
|
|
24
|
+
/** Codex's first line is the only place a thread's purpose is recorded. */
|
|
25
|
+
export declare function codexThreadKind(payload: Record<string, unknown>): ThreadKind;
|
|
26
|
+
/** The first line of a transcript: identity, project and purpose. Nothing else is read. */
|
|
27
|
+
export declare function readSessionHeader(path: string, harness: Harness, signal?: AbortSignal): Promise<SessionHeader | null>;
|
|
28
|
+
/**
|
|
29
|
+
* Parse a transcript into Turns. Codex records its own Turn boundary; a Pi Turn's end is inferred
|
|
30
|
+
* by the caller, so Pi is parsed as settled here and the caller withdraws a tail still in flight.
|
|
31
|
+
*/
|
|
32
|
+
export declare function readTranscript(path: string, harness: Harness, signal?: AbortSignal): Promise<NativeTranscript>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { codexArchivedSessionsDirectory, codexRolloutBytes, codexRolloutIdOf, codexSessionsDirectory, readNativeTranscript } from "./transcript.js";
|
|
5
|
+
const text = (value) => typeof value === "string" ? value : "";
|
|
6
|
+
const record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7
|
+
const HEADER_MAX_BYTES = 1024 * 1024;
|
|
8
|
+
/** Directories holding this harness's transcripts; a transcript's header says which project it is. */
|
|
9
|
+
export function transcriptRoots(harness) {
|
|
10
|
+
if (harness === "codex")
|
|
11
|
+
return [codexSessionsDirectory(), codexArchivedSessionsDirectory()];
|
|
12
|
+
const custom = process.env.PI_CODING_AGENT_SESSION_DIR?.trim();
|
|
13
|
+
if (custom)
|
|
14
|
+
return [custom];
|
|
15
|
+
return [join(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent"), "sessions")];
|
|
16
|
+
}
|
|
17
|
+
/** Pi's encoding of a project directory into its session folder name. */
|
|
18
|
+
export const piSessionDirectoryName = (cwd) => `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
19
|
+
/** Whether a Pi project folder may hold sessions of `cwd` or a subdirectory of it. */
|
|
20
|
+
function piProjectFolderMatches(folder, cwd) {
|
|
21
|
+
const project = piSessionDirectoryName(cwd).slice(0, -2);
|
|
22
|
+
const name = basename(folder);
|
|
23
|
+
return name === `${project}--` || name.startsWith(`${project}-`);
|
|
24
|
+
}
|
|
25
|
+
/** Whether a directory found under a root can hold transcripts of `cwd`. */
|
|
26
|
+
export function acceptsDirectory(path, root, harness, cwd) {
|
|
27
|
+
if (harness === "codex" || process.env.PI_CODING_AGENT_SESSION_DIR?.trim() || dirname(path) !== root)
|
|
28
|
+
return true;
|
|
29
|
+
return piProjectFolderMatches(path, cwd);
|
|
30
|
+
}
|
|
31
|
+
/** Name-only check: can this path hold a transcript of this harness for `cwd`? */
|
|
32
|
+
export function acceptsTranscript(path, harness, cwd) {
|
|
33
|
+
const name = basename(path);
|
|
34
|
+
if (harness === "codex")
|
|
35
|
+
return codexRolloutIdOf(name) !== null;
|
|
36
|
+
return name.endsWith(".jsonl") && (Boolean(process.env.PI_CODING_AGENT_SESSION_DIR?.trim()) || piProjectFolderMatches(dirname(path), cwd));
|
|
37
|
+
}
|
|
38
|
+
async function firstRecord(bytes) {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
let buffered = 0;
|
|
41
|
+
for await (const chunk of bytes) {
|
|
42
|
+
chunks.push(chunk);
|
|
43
|
+
buffered += chunk.length;
|
|
44
|
+
const newline = chunk.indexOf(10);
|
|
45
|
+
if (newline >= 0)
|
|
46
|
+
return record(JSON.parse(Buffer.concat(chunks, buffered).subarray(0, buffered - chunk.length + newline).toString("utf8")));
|
|
47
|
+
if (buffered > HEADER_MAX_BYTES)
|
|
48
|
+
throw new Error("Native session header is too large");
|
|
49
|
+
}
|
|
50
|
+
throw new Error("Native transcript is empty");
|
|
51
|
+
}
|
|
52
|
+
/** Codex's first line is the only place a thread's purpose is recorded. */
|
|
53
|
+
export function codexThreadKind(payload) {
|
|
54
|
+
const source = payload.source;
|
|
55
|
+
if (source && typeof source === "object")
|
|
56
|
+
return record(record(source).subagent).thread_spawn ? "spawned" : "internal";
|
|
57
|
+
return ["subagent", "guardian_review", "memory_consolidation"].includes(text(payload.thread_source)) ? "internal" : "user";
|
|
58
|
+
}
|
|
59
|
+
/** The first line of a transcript: identity, project and purpose. Nothing else is read. */
|
|
60
|
+
export async function readSessionHeader(path, harness, signal) {
|
|
61
|
+
if (harness === "pi") {
|
|
62
|
+
const header = await firstRecord(createReadStream(path, { signal }));
|
|
63
|
+
if (header.type !== "session" || !text(header.id) || !text(header.cwd))
|
|
64
|
+
return null;
|
|
65
|
+
return { harness, nativeSessionId: text(header.id), cwd: text(header.cwd), thread: "user" };
|
|
66
|
+
}
|
|
67
|
+
const header = await firstRecord(codexRolloutBytes(path, null, signal));
|
|
68
|
+
const payload = record(header.payload);
|
|
69
|
+
if (header.type !== "session_meta" || !text(payload.id) || !text(payload.cwd))
|
|
70
|
+
return null;
|
|
71
|
+
return { harness, nativeSessionId: text(payload.id), cwd: text(payload.cwd), thread: codexThreadKind(payload) };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parse a transcript into Turns. Codex records its own Turn boundary; a Pi Turn's end is inferred
|
|
75
|
+
* by the caller, so Pi is parsed as settled here and the caller withdraws a tail still in flight.
|
|
76
|
+
*/
|
|
77
|
+
export async function readTranscript(path, harness, signal) {
|
|
78
|
+
return await readNativeTranscript(path, harness, { settled: harness === "pi", signal });
|
|
79
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { type CodexTokenTotals } from "./codex-usage.js";
|
|
2
1
|
/** A structurally unrestorable archive (e.g. its history reference is not carried); never retried. */
|
|
3
2
|
export declare class ArchiveNotRestorableError extends Error {
|
|
4
3
|
constructor(message: string);
|
|
5
4
|
}
|
|
6
|
-
export declare function readCodexArchiveTotals(path: string): Promise<CodexTokenTotals | undefined>;
|
|
7
5
|
/** Change only the header of a working copy. Raw archive bytes remain untouched. */
|
|
8
6
|
export declare function importNativeArchive(input: {
|
|
9
7
|
source: string;
|
|
@@ -16,5 +14,4 @@ export declare function importNativeArchive(input: {
|
|
|
16
14
|
}): Promise<{
|
|
17
15
|
checksum: string;
|
|
18
16
|
nativeSessionId: string;
|
|
19
|
-
codexTokenTotals: CodexTokenTotals | undefined;
|
|
20
17
|
}>;
|
|
@@ -3,20 +3,10 @@ import { createReadStream } from "node:fs";
|
|
|
3
3
|
import { link, mkdir, open, rm } from "node:fs/promises";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
5
|
import { RUNTIME_MAX_FRAME_BYTES } from "@neta-art/cohub";
|
|
6
|
-
import { JsonLineDecoder } from "./json-rpc.js";
|
|
7
|
-
import { codexArchiveTotals } from "./codex-usage.js";
|
|
8
6
|
/** A structurally unrestorable archive (e.g. its history reference is not carried); never retried. */
|
|
9
7
|
export class ArchiveNotRestorableError extends Error {
|
|
10
8
|
constructor(message) { super(message); this.name = "ArchiveNotRestorableError"; }
|
|
11
9
|
}
|
|
12
|
-
export async function readCodexArchiveTotals(path) {
|
|
13
|
-
let totals;
|
|
14
|
-
const decoder = new JsonLineDecoder((row) => { totals = codexArchiveTotals([row]) ?? totals; });
|
|
15
|
-
for await (const bytes of createReadStream(path))
|
|
16
|
-
decoder.push(bytes);
|
|
17
|
-
decoder.end();
|
|
18
|
-
return totals;
|
|
19
|
-
}
|
|
20
10
|
/** Change only the header of a working copy. Raw archive bytes remain untouched. */
|
|
21
11
|
export async function importNativeArchive(input) {
|
|
22
12
|
await mkdir(dirname(input.target), { recursive: true, mode: 0o700 });
|
|
@@ -25,8 +15,6 @@ export async function importNativeArchive(input) {
|
|
|
25
15
|
const digest = createHash("sha256");
|
|
26
16
|
let headerReady = false, headerBytes = 0;
|
|
27
17
|
const fragments = [];
|
|
28
|
-
let totals;
|
|
29
|
-
const decoder = input.harness === "codex" ? new JsonLineDecoder((row) => { totals = codexArchiveTotals([row]) ?? totals; }) : null;
|
|
30
18
|
const write = async (bytes) => { digest.update(bytes); await file.writeFile(bytes); };
|
|
31
19
|
const writeHeader = async () => {
|
|
32
20
|
const header = JSON.parse(Buffer.concat(fragments).toString("utf8"));
|
|
@@ -39,8 +27,7 @@ export async function importNativeArchive(input) {
|
|
|
39
27
|
else {
|
|
40
28
|
if (header?.type !== "session_meta" || header.payload?.id !== input.nativeSessionId)
|
|
41
29
|
throw new Error("Codex archive identity mismatch");
|
|
42
|
-
// A leaf archive does not carry its ancestor rollout.
|
|
43
|
-
// silently truncated history; failing lets the caller rebuild from durable cloud Turns.
|
|
30
|
+
// A leaf archive does not carry its ancestor rollout.
|
|
44
31
|
if (header.payload?.history_base != null)
|
|
45
32
|
throw new ArchiveNotRestorableError("Codex archive references history it does not carry; rebuild from the Session instead");
|
|
46
33
|
header.payload.id = input.id;
|
|
@@ -55,7 +42,6 @@ export async function importNativeArchive(input) {
|
|
|
55
42
|
};
|
|
56
43
|
try {
|
|
57
44
|
for await (const bytes of createReadStream(input.source, { signal: input.signal })) {
|
|
58
|
-
decoder?.push(bytes);
|
|
59
45
|
if (headerReady) {
|
|
60
46
|
await write(bytes);
|
|
61
47
|
continue;
|
|
@@ -73,7 +59,6 @@ export async function importNativeArchive(input) {
|
|
|
73
59
|
}
|
|
74
60
|
if (!headerReady)
|
|
75
61
|
await writeHeader();
|
|
76
|
-
decoder?.end();
|
|
77
62
|
input.signal?.throwIfAborted();
|
|
78
63
|
await file.sync();
|
|
79
64
|
await file.close();
|
|
@@ -87,7 +72,7 @@ export async function importNativeArchive(input) {
|
|
|
87
72
|
await directory.close();
|
|
88
73
|
}
|
|
89
74
|
}
|
|
90
|
-
return { checksum: digest.digest("hex"), nativeSessionId: input.harness === "pi" ? input.nativeSessionId : input.id
|
|
75
|
+
return { checksum: digest.digest("hex"), nativeSessionId: input.harness === "pi" ? input.nativeSessionId : input.id };
|
|
91
76
|
}
|
|
92
77
|
finally {
|
|
93
78
|
await file.close();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Harness } from "./adapters.js";
|
|
2
|
+
import { type NativeConfig } from "./config.js";
|
|
3
|
+
export type NativeSyncReport = {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
harnesses: Harness[];
|
|
6
|
+
codexShared: boolean;
|
|
7
|
+
importHistory: boolean;
|
|
8
|
+
note?: string;
|
|
9
|
+
};
|
|
10
|
+
type Input = {
|
|
11
|
+
root: string;
|
|
12
|
+
spaceId: string;
|
|
13
|
+
identity: string;
|
|
14
|
+
harnesses: Harness[];
|
|
15
|
+
yes?: boolean;
|
|
16
|
+
executables?: {
|
|
17
|
+
pi?: string;
|
|
18
|
+
codex?: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
type Ask = (question: string) => Promise<boolean>;
|
|
22
|
+
/** Whether this directory already syncs every requested harness. */
|
|
23
|
+
export declare function nativeSyncSatisfied(config: NativeConfig | null, input: {
|
|
24
|
+
spaceId: string;
|
|
25
|
+
root: string;
|
|
26
|
+
harnesses: Harness[];
|
|
27
|
+
}): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Enable native sync after consent. Native history may contain secrets, so nothing is read before
|
|
30
|
+
* the user agrees; a failure here never blocks the Runtime, it only leaves sessions read-only.
|
|
31
|
+
*/
|
|
32
|
+
export declare function ensureNativeSync(input: Input, log?: (line: string) => void, ask?: Ask): Promise<NativeSyncReport>;
|
|
33
|
+
/**
|
|
34
|
+
* Before any harness starts, repair what earlier releases installed: an outdated Pi extension can keep
|
|
35
|
+
* Pi from starting, and legacy Codex hooks run a script that is gone. Both had consent back then.
|
|
36
|
+
*/
|
|
37
|
+
export declare function repairIntegrations(piBinary: string | undefined, log?: (line: string) => void): Promise<void>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { serializeDiagnosticError } from "../diagnostics.js";
|
|
3
|
+
import { CODEX_SHARED_MIN_VERSION, supportsSharedServer } from "./codex-link.js";
|
|
4
|
+
import { PI_MIN_VERSION } from "./pi-link.js";
|
|
5
|
+
import { atLeast, formatVersion, harnessVersion } from "./version.js";
|
|
6
|
+
import { readNativeConfig, runtimeStateRoot, writeNativeConfig } from "./config.js";
|
|
7
|
+
import { discoverTranscripts } from "./ingest.js";
|
|
8
|
+
import { installPiExtension, piExtensionState, removeLegacyCodexHooks } from "./install.js";
|
|
9
|
+
/** Whether this directory already syncs every requested harness. */
|
|
10
|
+
export function nativeSyncSatisfied(config, input) {
|
|
11
|
+
return Boolean(config && config.spaceId === input.spaceId && config.root === input.root && input.harnesses.every((harness) => config.harnesses.includes(harness)));
|
|
12
|
+
}
|
|
13
|
+
async function defaultAsk(question) {
|
|
14
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
15
|
+
try {
|
|
16
|
+
return /^(|y(es)?)$/i.test((await rl.question(question)).trim());
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
rl.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Enable native sync after consent. Native history may contain secrets, so nothing is read before
|
|
24
|
+
* the user agrees; a failure here never blocks the Runtime, it only leaves sessions read-only.
|
|
25
|
+
*/
|
|
26
|
+
export async function ensureNativeSync(input, log = (line) => process.stderr.write(line), ask = defaultAsk) {
|
|
27
|
+
const stateRoot = runtimeStateRoot(input.spaceId);
|
|
28
|
+
const previous = await readNativeConfig(stateRoot, input.identity).catch(() => null);
|
|
29
|
+
if (previous && nativeSyncSatisfied(previous, input)) {
|
|
30
|
+
await maintain(previous, log, input.executables?.pi);
|
|
31
|
+
return { enabled: true, harnesses: previous.harnesses, codexShared: previous.codexShared, importHistory: false };
|
|
32
|
+
}
|
|
33
|
+
const interactive = process.stdin.isTTY === true;
|
|
34
|
+
const confirm = async (question) => input.yes ? true : interactive ? await ask(question) : false;
|
|
35
|
+
if (!input.yes && !interactive) {
|
|
36
|
+
log("Native sync off · rerun interactively or with --yes to enable\n");
|
|
37
|
+
return { enabled: false, harnesses: input.harnesses, codexShared: false, importHistory: false, note: "non-interactive" };
|
|
38
|
+
}
|
|
39
|
+
const names = input.harnesses.map((harness) => harness === "pi" ? "Pi" : "Codex").join(" and ");
|
|
40
|
+
if (!await confirm(`Sync ${names} chats in this folder to this Space so they continue on the web? Their history may contain secrets. [Y/n] `)) {
|
|
41
|
+
return { enabled: false, harnesses: input.harnesses, codexShared: false, importHistory: false, note: "declined" };
|
|
42
|
+
}
|
|
43
|
+
let codexShared = previous?.codexShared ?? false;
|
|
44
|
+
if (input.harnesses.includes("codex") && !codexShared) {
|
|
45
|
+
const version = await harnessVersion(input.executables?.codex || "codex");
|
|
46
|
+
if (!supportsSharedServer(version)) {
|
|
47
|
+
log(`Codex ${formatVersion(version)} predates the shared app-server (${CODEX_SHARED_MIN_VERSION.join(".")}+); terminal Codex chats sync read-only\n`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
codexShared = await confirm("Let terminal Codex and the web drive the same live sessions? Cohub starts Codex's shared app-server, which keeps running in the background. [Y/n] ");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const harnesses = [...new Set([...(previous?.root === input.root ? previous.harnesses : []), ...input.harnesses])];
|
|
54
|
+
const config = {
|
|
55
|
+
version: 2, identity: input.identity, spaceId: input.spaceId, root: input.root, harnesses,
|
|
56
|
+
// History is whatever existed before the first consent for this directory.
|
|
57
|
+
enabledAt: previous?.root === input.root ? previous.enabledAt : new Date().toISOString(),
|
|
58
|
+
codexShared,
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
await writeNativeConfig(stateRoot, config);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
log(`Native sync unavailable; Runtime continues without it — ${serializeDiagnosticError(error).message}\n`);
|
|
65
|
+
return { enabled: false, harnesses: input.harnesses, codexShared: false, importHistory: false, note: "config" };
|
|
66
|
+
}
|
|
67
|
+
await maintain(config, log, input.executables?.pi, true);
|
|
68
|
+
const history = (await discoverTranscripts(input.root, input.harnesses, { headers: true }).catch(() => []))
|
|
69
|
+
.filter((candidate) => candidate.mtimeMs < Date.parse(config.enabledAt)).length;
|
|
70
|
+
let importHistory = false;
|
|
71
|
+
if (history && !previous) {
|
|
72
|
+
importHistory = await confirm(`Import ${history} earlier conversation${history === 1 ? "" : "s"} from this folder? [Y/n] `);
|
|
73
|
+
if (!importHistory)
|
|
74
|
+
log("Import them later with cohub runtime import\n");
|
|
75
|
+
}
|
|
76
|
+
return { enabled: true, harnesses, codexShared, importHistory };
|
|
77
|
+
}
|
|
78
|
+
/** Keep integrations current on every `up`. */
|
|
79
|
+
async function maintain(config, log, piBinary, announce = false) {
|
|
80
|
+
if (config.harnesses.includes("pi"))
|
|
81
|
+
await maintainPiExtension(piBinary, log, announce);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Before any harness starts, repair what earlier releases installed: an outdated Pi extension can keep
|
|
85
|
+
* Pi from starting, and legacy Codex hooks run a script that is gone. Both had consent back then.
|
|
86
|
+
*/
|
|
87
|
+
export async function repairIntegrations(piBinary, log = (line) => process.stderr.write(line)) {
|
|
88
|
+
await removeLegacyCodexHooks().catch(() => undefined);
|
|
89
|
+
if (await piExtensionState().catch(() => null) === "outdated")
|
|
90
|
+
await maintainPiExtension(piBinary, log);
|
|
91
|
+
}
|
|
92
|
+
/** Pi without the extension still syncs, read-only. */
|
|
93
|
+
async function maintainPiExtension(piBinary, log, announce = false) {
|
|
94
|
+
const version = await harnessVersion(piBinary || "pi");
|
|
95
|
+
if (!atLeast(version, PI_MIN_VERSION)) {
|
|
96
|
+
log(`Pi ${formatVersion(version)} predates the Cohub extension (${PI_MIN_VERSION.join(".")}+); Pi chats sync read-only\n`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const state = await piExtensionState();
|
|
101
|
+
if (state === "installed")
|
|
102
|
+
return;
|
|
103
|
+
if (state === "foreign" || state === "missing" && !announce) {
|
|
104
|
+
log("Pi extension not installed; Pi chats sync read-only. Install with cohub runtime attach --harness pi\n");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await installPiExtension();
|
|
108
|
+
log(state === "outdated" ? "Pi extension updated; run /reload in open Pi sessions\n" : "Pi extension installed; run /reload in open Pi sessions to connect them\n");
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
log(`Pi extension not installed — ${serializeDiagnosticError(error).message}; Pi chats sync read-only\n`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type JsonRpcProcess, type JsonRecord } from "../json-rpc.js";
|
|
2
|
+
import { type Version } from "./version.js";
|
|
3
|
+
/** First Codex release whose terminal client attaches to a shared app-server by default. */
|
|
4
|
+
export declare const CODEX_SHARED_MIN_VERSION: Version;
|
|
5
|
+
export type CodexEvent = {
|
|
6
|
+
method: string;
|
|
7
|
+
params: JsonRecord;
|
|
8
|
+
};
|
|
9
|
+
/** One JSON-RPC peer of an app-server, over either a shared control socket or a private stdio process. */
|
|
10
|
+
export interface CodexChannel {
|
|
11
|
+
request(method: string, params?: JsonRecord, timeoutMs?: number): Promise<JsonRecord>;
|
|
12
|
+
refuse(id: string | number, message: string): void;
|
|
13
|
+
onEvent(listener: (event: CodexEvent & {
|
|
14
|
+
id?: string | number;
|
|
15
|
+
}) => void): () => void;
|
|
16
|
+
onClose(listener: (error: Error) => void): () => void;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
readonly shared: boolean;
|
|
19
|
+
readonly closed: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare const supportsSharedServer: (version: Version | null) => boolean;
|
|
22
|
+
/** A private app-server over stdio, used when no shared one is available; it lives for one Turn. */
|
|
23
|
+
export declare function openPrivateServer(rpc: JsonRpcProcess): Promise<CodexChannel>;
|
|
24
|
+
/** The Runtime's connection to the shared app-server. */
|
|
25
|
+
export declare class CodexShared {
|
|
26
|
+
private readonly binary;
|
|
27
|
+
private readonly cwd;
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
private channel;
|
|
30
|
+
private opening;
|
|
31
|
+
private ensured;
|
|
32
|
+
private readonly connectListeners;
|
|
33
|
+
readonly driving: Set<string>;
|
|
34
|
+
constructor(binary: string, cwd: string, enabled: boolean);
|
|
35
|
+
get(): Promise<CodexChannel | null>;
|
|
36
|
+
onConnect(listener: (channel: CodexChannel) => void): () => void;
|
|
37
|
+
join(threadId: string): Promise<JsonRecord | null>;
|
|
38
|
+
interrupt(threadId: string, turnId: string): Promise<boolean>;
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
}
|