@neta-art/cohub-cli 8.0.0 → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/runtime.js +53 -53
- package/dist/commands/sandboxd-binary.d.ts +1 -1
- package/dist/commands/sandboxd-binary.js +7 -1
- package/dist/runtime/archive-store.d.ts +3 -1
- package/dist/runtime/archive-store.js +2 -0
- package/dist/runtime/connection.js +34 -22
- package/dist/runtime/harness.d.ts +4 -0
- package/dist/runtime/harness.js +29 -16
- package/dist/runtime/instance.js +5 -5
- package/dist/runtime/json-rpc.d.ts +2 -0
- package/dist/runtime/json-rpc.js +2 -0
- package/dist/runtime/launch.js +22 -22
- package/dist/runtime/native-codex-hook.js +3 -3
- package/dist/runtime/native-install.js +9 -9
- package/dist/runtime/native-ipc.js +4 -4
- package/dist/runtime/native-pi-extension.js +1 -1
- package/dist/runtime/native-sync-store.js +19 -19
- package/dist/runtime/native-sync.js +5 -5
- package/dist/runtime/native-transcript.js +13 -13
- package/dist/runtime/presentation.js +31 -29
- package/dist/runtime/process-group.d.ts +2 -0
- package/dist/runtime/process-group.js +124 -31
- package/dist/runtime/session-store.d.ts +15 -2
- package/dist/runtime/session-store.js +79 -5
- package/dist/runtime/space-binding.js +3 -3
- package/dist/runtime/supervisor.js +4 -4
- package/package.json +1 -1
package/dist/commands/runtime.js
CHANGED
|
@@ -13,23 +13,23 @@ import { RuntimeSessionStore } from "../runtime/session-store.js";
|
|
|
13
13
|
import { readRuntimeDiagnosticEvents, RuntimeDiagnosticReader, runtimeDiagnosticsDirectory, serializeDiagnosticError } from "../runtime/diagnostics.js";
|
|
14
14
|
export { resolveLocalSpaceName, parseRuntimeHarnesses } from "../runtime/launch.js";
|
|
15
15
|
const reportFailure = (cause) => {
|
|
16
|
-
process.stderr.write(`Runtime failed
|
|
16
|
+
process.stderr.write(`Runtime failed: ${serializeDiagnosticError(cause).message}\n`);
|
|
17
17
|
process.exitCode = 1;
|
|
18
18
|
};
|
|
19
19
|
export function registerRuntime(program) {
|
|
20
|
-
const runtime = program.command("runtime").description("Connect a local workspace
|
|
20
|
+
const runtime = program.command("runtime").description("Connect a local workspace");
|
|
21
21
|
runtime.command("up [dir]")
|
|
22
|
-
.description("Connect local Harnesses and files
|
|
23
|
-
.option("-s, --space <id>", "Target Space
|
|
24
|
-
.option("-n, --new", "Create a new Space
|
|
25
|
-
.option("--name <name>", "New Space name
|
|
26
|
-
.option("-d, --detach", "Run in the background
|
|
27
|
-
.option("--harness <name>", "Pi or Codex; repeatable
|
|
28
|
-
.option("--pi <path>", "Pi executable
|
|
29
|
-
.option("--codex <path>", "Codex executable
|
|
30
|
-
.option("-y, --yes", "Accept defaults and local execution
|
|
31
|
-
.option("--verbose", "Show diagnostic details
|
|
32
|
-
.option("--json", "JSON output
|
|
22
|
+
.description("Connect local Harnesses and files")
|
|
23
|
+
.option("-s, --space <id>", "Target Space")
|
|
24
|
+
.option("-n, --new", "Create a new Space")
|
|
25
|
+
.option("--name <name>", "New Space name")
|
|
26
|
+
.option("-d, --detach", "Run in the background")
|
|
27
|
+
.option("--harness <name>", "Pi or Codex; repeatable", (value, previous) => [...previous, value], [])
|
|
28
|
+
.option("--pi <path>", "Pi executable")
|
|
29
|
+
.option("--codex <path>", "Codex executable")
|
|
30
|
+
.option("-y, --yes", "Accept defaults and authorize local execution")
|
|
31
|
+
.option("--verbose", "Show diagnostic details")
|
|
32
|
+
.option("--json", "JSON output")
|
|
33
33
|
.action(async (dir, options) => {
|
|
34
34
|
try {
|
|
35
35
|
await runtimeUp(program, dir, { ...options, json: jsonRequested(options) });
|
|
@@ -40,36 +40,36 @@ export function registerRuntime(program) {
|
|
|
40
40
|
});
|
|
41
41
|
for (const action of ["attach", "detach"])
|
|
42
42
|
runtime.command(action)
|
|
43
|
-
.description(action === "attach" ? "Sync native Pi / Codex Turns
|
|
44
|
-
.option("-s, --space <id>", "Target Space
|
|
45
|
-
.option("--harness <name>", "Pi or Codex; repeatable
|
|
46
|
-
.option("--pi <path>", "Pi executable for capability checks
|
|
47
|
-
.option("--codex <path>", "Codex executable for capability checks
|
|
48
|
-
.option("-y, --yes", "Authorize project conversation and native archive uploads
|
|
49
|
-
.option("--json", "JSON output
|
|
43
|
+
.description(action === "attach" ? "Sync native Pi / Codex Turns" : "Pause native sync; retain all receipts")
|
|
44
|
+
.option("-s, --space <id>", "Target Space")
|
|
45
|
+
.option("--harness <name>", "Pi or Codex; repeatable", (value, previous) => [...previous, value], [])
|
|
46
|
+
.option("--pi <path>", "Pi executable for capability checks")
|
|
47
|
+
.option("--codex <path>", "Codex executable for capability checks")
|
|
48
|
+
.option("-y, --yes", "Authorize project conversation and native archive uploads")
|
|
49
|
+
.option("--json", "JSON output")
|
|
50
50
|
.action(async (options) => {
|
|
51
51
|
try {
|
|
52
52
|
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
53
53
|
const identity = currentIdentityKey();
|
|
54
54
|
if (!identity)
|
|
55
|
-
throw new Error("Sign in first
|
|
55
|
+
throw new Error("Sign in first");
|
|
56
56
|
const root = await canonicalRuntimeRoot(process.cwd());
|
|
57
57
|
if (action === "attach" && (await getRuntimeSpaceBinding(root, identity))?.spaceId !== spaceId)
|
|
58
|
-
throw new Error("Bind this directory with runtime up --space first
|
|
58
|
+
throw new Error("Bind this directory with runtime up --space first");
|
|
59
59
|
const instance = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
|
|
60
60
|
if (action === "attach" && (!instance || instance.root !== root))
|
|
61
|
-
throw new Error("Start this directory's Runtime first: cohub runtime up -d
|
|
61
|
+
throw new Error("Start this directory's Runtime first: cohub runtime up -d");
|
|
62
62
|
if (action === "attach" && !instance?.nativeSync)
|
|
63
|
-
throw new Error("Restart the Runtime with this CLI before attaching
|
|
63
|
+
throw new Error("Restart the Runtime with this CLI before attaching");
|
|
64
64
|
const harnesses = parseRuntimeHarnesses(options.harness.length ? options.harness : instance?.harnesses ?? ["pi", "codex"]);
|
|
65
65
|
if (action === "attach" && harnesses.some((harness) => !instance?.harnesses.includes(harness)))
|
|
66
|
-
throw new Error("Enable these Harnesses with runtime up first
|
|
66
|
+
throw new Error("Enable these Harnesses with runtime up first");
|
|
67
67
|
if (action === "attach" && !options.yes) {
|
|
68
68
|
if (!process.stdin.isTTY)
|
|
69
|
-
throw new Error("Use --yes to authorize native sync
|
|
69
|
+
throw new Error("Use --yes to authorize native sync");
|
|
70
70
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
71
71
|
try {
|
|
72
|
-
const answer = await rl.question(`Install user-level ${harnesses.join(" / ")} integration and upload this project's opened conversations, tool output and raw archives to this Space? History may contain secrets. [y/N]
|
|
72
|
+
const answer = await rl.question(`Install user-level ${harnesses.join(" / ")} integration and upload this project's opened conversations, tool output and raw archives to this Space? History may contain secrets. [y/N] `);
|
|
73
73
|
if (!/^y(es)?$/i.test(answer.trim()))
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
@@ -82,23 +82,23 @@ export function registerRuntime(program) {
|
|
|
82
82
|
outJson(result);
|
|
83
83
|
else
|
|
84
84
|
process.stdout.write(action === "attach"
|
|
85
|
-
? `Native sync enabled. Reload Pi or restart Codex and review its hook trust prompt
|
|
86
|
-
: "Native sync paused; all local records retained
|
|
85
|
+
? `Native sync enabled. Reload Pi or restart Codex and review its hook trust prompt.\n${result.configPath}\n`
|
|
86
|
+
: "Native sync paused; all local records retained\n");
|
|
87
87
|
}
|
|
88
88
|
catch (cause) {
|
|
89
89
|
reportFailure(cause);
|
|
90
90
|
}
|
|
91
91
|
});
|
|
92
|
-
runtime.command("status").description("Local and server status
|
|
93
|
-
.option("-s, --space <id>", "Target Space
|
|
94
|
-
.option("--json", "JSON output
|
|
92
|
+
runtime.command("status").description("Local and server status")
|
|
93
|
+
.option("-s, --space <id>", "Target Space")
|
|
94
|
+
.option("--json", "JSON output")
|
|
95
95
|
.action(async (options) => {
|
|
96
96
|
try {
|
|
97
97
|
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
98
98
|
const identity = currentIdentityKey();
|
|
99
99
|
const local = identity ? await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId)) : null;
|
|
100
100
|
const space = createClient().space(spaceId);
|
|
101
|
-
const store = new RuntimeSessionStore(spaceId, { projectionSource: space });
|
|
101
|
+
const store = new RuntimeSessionStore(spaceId, { projectionSource: space, archiveTransport: null });
|
|
102
102
|
const [remote, pendingLocalArchives, failedLocalArchives, nativeStores] = await Promise.all([
|
|
103
103
|
space.getRuntime(undefined, { signal: AbortSignal.timeout(5000) }).then((value) => ({ value, error: null })).catch((error) => ({ value: null, error: serializeDiagnosticError(error).message })),
|
|
104
104
|
store.archives.pendingCount(), store.archives.failedCaptureCount(),
|
|
@@ -112,26 +112,26 @@ export function registerRuntime(program) {
|
|
|
112
112
|
if (local)
|
|
113
113
|
printRuntimeSummary(local);
|
|
114
114
|
else
|
|
115
|
-
process.stdout.write(`Local process
|
|
116
|
-
process.stdout.write(`Server
|
|
115
|
+
process.stdout.write(`Local process Not running\nSpace ${spaceId}\nLogs ${result.diagnosticsPath}\n`);
|
|
116
|
+
process.stdout.write(`Server ${remote.error ? `Unknown — ${remote.error}` : remote.value?.online ? "Harness connected" : "Offline"}\nArchives ${pendingLocalArchives} pending · ${failedLocalArchives} failed\n`);
|
|
117
117
|
if (nativeSessions.length)
|
|
118
|
-
process.stdout.write(`Native chats
|
|
118
|
+
process.stdout.write(`Native chats ${nativeSessions.length} · ${nativeSessions.reduce((sum, session) => sum + session.pendingTurns, 0)} Turns pending · ${nativeSessions.reduce((sum, session) => sum + session.pendingArchives, 0)} archives pending\n`);
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
catch (cause) {
|
|
122
122
|
reportFailure(cause);
|
|
123
123
|
}
|
|
124
124
|
});
|
|
125
|
-
runtime.command("down").description("Stop this local Runtime; retain all data
|
|
126
|
-
.option("-s, --space <id>", "Target Space
|
|
127
|
-
.option("-y, --yes", "Stop even with unconfirmed executions
|
|
128
|
-
.option("--json", "JSON output
|
|
125
|
+
runtime.command("down").description("Stop this local Runtime; retain all data")
|
|
126
|
+
.option("-s, --space <id>", "Target Space")
|
|
127
|
+
.option("-y, --yes", "Stop even with unconfirmed executions")
|
|
128
|
+
.option("--json", "JSON output")
|
|
129
129
|
.action(async (options) => {
|
|
130
130
|
try {
|
|
131
131
|
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
132
132
|
const identity = currentIdentityKey();
|
|
133
133
|
if (!identity)
|
|
134
|
-
throw new Error("Sign in to the Runtime account
|
|
134
|
+
throw new Error("Sign in to the Runtime account");
|
|
135
135
|
const directory = runtimeInstanceDirectory(identity, spaceId);
|
|
136
136
|
const local = await requestRuntimeInstance(directory, "stop", Boolean(options.yes));
|
|
137
137
|
const until = Date.now() + 15_000;
|
|
@@ -146,33 +146,33 @@ export function registerRuntime(program) {
|
|
|
146
146
|
} // An unreachable control socket does not prove the process stopped.
|
|
147
147
|
}
|
|
148
148
|
if (running)
|
|
149
|
-
throw new Error("Runtime is still stopping; inspect logs
|
|
149
|
+
throw new Error("Runtime is still stopping; inspect logs");
|
|
150
150
|
if (jsonRequested(options))
|
|
151
151
|
outJson({ spaceId, stopped: true });
|
|
152
152
|
else
|
|
153
|
-
process.stdout.write("Runtime stopped; data retained
|
|
153
|
+
process.stdout.write("Runtime stopped; data retained\n");
|
|
154
154
|
}
|
|
155
155
|
catch (cause) {
|
|
156
156
|
reportFailure(cause);
|
|
157
157
|
}
|
|
158
158
|
});
|
|
159
|
-
runtime.command("logs").description("Read local Runtime diagnostics
|
|
160
|
-
.option("-s, --space <id>", "Target Space
|
|
161
|
-
.option("-l, --limit <count>", "Number of events
|
|
162
|
-
.option("--level <level>", "Minimum level: debug, info, warn, error
|
|
163
|
-
.option("-f, --follow", "Keep watching
|
|
164
|
-
.option("--json", "Raw diagnostic events
|
|
159
|
+
runtime.command("logs").description("Read local Runtime diagnostics")
|
|
160
|
+
.option("-s, --space <id>", "Target Space")
|
|
161
|
+
.option("-l, --limit <count>", "Number of events", "100")
|
|
162
|
+
.option("--level <level>", "Minimum level: debug, info, warn, error", "info")
|
|
163
|
+
.option("-f, --follow", "Keep watching")
|
|
164
|
+
.option("--json", "Raw diagnostic events")
|
|
165
165
|
.action(async (options) => {
|
|
166
166
|
const controller = new AbortController();
|
|
167
167
|
const stop = () => controller.abort();
|
|
168
168
|
try {
|
|
169
169
|
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
170
|
-
const store = new RuntimeSessionStore(spaceId, { projectionSource: createClient().space(spaceId) });
|
|
170
|
+
const store = new RuntimeSessionStore(spaceId, { projectionSource: createClient().space(spaceId), archiveTransport: null });
|
|
171
171
|
const limit = Number(options.limit);
|
|
172
172
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10_000)
|
|
173
|
-
throw new Error("Use a limit from 1 to 10000
|
|
173
|
+
throw new Error("Use a limit from 1 to 10000");
|
|
174
174
|
if (!diagnosticLevels.includes(options.level))
|
|
175
|
-
throw new Error("Use debug, info, warn or error
|
|
175
|
+
throw new Error("Use debug, info, warn or error");
|
|
176
176
|
const asJson = jsonRequested(options);
|
|
177
177
|
const reader = new RuntimeDiagnosticReader(store.root);
|
|
178
178
|
process.once("SIGINT", stop);
|
|
@@ -187,7 +187,7 @@ export function registerRuntime(program) {
|
|
|
187
187
|
process.stdout.write(asJson ? `${JSON.stringify(event)}\n` : formatDiagnostic(event, true));
|
|
188
188
|
if (!options.follow) {
|
|
189
189
|
if (!asJson && !events.length)
|
|
190
|
-
process.stdout.write("No matching diagnostics
|
|
190
|
+
process.stdout.write("No matching diagnostics\n");
|
|
191
191
|
break;
|
|
192
192
|
}
|
|
193
193
|
await delay(1000, undefined, { signal: controller.signal }).catch(() => undefined);
|
|
@@ -21,7 +21,13 @@ import { Readable } from "node:stream";
|
|
|
21
21
|
// carries the optional workspace-search runner download. v2.53.1 already has
|
|
22
22
|
// the native FSEvents backends and the `runtimeId` control frame, and older
|
|
23
23
|
// releases stay usable through the compatibility readiness/restart path.
|
|
24
|
-
|
|
24
|
+
//
|
|
25
|
+
// v2.54.1 keeps the same wire protocol and adds relay diagnostics: data-channel
|
|
26
|
+
// pairing outlives the runner's dial timeout, dial failures distinguish a
|
|
27
|
+
// timeout from an explicit rejection, and teardown-time websocket write
|
|
28
|
+
// failures log at debug instead of warn. No new capability is required, so this
|
|
29
|
+
// pin is a diagnostics upgrade only.
|
|
30
|
+
export const SANDBOXD_VERSION = "v2.54.1";
|
|
25
31
|
const BINARY_NAME = "cohub-sandboxd";
|
|
26
32
|
// Public CDN prefix hosting the release archives (the repo is private, so the
|
|
27
33
|
// GitHub Release assets are not publicly downloadable). Overridable for staging
|
|
@@ -24,7 +24,9 @@ export declare class RuntimeArchiveStore {
|
|
|
24
24
|
private flushing;
|
|
25
25
|
private readonly capturing;
|
|
26
26
|
private errorReporter;
|
|
27
|
-
constructor(root: string, transport?: ArchiveTransport | undefined);
|
|
27
|
+
constructor(root: string, transport?: ArchiveTransport | null | undefined);
|
|
28
|
+
/** Distinguishes "no transport configured" from transient upload failures. */
|
|
29
|
+
get hasTransport(): boolean;
|
|
28
30
|
setErrorReporter(reporter: ((error: unknown, index?: HarnessArchiveIndex) => void) | null): void;
|
|
29
31
|
pendingCount(): Promise<number>;
|
|
30
32
|
failedCaptureCount(): Promise<number>;
|
|
@@ -49,6 +49,8 @@ export class RuntimeArchiveStore {
|
|
|
49
49
|
this.root = root;
|
|
50
50
|
this.transport = transport;
|
|
51
51
|
}
|
|
52
|
+
/** Distinguishes "no transport configured" from transient upload failures. */
|
|
53
|
+
get hasTransport() { return this.transport != null; }
|
|
52
54
|
setErrorReporter(reporter) {
|
|
53
55
|
this.errorReporter = reporter;
|
|
54
56
|
}
|
|
@@ -56,7 +56,7 @@ export async function serveRuntime(options) {
|
|
|
56
56
|
if (options.signal.aborted)
|
|
57
57
|
return;
|
|
58
58
|
if (outcome === "fatal")
|
|
59
|
-
throw new Error("Runtime connection rejected
|
|
59
|
+
throw new Error("Runtime connection rejected; check permissions or upgrade the CLI");
|
|
60
60
|
if (readyAt && Date.now() - readyAt >= 60_000) {
|
|
61
61
|
backoff = 500;
|
|
62
62
|
attempt = 0;
|
|
@@ -139,10 +139,11 @@ async function connect(options) {
|
|
|
139
139
|
}
|
|
140
140
|
socket.send(data);
|
|
141
141
|
};
|
|
142
|
-
const
|
|
142
|
+
const closeTransport = () => socket.close();
|
|
143
|
+
const shutdown = () => {
|
|
143
144
|
for (const execution of active.values())
|
|
144
145
|
execution.controller.abort();
|
|
145
|
-
|
|
146
|
+
closeTransport();
|
|
146
147
|
};
|
|
147
148
|
const sendNative = (event) => new Promise((resolve, reject) => {
|
|
148
149
|
if (!nativeChannelReady || !connectionId) {
|
|
@@ -161,7 +162,7 @@ async function connect(options) {
|
|
|
161
162
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
162
163
|
}
|
|
163
164
|
});
|
|
164
|
-
options.signal.addEventListener("abort",
|
|
165
|
+
options.signal.addEventListener("abort", shutdown, { once: true });
|
|
165
166
|
const heartbeat = setInterval(() => {
|
|
166
167
|
const heartbeatAgeMs = Date.now() - lastHeartbeat;
|
|
167
168
|
if (heartbeatAgeMs > 30_000) {
|
|
@@ -169,12 +170,7 @@ async function connect(options) {
|
|
|
169
170
|
ageMs: heartbeatAgeMs,
|
|
170
171
|
activeExecutions: active.size,
|
|
171
172
|
}, { connectionId });
|
|
172
|
-
|
|
173
|
-
log("error", "runtime.execution_heartbeat_timeout", {
|
|
174
|
-
ageMs: heartbeatAgeMs,
|
|
175
|
-
}, executionContext(connectionId, execution));
|
|
176
|
-
}
|
|
177
|
-
stop();
|
|
173
|
+
closeTransport();
|
|
178
174
|
}
|
|
179
175
|
else if (connectionId) {
|
|
180
176
|
try {
|
|
@@ -184,7 +180,7 @@ async function connect(options) {
|
|
|
184
180
|
log("warn", "runtime.heartbeat_send_failed", {
|
|
185
181
|
error: serializeDiagnosticError(error),
|
|
186
182
|
}, { connectionId });
|
|
187
|
-
|
|
183
|
+
closeTransport();
|
|
188
184
|
}
|
|
189
185
|
void options.token().then((next) => {
|
|
190
186
|
if (next !== currentToken) {
|
|
@@ -197,12 +193,12 @@ async function connect(options) {
|
|
|
197
193
|
log("warn", "runtime.auth_refresh_send_failed", {
|
|
198
194
|
error: serializeDiagnosticError(error),
|
|
199
195
|
}, { connectionId });
|
|
200
|
-
|
|
196
|
+
closeTransport();
|
|
201
197
|
}
|
|
202
198
|
}
|
|
203
199
|
}).catch((error) => {
|
|
204
200
|
log("warn", "runtime.auth_refresh_failed", { error: serializeDiagnosticError(error) }, { connectionId });
|
|
205
|
-
|
|
201
|
+
closeTransport();
|
|
206
202
|
});
|
|
207
203
|
}
|
|
208
204
|
}, 10_000);
|
|
@@ -227,12 +223,14 @@ async function connect(options) {
|
|
|
227
223
|
}
|
|
228
224
|
nativePending.clear();
|
|
229
225
|
options.onDisconnected?.();
|
|
226
|
+
const executionMustStop = options.signal.aborted || fatal || conflict;
|
|
230
227
|
for (const execution of active.values()) {
|
|
231
|
-
log("error", "runtime.
|
|
228
|
+
log(executionMustStop ? "error" : "warn", executionMustStop ? "runtime.execution_transport_invalidated" : "runtime.execution_transport_detached", {
|
|
232
229
|
code: event.code,
|
|
233
230
|
reason: event.reason,
|
|
234
231
|
}, executionContext(connectionId, execution));
|
|
235
|
-
|
|
232
|
+
if (executionMustStop)
|
|
233
|
+
execution.controller.abort();
|
|
236
234
|
}
|
|
237
235
|
resolve();
|
|
238
236
|
}, { once: true });
|
|
@@ -261,7 +259,7 @@ async function connect(options) {
|
|
|
261
259
|
}
|
|
262
260
|
catch (error) {
|
|
263
261
|
log("error", "runtime.hello_failed", { error: serializeDiagnosticError(error) });
|
|
264
|
-
|
|
262
|
+
closeTransport();
|
|
265
263
|
}
|
|
266
264
|
});
|
|
267
265
|
socket.addEventListener("message", (event) => {
|
|
@@ -274,6 +272,8 @@ async function connect(options) {
|
|
|
274
272
|
log("error", "runtime.protocol.invalid_json", { error: serializeDiagnosticError(error) }, { connectionId });
|
|
275
273
|
throw error;
|
|
276
274
|
}
|
|
275
|
+
// Any inbound frame proves the transport is live; heartbeat frames are one case.
|
|
276
|
+
lastHeartbeat = Date.now();
|
|
277
277
|
if (raw.type === "runtime.ready") {
|
|
278
278
|
const frame = runtimeReadySchema.parse(raw);
|
|
279
279
|
if (connectionId === frame.connectionId)
|
|
@@ -303,7 +303,6 @@ async function connect(options) {
|
|
|
303
303
|
if (!connectionId)
|
|
304
304
|
throw new Error("Runtime handshake is incomplete");
|
|
305
305
|
if (raw.type === "runtime.heartbeat") {
|
|
306
|
-
lastHeartbeat = Date.now();
|
|
307
306
|
return;
|
|
308
307
|
}
|
|
309
308
|
if (raw.type === "runtime.native.result") {
|
|
@@ -508,7 +507,17 @@ async function connect(options) {
|
|
|
508
507
|
const emit = (value) => {
|
|
509
508
|
if (value.type === "message.commit")
|
|
510
509
|
durableEvents.push(value);
|
|
511
|
-
|
|
510
|
+
// Streaming is best-effort. A detached transport must not cancel local model or tool work;
|
|
511
|
+
// durable commits and the final result are replayed after reconnect.
|
|
512
|
+
if (disconnected.signal.aborted || socket.readyState !== WebSocket.OPEN || socket.bufferedAmount > RUNTIME_MAX_FRAME_BYTES)
|
|
513
|
+
return;
|
|
514
|
+
try {
|
|
515
|
+
send({ type: "runtime.event", requestId: frame.requestId, event: value });
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
if (!disconnected.signal.aborted && socket.readyState === WebSocket.OPEN)
|
|
519
|
+
throw error;
|
|
520
|
+
}
|
|
512
521
|
};
|
|
513
522
|
execution.promise = (async () => {
|
|
514
523
|
try {
|
|
@@ -541,7 +550,10 @@ async function connect(options) {
|
|
|
541
550
|
frame.input.context = await requestContext(controller.signal);
|
|
542
551
|
}
|
|
543
552
|
}
|
|
544
|
-
await options.store.recordResult(execution.result.state, frame.requestId, [...durableEvents, execution.result.event]);
|
|
553
|
+
await options.store.recordResult(execution.result.state, frame.requestId, [...durableEvents, execution.result.event], execution.result.uncertainCleanup ? { uncertainCleanup: execution.result.uncertainCleanup } : undefined);
|
|
554
|
+
if (execution.result.uncertainCleanup) {
|
|
555
|
+
log("warn", "runtime.turn_cleanup_pending", { cleanup: execution.result.uncertainCleanup.error }, context);
|
|
556
|
+
}
|
|
545
557
|
emit(execution.result.event);
|
|
546
558
|
}
|
|
547
559
|
catch (error) {
|
|
@@ -560,7 +572,7 @@ async function connect(options) {
|
|
|
560
572
|
})();
|
|
561
573
|
})().catch((error) => {
|
|
562
574
|
log("error", "runtime.protocol_error", { error: serializeDiagnosticError(error) }, { connectionId });
|
|
563
|
-
|
|
575
|
+
closeTransport();
|
|
564
576
|
});
|
|
565
577
|
});
|
|
566
578
|
readyTimer = setTimeout(() => {
|
|
@@ -568,7 +580,7 @@ async function connect(options) {
|
|
|
568
580
|
socket.close(4408, "Runtime handshake timed out");
|
|
569
581
|
}, 15_000);
|
|
570
582
|
if (options.signal.aborted)
|
|
571
|
-
|
|
583
|
+
shutdown();
|
|
572
584
|
try {
|
|
573
585
|
await closed;
|
|
574
586
|
await Promise.allSettled([...active.values()].map((entry) => entry.promise));
|
|
@@ -576,7 +588,7 @@ async function connect(options) {
|
|
|
576
588
|
finally {
|
|
577
589
|
clearTimeout(readyTimer);
|
|
578
590
|
clearInterval(heartbeat);
|
|
579
|
-
options.signal.removeEventListener("abort",
|
|
591
|
+
options.signal.removeEventListener("abort", shutdown);
|
|
580
592
|
}
|
|
581
593
|
if (unauthorized && !options.signal.aborted)
|
|
582
594
|
await options.token(true);
|
|
@@ -14,6 +14,10 @@ export type HarnessResult = {
|
|
|
14
14
|
event: Extract<RuntimeExecutionEvent, {
|
|
15
15
|
type: "turn.end";
|
|
16
16
|
}>;
|
|
17
|
+
uncertainCleanup?: {
|
|
18
|
+
processGroupId: number;
|
|
19
|
+
error: string;
|
|
20
|
+
};
|
|
17
21
|
};
|
|
18
22
|
export declare function piContent(value: unknown): ContentBlock[];
|
|
19
23
|
export declare function promptText(content: ContentBlock[]): string;
|
package/dist/runtime/harness.js
CHANGED
|
@@ -2,6 +2,7 @@ import { access, stat } from "node:fs/promises";
|
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { delimiter, join, resolve, win32 } from "node:path";
|
|
4
4
|
import { JsonRpcProcess, record } from "./json-rpc.js";
|
|
5
|
+
import { ProcessCleanupUncertainError } from "./process-group.js";
|
|
5
6
|
import { codexModelCatalog } from "./model-catalog.js";
|
|
6
7
|
import { codexTokenTotals, codexUsage, subtractCodexTokens } from "./codex-usage.js";
|
|
7
8
|
import { downloadPublicImage } from "../safe-remote-image.js";
|
|
@@ -85,6 +86,16 @@ async function finishHarnessTurn(store, state, message, resume, turnId, diagnost
|
|
|
85
86
|
const archive = await store.archive(state, turnId, diagnosticContext).catch(() => null);
|
|
86
87
|
return { state, event: { type: "turn.end", message, resume, archive } };
|
|
87
88
|
}
|
|
89
|
+
/** Close the RPC process. An unconfirmable cleanup degrades the result to uncertain, never masks it. */
|
|
90
|
+
async function closeHarness(rpc, executionError) {
|
|
91
|
+
return rpc.close().then(() => undefined, (cleanupError) => {
|
|
92
|
+
const processGroupId = rpc.processGroupId;
|
|
93
|
+
if (!(cleanupError instanceof ProcessCleanupUncertainError) || processGroupId == null)
|
|
94
|
+
throw cleanupError;
|
|
95
|
+
const detail = executionError ? `; execution error: ${executionError}` : "";
|
|
96
|
+
return { processGroupId, error: `${cleanupError.message}${detail}` };
|
|
97
|
+
});
|
|
98
|
+
}
|
|
88
99
|
export async function discoverHarnesses(harnesses, options, cwd) {
|
|
89
100
|
const models = [];
|
|
90
101
|
await Promise.all(harnesses.map(async (harness) => {
|
|
@@ -133,6 +144,7 @@ export async function executePi(input, options, cwd, store, emit, signal, diagno
|
|
|
133
144
|
let currentContent = [];
|
|
134
145
|
let last = { ordinal: 0, content: [] };
|
|
135
146
|
const abortEscalation = createAbortEscalation(rpc, signal, () => { void rpc.request("abort").catch(() => undefined); });
|
|
147
|
+
let uncertainCleanup;
|
|
136
148
|
const stopFailureLogging = diagnostics
|
|
137
149
|
? rpc.onFailure((error) => diagnostics.log("error", "harness.rpc_process_failed", { error: serializeDiagnosticError(error) }, { ...diagnosticContext, component: "harness" }))
|
|
138
150
|
: () => undefined;
|
|
@@ -235,17 +247,17 @@ export async function executePi(input, options, cwd, store, emit, signal, diagno
|
|
|
235
247
|
abortEscalation.clear();
|
|
236
248
|
stopFailureLogging();
|
|
237
249
|
stopTimeoutLogging();
|
|
238
|
-
await rpc.
|
|
250
|
+
uncertainCleanup = await closeHarness(rpc, last.errorMessage);
|
|
239
251
|
}
|
|
240
252
|
if (signal.aborted)
|
|
241
253
|
last = { ...last, stopReason: "aborted" };
|
|
242
|
-
return finishHarnessTurn(store, state, last, resume, input.turnId, {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
254
|
+
return { ...await finishHarnessTurn(store, state, last, resume, input.turnId, {
|
|
255
|
+
component: "harness",
|
|
256
|
+
sessionId: input.sessionId,
|
|
257
|
+
turnId: input.turnId,
|
|
258
|
+
harness: "pi",
|
|
259
|
+
traceContext: input.traceContext,
|
|
260
|
+
}), ...(uncertainCleanup ? { uncertainCleanup } : {}) };
|
|
249
261
|
}
|
|
250
262
|
export function codexItemContent(item) {
|
|
251
263
|
if (item.type === "agentMessage" || item.type === "plan")
|
|
@@ -295,6 +307,7 @@ export async function executeCodex(input, options, cwd, store, emit, signal, dia
|
|
|
295
307
|
if (nativeTurnId)
|
|
296
308
|
void rpc.request("turn/interrupt", { threadId: state.nativeSessionId, turnId: nativeTurnId }).catch(() => undefined);
|
|
297
309
|
});
|
|
310
|
+
let uncertainCleanup;
|
|
298
311
|
try {
|
|
299
312
|
await initializeCodex(rpc);
|
|
300
313
|
const threadOptions = {
|
|
@@ -451,17 +464,17 @@ export async function executeCodex(input, options, cwd, store, emit, signal, dia
|
|
|
451
464
|
abortEscalation.clear();
|
|
452
465
|
stopFailureLogging();
|
|
453
466
|
stopTimeoutLogging();
|
|
454
|
-
await rpc.
|
|
467
|
+
uncertainCleanup = await closeHarness(rpc, final.errorMessage);
|
|
455
468
|
}
|
|
456
469
|
if (signal.aborted)
|
|
457
470
|
final = { ...final, stopReason: "aborted" };
|
|
458
471
|
if (usage)
|
|
459
472
|
final = { ...final, usage };
|
|
460
|
-
return finishHarnessTurn(store, state, final, resume, input.turnId, {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
473
|
+
return { ...await finishHarnessTurn(store, state, final, resume, input.turnId, {
|
|
474
|
+
component: "harness",
|
|
475
|
+
sessionId: input.sessionId,
|
|
476
|
+
turnId: input.turnId,
|
|
477
|
+
harness: "codex",
|
|
478
|
+
traceContext: input.traceContext,
|
|
479
|
+
}), ...(uncertainCleanup ? { uncertainCleanup } : {}) };
|
|
467
480
|
}
|
package/dist/runtime/instance.js
CHANGED
|
@@ -23,7 +23,7 @@ async function readRecord(directory) {
|
|
|
23
23
|
try {
|
|
24
24
|
const value = JSON.parse(await readFile(join(directory, "owner.json"), "utf8"));
|
|
25
25
|
if (!Number.isSafeInteger(value.pid) || typeof value.nonce !== "string" || typeof value.socket !== "string")
|
|
26
|
-
throw new Error("Invalid Runtime instance record
|
|
26
|
+
throw new Error("Invalid Runtime instance record");
|
|
27
27
|
return value;
|
|
28
28
|
}
|
|
29
29
|
catch (error) {
|
|
@@ -35,7 +35,7 @@ async function readRecord(directory) {
|
|
|
35
35
|
function request(record, action, force = false) {
|
|
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")); }, 3000);
|
|
39
39
|
let buffer = "";
|
|
40
40
|
let settled = false;
|
|
41
41
|
const finish = (error, value) => {
|
|
@@ -51,7 +51,7 @@ function request(record, action, force = false) {
|
|
|
51
51
|
};
|
|
52
52
|
socket.on("connect", () => socket.write(`${JSON.stringify({ nonce: record.nonce, action, force })}\n`));
|
|
53
53
|
socket.on("error", (error) => finish(error));
|
|
54
|
-
socket.on("close", () => finish(new Error("Runtime control closed
|
|
54
|
+
socket.on("close", () => finish(new Error("Runtime control closed")));
|
|
55
55
|
socket.on("data", (chunk) => {
|
|
56
56
|
buffer += chunk.toString();
|
|
57
57
|
if (buffer.length > 64 * 1024) {
|
|
@@ -65,7 +65,7 @@ function request(record, action, force = false) {
|
|
|
65
65
|
if (response.error)
|
|
66
66
|
finish(new Error(response.error));
|
|
67
67
|
else if (response.nonce !== record.nonce || response.status?.pid !== record.pid)
|
|
68
|
-
finish(new Error("Runtime identity changed
|
|
68
|
+
finish(new Error("Runtime identity changed"));
|
|
69
69
|
else
|
|
70
70
|
finish(undefined, response.status);
|
|
71
71
|
}
|
|
@@ -95,7 +95,7 @@ export async function ownRuntimeInstance(directory, status, stop) {
|
|
|
95
95
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
96
96
|
return withRuntimeSpaceBindingsLock(async () => {
|
|
97
97
|
if (await requestRuntimeInstance(directory))
|
|
98
|
-
throw new Error("Runtime already running; use status
|
|
98
|
+
throw new Error("Runtime already running; use status");
|
|
99
99
|
const socketPath = process.platform === "win32"
|
|
100
100
|
? `\\\\.\\pipe\\cohub-${createHash("sha256").update(directory).digest("hex").slice(0, 24)}`
|
|
101
101
|
: join(directory, "control.sock");
|
|
@@ -17,6 +17,8 @@ export declare function harnessEnvironment(): NodeJS.ProcessEnv;
|
|
|
17
17
|
export declare class JsonRpcProcess {
|
|
18
18
|
private mode;
|
|
19
19
|
private child;
|
|
20
|
+
/** Process-group id of the harness process, for deferred cleanup confirmation. */
|
|
21
|
+
get processGroupId(): number | null;
|
|
20
22
|
private pending;
|
|
21
23
|
private listeners;
|
|
22
24
|
private failureListeners;
|
package/dist/runtime/json-rpc.js
CHANGED
|
@@ -50,6 +50,8 @@ export function harnessEnvironment() {
|
|
|
50
50
|
export class JsonRpcProcess {
|
|
51
51
|
mode;
|
|
52
52
|
child;
|
|
53
|
+
/** Process-group id of the harness process, for deferred cleanup confirmation. */
|
|
54
|
+
get processGroupId() { return this.child.pid ?? null; }
|
|
53
55
|
pending = new Map();
|
|
54
56
|
listeners = new Set();
|
|
55
57
|
failureListeners = new Set();
|