@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.
- package/README.md +40 -2
- package/dist/auth.js +38 -5
- package/dist/client.js +5 -2
- package/dist/commands/runtime.d.ts +1 -2
- package/dist/commands/runtime.js +178 -302
- package/dist/commands/sandboxd-binary.d.ts +1 -1
- package/dist/commands/sandboxd-binary.js +7 -5
- package/dist/runtime/archive-store.d.ts +2 -0
- package/dist/runtime/archive-store.js +20 -6
- package/dist/runtime/connection.d.ts +4 -2
- package/dist/runtime/connection.js +80 -23
- package/dist/runtime/diagnostics.d.ts +3 -0
- package/dist/runtime/diagnostics.js +3 -0
- package/dist/runtime/harness.d.ts +3 -0
- package/dist/runtime/harness.js +34 -1
- package/dist/runtime/instance.d.ts +5 -0
- package/dist/runtime/instance.js +159 -0
- package/dist/runtime/launch.d.ts +20 -0
- package/dist/runtime/launch.js +176 -0
- package/dist/runtime/native-codex-hook.d.ts +1 -0
- package/dist/runtime/native-codex-hook.js +28 -0
- package/dist/runtime/native-install.d.ts +21 -0
- package/dist/runtime/native-install.js +130 -0
- package/dist/runtime/native-ipc.d.ts +26 -0
- package/dist/runtime/native-ipc.js +101 -0
- package/dist/runtime/native-pi-extension.d.ts +20 -0
- package/dist/runtime/native-pi-extension.js +47 -0
- package/dist/runtime/native-sync-store.d.ts +97 -0
- package/dist/runtime/native-sync-store.js +365 -0
- package/dist/runtime/native-sync.d.ts +25 -0
- package/dist/runtime/native-sync.js +128 -0
- package/dist/runtime/native-transcript.d.ts +27 -0
- package/dist/runtime/native-transcript.js +281 -0
- package/dist/runtime/presentation.d.ts +21 -0
- package/dist/runtime/presentation.js +76 -0
- package/dist/runtime/session-store.d.ts +2 -0
- package/dist/runtime/session-store.js +40 -5
- package/dist/runtime/space-binding.d.ts +3 -0
- package/dist/runtime/space-binding.js +43 -6
- package/dist/runtime/supervisor.d.ts +16 -0
- package/dist/runtime/supervisor.js +277 -0
- package/dist/runtime/worker.d.ts +1 -0
- package/dist/runtime/worker.js +20 -0
- package/package.json +3 -2
package/dist/commands/runtime.js
CHANGED
|
@@ -1,328 +1,204 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
10
|
-
import { currentIdentityKey
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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 {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 / Runtime 失败: ${serializeDiagnosticError(cause).message}\n`);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
};
|
|
87
19
|
export function registerRuntime(program) {
|
|
88
|
-
const runtime = program.command("runtime").description("Connect a local workspace");
|
|
20
|
+
const runtime = program.command("runtime").description("Connect a local workspace / 连接本地工作区");
|
|
89
21
|
runtime.command("up [dir]")
|
|
90
|
-
.description("Connect local Harnesses and files")
|
|
91
|
-
.option("-s, --space <id>", "Target Space")
|
|
92
|
-
.option("-n, --
|
|
93
|
-
.option("--
|
|
94
|
-
.option("--
|
|
95
|
-
.option("--
|
|
96
|
-
.option("
|
|
97
|
-
.option("--
|
|
22
|
+
.description("Connect local Harnesses and files / 连接本地 Harness 和文件")
|
|
23
|
+
.option("-s, --space <id>", "Target Space / 目标 Space")
|
|
24
|
+
.option("-n, --new", "Create a new Space / 创建新 Space")
|
|
25
|
+
.option("--name <name>", "New Space name / 新 Space 名称")
|
|
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 / Pi 可执行文件")
|
|
29
|
+
.option("--codex <path>", "Codex executable / Codex 可执行文件")
|
|
30
|
+
.option("-y, --yes", "Accept defaults and local execution / 接受默认选择并授权本地执行")
|
|
31
|
+
.option("--verbose", "Show diagnostic details / 显示诊断详情")
|
|
32
|
+
.option("--json", "JSON output / JSON 输出")
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 / 同步原生 Pi / Codex 对话" : "Pause native sync; retain all receipts / 暂停原生同步,保留所有回执")
|
|
44
|
+
.option("-s, --space <id>", "Target Space / 目标 Space")
|
|
45
|
+
.option("--harness <name>", "Pi or Codex; repeatable / 可重复指定", (value, previous) => [...previous, value], [])
|
|
46
|
+
.option("--pi <path>", "Pi executable for capability checks / 用于能力检查的 Pi 路径")
|
|
47
|
+
.option("--codex <path>", "Codex executable for capability checks / 用于能力检查的 Codex 路径")
|
|
48
|
+
.option("-y, --yes", "Authorize project conversation and native archive uploads / 授权上传项目对话及原生归档")
|
|
49
|
+
.option("--json", "JSON output / JSON 输出")
|
|
50
|
+
.action(async (options) => {
|
|
152
51
|
try {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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 / 请先使用 runtime up --space 绑定当前目录");
|
|
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 / 请先启动当前目录的 Runtime");
|
|
62
|
+
if (action === "attach" && !instance?.nativeSync)
|
|
63
|
+
throw new Error("Restart the Runtime with this CLI before attaching / 请先使用新版 CLI 重启 Runtime,再接入原生客户端");
|
|
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 / 请先通过 runtime up 启用对应 Harness");
|
|
67
|
+
if (action === "attach" && !options.yes) {
|
|
68
|
+
if (!process.stdin.isTTY)
|
|
69
|
+
throw new Error("Use --yes to authorize native sync / 请使用 --yes 授权原生同步");
|
|
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]\n安装用户级原生集成,并将当前项目打开的对话、工具输出和原始归档上传至此 Space?历史可能包含敏感信息。[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. / 原生同步已启用。请重载 Pi 或重启 Codex,并审核 Hook 信任提示。\n${result.configPath}\n`
|
|
86
|
+
: "Native sync paused; all local records retained / 原生同步已暂停,所有本地记录已保留\n");
|
|
221
87
|
}
|
|
222
88
|
catch (cause) {
|
|
223
|
-
|
|
224
|
-
throw cause;
|
|
89
|
+
reportFailure(cause);
|
|
225
90
|
}
|
|
226
|
-
|
|
227
|
-
|
|
91
|
+
});
|
|
92
|
+
runtime.command("status").description("Local and server status / 本地与服务端状态")
|
|
93
|
+
.option("-s, --space <id>", "Target Space / 目标 Space")
|
|
94
|
+
.option("--json", "JSON output / JSON 输出")
|
|
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 });
|
|
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 / Harness 已连接" : "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 / Turn 待同步 · ${nativeSessions.reduce((sum, session) => sum + session.pendingArchives, 0)} archives pending / 归档待同步\n`);
|
|
228
119
|
}
|
|
229
120
|
}
|
|
230
121
|
catch (cause) {
|
|
231
|
-
|
|
232
|
-
error("Runtime failed", cause instanceof Error ? cause.message : String(cause));
|
|
122
|
+
reportFailure(cause);
|
|
233
123
|
}
|
|
234
|
-
finally {
|
|
235
|
-
process.removeListener("SIGINT", stop);
|
|
236
|
-
process.removeListener("SIGTERM", stop);
|
|
237
|
-
}
|
|
238
|
-
});
|
|
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
|
-
});
|
|
255
124
|
});
|
|
256
|
-
runtime.command("
|
|
257
|
-
.
|
|
258
|
-
.option("-
|
|
259
|
-
.option("
|
|
260
|
-
.option("--follow", "Keep watching for new events")
|
|
261
|
-
.option("--json", "Print raw diagnostic events")
|
|
125
|
+
runtime.command("down").description("Stop this local Runtime; retain all data / 停止本地 Runtime,保留所有数据")
|
|
126
|
+
.option("-s, --space <id>", "Target Space / 目标 Space")
|
|
127
|
+
.option("-y, --yes", "Stop even with unconfirmed executions / 确认停止包含未确认执行的 Runtime")
|
|
128
|
+
.option("--json", "JSON output / JSON 输出")
|
|
262
129
|
.action(async (options) => {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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);
|
|
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 / 请登录 Runtime 所属账号");
|
|
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.
|
|
289
147
|
}
|
|
290
|
-
|
|
148
|
+
if (running)
|
|
149
|
+
throw new Error("Runtime is still stopping; inspect logs / Runtime 仍在停止,请检查日志");
|
|
150
|
+
if (jsonRequested(options))
|
|
151
|
+
outJson({ spaceId, stopped: true });
|
|
152
|
+
else
|
|
153
|
+
process.stdout.write("Runtime stopped; data retained / Runtime 已停止,数据已保留\n");
|
|
154
|
+
}
|
|
155
|
+
catch (cause) {
|
|
156
|
+
reportFailure(cause);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
runtime.command("logs").description("Read local Runtime diagnostics / 查看本地 Runtime 日志")
|
|
160
|
+
.option("-s, --space <id>", "Target Space / 目标 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
|
+
.action(async (options) => {
|
|
166
|
+
const controller = new AbortController();
|
|
167
|
+
const stop = () => controller.abort();
|
|
291
168
|
try {
|
|
292
|
-
await
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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) });
|
|
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 / 数量范围为 1 到 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
|
-
|
|
197
|
+
reportFailure(cause);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
process.removeListener("SIGINT", stop);
|
|
201
|
+
process.removeListener("SIGTERM", stop);
|
|
326
202
|
}
|
|
327
203
|
});
|
|
328
204
|
}
|
|
@@ -15,11 +15,13 @@ 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.
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
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
|
+
export const SANDBOXD_VERSION = "v2.54.0";
|
|
23
25
|
const BINARY_NAME = "cohub-sandboxd";
|
|
24
26
|
// Public CDN prefix hosting the release archives (the repo is private, so the
|
|
25
27
|
// GitHub Release assets are not publicly downloadable). Overridable for staging
|
|
@@ -38,6 +38,8 @@ export declare class RuntimeArchiveStore {
|
|
|
38
38
|
harness: "pi" | "codex";
|
|
39
39
|
nativeSessionId: string;
|
|
40
40
|
path: string;
|
|
41
|
+
sizeBytes?: number;
|
|
42
|
+
expectedChecksum?: string;
|
|
41
43
|
}, turnId: string): Promise<HarnessArchive>;
|
|
42
44
|
private capture;
|
|
43
45
|
flush(signal: AbortSignal): Promise<void>;
|
|
@@ -115,6 +115,8 @@ export class RuntimeArchiveStore {
|
|
|
115
115
|
if (saved) {
|
|
116
116
|
if (saved.sessionId !== state.sessionId || saved.harness !== state.harness)
|
|
117
117
|
throw new Error("Archive identity mismatch");
|
|
118
|
+
if (state.expectedChecksum && state.expectedChecksum !== saved.sha256)
|
|
119
|
+
throw new Error("Native Turn bytes changed; original archive retained");
|
|
118
120
|
const committed = await stat(join(this.root, "ready", `${turnId}.json`)).catch((error) => { if (missing(error))
|
|
119
121
|
return null; throw error; });
|
|
120
122
|
if (!committed)
|
|
@@ -130,12 +132,15 @@ export class RuntimeArchiveStore {
|
|
|
130
132
|
const before = await file.stat();
|
|
131
133
|
if (!before.isFile() || !before.size)
|
|
132
134
|
throw new Error("Native archive is empty");
|
|
135
|
+
const sizeBytes = state.sizeBytes ?? before.size;
|
|
136
|
+
if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 1 || sizeBytes > before.size)
|
|
137
|
+
throw new Error("Native archive boundary is unavailable");
|
|
133
138
|
const buffer = Buffer.alloc(RUNTIME_ARCHIVE_SEGMENT_BYTES);
|
|
134
139
|
let offset = 0;
|
|
135
140
|
let digest = createHash("sha256");
|
|
136
141
|
let parent = null;
|
|
137
142
|
// Hash the old prefix, not just its size: equal-size and growing rewrites are valid.
|
|
138
|
-
if (previous && previous.nativeSessionId === state.nativeSessionId && previous.sizeBytes <=
|
|
143
|
+
if (previous && previous.nativeSessionId === state.nativeSessionId && previous.sizeBytes <= sizeBytes) {
|
|
139
144
|
while (offset < previous.sizeBytes) {
|
|
140
145
|
const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, previous.sizeBytes - offset), offset);
|
|
141
146
|
if (!bytesRead)
|
|
@@ -152,8 +157,8 @@ export class RuntimeArchiveStore {
|
|
|
152
157
|
}
|
|
153
158
|
const segments = [];
|
|
154
159
|
await mkdir(join(this.root, "objects"), { recursive: true, mode: 0o700 });
|
|
155
|
-
while (offset <
|
|
156
|
-
const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length,
|
|
160
|
+
while (offset < sizeBytes) {
|
|
161
|
+
const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, sizeBytes - offset), offset);
|
|
157
162
|
if (!bytesRead)
|
|
158
163
|
throw new Error("Native file changed during capture");
|
|
159
164
|
const bytes = buffer.subarray(0, bytesRead);
|
|
@@ -164,8 +169,16 @@ export class RuntimeArchiveStore {
|
|
|
164
169
|
offset += bytesRead;
|
|
165
170
|
}
|
|
166
171
|
const after = await stat(state.path);
|
|
167
|
-
if (before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs)
|
|
172
|
+
if (before.ino !== after.ino || after.size < sizeBytes || state.sizeBytes === undefined && (before.size !== after.size || before.mtimeMs !== after.mtimeMs))
|
|
168
173
|
throw new Error("Native file changed during capture");
|
|
174
|
+
if (state.sizeBytes !== undefined) {
|
|
175
|
+
// Native clients may append while an earlier Turn is captured. Validate the exact prefix twice.
|
|
176
|
+
const verified = createHash("sha256");
|
|
177
|
+
for await (const bytes of createReadStream(state.path, { end: sizeBytes - 1 }))
|
|
178
|
+
verified.update(bytes);
|
|
179
|
+
if (verified.digest("hex") !== digest.copy().digest("hex"))
|
|
180
|
+
throw new Error("Native prefix changed during capture");
|
|
181
|
+
}
|
|
169
182
|
if (process.platform !== "win32") {
|
|
170
183
|
const directory = await open(join(this.root, "objects"), "r");
|
|
171
184
|
try {
|
|
@@ -177,8 +190,10 @@ export class RuntimeArchiveStore {
|
|
|
177
190
|
}
|
|
178
191
|
index = harnessArchiveIndexSchema.parse({ ...identity, version: 1, nativeSessionId: state.nativeSessionId,
|
|
179
192
|
nativeFormat: state.harness === "pi" ? "pi.jsonl" : "codex.rollout", parentTurnId: parent?.turnId ?? null,
|
|
180
|
-
sizeBytes
|
|
193
|
+
sizeBytes, sha256: digest.digest("hex"), segments });
|
|
181
194
|
validateArchiveBoundary(index, parent);
|
|
195
|
+
if (state.expectedChecksum && index.sha256 !== state.expectedChecksum)
|
|
196
|
+
throw new Error("Native Turn bytes changed during capture; original retained");
|
|
182
197
|
}
|
|
183
198
|
finally {
|
|
184
199
|
await file.close();
|
|
@@ -254,7 +269,6 @@ export class RuntimeArchiveStore {
|
|
|
254
269
|
catch (error) {
|
|
255
270
|
if (!signal.aborted) {
|
|
256
271
|
this.errorReporter?.(error, index);
|
|
257
|
-
console.error("Archive pending; native segments retained:", error);
|
|
258
272
|
}
|
|
259
273
|
}
|
|
260
274
|
}
|