@neta-art/cohub-cli 8.0.0 → 8.1.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 +7 -16
- package/dist/commands/runtime.js +190 -87
- package/dist/commands/sandboxd-binary.d.ts +1 -1
- package/dist/commands/sandboxd-binary.js +8 -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/diagnostics.js +8 -1
- 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 +29 -23
- package/dist/runtime/native-attach.d.ts +28 -0
- package/dist/runtime/native-attach.js +47 -0
- package/dist/runtime/native-codex-hook.js +3 -3
- package/dist/runtime/native-install.js +9 -9
- package/dist/runtime/native-ipc.d.ts +2 -0
- package/dist/runtime/native-ipc.js +7 -5
- package/dist/runtime/native-pi-extension.js +1 -1
- package/dist/runtime/native-sync-store.d.ts +6 -1
- package/dist/runtime/native-sync-store.js +24 -21
- package/dist/runtime/native-sync.d.ts +25 -0
- package/dist/runtime/native-sync.js +89 -8
- package/dist/runtime/native-transcript.d.ts +8 -0
- package/dist/runtime/native-transcript.js +45 -16
- package/dist/runtime/presentation.d.ts +10 -1
- package/dist/runtime/presentation.js +47 -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 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -85,26 +85,21 @@ COHUB_SPACE_ID=<spaceId> cohub spaces prompt "message" --json
|
|
|
85
85
|
Connect one local workspace to a Space and select Pi or Codex per turn.
|
|
86
86
|
|
|
87
87
|
To keep using native terminals with ordinary Cohub Chats / Turns:
|
|
88
|
-
使用原生终端,并同步到普通 Cohub Chat / Turn:
|
|
89
88
|
|
|
90
89
|
```bash
|
|
91
90
|
cohub runtime up -d --harness pi --harness codex
|
|
92
|
-
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
cohub runtime detach --harness pi --harness codex # Pause; retain data / 暂停,保留数据
|
|
91
|
+
# Answer the native sync consent (default yes), then reload Pi / restart Codex
|
|
92
|
+
# and approve its hook trust prompt.
|
|
93
|
+
cohub runtime detach --harness pi --harness codex # Pause; retain data
|
|
96
94
|
```
|
|
97
95
|
|
|
98
|
-
`
|
|
99
|
-
|
|
96
|
+
`up` installs native sync by default after one explicit consent (default yes); declining or a
|
|
97
|
+
capability failure keeps the Runtime running without it, and `up` is idempotent. The Runtime
|
|
98
|
+
Supervisor is the single local Daemon: Pi / Codex integrations use its private IPC,
|
|
100
99
|
while native Turn events use the existing Runtime WebSocket. Configuration is backed up, existing
|
|
101
100
|
Runtime bindings are reused, and competing continuations fork at complete Turns without blocking native work. Requires Pi 0.85.1+ or Codex with stable
|
|
102
101
|
Hooks enabled. User-level integrations remain inert outside opted-in directories. See
|
|
103
|
-
[boundaries and privacy](../../docs/local-runtime.md#native-clients
|
|
104
|
-
|
|
105
|
-
`attach` 明确授权上传当前项目打开的对话历史与原生归档;复用现有绑定,配置修改前备份。
|
|
106
|
-
冲突时按完整 Turn 分支,不阻塞本地执行。用户级集成在未授权目录中不会采集数据。
|
|
107
|
-
|
|
102
|
+
[boundaries and privacy](../../docs/local-runtime.md#native-clients).
|
|
108
103
|
|
|
109
104
|
```bash
|
|
110
105
|
cohub runtime up ./project --harness pi --harness codex
|
|
@@ -142,10 +137,6 @@ local process and remote component status. `down` retains all data and requires
|
|
|
142
137
|
when executions remain unconfirmed. `logs --level warn --follow` shows sensitive events;
|
|
143
138
|
foreground startup prints these automatically. Runtime commands never fall back to Home.
|
|
144
139
|
|
|
145
|
-
普通使用只需 `cohub runtime up`,首次提示新建,后续优先复用。`-n` 是 `--new` 的简写,
|
|
146
|
-
`--name` 单独指定名称。`-d` 后台运行并返回链接、PID 和日志位置;`down` 停止但保留所有数据。
|
|
147
|
-
断网及临时凭证错误自动重试;断连不代表任务已停止,也不会自动重跑工具。
|
|
148
|
-
|
|
149
140
|
## Chats and prompts
|
|
150
141
|
|
|
151
142
|
Use `spaces prompt` for immediate sends, delayed sends, one-time schedules, recurring schedules, new Chats, and existing Chats.
|
package/dist/commands/runtime.js
CHANGED
|
@@ -1,35 +1,36 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
-
import { createInterface } from "node:readline/promises";
|
|
3
2
|
import { createClient } from "../client.js";
|
|
4
3
|
import { json as outJson, jsonRequested } from "../output.js";
|
|
5
4
|
import { currentIdentityKey } from "../space.js";
|
|
6
5
|
import { resolveRuntimeTarget, runtimeUp, parseRuntimeHarnesses } from "../runtime/launch.js";
|
|
7
6
|
import { canonicalRuntimeRoot, getRuntimeSpaceBinding } from "../runtime/space-binding.js";
|
|
8
7
|
import { installNativeSync } from "../runtime/native-install.js";
|
|
8
|
+
import { discoverNativeImportCandidates, nativeRuntimeRoot, readNativeSyncConfig } from "../runtime/native-sync.js";
|
|
9
9
|
import { listNativeSyncStores } from "../runtime/native-sync-store.js";
|
|
10
|
+
import { requestNativeDaemon } from "../runtime/native-ipc.js";
|
|
10
11
|
import { requestRuntimeInstance, runtimeInstanceDirectory } from "../runtime/instance.js";
|
|
11
|
-
import { atLeastLevel, diagnosticLevels, formatDiagnostic, printRuntimeSummary } from "../runtime/presentation.js";
|
|
12
|
+
import { atLeastLevel, diagnosticLevels, formatDiagnostic, formatNativeSync, printRuntimeSummary } from "../runtime/presentation.js";
|
|
12
13
|
import { RuntimeSessionStore } from "../runtime/session-store.js";
|
|
13
14
|
import { readRuntimeDiagnosticEvents, RuntimeDiagnosticReader, runtimeDiagnosticsDirectory, serializeDiagnosticError } from "../runtime/diagnostics.js";
|
|
14
15
|
export { resolveLocalSpaceName, parseRuntimeHarnesses } from "../runtime/launch.js";
|
|
15
16
|
const reportFailure = (cause) => {
|
|
16
|
-
process.stderr.write(`Runtime failed
|
|
17
|
+
process.stderr.write(`Runtime failed: ${serializeDiagnosticError(cause).message}\n`);
|
|
17
18
|
process.exitCode = 1;
|
|
18
19
|
};
|
|
19
20
|
export function registerRuntime(program) {
|
|
20
|
-
const runtime = program.command("runtime").description("Connect a local workspace
|
|
21
|
+
const runtime = program.command("runtime").description("Connect a local workspace");
|
|
21
22
|
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
|
|
23
|
+
.description("Connect local Harnesses and files")
|
|
24
|
+
.option("-s, --space <id>", "Target Space")
|
|
25
|
+
.option("-n, --new", "Create a new Space")
|
|
26
|
+
.option("--name <name>", "New Space name")
|
|
27
|
+
.option("-d, --detach", "Run in the background")
|
|
28
|
+
.option("--harness <name>", "Pi or Codex; repeatable", (value, previous) => [...previous, value], [])
|
|
29
|
+
.option("--pi <path>", "Pi executable")
|
|
30
|
+
.option("--codex <path>", "Codex executable")
|
|
31
|
+
.option("-y, --yes", "Accept defaults and authorize local execution and native sync")
|
|
32
|
+
.option("--verbose", "Show diagnostic details")
|
|
33
|
+
.option("--json", "JSON output")
|
|
33
34
|
.action(async (dir, options) => {
|
|
34
35
|
try {
|
|
35
36
|
await runtimeUp(program, dir, { ...options, json: jsonRequested(options) });
|
|
@@ -38,100 +39,202 @@ export function registerRuntime(program) {
|
|
|
38
39
|
reportFailure(cause);
|
|
39
40
|
}
|
|
40
41
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
42
|
+
runtime.command("detach")
|
|
43
|
+
.description("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("--json", "JSON output")
|
|
47
|
+
.action(async (options) => {
|
|
48
|
+
try {
|
|
49
|
+
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
50
|
+
const identity = currentIdentityKey();
|
|
51
|
+
if (!identity)
|
|
52
|
+
throw new Error("Sign in first");
|
|
53
|
+
const root = await canonicalRuntimeRoot(process.cwd());
|
|
54
|
+
if ((await getRuntimeSpaceBinding(root, identity))?.spaceId !== spaceId)
|
|
55
|
+
throw new Error("Bind this directory with runtime up first");
|
|
56
|
+
const instance = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
|
|
57
|
+
const harnesses = parseRuntimeHarnesses(options.harness.length ? options.harness : instance?.harnesses ?? ["pi", "codex"]);
|
|
58
|
+
const result = await installNativeSync({ root, spaceId, identity, harnesses, disabled: true });
|
|
59
|
+
if (jsonRequested(options))
|
|
60
|
+
outJson({ ...result, enabled: false });
|
|
61
|
+
else
|
|
62
|
+
process.stdout.write("Native sync paused; all local records retained\n");
|
|
63
|
+
}
|
|
64
|
+
catch (cause) {
|
|
65
|
+
reportFailure(cause);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
runtime.command("import [dir]")
|
|
69
|
+
.description("Import local conversations")
|
|
70
|
+
.option("-s, --space <id>", "Target Space")
|
|
71
|
+
.option("--harness <name>", "Filter by harness; repeatable", (value, previous) => [...previous, value], [])
|
|
72
|
+
.option("--session <id>", "Filter by native session ID")
|
|
73
|
+
.option("--dry-run", "Preview without importing")
|
|
74
|
+
.option("-y, --yes", "Skip confirmation")
|
|
75
|
+
.option("--json", "Output as JSON")
|
|
76
|
+
.action(async (dir, options) => {
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const stop = () => controller.abort();
|
|
79
|
+
process.once("SIGINT", stop);
|
|
80
|
+
process.once("SIGTERM", stop);
|
|
81
|
+
try {
|
|
82
|
+
const identity = currentIdentityKey();
|
|
83
|
+
if (!identity)
|
|
84
|
+
throw new Error("Sign in first");
|
|
85
|
+
const root = await canonicalRuntimeRoot(dir ?? process.cwd());
|
|
86
|
+
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
87
|
+
const binding = await getRuntimeSpaceBinding(root, identity);
|
|
88
|
+
if (!binding || binding.spaceId !== spaceId)
|
|
89
|
+
throw new Error("Bind this directory with runtime up first");
|
|
90
|
+
const local = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
|
|
91
|
+
if (!local)
|
|
92
|
+
throw new Error("Local Runtime is not running");
|
|
93
|
+
const config = await readNativeSyncConfig(nativeRuntimeRoot(spaceId), identity);
|
|
94
|
+
if (!config || config.root !== root)
|
|
95
|
+
throw new Error("Native sync is not enabled for this directory");
|
|
96
|
+
const harnesses = parseRuntimeHarnesses(options.harness.length ? options.harness : config.harnesses);
|
|
97
|
+
let lastProgress = 0;
|
|
98
|
+
const discovered = await discoverNativeImportCandidates(root, harnesses, {
|
|
99
|
+
signal: controller.signal,
|
|
100
|
+
onProgress: ({ harness, scanned, candidates: found }) => {
|
|
101
|
+
if (jsonRequested(options) || scanned - lastProgress < 25)
|
|
102
|
+
return;
|
|
103
|
+
lastProgress = scanned;
|
|
104
|
+
process.stderr.write(`Scanned ${scanned} ${harness} transcript${scanned === 1 ? "" : "s"}; found ${found}\n`);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
const candidates = discovered.candidates.filter((candidate) => !options.session || candidate.nativeSessionId === options.session);
|
|
108
|
+
const result = {
|
|
109
|
+
spaceId, root, harnesses, candidates, errors: discovered.errors,
|
|
110
|
+
imported: 0, failed: [], dryRun: Boolean(options.dryRun),
|
|
111
|
+
complete: discovered.errors.length === 0,
|
|
112
|
+
};
|
|
113
|
+
if (jsonRequested(options) && options.dryRun) {
|
|
114
|
+
result.complete = result.complete && result.failed.length === 0;
|
|
115
|
+
outJson(result);
|
|
116
|
+
if (!result.complete)
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!candidates.length) {
|
|
121
|
+
if (!result.complete)
|
|
122
|
+
process.exitCode = 1;
|
|
81
123
|
if (jsonRequested(options))
|
|
82
124
|
outJson(result);
|
|
83
125
|
else
|
|
84
|
-
process.stdout.write(
|
|
85
|
-
|
|
86
|
-
: "Native sync paused; all local records retained / 原生同步已暂停,所有本地记录已保留\n");
|
|
126
|
+
process.stdout.write("No existing native conversations found\n");
|
|
127
|
+
return;
|
|
87
128
|
}
|
|
88
|
-
|
|
89
|
-
|
|
129
|
+
if (options.dryRun) {
|
|
130
|
+
if (jsonRequested(options))
|
|
131
|
+
outJson(result);
|
|
132
|
+
else {
|
|
133
|
+
process.stdout.write(`Found ${candidates.length} native conversation${candidates.length === 1 ? "" : "s"}\n`);
|
|
134
|
+
for (const candidate of candidates)
|
|
135
|
+
process.stdout.write(` ${candidate.harness} ${candidate.nativeSessionId} ${candidate.turnCount} Turn${candidate.turnCount === 1 ? "" : "s"} ${candidate.path}\n`);
|
|
136
|
+
for (const error of discovered.errors)
|
|
137
|
+
process.stdout.write(`Skipped ${error.path}: ${error.message}\n`);
|
|
138
|
+
}
|
|
139
|
+
if (!result.complete)
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!options.yes) {
|
|
144
|
+
if (!process.stdin.isTTY)
|
|
145
|
+
throw new Error("Use --yes in non-interactive mode");
|
|
146
|
+
const { createInterface } = await import("node:readline/promises");
|
|
147
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
148
|
+
try {
|
|
149
|
+
const answer = await rl.question(`Import ${candidates.length} native conversation${candidates.length === 1 ? "" : "s"} to Space ${spaceId}? [Y/n] `);
|
|
150
|
+
if (!/^(|y(es)?)$/i.test(answer.trim())) {
|
|
151
|
+
if (jsonRequested(options))
|
|
152
|
+
outJson({ ...result, cancelled: true });
|
|
153
|
+
else
|
|
154
|
+
process.stdout.write("Import cancelled\n");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
rl.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const candidate of candidates) {
|
|
163
|
+
try {
|
|
164
|
+
const response = await requestNativeDaemon({ harness: candidate.harness, cwd: root, path: candidate.path, nativeSessionId: candidate.nativeSessionId,
|
|
165
|
+
sessionStartedAt: candidate.sessionStartedAt, origin: "local_import", settled: true });
|
|
166
|
+
if (!response.ok)
|
|
167
|
+
throw new Error(response.message);
|
|
168
|
+
result.imported += 1;
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
result.failed.push({ path: candidate.path, message: error instanceof Error ? error.message : String(error) });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
result.complete = result.complete && result.failed.length === 0;
|
|
175
|
+
if (jsonRequested(options))
|
|
176
|
+
outJson(result);
|
|
177
|
+
else {
|
|
178
|
+
process.stdout.write(`Imported ${result.imported}/${candidates.length} native conversation${candidates.length === 1 ? "" : "s"}\n`);
|
|
179
|
+
for (const error of [...discovered.errors, ...result.failed])
|
|
180
|
+
process.stdout.write(`Skipped ${error.path}: ${error.message}\n`);
|
|
181
|
+
process.stdout.write("Uploads continue in the local Runtime background\n");
|
|
90
182
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
183
|
+
if (!result.complete)
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
}
|
|
186
|
+
catch (cause) {
|
|
187
|
+
reportFailure(cause);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
process.removeListener("SIGINT", stop);
|
|
191
|
+
process.removeListener("SIGTERM", stop);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
runtime.command("status").description("Local and server status")
|
|
195
|
+
.option("-s, --space <id>", "Target Space")
|
|
196
|
+
.option("--json", "JSON output")
|
|
95
197
|
.action(async (options) => {
|
|
96
198
|
try {
|
|
97
199
|
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
98
200
|
const identity = currentIdentityKey();
|
|
99
201
|
const local = identity ? await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId)) : null;
|
|
100
202
|
const space = createClient().space(spaceId);
|
|
101
|
-
const store = new RuntimeSessionStore(spaceId, { projectionSource: space });
|
|
203
|
+
const store = new RuntimeSessionStore(spaceId, { projectionSource: space, archiveTransport: null });
|
|
102
204
|
const [remote, pendingLocalArchives, failedLocalArchives, nativeStores] = await Promise.all([
|
|
103
205
|
space.getRuntime(undefined, { signal: AbortSignal.timeout(5000) }).then((value) => ({ value, error: null })).catch((error) => ({ value: null, error: serializeDiagnosticError(error).message })),
|
|
104
206
|
store.archives.pendingCount(), store.archives.failedCaptureCount(),
|
|
105
207
|
identity ? listNativeSyncStores(store.root, spaceId, identity) : [],
|
|
106
208
|
]);
|
|
107
209
|
const nativeSessions = await Promise.all(nativeStores.map((native) => native.status()));
|
|
108
|
-
|
|
210
|
+
// A corrupt native sync config must never take down the whole status report.
|
|
211
|
+
const nativeSync = identity ? await readNativeSyncConfig(store.root, identity).then((config) => ({ config, error: null }), (error) => ({ config: null, error: serializeDiagnosticError(error).message })) : { config: null, error: null };
|
|
212
|
+
const result = { ...remote.value, spaceId, local, remote: remote.value, remoteError: remote.error, diagnosticsPath: runtimeDiagnosticsDirectory(store.root), pendingLocalArchives, failedLocalArchives, nativeSync: nativeSync.config, nativeSyncError: nativeSync.error, nativeSessions };
|
|
109
213
|
if (jsonRequested(options))
|
|
110
214
|
outJson(result);
|
|
111
215
|
else {
|
|
112
216
|
if (local)
|
|
113
217
|
printRuntimeSummary(local);
|
|
114
218
|
else
|
|
115
|
-
process.stdout.write(`Local process
|
|
116
|
-
process.stdout.write(`Server
|
|
117
|
-
|
|
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`);
|
|
219
|
+
process.stdout.write(`Local process Not running\nSpace ${spaceId}\nLogs ${result.diagnosticsPath}\n`);
|
|
220
|
+
process.stdout.write(`Server ${remote.error ? `Unknown — ${remote.error}` : remote.value?.online ? "Harness connected" : "Offline"}\nArchives ${pendingLocalArchives} pending · ${failedLocalArchives} failed\n`);
|
|
221
|
+
process.stdout.write(formatNativeSync(nativeSync.config, nativeSessions, nativeSync.error));
|
|
119
222
|
}
|
|
120
223
|
}
|
|
121
224
|
catch (cause) {
|
|
122
225
|
reportFailure(cause);
|
|
123
226
|
}
|
|
124
227
|
});
|
|
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
|
|
228
|
+
runtime.command("down").description("Stop this local Runtime; retain all data")
|
|
229
|
+
.option("-s, --space <id>", "Target Space")
|
|
230
|
+
.option("-y, --yes", "Stop even with unconfirmed executions")
|
|
231
|
+
.option("--json", "JSON output")
|
|
129
232
|
.action(async (options) => {
|
|
130
233
|
try {
|
|
131
234
|
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
132
235
|
const identity = currentIdentityKey();
|
|
133
236
|
if (!identity)
|
|
134
|
-
throw new Error("Sign in to the Runtime account
|
|
237
|
+
throw new Error("Sign in to the Runtime account");
|
|
135
238
|
const directory = runtimeInstanceDirectory(identity, spaceId);
|
|
136
239
|
const local = await requestRuntimeInstance(directory, "stop", Boolean(options.yes));
|
|
137
240
|
const until = Date.now() + 15_000;
|
|
@@ -146,33 +249,33 @@ export function registerRuntime(program) {
|
|
|
146
249
|
} // An unreachable control socket does not prove the process stopped.
|
|
147
250
|
}
|
|
148
251
|
if (running)
|
|
149
|
-
throw new Error("Runtime is still stopping; inspect logs
|
|
252
|
+
throw new Error("Runtime is still stopping; inspect logs");
|
|
150
253
|
if (jsonRequested(options))
|
|
151
254
|
outJson({ spaceId, stopped: true });
|
|
152
255
|
else
|
|
153
|
-
process.stdout.write("Runtime stopped; data retained
|
|
256
|
+
process.stdout.write("Runtime stopped; data retained\n");
|
|
154
257
|
}
|
|
155
258
|
catch (cause) {
|
|
156
259
|
reportFailure(cause);
|
|
157
260
|
}
|
|
158
261
|
});
|
|
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
|
|
262
|
+
runtime.command("logs").description("Read local Runtime diagnostics")
|
|
263
|
+
.option("-s, --space <id>", "Target Space")
|
|
264
|
+
.option("-l, --limit <count>", "Number of events", "100")
|
|
265
|
+
.option("--level <level>", "Minimum level: debug, info, warn, error", "info")
|
|
266
|
+
.option("-f, --follow", "Keep watching")
|
|
267
|
+
.option("--json", "Raw diagnostic events")
|
|
165
268
|
.action(async (options) => {
|
|
166
269
|
const controller = new AbortController();
|
|
167
270
|
const stop = () => controller.abort();
|
|
168
271
|
try {
|
|
169
272
|
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
170
|
-
const store = new RuntimeSessionStore(spaceId, { projectionSource: createClient().space(spaceId) });
|
|
273
|
+
const store = new RuntimeSessionStore(spaceId, { projectionSource: createClient().space(spaceId), archiveTransport: null });
|
|
171
274
|
const limit = Number(options.limit);
|
|
172
275
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10_000)
|
|
173
|
-
throw new Error("Use a limit from 1 to 10000
|
|
276
|
+
throw new Error("Use a limit from 1 to 10000");
|
|
174
277
|
if (!diagnosticLevels.includes(options.level))
|
|
175
|
-
throw new Error("Use debug, info, warn or error
|
|
278
|
+
throw new Error("Use debug, info, warn or error");
|
|
176
279
|
const asJson = jsonRequested(options);
|
|
177
280
|
const reader = new RuntimeDiagnosticReader(store.root);
|
|
178
281
|
process.once("SIGINT", stop);
|
|
@@ -187,7 +290,7 @@ export function registerRuntime(program) {
|
|
|
187
290
|
process.stdout.write(asJson ? `${JSON.stringify(event)}\n` : formatDiagnostic(event, true));
|
|
188
291
|
if (!options.follow) {
|
|
189
292
|
if (!asJson && !events.length)
|
|
190
|
-
process.stdout.write("No matching diagnostics
|
|
293
|
+
process.stdout.write("No matching diagnostics\n");
|
|
191
294
|
break;
|
|
192
295
|
}
|
|
193
296
|
await delay(1000, undefined, { signal: controller.signal }).catch(() => undefined);
|
|
@@ -21,7 +21,14 @@ 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. v2.55.0 republishes that compatible
|
|
29
|
+
// runner alongside the matching platform release; no new runner capability is
|
|
30
|
+
// required.
|
|
31
|
+
export const SANDBOXD_VERSION = "v2.55.0";
|
|
25
32
|
const BINARY_NAME = "cohub-sandboxd";
|
|
26
33
|
// Public CDN prefix hosting the release archives (the repo is private, so the
|
|
27
34
|
// 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);
|