@neta-art/cohub-cli 7.1.2 → 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.
Files changed (48) hide show
  1. package/README.md +40 -2
  2. package/dist/auth.js +38 -5
  3. package/dist/client.js +5 -2
  4. package/dist/commands/runtime.d.ts +1 -2
  5. package/dist/commands/runtime.js +169 -293
  6. package/dist/commands/sandboxd-binary.d.ts +1 -1
  7. package/dist/commands/sandboxd-binary.js +13 -5
  8. package/dist/runtime/archive-store.d.ts +5 -1
  9. package/dist/runtime/archive-store.js +22 -6
  10. package/dist/runtime/connection.d.ts +4 -2
  11. package/dist/runtime/connection.js +113 -44
  12. package/dist/runtime/diagnostics.d.ts +3 -0
  13. package/dist/runtime/diagnostics.js +3 -0
  14. package/dist/runtime/harness.d.ts +7 -0
  15. package/dist/runtime/harness.js +63 -17
  16. package/dist/runtime/instance.d.ts +5 -0
  17. package/dist/runtime/instance.js +159 -0
  18. package/dist/runtime/json-rpc.d.ts +2 -0
  19. package/dist/runtime/json-rpc.js +2 -0
  20. package/dist/runtime/launch.d.ts +20 -0
  21. package/dist/runtime/launch.js +176 -0
  22. package/dist/runtime/native-codex-hook.d.ts +1 -0
  23. package/dist/runtime/native-codex-hook.js +28 -0
  24. package/dist/runtime/native-install.d.ts +21 -0
  25. package/dist/runtime/native-install.js +130 -0
  26. package/dist/runtime/native-ipc.d.ts +26 -0
  27. package/dist/runtime/native-ipc.js +101 -0
  28. package/dist/runtime/native-pi-extension.d.ts +20 -0
  29. package/dist/runtime/native-pi-extension.js +47 -0
  30. package/dist/runtime/native-sync-store.d.ts +97 -0
  31. package/dist/runtime/native-sync-store.js +365 -0
  32. package/dist/runtime/native-sync.d.ts +25 -0
  33. package/dist/runtime/native-sync.js +128 -0
  34. package/dist/runtime/native-transcript.d.ts +27 -0
  35. package/dist/runtime/native-transcript.js +281 -0
  36. package/dist/runtime/presentation.d.ts +21 -0
  37. package/dist/runtime/presentation.js +78 -0
  38. package/dist/runtime/process-group.d.ts +2 -0
  39. package/dist/runtime/process-group.js +124 -31
  40. package/dist/runtime/session-store.d.ts +17 -2
  41. package/dist/runtime/session-store.js +118 -9
  42. package/dist/runtime/space-binding.d.ts +3 -0
  43. package/dist/runtime/space-binding.js +43 -6
  44. package/dist/runtime/supervisor.d.ts +16 -0
  45. package/dist/runtime/supervisor.js +277 -0
  46. package/dist/runtime/worker.d.ts +1 -0
  47. package/dist/runtime/worker.js +20 -0
  48. package/package.json +3 -2
@@ -1,328 +1,204 @@
1
- import { spawn } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
3
- import { stat } from "node:fs/promises";
4
- import { basename, resolve } from "node:path";
1
+ import { setTimeout as delay } from "node:timers/promises";
5
2
  import { createInterface } from "node:readline/promises";
6
- import { isLocalHarness, resolveCohubEnvironment, resolveWebsocketUrl } from "@neta-art/cohub";
7
- import { requireAccessToken } from "../auth.js";
8
3
  import { createClient } from "../client.js";
9
- import { error, json as outJson, jsonRequested } from "../output.js";
10
- import { currentIdentityKey, explicitSpace, resolveSpace } from "../space.js";
11
- import { canonicalRuntimeRoot, resolveRuntimeSpace } from "../runtime/space-binding.js";
12
- import { discoverHarnesses } from "../runtime/harness.js";
13
- import { serveRuntime } from "../runtime/connection.js";
4
+ import { json as outJson, jsonRequested } from "../output.js";
5
+ import { currentIdentityKey } from "../space.js";
6
+ import { resolveRuntimeTarget, runtimeUp, parseRuntimeHarnesses } from "../runtime/launch.js";
7
+ import { canonicalRuntimeRoot, getRuntimeSpaceBinding } from "../runtime/space-binding.js";
8
+ import { installNativeSync } from "../runtime/native-install.js";
9
+ import { listNativeSyncStores } from "../runtime/native-sync-store.js";
10
+ import { requestRuntimeInstance, runtimeInstanceDirectory } from "../runtime/instance.js";
11
+ import { atLeastLevel, diagnosticLevels, formatDiagnostic, printRuntimeSummary } from "../runtime/presentation.js";
14
12
  import { RuntimeSessionStore } from "../runtime/session-store.js";
15
- import { ensureSandboxdBinary } from "./sandboxd-binary.js";
16
- import { readRuntimeDiagnosticEvents, RuntimeDiagnosticReader, RuntimeDiagnostics, runtimeDiagnosticsDirectory, serializeDiagnosticError, } from "../runtime/diagnostics.js";
17
- export const resolveLocalSpaceName = (root, name) => name?.trim() || basename(root) || "local-space";
18
- function sandboxOutputLevel(value, stream) {
19
- const level = typeof value === "string" ? value.toLowerCase() : "";
20
- if (level.includes("error"))
21
- return "error";
22
- if (level.includes("warn"))
23
- return "warn";
24
- return stream === "stderr" ? "error" : "debug";
25
- }
26
- function captureSandboxOutput(stream, streamName, diagnostics) {
27
- if (!stream)
28
- return;
29
- let pending = "";
30
- const consume = (chunk) => {
31
- pending += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
32
- let newline = pending.indexOf("\n");
33
- while (newline >= 0) {
34
- const line = pending.slice(0, newline).trim();
35
- pending = pending.slice(newline + 1);
36
- if (line)
37
- recordSandboxOutput(line, streamName, diagnostics);
38
- newline = pending.indexOf("\n");
39
- }
40
- };
41
- stream.on("data", consume);
42
- stream.on("end", () => {
43
- if (pending.trim())
44
- recordSandboxOutput(pending.trim(), streamName, diagnostics);
45
- });
46
- }
47
- function recordSandboxOutput(line, streamName, diagnostics) {
48
- let parsed = null;
49
- try {
50
- const value = JSON.parse(line);
51
- if (value && typeof value === "object" && !Array.isArray(value))
52
- parsed = value;
53
- }
54
- catch {
55
- // Older or third-party binaries may still emit text logs.
56
- }
57
- const level = sandboxOutputLevel(parsed?.level, streamName);
58
- const message = typeof parsed?.msg === "string" ? parsed.msg : line;
59
- const data = parsed
60
- ? Object.fromEntries(Object.entries(parsed).filter(([key]) => !["msg", "level", "time"].includes(key)))
61
- : { message: line };
62
- diagnostics.log(level, "sandboxd.log", {
63
- stream: streamName,
64
- message,
65
- ...(level === "error" ? { error: { message } } : {}),
66
- ...data,
67
- }, { component: "sandboxd" });
68
- }
69
- function printDiagnostic(event) {
70
- const scope = [event.component, event.event].filter(Boolean).join(".");
71
- const context = [
72
- event.connectionId && `connection=${event.connectionId}`,
73
- event.sessionId && `session=${event.sessionId}`,
74
- event.turnId && `turn=${event.turnId}`,
75
- event.traceContext?.requestId && `request=${event.traceContext.requestId}`,
76
- event.traceContext?.traceId && `trace=${event.traceContext.traceId}`,
77
- ].filter(Boolean).join(" ");
78
- const data = event.data && Object.keys(event.data).length > 0 ? ` ${JSON.stringify(event.data)}` : "";
79
- process.stdout.write(`${event.timestamp} ${event.level.toUpperCase().padEnd(5)} ${scope}${context ? ` ${context}` : ""}${data}${event.error ? ` ${JSON.stringify(event.error)}` : ""}\n`);
80
- }
81
- export function parseRuntimeHarnesses(values) {
82
- const names = values.flatMap((value) => value.split(",")).map((name) => name.trim()).filter(Boolean);
83
- if (names.some((name) => !isLocalHarness(name)))
84
- throw new Error("Harness must be pi or codex");
85
- return [...new Set(names.length ? names : ["pi"])];
86
- }
13
+ import { readRuntimeDiagnosticEvents, RuntimeDiagnosticReader, runtimeDiagnosticsDirectory, serializeDiagnosticError } from "../runtime/diagnostics.js";
14
+ export { resolveLocalSpaceName, parseRuntimeHarnesses } from "../runtime/launch.js";
15
+ const reportFailure = (cause) => {
16
+ process.stderr.write(`Runtime failed: ${serializeDiagnosticError(cause).message}\n`);
17
+ process.exitCode = 1;
18
+ };
87
19
  export function registerRuntime(program) {
88
20
  const runtime = program.command("runtime").description("Connect a local workspace");
89
21
  runtime.command("up [dir]")
90
22
  .description("Connect local Harnesses and files")
91
23
  .option("-s, --space <id>", "Target Space")
92
- .option("-n, --name <name>", "New Space name")
24
+ .option("-n, --new", "Create a new Space")
25
+ .option("--name <name>", "New Space name")
26
+ .option("-d, --detach", "Run in the background")
93
27
  .option("--harness <name>", "Pi or Codex; repeatable", (value, previous) => [...previous, value], [])
94
28
  .option("--pi <path>", "Pi executable")
95
29
  .option("--codex <path>", "Codex executable")
96
- .option("-y, --yes", "Accept local execution access")
30
+ .option("-y, --yes", "Accept defaults and authorize local execution")
31
+ .option("--verbose", "Show diagnostic details")
97
32
  .option("--json", "JSON output")
98
33
  .action(async (dir, options) => {
99
- const controller = new AbortController();
100
- const stop = () => controller.abort();
101
- process.once("SIGINT", stop);
102
- process.once("SIGTERM", stop);
103
34
  try {
104
- const requestedRoot = resolve(dir ?? process.cwd());
105
- if (!(await stat(requestedRoot)).isDirectory())
106
- throw new Error("Workspace is not a directory");
107
- const root = await canonicalRuntimeRoot(requestedRoot);
108
- const harnesses = parseRuntimeHarnesses(options.harness);
109
- if (!options.yes) {
110
- if (!process.stdin.isTTY)
111
- throw new Error("Use --yes to authorize local execution");
112
- const rl = createInterface({ input: process.stdin, output: process.stderr });
113
- try {
114
- const answer = await rl.question(`Connect ${root}? Space collaborators can run commands as your OS user, beyond this folder. [y/N] `);
115
- if (!/^y(es)?$/i.test(answer.trim()))
116
- return;
117
- }
118
- finally {
119
- rl.close();
120
- }
121
- }
122
- const capabilities = await discoverHarnesses(harnesses, options, root);
123
- const client = createClient();
124
- const requested = options.space?.trim() || explicitSpace(program);
125
- const validateLocalRuntime = async (spaceId) => {
126
- const sandbox = (await client.space(spaceId).sandbox.get()).sandbox;
127
- if (sandbox?.provider !== "local")
128
- throw new Error("Space does not have a local Runtime");
129
- };
130
- const { spaceId } = await resolveRuntimeSpace({
131
- root,
132
- identityKey: currentIdentityKey(),
133
- explicitSpaceId: requested,
134
- createSpace: async () => (await client.spaces.create({
135
- name: resolveLocalSpaceName(root, options.name),
136
- config: { sandbox: { provider: "local" } },
137
- })).space.id,
138
- validateSpace: validateLocalRuntime,
139
- });
140
- const spaceClient = client.space(spaceId);
141
- const store = new RuntimeSessionStore(spaceId, { projectionSource: spaceClient });
142
- const runtimeId = randomUUID();
143
- const diagnostics = new RuntimeDiagnostics({ root: store.root, spaceId, runtimeId });
144
- store.setDiagnostics(diagnostics);
145
- diagnostics.log("info", "runtime.cli_started", {
146
- platform: process.platform,
147
- arch: process.arch,
148
- node: process.versions.node,
149
- harnesses,
150
- proxyConfigured: ["HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY"].some((key) => Boolean(process.env[key]?.trim())),
151
- });
35
+ await runtimeUp(program, dir, { ...options, json: jsonRequested(options) });
36
+ }
37
+ catch (cause) {
38
+ reportFailure(cause);
39
+ }
40
+ });
41
+ for (const action of ["attach", "detach"])
42
+ runtime.command(action)
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
+ .action(async (options) => {
152
51
  try {
153
- const binary = await ensureSandboxdBinary({
154
- onStatus: (message) => diagnostics.log("info", "sandboxd.download", { message }, { component: "sandboxd" }),
155
- });
156
- const wsBase = resolveWebsocketUrl({ url: process.env.COHUB_WS_URL });
157
- const url = new URL(wsBase);
158
- url.pathname = "/runtime/relay";
159
- const relay = new URL(wsBase);
160
- relay.pathname = "/sandbox/relay";
161
- let bridge = null;
162
- let bridgeClosed = Promise.resolve();
163
- let announced = false;
164
- const token = await requireAccessToken();
165
- try {
166
- await serveRuntime({
167
- spaceId,
168
- cwd: root,
169
- url: url.toString(),
170
- capabilities,
171
- harnesses: options,
172
- runtimeId,
173
- diagnostics,
174
- token: requireAccessToken,
175
- signal: controller.signal,
176
- store,
177
- onReady: () => {
178
- if (!bridge) {
179
- bridge = spawn(binary, ["--local", "--space", spaceId, "--root", root, "--relay", process.env.COHUB_RELAY_URL?.trim() || relay.toString()], {
180
- stdio: ["ignore", "pipe", "pipe"],
181
- env: {
182
- ...process.env,
183
- COHUB_RELAY_TOKEN: token,
184
- COHUB_RUNTIME_ID: runtimeId,
185
- COHUB_LOG_FORMAT: "json",
186
- },
187
- });
188
- captureSandboxOutput(bridge.stdout, "stdout", diagnostics);
189
- captureSandboxOutput(bridge.stderr, "stderr", diagnostics);
190
- bridgeClosed = new Promise((resolveClosed) => bridge?.once("close", () => resolveClosed()));
191
- bridge.on("error", (cause) => {
192
- diagnostics.log("error", "sandboxd.process_error", { error: serializeDiagnosticError(cause) }, { component: "sandboxd" });
193
- console.error(cause);
194
- controller.abort();
195
- });
196
- bridge.once("exit", (code, signal) => {
197
- diagnostics.log(code === 0 ? "info" : "error", "sandboxd.process_exit", { code, signal }, { component: "sandboxd" });
198
- controller.abort();
199
- });
200
- }
201
- if (announced)
202
- return;
203
- announced = true;
204
- const webUrl = `${resolveCohubEnvironment() === "prod" ? "https://cohub.live" : "https://dev.cohub.live"}/spaces/${spaceId}`;
205
- if (jsonRequested(options))
206
- outJson({ spaceId, root, harnesses, runtimeId, diagnosticsPath: diagnostics.logPath, url: webUrl });
207
- else
208
- console.error(`Runtime connected: ${webUrl} (runtimeId=${runtimeId}, logs=${diagnostics.logPath})`);
209
- },
210
- });
211
- }
212
- finally {
213
- if (bridge) {
214
- const child = bridge;
215
- child.kill("SIGTERM");
216
- const timeout = setTimeout(() => child.kill("SIGKILL"), 3000);
217
- await bridgeClosed;
218
- clearTimeout(timeout);
52
+ const spaceId = await resolveRuntimeTarget(program, options.space);
53
+ const identity = currentIdentityKey();
54
+ if (!identity)
55
+ throw new Error("Sign in first");
56
+ const root = await canonicalRuntimeRoot(process.cwd());
57
+ if (action === "attach" && (await getRuntimeSpaceBinding(root, identity))?.spaceId !== spaceId)
58
+ throw new Error("Bind this directory with runtime up --space first");
59
+ const instance = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
60
+ if (action === "attach" && (!instance || instance.root !== root))
61
+ throw new Error("Start this directory's Runtime first: cohub runtime up -d");
62
+ if (action === "attach" && !instance?.nativeSync)
63
+ throw new Error("Restart the Runtime with this CLI before attaching");
64
+ const harnesses = parseRuntimeHarnesses(options.harness.length ? options.harness : instance?.harnesses ?? ["pi", "codex"]);
65
+ if (action === "attach" && harnesses.some((harness) => !instance?.harnesses.includes(harness)))
66
+ throw new Error("Enable these Harnesses with runtime up first");
67
+ if (action === "attach" && !options.yes) {
68
+ if (!process.stdin.isTTY)
69
+ throw new Error("Use --yes to authorize native sync");
70
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
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] `);
73
+ if (!/^y(es)?$/i.test(answer.trim()))
74
+ return;
75
+ }
76
+ finally {
77
+ rl.close();
219
78
  }
220
79
  }
80
+ const result = await installNativeSync({ root, spaceId, identity, harnesses, disabled: action === "detach", executables: { pi: options.pi, codex: options.codex } });
81
+ if (jsonRequested(options))
82
+ outJson(result);
83
+ else
84
+ process.stdout.write(action === "attach"
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");
221
87
  }
222
88
  catch (cause) {
223
- diagnostics.log("error", "runtime.start_failed", { error: serializeDiagnosticError(cause) });
224
- throw cause;
89
+ reportFailure(cause);
225
90
  }
226
- finally {
227
- await diagnostics.close().catch((error) => console.error("Runtime diagnostics close failed:", error));
91
+ });
92
+ runtime.command("status").description("Local and server status")
93
+ .option("-s, --space <id>", "Target Space")
94
+ .option("--json", "JSON output")
95
+ .action(async (options) => {
96
+ try {
97
+ const spaceId = await resolveRuntimeTarget(program, options.space);
98
+ const identity = currentIdentityKey();
99
+ const local = identity ? await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId)) : null;
100
+ const space = createClient().space(spaceId);
101
+ const store = new RuntimeSessionStore(spaceId, { projectionSource: space, archiveTransport: null });
102
+ const [remote, pendingLocalArchives, failedLocalArchives, nativeStores] = await Promise.all([
103
+ space.getRuntime(undefined, { signal: AbortSignal.timeout(5000) }).then((value) => ({ value, error: null })).catch((error) => ({ value: null, error: serializeDiagnosticError(error).message })),
104
+ store.archives.pendingCount(), store.archives.failedCaptureCount(),
105
+ identity ? listNativeSyncStores(store.root, spaceId, identity) : [],
106
+ ]);
107
+ const nativeSessions = await Promise.all(nativeStores.map((native) => native.status()));
108
+ const result = { ...remote.value, spaceId, local, remote: remote.value, remoteError: remote.error, diagnosticsPath: runtimeDiagnosticsDirectory(store.root), pendingLocalArchives, failedLocalArchives, nativeSessions };
109
+ if (jsonRequested(options))
110
+ outJson(result);
111
+ else {
112
+ if (local)
113
+ printRuntimeSummary(local);
114
+ else
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
+ if (nativeSessions.length)
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`);
228
119
  }
229
120
  }
230
121
  catch (cause) {
231
- if (!controller.signal.aborted)
232
- error("Runtime failed", cause instanceof Error ? cause.message : String(cause));
233
- }
234
- finally {
235
- process.removeListener("SIGINT", stop);
236
- process.removeListener("SIGTERM", stop);
122
+ reportFailure(cause);
237
123
  }
238
124
  });
239
- runtime.command("status").description("Runtime status").option("-s, --space <id>", "Target Space").action(async (options) => {
240
- const spaceId = options.space?.trim() || await resolveSpace(program);
241
- const client = createClient();
242
- const spaceClient = client.space(spaceId);
243
- const store = new RuntimeSessionStore(spaceId, { projectionSource: spaceClient });
244
- const [status, pendingLocalArchives, failedLocalArchives] = await Promise.all([
245
- spaceClient.getRuntime(),
246
- store.archives.pendingCount(),
247
- store.archives.failedCaptureCount(),
248
- ]);
249
- outJson({
250
- ...status,
251
- diagnosticsPath: runtimeDiagnosticsDirectory(store.root),
252
- pendingLocalArchives,
253
- failedLocalArchives,
254
- });
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
+ .action(async (options) => {
130
+ try {
131
+ const spaceId = await resolveRuntimeTarget(program, options.space);
132
+ const identity = currentIdentityKey();
133
+ if (!identity)
134
+ throw new Error("Sign in to the Runtime account");
135
+ const directory = runtimeInstanceDirectory(identity, spaceId);
136
+ const local = await requestRuntimeInstance(directory, "stop", Boolean(options.yes));
137
+ const until = Date.now() + 15_000;
138
+ let running = Boolean(local);
139
+ while (running && Date.now() < until) {
140
+ await delay(250);
141
+ try {
142
+ running = Boolean(await requestRuntimeInstance(directory));
143
+ }
144
+ catch {
145
+ running = true;
146
+ } // An unreachable control socket does not prove the process stopped.
147
+ }
148
+ if (running)
149
+ throw new Error("Runtime is still stopping; inspect logs");
150
+ if (jsonRequested(options))
151
+ outJson({ spaceId, stopped: true });
152
+ else
153
+ process.stdout.write("Runtime stopped; data retained\n");
154
+ }
155
+ catch (cause) {
156
+ reportFailure(cause);
157
+ }
255
158
  });
256
- runtime.command("logs")
257
- .description("Read local Runtime diagnostics")
159
+ runtime.command("logs").description("Read local Runtime diagnostics")
258
160
  .option("-s, --space <id>", "Target Space")
259
161
  .option("-l, --limit <count>", "Number of events", "100")
260
- .option("--follow", "Keep watching for new events")
261
- .option("--json", "Print raw diagnostic events")
162
+ .option("--level <level>", "Minimum level: debug, info, warn, error", "info")
163
+ .option("-f, --follow", "Keep watching")
164
+ .option("--json", "Raw diagnostic events")
262
165
  .action(async (options) => {
263
- const spaceId = options.space?.trim() || await resolveSpace(program);
264
- const store = new RuntimeSessionStore(spaceId, { projectionSource: createClient().space(spaceId) });
265
- const limit = Number(options.limit ?? "100");
266
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10_000)
267
- return error("Invalid diagnostic limit", "Use an integer between 1 and 10000 / 使用 1 到 10000 之间的整数");
268
- const asJson = jsonRequested(options);
269
- const reader = options.follow ? new RuntimeDiagnosticReader(store.root) : null;
270
- const render = async () => {
271
- const fresh = options.follow
272
- ? await reader?.read({ limit }) ?? []
273
- : await readRuntimeDiagnosticEvents(store.root, { limit });
274
- if (options.follow && fresh.length === 0)
275
- return;
276
- if (asJson && options.follow) {
277
- for (const event of fresh)
278
- process.stdout.write(`${JSON.stringify(event)}\n`);
279
- }
280
- else if (asJson) {
281
- outJson(fresh);
282
- }
283
- else if (fresh.length === 0) {
284
- process.stdout.write("No Runtime diagnostics / 未找到 Runtime 诊断记录\n");
285
- }
286
- else {
287
- for (const event of fresh)
288
- printDiagnostic(event);
289
- }
290
- };
166
+ const controller = new AbortController();
167
+ const stop = () => controller.abort();
291
168
  try {
292
- await render();
293
- if (!options.follow)
294
- return;
295
- await new Promise((resolve) => {
296
- let timer = null;
297
- let stopped = false;
298
- const stop = () => {
299
- stopped = true;
300
- if (timer)
301
- clearTimeout(timer);
302
- process.removeListener("SIGINT", stop);
303
- process.removeListener("SIGTERM", stop);
304
- resolve();
305
- };
306
- const poll = async () => {
307
- if (stopped)
308
- return;
309
- try {
310
- await render();
311
- }
312
- catch {
313
- stop();
314
- return;
315
- }
316
- if (!stopped)
317
- timer = setTimeout(() => void poll(), 2_000);
318
- };
319
- timer = setTimeout(() => void poll(), 2_000);
320
- process.once("SIGINT", stop);
321
- process.once("SIGTERM", stop);
322
- });
169
+ const spaceId = await resolveRuntimeTarget(program, options.space);
170
+ const store = new RuntimeSessionStore(spaceId, { projectionSource: createClient().space(spaceId), archiveTransport: null });
171
+ const limit = Number(options.limit);
172
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10_000)
173
+ throw new Error("Use a limit from 1 to 10000");
174
+ if (!diagnosticLevels.includes(options.level))
175
+ throw new Error("Use debug, info, warn or error");
176
+ const asJson = jsonRequested(options);
177
+ const reader = new RuntimeDiagnosticReader(store.root);
178
+ process.once("SIGINT", stop);
179
+ process.once("SIGTERM", stop);
180
+ do {
181
+ const events = (options.follow ? await reader.read({ limit }) : await readRuntimeDiagnosticEvents(store.root, { limit }))
182
+ .filter((event) => atLeastLevel(event.level, options.level));
183
+ if (asJson && !options.follow)
184
+ outJson(events);
185
+ else
186
+ for (const event of events)
187
+ process.stdout.write(asJson ? `${JSON.stringify(event)}\n` : formatDiagnostic(event, true));
188
+ if (!options.follow) {
189
+ if (!asJson && !events.length)
190
+ process.stdout.write("No matching diagnostics\n");
191
+ break;
192
+ }
193
+ await delay(1000, undefined, { signal: controller.signal }).catch(() => undefined);
194
+ } while (!controller.signal.aborted);
323
195
  }
324
196
  catch (cause) {
325
- error("Runtime logs failed", cause instanceof Error ? cause.message : String(cause));
197
+ reportFailure(cause);
198
+ }
199
+ finally {
200
+ process.removeListener("SIGINT", stop);
201
+ process.removeListener("SIGTERM", stop);
326
202
  }
327
203
  });
328
204
  }
@@ -1,4 +1,4 @@
1
- export declare const SANDBOXD_VERSION = "v2.53.1";
1
+ export declare const SANDBOXD_VERSION = "v2.54.1";
2
2
  export declare class SandboxdDownloadError extends Error {
3
3
  name: string;
4
4
  }
@@ -15,11 +15,19 @@ import { Readable } from "node:stream";
15
15
  // that tag's publish-cdn job has succeeded, otherwise `runtime up` 404s on the
16
16
  // default download.
17
17
  //
18
- // v2.53.1 is the first published tag with the native FSEvents file-monitoring
19
- // backends and the `runtimeId` control frame (earlier `v2.52.0` predates them,
20
- // and the old sandbox-only `v1.x` line stopped at `v1.82.4`). Running anything
21
- // older keeps exhausting file descriptors on macOS.
22
- export const SANDBOXD_VERSION = "v2.53.1";
18
+ // v2.54.0 is the first published tag with the private managed Runtime control
19
+ // pipe (`COHUB_RUNTIME_MANAGED` over fd 3), so `runtime up` reads connection
20
+ // state from the daemon instead of polling the API every five seconds. It also
21
+ // carries the optional workspace-search runner download. v2.53.1 already has
22
+ // the native FSEvents backends and the `runtimeId` control frame, and older
23
+ // releases stay usable through the compatibility readiness/restart path.
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";
23
31
  const BINARY_NAME = "cohub-sandboxd";
24
32
  // Public CDN prefix hosting the release archives (the repo is private, so the
25
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>;
@@ -38,6 +40,8 @@ export declare class RuntimeArchiveStore {
38
40
  harness: "pi" | "codex";
39
41
  nativeSessionId: string;
40
42
  path: string;
43
+ sizeBytes?: number;
44
+ expectedChecksum?: string;
41
45
  }, turnId: string): Promise<HarnessArchive>;
42
46
  private capture;
43
47
  flush(signal: AbortSignal): Promise<void>;