@neta-art/cohub-cli 7.1.2 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) 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 +178 -302
  6. package/dist/commands/sandboxd-binary.d.ts +1 -1
  7. package/dist/commands/sandboxd-binary.js +7 -5
  8. package/dist/runtime/archive-store.d.ts +2 -0
  9. package/dist/runtime/archive-store.js +20 -6
  10. package/dist/runtime/connection.d.ts +4 -2
  11. package/dist/runtime/connection.js +80 -23
  12. package/dist/runtime/diagnostics.d.ts +3 -0
  13. package/dist/runtime/diagnostics.js +3 -0
  14. package/dist/runtime/harness.d.ts +3 -0
  15. package/dist/runtime/harness.js +34 -1
  16. package/dist/runtime/instance.d.ts +5 -0
  17. package/dist/runtime/instance.js +159 -0
  18. package/dist/runtime/launch.d.ts +20 -0
  19. package/dist/runtime/launch.js +176 -0
  20. package/dist/runtime/native-codex-hook.d.ts +1 -0
  21. package/dist/runtime/native-codex-hook.js +28 -0
  22. package/dist/runtime/native-install.d.ts +21 -0
  23. package/dist/runtime/native-install.js +130 -0
  24. package/dist/runtime/native-ipc.d.ts +26 -0
  25. package/dist/runtime/native-ipc.js +101 -0
  26. package/dist/runtime/native-pi-extension.d.ts +20 -0
  27. package/dist/runtime/native-pi-extension.js +47 -0
  28. package/dist/runtime/native-sync-store.d.ts +97 -0
  29. package/dist/runtime/native-sync-store.js +365 -0
  30. package/dist/runtime/native-sync.d.ts +25 -0
  31. package/dist/runtime/native-sync.js +128 -0
  32. package/dist/runtime/native-transcript.d.ts +27 -0
  33. package/dist/runtime/native-transcript.js +281 -0
  34. package/dist/runtime/presentation.d.ts +21 -0
  35. package/dist/runtime/presentation.js +76 -0
  36. package/dist/runtime/session-store.d.ts +2 -0
  37. package/dist/runtime/session-store.js +40 -5
  38. package/dist/runtime/space-binding.d.ts +3 -0
  39. package/dist/runtime/space-binding.js +43 -6
  40. package/dist/runtime/supervisor.d.ts +16 -0
  41. package/dist/runtime/supervisor.js +277 -0
  42. package/dist/runtime/worker.d.ts +1 -0
  43. package/dist/runtime/worker.js +20 -0
  44. package/package.json +3 -2
@@ -1,4 +1,4 @@
1
- import { type RuntimeCapabilities } from "@neta-art/cohub";
1
+ import { type RuntimeCapabilities, type NativeRuntimeEvent } from "@neta-art/cohub";
2
2
  import { type HarnessOptions } from "./harness.js";
3
3
  import { type RuntimeSessionStore } from "./session-store.js";
4
4
  import { type RuntimeDiagnostics } from "./diagnostics.js";
@@ -8,12 +8,14 @@ export type RuntimeConnectionOptions = {
8
8
  url: string;
9
9
  capabilities: RuntimeCapabilities;
10
10
  harnesses: HarnessOptions;
11
- token: () => Promise<string>;
11
+ token: (forceRefresh?: boolean) => Promise<string>;
12
12
  signal: AbortSignal;
13
13
  store: RuntimeSessionStore;
14
14
  onReady: () => void;
15
+ onDisconnected?: () => void;
15
16
  runtimeId?: string;
16
17
  diagnostics?: RuntimeDiagnostics;
17
18
  leaseConflictTimeoutMs?: number;
19
+ onNativeChannel?: (send: (event: NativeRuntimeEvent) => Promise<unknown>) => void;
18
20
  };
19
21
  export declare function serveRuntime(options: RuntimeConnectionOptions): Promise<void>;
@@ -16,7 +16,7 @@ export async function serveRuntime(options) {
16
16
  const flush = () => options.store.flushArchives(uploadSignal).catch((error) => {
17
17
  if (!uploadSignal.aborted) {
18
18
  log("warn", "archive.flush_failed", { error: serializeDiagnosticError(error) });
19
- console.error("Archive pending:", error);
19
+ // The diagnostic sink handles terminal presentation and throttling.
20
20
  }
21
21
  });
22
22
  const timer = setInterval(() => {
@@ -31,22 +31,36 @@ export async function serveRuntime(options) {
31
31
  try {
32
32
  while (!options.signal.aborted) {
33
33
  attempt += 1;
34
- const outcome = await connect({
35
- ...options,
36
- runtimeId,
37
- attempt,
38
- onReady: () => {
39
- backoff = 500;
40
- attempt = 0;
41
- conflictSince = null;
42
- options.onReady();
43
- void flush();
44
- },
45
- });
34
+ let readyAt = 0;
35
+ let outcome;
36
+ try {
37
+ outcome = await connect({
38
+ ...options,
39
+ runtimeId,
40
+ attempt,
41
+ onReady: () => {
42
+ readyAt = Date.now();
43
+ conflictSince = null;
44
+ options.onReady();
45
+ void flush();
46
+ },
47
+ });
48
+ }
49
+ catch (error) {
50
+ if (options.signal.aborted)
51
+ return;
52
+ log("warn", "runtime.connection_failed", { error: serializeDiagnosticError(error) });
53
+ outcome = "retry";
54
+ }
55
+ options.onDisconnected?.();
46
56
  if (options.signal.aborted)
47
57
  return;
48
58
  if (outcome === "fatal")
49
- throw new Error("Runtime connection rejected");
59
+ throw new Error("Runtime connection rejected / Runtime 连接被拒绝,请检查权限或升级 CLI");
60
+ if (readyAt && Date.now() - readyAt >= 60_000) {
61
+ backoff = 500;
62
+ attempt = 0;
63
+ }
50
64
  if (outcome === "conflict") {
51
65
  conflictSince ??= Date.now();
52
66
  if (Date.now() - conflictSince >= (options.leaseConflictTimeoutMs ?? 90_000)) {
@@ -58,8 +72,8 @@ export async function serveRuntime(options) {
58
72
  delayMs: backoff,
59
73
  outcome,
60
74
  });
61
- await delay(backoff, undefined, { signal: options.signal }).catch(() => undefined);
62
- backoff = Math.min(10_000, backoff * 2);
75
+ await delay(backoff / 2 + Math.random() * backoff / 2, undefined, { signal: options.signal }).catch(() => undefined);
76
+ backoff = Math.min(30_000, backoff * 2);
63
77
  }
64
78
  }
65
79
  finally {
@@ -91,7 +105,7 @@ async function connect(options) {
91
105
  currentToken = await options.token();
92
106
  }
93
107
  catch (error) {
94
- log("error", "runtime.auth_token_failed", { error: serializeDiagnosticError(error) });
108
+ log("warn", error instanceof Error && error.name === "AuthRequiredError" ? "runtime.auth_required" : "runtime.auth_token_failed", { error: serializeDiagnosticError(error) });
95
109
  throw error;
96
110
  }
97
111
  const runtimeUrl = new URL(options.url);
@@ -105,7 +119,10 @@ async function connect(options) {
105
119
  let connectionId = null;
106
120
  let fatal = false;
107
121
  let conflict = false;
122
+ let unauthorized = false;
108
123
  let readyTimer;
124
+ const nativePending = new Map();
125
+ let nativeChannelReady = false;
109
126
  const send = (frame) => {
110
127
  if (socket.readyState !== WebSocket.OPEN || socket.bufferedAmount > RUNTIME_MAX_FRAME_BYTES) {
111
128
  log("warn", "runtime.frame_send_unavailable", {
@@ -127,6 +144,23 @@ async function connect(options) {
127
144
  execution.controller.abort();
128
145
  socket.close();
129
146
  };
147
+ const sendNative = (event) => new Promise((resolve, reject) => {
148
+ if (!nativeChannelReady || !connectionId) {
149
+ reject(new Error("Runtime native channel is unavailable"));
150
+ return;
151
+ }
152
+ const requestId = randomUUID();
153
+ const timer = setTimeout(() => { nativePending.delete(requestId); reject(new Error("Native Runtime event timed out")); }, 30_000);
154
+ nativePending.set(requestId, { resolve, reject, timer });
155
+ try {
156
+ send({ type: "runtime.native", requestId, event });
157
+ }
158
+ catch (error) {
159
+ clearTimeout(timer);
160
+ nativePending.delete(requestId);
161
+ reject(error instanceof Error ? error : new Error(String(error)));
162
+ }
163
+ });
130
164
  options.signal.addEventListener("abort", stop, { once: true });
131
165
  const heartbeat = setInterval(() => {
132
166
  const heartbeatAgeMs = Date.now() - lastHeartbeat;
@@ -174,10 +208,11 @@ async function connect(options) {
174
208
  }, 10_000);
175
209
  const closed = new Promise((resolve) => {
176
210
  socket.addEventListener("close", (event) => {
177
- fatal = [4400, 4401, 4403].includes(event.code);
211
+ unauthorized = event.code === 4401;
212
+ fatal = [4400, 4403].includes(event.code);
178
213
  conflict = event.code === 4409;
179
214
  disconnected.abort();
180
- log(fatal || conflict ? "error" : "warn", "runtime.websocket.closed", {
215
+ log(options.signal.aborted ? "debug" : fatal || conflict ? "error" : "warn", "runtime.websocket.closed", {
181
216
  code: event.code,
182
217
  reason: event.reason,
183
218
  durationMs: Date.now() - connectedAt,
@@ -185,8 +220,13 @@ async function connect(options) {
185
220
  fatal,
186
221
  conflict,
187
222
  }, { connectionId });
188
- if (!options.signal.aborted)
189
- console.error(`Runtime disconnected (${event.code}): ${event.reason}`);
223
+ nativeChannelReady = false;
224
+ for (const pending of nativePending.values()) {
225
+ clearTimeout(pending.timer);
226
+ pending.reject(new Error("Runtime native channel disconnected"));
227
+ }
228
+ nativePending.clear();
229
+ options.onDisconnected?.();
190
230
  for (const execution of active.values()) {
191
231
  log("error", "runtime.execution_transport_lost", {
192
232
  code: event.code,
@@ -246,6 +286,8 @@ async function connect(options) {
246
286
  connectionId,
247
287
  durationMs: Date.now() - connectedAt,
248
288
  }, { connectionId });
289
+ nativeChannelReady = true;
290
+ options.onNativeChannel?.(sendNative);
249
291
  options.onReady();
250
292
  let recoveryBatch = 0;
251
293
  for await (const executions of options.store.pendingExecutionBatches()) {
@@ -264,6 +306,19 @@ async function connect(options) {
264
306
  lastHeartbeat = Date.now();
265
307
  return;
266
308
  }
309
+ if (raw.type === "runtime.native.result") {
310
+ const result = raw;
311
+ const pending = result.requestId ? nativePending.get(result.requestId) : null;
312
+ if (!pending)
313
+ return;
314
+ clearTimeout(pending.timer);
315
+ nativePending.delete(result.requestId ?? "");
316
+ if (result.error)
317
+ pending.reject(new Error(result.error));
318
+ else
319
+ pending.resolve(result.result);
320
+ return;
321
+ }
267
322
  const frame = runtimeCommandSchema.parse(raw);
268
323
  if (frame.type === "session.context") {
269
324
  log("debug", "runtime.context_received", {
@@ -294,7 +349,7 @@ async function connect(options) {
294
349
  revision: frame.revision,
295
350
  error: serializeDiagnosticError(error),
296
351
  }, context);
297
- console.error("Runtime acknowledgement failed; result retained:", error);
352
+ // Keep the receipt; the next reconciliation can retry acknowledgement.
298
353
  active.delete(frame.requestId);
299
354
  send({ type: "runtime.event", requestId: frame.requestId, event: { type: "turn.error", message: "Local acknowledgement failed; result retained" } });
300
355
  return;
@@ -353,7 +408,7 @@ async function connect(options) {
353
408
  catch (error) {
354
409
  if (!recovery.controller.signal.aborted) {
355
410
  log("error", "runtime.recovery_failed", { error: serializeDiagnosticError(error) }, context);
356
- console.error("Runtime recovery failed; original files retained:", error);
411
+ // Original files and receipts remain available for reconciliation.
357
412
  try {
358
413
  send({ type: "runtime.event", requestId: frame.requestId, event: { type: "turn.error", uncertain: true, message: "Result unavailable; files retained" } });
359
414
  }
@@ -523,5 +578,7 @@ async function connect(options) {
523
578
  clearInterval(heartbeat);
524
579
  options.signal.removeEventListener("abort", stop);
525
580
  }
581
+ if (unauthorized && !options.signal.aborted)
582
+ await options.token(true);
526
583
  return fatal ? "fatal" : conflict ? "conflict" : "retry";
527
584
  }
@@ -50,6 +50,8 @@ export type RuntimeDiagnosticsOptions = {
50
50
  logFlushIntervalMs?: number;
51
51
  maxLogFileBytes?: number;
52
52
  maxTotalLogBytes?: number;
53
+ /** Receives only the redacted event, before asynchronous disk I/O. */
54
+ onEvent?: (event: RuntimeDiagnostic) => void;
53
55
  };
54
56
  export type ReadRuntimeDiagnosticsOptions = {
55
57
  limit?: number;
@@ -64,6 +66,7 @@ export declare class RuntimeDiagnostics {
64
66
  readonly logPath: string;
65
67
  private readonly spaceId;
66
68
  private readonly component;
69
+ private readonly onEvent?;
67
70
  private readonly logFlushIntervalMs;
68
71
  private readonly maxLogFileBytes;
69
72
  private readonly maxTotalLogBytes;
@@ -134,6 +134,7 @@ export class RuntimeDiagnostics {
134
134
  logPath;
135
135
  spaceId;
136
136
  component;
137
+ onEvent;
137
138
  logFlushIntervalMs;
138
139
  maxLogFileBytes;
139
140
  maxTotalLogBytes;
@@ -153,6 +154,7 @@ export class RuntimeDiagnostics {
153
154
  this.runtimeId = options.runtimeId ?? randomUUID();
154
155
  this.spaceId = options.spaceId;
155
156
  this.component = options.component ?? "runtime";
157
+ this.onEvent = options.onEvent;
156
158
  this.logFlushIntervalMs = options.logFlushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
157
159
  this.maxLogFileBytes = options.maxLogFileBytes ?? DEFAULT_MAX_LOG_FILE_BYTES;
158
160
  this.maxTotalLogBytes = options.maxTotalLogBytes ?? DEFAULT_MAX_TOTAL_LOG_BYTES;
@@ -193,6 +195,7 @@ export class RuntimeDiagnostics {
193
195
  ...(diagnosticError ? { error: diagnosticError } : {}),
194
196
  });
195
197
  this.enqueueWrite(value);
198
+ this.onEvent?.(value);
196
199
  }
197
200
  async close() {
198
201
  this.closed = true;
@@ -6,6 +6,9 @@ export type HarnessOptions = {
6
6
  pi?: string;
7
7
  codex?: string;
8
8
  };
9
+ export declare function harnessExecutableCandidates(binary: string, cwd: string, path: string, platform?: NodeJS.Platform, pathExt?: string | undefined): string[];
10
+ /** Discover installed executables only; authentication/capability errors stay explicit. */
11
+ export declare function installedHarnesses(cwd: string, options: HarnessOptions, path?: string): Promise<("pi" | "codex")[]>;
9
12
  export type HarnessResult = {
10
13
  state: NativeSession;
11
14
  event: Extract<RuntimeExecutionEvent, {
@@ -1,8 +1,40 @@
1
+ import { access, stat } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { delimiter, join, resolve, win32 } from "node:path";
1
4
  import { JsonRpcProcess, record } from "./json-rpc.js";
2
5
  import { codexModelCatalog } from "./model-catalog.js";
3
6
  import { codexTokenTotals, codexUsage, subtractCodexTokens } from "./codex-usage.js";
4
7
  import { downloadPublicImage } from "../safe-remote-image.js";
5
8
  import { serializeDiagnosticError } from "./diagnostics.js";
9
+ export function harnessExecutableCandidates(binary, cwd, path, platform = process.platform, pathExt = process.env.PATHEXT) {
10
+ const windows = platform === "win32";
11
+ const paths = windows ? win32 : { delimiter, join, resolve };
12
+ const extensions = windows && !win32.extname(binary)
13
+ ? ["", ...(pathExt || ".COM;.EXE;.BAT;.CMD").split(";").filter((ext) => /^\.[a-z0-9]+$/i.test(ext))]
14
+ : [""];
15
+ const roots = binary.includes("/") || binary.includes("\\")
16
+ ? [paths.resolve(cwd, binary)]
17
+ : path.split(paths.delimiter).map((directory) => directory.replace(/^"(.*)"$/, "$1")).filter(Boolean).map((directory) => paths.resolve(cwd, directory, binary));
18
+ return roots.flatMap((root) => extensions.map((ext) => `${root}${ext}`));
19
+ }
20
+ /** Discover installed executables only; authentication/capability errors stay explicit. */
21
+ export async function installedHarnesses(cwd, options, path = process.env.PATH ?? "") {
22
+ const names = options.pi || options.codex ? ["pi", "codex"].filter((name) => options[name]) : ["pi", "codex"];
23
+ const found = await Promise.all(names.map(async (name) => {
24
+ const binary = options[name] || name;
25
+ const candidates = harnessExecutableCandidates(binary, cwd, path);
26
+ for (const candidate of candidates) {
27
+ try {
28
+ await access(candidate, process.platform === "win32" ? constants.F_OK : constants.X_OK);
29
+ if ((await stat(candidate)).isFile())
30
+ return name;
31
+ }
32
+ catch { /* Try the next PATH entry. */ }
33
+ }
34
+ return null;
35
+ }));
36
+ return found.filter((name) => name !== null);
37
+ }
6
38
  const runtimeEnvironment = (input) => ({ COHUB_SPACE_ID: input.spaceId, COHUB_SESSION_ID: input.sessionId, COHUB_TURN_ID: input.turnId });
7
39
  const array = (value) => Array.isArray(value) ? value : [];
8
40
  const text = (value) => typeof value === "string" ? value : "";
@@ -49,7 +81,8 @@ function createAbortEscalation(rpc, signal, interrupt) {
49
81
  }
50
82
  /** Native files stay authoritative; archival failure only degrades cross-host resume. */
51
83
  async function finishHarnessTurn(store, state, message, resume, turnId, diagnosticContext) {
52
- const archive = await store.archive(state, turnId, diagnosticContext).catch((error) => { console.error("Native archive unavailable; local files retained:", error); return null; });
84
+ // The store records a redacted diagnostic; native files remain authoritative.
85
+ const archive = await store.archive(state, turnId, diagnosticContext).catch(() => null);
53
86
  return { state, event: { type: "turn.end", message, resume, archive } };
54
87
  }
55
88
  export async function discoverHarnesses(harnesses, options, cwd) {
@@ -0,0 +1,5 @@
1
+ import type { RuntimeSummary } from "./presentation.js";
2
+ export declare function runtimeInstanceDirectory(identity: string, spaceId: string): string;
3
+ export declare function requestRuntimeInstance(directory: string, action?: "status" | "stop", force?: boolean): Promise<RuntimeSummary | null>;
4
+ /** Private local IPC is both the single-instance guard and the control surface. */
5
+ export declare function ownRuntimeInstance(directory: string, status: () => RuntimeSummary, stop: (force: boolean) => Promise<void>): Promise<() => Promise<void>>;
@@ -0,0 +1,159 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import { createConnection, createServer } from "node:net";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { withRuntimeSpaceBindingsLock } from "./space-binding.js";
7
+ export function runtimeInstanceDirectory(identity, spaceId) {
8
+ const key = createHash("sha256").update(`${identity}\0${spaceId}`).digest("hex").slice(0, 24);
9
+ return join(homedir(), ".local", "state", "cohub", "instances", key);
10
+ }
11
+ function alive(pid) {
12
+ if (!Number.isSafeInteger(pid) || pid <= 0)
13
+ return true;
14
+ try {
15
+ process.kill(pid, 0);
16
+ return true;
17
+ }
18
+ catch (error) {
19
+ return error.code !== "ESRCH";
20
+ }
21
+ }
22
+ async function readRecord(directory) {
23
+ try {
24
+ const value = JSON.parse(await readFile(join(directory, "owner.json"), "utf8"));
25
+ if (!Number.isSafeInteger(value.pid) || typeof value.nonce !== "string" || typeof value.socket !== "string")
26
+ throw new Error("Invalid Runtime instance record / Runtime 实例记录无效");
27
+ return value;
28
+ }
29
+ catch (error) {
30
+ if (error.code === "ENOENT")
31
+ return null;
32
+ throw error;
33
+ }
34
+ }
35
+ function request(record, action, force = false) {
36
+ return new Promise((resolve, reject) => {
37
+ const socket = createConnection(record.socket);
38
+ const timer = setTimeout(() => { socket.destroy(); reject(new Error("Runtime control timed out / Runtime 控制连接超时")); }, 3000);
39
+ let buffer = "";
40
+ let settled = false;
41
+ const finish = (error, value) => {
42
+ if (settled)
43
+ return;
44
+ settled = true;
45
+ clearTimeout(timer);
46
+ socket.destroy();
47
+ if (error)
48
+ reject(error);
49
+ else if (value)
50
+ resolve(value);
51
+ };
52
+ socket.on("connect", () => socket.write(`${JSON.stringify({ nonce: record.nonce, action, force })}\n`));
53
+ socket.on("error", (error) => finish(error));
54
+ socket.on("close", () => finish(new Error("Runtime control closed / Runtime 控制连接已关闭")));
55
+ socket.on("data", (chunk) => {
56
+ buffer += chunk.toString();
57
+ if (buffer.length > 64 * 1024) {
58
+ finish(new Error("Runtime response too large"));
59
+ return;
60
+ }
61
+ if (!buffer.includes("\n"))
62
+ return;
63
+ try {
64
+ const response = JSON.parse(buffer.slice(0, buffer.indexOf("\n")));
65
+ if (response.error)
66
+ finish(new Error(response.error));
67
+ else if (response.nonce !== record.nonce || response.status?.pid !== record.pid)
68
+ finish(new Error("Runtime identity changed / Runtime 身份已变化"));
69
+ else
70
+ finish(undefined, response.status);
71
+ }
72
+ catch (error) {
73
+ finish(error instanceof Error ? error : new Error(String(error)));
74
+ }
75
+ });
76
+ });
77
+ }
78
+ export async function requestRuntimeInstance(directory, action = "status", force = false) {
79
+ const record = await readRecord(directory);
80
+ if (!record || !alive(record.pid))
81
+ return null;
82
+ try {
83
+ return await request(record, action, force);
84
+ }
85
+ catch (error) {
86
+ // A reused PID is not proof of ownership. Missing/refused endpoints cannot
87
+ // belong to a serving Runtime; timeouts and permission errors remain uncertain.
88
+ if (["ENOENT", "ECONNREFUSED"].includes(error.code ?? ""))
89
+ return null;
90
+ throw error;
91
+ }
92
+ }
93
+ /** Private local IPC is both the single-instance guard and the control surface. */
94
+ export async function ownRuntimeInstance(directory, status, stop) {
95
+ await mkdir(directory, { recursive: true, mode: 0o700 });
96
+ return withRuntimeSpaceBindingsLock(async () => {
97
+ if (await requestRuntimeInstance(directory))
98
+ throw new Error("Runtime already running; use status / Runtime 已在运行,请使用 status");
99
+ const socketPath = process.platform === "win32"
100
+ ? `\\\\.\\pipe\\cohub-${createHash("sha256").update(directory).digest("hex").slice(0, 24)}`
101
+ : join(directory, "control.sock");
102
+ if (process.platform !== "win32")
103
+ await rm(socketPath, { force: true });
104
+ const record = { pid: process.pid, nonce: randomUUID(), socket: socketPath };
105
+ const clients = new Set();
106
+ const server = createServer((socket) => {
107
+ clients.add(socket);
108
+ socket.setTimeout(3000, () => socket.destroy());
109
+ socket.on("error", () => socket.destroy());
110
+ socket.on("close", () => clients.delete(socket));
111
+ let input = "";
112
+ socket.on("data", (chunk) => {
113
+ input += chunk.toString();
114
+ if (input.length > 4096) {
115
+ socket.destroy();
116
+ return;
117
+ }
118
+ if (!input.includes("\n"))
119
+ return;
120
+ socket.removeAllListeners("data");
121
+ void (async () => {
122
+ const message = JSON.parse(input.slice(0, input.indexOf("\n")));
123
+ if (message.nonce !== record.nonce) {
124
+ socket.destroy();
125
+ return;
126
+ }
127
+ if (message.action === "stop")
128
+ await stop(message.force === true);
129
+ else if (message.action !== "status")
130
+ throw new Error("Unknown Runtime control request");
131
+ socket.end(`${JSON.stringify({ nonce: record.nonce, status: status() })}\n`);
132
+ })().catch((error) => socket.end(`${JSON.stringify({ error: error instanceof Error ? error.message : String(error) })}\n`));
133
+ });
134
+ });
135
+ await new Promise((resolve, reject) => {
136
+ server.once("error", reject);
137
+ server.listen(socketPath, () => { server.removeListener("error", reject); resolve(); });
138
+ });
139
+ try {
140
+ if (process.platform !== "win32")
141
+ await chmod(socketPath, 0o600);
142
+ const temporary = join(directory, `${record.nonce}.tmp`);
143
+ await writeFile(temporary, JSON.stringify(record), { mode: 0o600 });
144
+ await rename(temporary, join(directory, "owner.json"));
145
+ }
146
+ catch (error) {
147
+ server.close();
148
+ throw error;
149
+ }
150
+ return async () => {
151
+ for (const client of clients)
152
+ client.destroy();
153
+ await new Promise((resolve) => server.close(() => resolve()));
154
+ // Never remove a replacement owner's files.
155
+ if ((await readRecord(directory))?.nonce === record.nonce)
156
+ await rm(join(directory, "owner.json"));
157
+ };
158
+ }, { path: join(directory, "owner.json") });
159
+ }
@@ -0,0 +1,20 @@
1
+ import type { Command } from "commander";
2
+ import { type RuntimeSummary } from "./presentation.js";
3
+ import { type RuntimeLaunch } from "./supervisor.js";
4
+ export type RuntimeUpOptions = {
5
+ space?: string;
6
+ new?: boolean;
7
+ name?: string;
8
+ harness: string[];
9
+ pi?: string;
10
+ codex?: string;
11
+ yes?: boolean;
12
+ json?: boolean;
13
+ detach?: boolean;
14
+ verbose?: boolean;
15
+ };
16
+ export declare const resolveLocalSpaceName: (root: string, name?: string) => string;
17
+ export declare function parseRuntimeHarnesses(values: string[]): ("pi" | "codex")[];
18
+ export declare function resolveRuntimeTarget(program: Command, target?: string): Promise<string>;
19
+ export declare function startBackgroundRuntime(config: RuntimeLaunch): Promise<RuntimeSummary>;
20
+ export declare function runtimeUp(program: Command, dir: string | undefined, options: RuntimeUpOptions): Promise<void>;