@neta-art/cohub-cli 8.0.1 → 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 +155 -52
- package/dist/commands/sandboxd-binary.d.ts +1 -1
- package/dist/commands/sandboxd-binary.js +4 -3
- package/dist/runtime/diagnostics.js +8 -1
- package/dist/runtime/launch.js +8 -2
- package/dist/runtime/native-attach.d.ts +28 -0
- package/dist/runtime/native-attach.js +47 -0
- package/dist/runtime/native-ipc.d.ts +2 -0
- package/dist/runtime/native-ipc.js +3 -1
- package/dist/runtime/native-sync-store.d.ts +6 -1
- package/dist/runtime/native-sync-store.js +5 -2
- package/dist/runtime/native-sync.d.ts +25 -0
- package/dist/runtime/native-sync.js +84 -3
- package/dist/runtime/native-transcript.d.ts +8 -0
- package/dist/runtime/native-transcript.js +34 -5
- package/dist/runtime/presentation.d.ts +10 -1
- package/dist/runtime/presentation.js +16 -0
- package/dist/runtime/supervisor.js +0 -1
- 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,14 +1,15 @@
|
|
|
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";
|
|
@@ -27,7 +28,7 @@ export function registerRuntime(program) {
|
|
|
27
28
|
.option("--harness <name>", "Pi or Codex; repeatable", (value, previous) => [...previous, value], [])
|
|
28
29
|
.option("--pi <path>", "Pi executable")
|
|
29
30
|
.option("--codex <path>", "Codex executable")
|
|
30
|
-
.option("-y, --yes", "Accept defaults and authorize local execution")
|
|
31
|
+
.option("-y, --yes", "Accept defaults and authorize local execution and native sync")
|
|
31
32
|
.option("--verbose", "Show diagnostic details")
|
|
32
33
|
.option("--json", "JSON output")
|
|
33
34
|
.action(async (dir, options) => {
|
|
@@ -38,57 +39,158 @@ 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
|
-
|
|
126
|
+
process.stdout.write("No existing native conversations found\n");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
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
|
+
}
|
|
87
173
|
}
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
+
});
|
|
92
194
|
runtime.command("status").description("Local and server status")
|
|
93
195
|
.option("-s, --space <id>", "Target Space")
|
|
94
196
|
.option("--json", "JSON output")
|
|
@@ -105,7 +207,9 @@ export function registerRuntime(program) {
|
|
|
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 {
|
|
@@ -114,8 +218,7 @@ export function registerRuntime(program) {
|
|
|
114
218
|
else
|
|
115
219
|
process.stdout.write(`Local process Not running\nSpace ${spaceId}\nLogs ${result.diagnosticsPath}\n`);
|
|
116
220
|
process.stdout.write(`Server ${remote.error ? `Unknown — ${remote.error}` : remote.value?.online ? "Harness connected" : "Offline"}\nArchives ${pendingLocalArchives} pending · ${failedLocalArchives} failed\n`);
|
|
117
|
-
|
|
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`);
|
|
221
|
+
process.stdout.write(formatNativeSync(nativeSync.config, nativeSessions, nativeSync.error));
|
|
119
222
|
}
|
|
120
223
|
}
|
|
121
224
|
catch (cause) {
|
|
@@ -25,9 +25,10 @@ import { Readable } from "node:stream";
|
|
|
25
25
|
// v2.54.1 keeps the same wire protocol and adds relay diagnostics: data-channel
|
|
26
26
|
// pairing outlives the runner's dial timeout, dial failures distinguish a
|
|
27
27
|
// timeout from an explicit rejection, and teardown-time websocket write
|
|
28
|
-
// failures log at debug instead of warn.
|
|
29
|
-
//
|
|
30
|
-
|
|
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";
|
|
31
32
|
const BINARY_NAME = "cohub-sandboxd";
|
|
32
33
|
// Public CDN prefix hosting the release archives (the repo is private, so the
|
|
33
34
|
// GitHub Release assets are not publicly downloadable). Overridable for staging
|
|
@@ -21,6 +21,12 @@ function isRecord(value) {
|
|
|
21
21
|
function redactUrl(value) {
|
|
22
22
|
try {
|
|
23
23
|
const url = new URL(value);
|
|
24
|
+
// Redaction must not rewrite the string when there is nothing to hide;
|
|
25
|
+
// URL.toString() re-encodes (e.g. quotes from wrapped log lines) and
|
|
26
|
+
// makes healthy URLs look corrupted.
|
|
27
|
+
const sensitive = [...url.searchParams.keys()].some((key) => SENSITIVE_KEY.test(key));
|
|
28
|
+
if (!sensitive)
|
|
29
|
+
return value;
|
|
24
30
|
for (const key of [...url.searchParams.keys()]) {
|
|
25
31
|
if (SENSITIVE_KEY.test(key))
|
|
26
32
|
url.searchParams.set(key, "[REDACTED]");
|
|
@@ -37,7 +43,8 @@ export function redactDiagnosticString(value) {
|
|
|
37
43
|
: value;
|
|
38
44
|
return shortened
|
|
39
45
|
.replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [REDACTED]")
|
|
40
|
-
|
|
46
|
+
// Trailing quotes/brackets from wrapped log lines are not part of the URL.
|
|
47
|
+
.replace(/\b(rediss?|https?|wss?):\/\/[^\s"'<>]+/gi, (url) => redactUrl(url))
|
|
41
48
|
.replace(/\b(authorization|password|secret|token|access[_-]?key|refresh[_-]?token)([\s:=]+)([^\s,;]+)/gi, "$1$2[REDACTED]");
|
|
42
49
|
}
|
|
43
50
|
function redactValue(value, seen = new WeakSet(), depth = 0) {
|
package/dist/runtime/launch.js
CHANGED
|
@@ -10,6 +10,7 @@ import { createClient } from "../client.js";
|
|
|
10
10
|
import { currentIdentityKey, explicitSpace } from "../space.js";
|
|
11
11
|
import { canonicalRuntimeRoot, getRuntimeSpaceBinding, resolveRuntimeSpace } from "./space-binding.js";
|
|
12
12
|
import { requestRuntimeInstance, runtimeInstanceDirectory } from "./instance.js";
|
|
13
|
+
import { ensureNativeSync } from "./native-attach.js";
|
|
13
14
|
import { createDiagnosticConsole, printRuntimeSummary, runtimeWebUrl } from "./presentation.js";
|
|
14
15
|
import { runRuntime } from "./supervisor.js";
|
|
15
16
|
export const resolveLocalSpaceName = (root, name) => name?.trim() || basename(root) || "local-space";
|
|
@@ -90,6 +91,9 @@ export async function runtimeUp(program, dir, options) {
|
|
|
90
91
|
if (existing.root !== root || options.harness.length && [...existing.harnesses].sort().join() !== [...harnesses].sort().join() || options.pi || options.codex) {
|
|
91
92
|
throw new Error("Runtime is running with a different configuration. Use down first");
|
|
92
93
|
}
|
|
94
|
+
// `up` is the single idempotent entry: a reused instance adopts its running
|
|
95
|
+
// Harnesses (unless explicitly overridden) and completes native sync setup.
|
|
96
|
+
await ensureNativeSync({ root, spaceId: existing.spaceId, identity, harnesses: options.harness.length ? harnesses : parseRuntimeHarnesses(existing.harnesses), yes: options.yes, executables: { pi: options.pi, codex: options.codex } });
|
|
93
97
|
printRuntimeSummary(existing, options.json, true);
|
|
94
98
|
return;
|
|
95
99
|
}
|
|
@@ -122,8 +126,8 @@ export async function runtimeUp(program, dir, options) {
|
|
|
122
126
|
if (!requested && (!binding || createNew))
|
|
123
127
|
name = (await rl.question(`Space name [${name}]: `)).trim() || name;
|
|
124
128
|
process.stderr.write(`\nDirectory ${root}\n${requested ? `Space ${runtimeWebUrl(requested)}\n` : ""}`);
|
|
125
|
-
const answer = await rl.question("Collaborators can execute as your OS user, beyond this folder. Allow? [
|
|
126
|
-
if (
|
|
129
|
+
const answer = await rl.question("Collaborators can execute as your OS user, beyond this folder. Allow? [Y/n] ");
|
|
130
|
+
if (/^n(o)?$/i.test(answer.trim()))
|
|
127
131
|
return;
|
|
128
132
|
}
|
|
129
133
|
finally {
|
|
@@ -148,6 +152,8 @@ export async function runtimeUp(program, dir, options) {
|
|
|
148
152
|
throw new Error("Space does not have a local Runtime");
|
|
149
153
|
},
|
|
150
154
|
});
|
|
155
|
+
// Consent (or --yes) precedes any user-level integration install; failure never blocks startup.
|
|
156
|
+
await ensureNativeSync({ root, spaceId, identity, harnesses, yes: options.yes, executables: { pi: options.pi, codex: options.codex } });
|
|
151
157
|
const config = { spaceId, root, identity, harnesses, capabilities, executables: { pi: options.pi, codex: options.codex }, background: Boolean(options.detach), verbose: options.verbose };
|
|
152
158
|
const existing = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
|
|
153
159
|
if (existing) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type NativeSyncConfig } from "./native-sync.js";
|
|
2
|
+
type Harness = "pi" | "codex";
|
|
3
|
+
export type NativeSyncReport = {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
harnesses: Harness[];
|
|
6
|
+
note?: string;
|
|
7
|
+
};
|
|
8
|
+
/** Already enabled for this exact directory and every requested Harness: installation is idempotent. */
|
|
9
|
+
export declare function nativeSyncSatisfied(config: NativeSyncConfig | null, input: {
|
|
10
|
+
spaceId: string;
|
|
11
|
+
root: string;
|
|
12
|
+
harnesses: Harness[];
|
|
13
|
+
}): boolean;
|
|
14
|
+
/** One explicit consent prompt, defaulting to yes; native history may contain secrets. */
|
|
15
|
+
export declare function askNativeSyncConsent(harnesses: Harness[], ask?: (question: string) => Promise<string>): Promise<boolean>;
|
|
16
|
+
/** Enable native sync after consent; capability or write failures warn and never block the Runtime. */
|
|
17
|
+
export declare function ensureNativeSync(input: {
|
|
18
|
+
root: string;
|
|
19
|
+
spaceId: string;
|
|
20
|
+
identity: string;
|
|
21
|
+
harnesses: Harness[];
|
|
22
|
+
yes?: boolean;
|
|
23
|
+
executables?: {
|
|
24
|
+
pi?: string;
|
|
25
|
+
codex?: string;
|
|
26
|
+
};
|
|
27
|
+
}, log?: (line: string) => void): Promise<NativeSyncReport>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { installNativeSync } from "./native-install.js";
|
|
3
|
+
import { nativeRuntimeRoot, readNativeSyncConfig } from "./native-sync.js";
|
|
4
|
+
import { serializeDiagnosticError } from "./diagnostics.js";
|
|
5
|
+
/** Already enabled for this exact directory and every requested Harness: installation is idempotent. */
|
|
6
|
+
export function nativeSyncSatisfied(config, input) {
|
|
7
|
+
return Boolean(config && config.spaceId === input.spaceId && config.root === input.root && input.harnesses.every((harness) => config.harnesses.includes(harness)));
|
|
8
|
+
}
|
|
9
|
+
/** One explicit consent prompt, defaulting to yes; native history may contain secrets. */
|
|
10
|
+
export async function askNativeSyncConsent(harnesses, ask = defaultAsk) {
|
|
11
|
+
const answer = await ask(`Sync native ${harnesses.join(" / ")} chats in this folder and upload their history (may contain secrets) to this Space? [Y/n] `);
|
|
12
|
+
return /^(|y(es)?)$/i.test(answer.trim());
|
|
13
|
+
}
|
|
14
|
+
async function defaultAsk(question) {
|
|
15
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
16
|
+
try {
|
|
17
|
+
return await rl.question(question);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
rl.close();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Enable native sync after consent; capability or write failures warn and never block the Runtime. */
|
|
24
|
+
export async function ensureNativeSync(input, log = (line) => process.stderr.write(line)) {
|
|
25
|
+
const config = await readNativeSyncConfig(nativeRuntimeRoot(input.spaceId), input.identity).catch(() => null);
|
|
26
|
+
if (nativeSyncSatisfied(config, input))
|
|
27
|
+
return { enabled: true, harnesses: config?.harnesses ?? [] };
|
|
28
|
+
if (!input.yes) {
|
|
29
|
+
// Non-interactive sessions cannot consent; they continue without sync instead of failing `up`.
|
|
30
|
+
if (!process.stdin.isTTY) {
|
|
31
|
+
log("Native sync off · rerun interactively or with --yes to enable\n");
|
|
32
|
+
return { enabled: false, harnesses: input.harnesses, note: "non-interactive" };
|
|
33
|
+
}
|
|
34
|
+
if (!await askNativeSyncConsent(input.harnesses))
|
|
35
|
+
return { enabled: false, harnesses: input.harnesses, note: "declined" };
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const result = await installNativeSync(input);
|
|
39
|
+
log(`Native sync enabled. Reload Pi or restart Codex and review its hook trust prompt.\n${result.configPath}`);
|
|
40
|
+
return { enabled: true, harnesses: result.harnesses };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = serializeDiagnosticError(error).message;
|
|
44
|
+
log(`Native sync unavailable; Runtime continues without it — ${message}\n`);
|
|
45
|
+
return { enabled: false, harnesses: input.harnesses, note: message };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -9,7 +9,9 @@ const socketPath = (runtimeRoot) => join(runtimeRoot, "native", "daemon.sock");
|
|
|
9
9
|
const parse = (raw) => {
|
|
10
10
|
const value = JSON.parse(raw);
|
|
11
11
|
if (value?.type !== "native.capture" || !["pi", "codex"].includes(value.harness)
|
|
12
|
-
|| typeof value.cwd !== "string" || typeof value.path !== "string" || typeof value.nativeSessionId !== "string"
|
|
12
|
+
|| typeof value.cwd !== "string" || typeof value.path !== "string" || typeof value.nativeSessionId !== "string"
|
|
13
|
+
|| value.origin !== undefined && value.origin !== "local_import"
|
|
14
|
+
|| value.sessionStartedAt !== undefined && typeof value.sessionStartedAt !== "string") {
|
|
13
15
|
throw new Error("Invalid native daemon request");
|
|
14
16
|
}
|
|
15
17
|
return value;
|
|
@@ -32,6 +32,8 @@ export type NativeTurnReceipt = {
|
|
|
32
32
|
endBytes: number;
|
|
33
33
|
contentEndBytes?: number;
|
|
34
34
|
result: NativeTurnComplete | null;
|
|
35
|
+
sessionStartedAt?: string;
|
|
36
|
+
origin?: "local_import";
|
|
35
37
|
progress?: NativeTurnProgress;
|
|
36
38
|
};
|
|
37
39
|
export type NativeSyncTransport = ArchiveTransport & {
|
|
@@ -81,7 +83,10 @@ export declare class NativeSyncStore {
|
|
|
81
83
|
private cloudBindingPath;
|
|
82
84
|
binding(): Promise<NativeBinding>;
|
|
83
85
|
private initialize;
|
|
84
|
-
capture(path: string, transcript: NativeTranscript
|
|
86
|
+
capture(path: string, transcript: NativeTranscript, context?: {
|
|
87
|
+
sessionStartedAt?: string;
|
|
88
|
+
origin?: "local_import";
|
|
89
|
+
}): Promise<void>;
|
|
85
90
|
receipts(pendingOnly?: boolean): Promise<NativeTurnReceipt[]>;
|
|
86
91
|
status(): Promise<{
|
|
87
92
|
harness: "codex" | "pi";
|
|
@@ -112,7 +112,7 @@ export class NativeSyncStore {
|
|
|
112
112
|
await atomicRuntimeJson(this.bindingPath(), binding);
|
|
113
113
|
return binding;
|
|
114
114
|
}
|
|
115
|
-
async capture(path, transcript) {
|
|
115
|
+
async capture(path, transcript, context = {}) {
|
|
116
116
|
if (transcript.nativeSessionId !== this.options.nativeSessionId)
|
|
117
117
|
throw new Error("Native session identity mismatch");
|
|
118
118
|
await withRuntimeSpaceBindingsLock(async () => {
|
|
@@ -156,6 +156,8 @@ export class NativeSyncStore {
|
|
|
156
156
|
const result = turn.result ? nativeTurnCompleteSchema.parse(turn.result) : null;
|
|
157
157
|
const receipt = { version: 1, turnId, key: turn.key, parentKey, parentCloudTurnId: parentKey ? null : parentCloudTurnId,
|
|
158
158
|
userContent: turn.userContent, startedAt: turn.startedAt, endBytes: turn.endBytes, contentEndBytes: turn.contentEndBytes, result,
|
|
159
|
+
...(context.sessionStartedAt ? { sessionStartedAt: context.sessionStartedAt } : {}),
|
|
160
|
+
...(context.origin ? { origin: context.origin } : {}),
|
|
159
161
|
...(!result ? { progress: nativeTurnProgressSchema.parse({ revision: turn.endBytes, messages: turn.messages }) } : {}) };
|
|
160
162
|
if (old && (JSON.stringify(old.userContent) !== JSON.stringify(receipt.userContent) || old.parentKey !== receipt.parentKey))
|
|
161
163
|
throw new Error("Native Turn changed; original receipt retained");
|
|
@@ -238,7 +240,8 @@ export class NativeSyncStore {
|
|
|
238
240
|
let request = await readJson(this.requestPath(receipt.turnId));
|
|
239
241
|
if (!request) {
|
|
240
242
|
request = nativeTurnStartSchema.parse({ turnId: receipt.turnId, sessionId: parent?.sessionId ?? binding.sessionId, parentTurnId: parent?.turnId ?? receipt.parentCloudTurnId,
|
|
241
|
-
branchSessionId: nativeStableId(`${receipt.turnId}:branch`), harness: binding.harness, nativeSessionId: binding.nativeSessionId, userContent: receipt.userContent, startedAt: receipt.startedAt
|
|
243
|
+
branchSessionId: nativeStableId(`${receipt.turnId}:branch`), harness: binding.harness, nativeSessionId: binding.nativeSessionId, userContent: receipt.userContent, startedAt: receipt.startedAt,
|
|
244
|
+
...(receipt.sessionStartedAt ? { sessionStartedAt: receipt.sessionStartedAt } : {}), ...(receipt.origin ? { origin: receipt.origin } : {}) });
|
|
242
245
|
await atomicRuntimeJson(this.requestPath(receipt.turnId), request);
|
|
243
246
|
}
|
|
244
247
|
let remote = await readJson(this.cloudBindingPath(receipt.turnId));
|
|
@@ -9,6 +9,29 @@ export type NativeSyncConfig = {
|
|
|
9
9
|
};
|
|
10
10
|
export declare const nativeRuntimeRoot: (spaceId: string) => string;
|
|
11
11
|
export declare const nativeSyncConfigPath: (runtimeRoot: string, identity: string) => string;
|
|
12
|
+
export type NativeImportCandidate = {
|
|
13
|
+
harness: "pi" | "codex";
|
|
14
|
+
path: string;
|
|
15
|
+
nativeSessionId: string;
|
|
16
|
+
turnCount: number;
|
|
17
|
+
sessionStartedAt: string | undefined;
|
|
18
|
+
};
|
|
19
|
+
/** Find existing native transcripts for exactly one bound project. */
|
|
20
|
+
export declare function discoverNativeImportCandidates(root: string, harnesses: ("pi" | "codex")[], options?: {
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
onProgress?: (progress: {
|
|
23
|
+
harness: "pi" | "codex";
|
|
24
|
+
scanned: number;
|
|
25
|
+
candidates: number;
|
|
26
|
+
}) => void;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
candidates: NativeImportCandidate[];
|
|
29
|
+
errors: {
|
|
30
|
+
harness: "pi" | "codex";
|
|
31
|
+
path: string;
|
|
32
|
+
message: string;
|
|
33
|
+
}[];
|
|
34
|
+
}>;
|
|
12
35
|
export declare function nativeArchiveTransport(spaceId: string, identity: string): Pick<NativeSyncTransport, "prepareRuntimeArchive" | "commitRuntimeArchive" | "getRuntimeArchive">;
|
|
13
36
|
export declare function readNativeSyncConfig(runtimeRoot: string, identity: string): Promise<NativeSyncConfig | null>;
|
|
14
37
|
/** Local capture only. Neither Pi callbacks nor Codex hooks wait for Cohub's network. */
|
|
@@ -19,6 +42,8 @@ export declare function captureNativeSession(input: {
|
|
|
19
42
|
nativeSessionId?: string;
|
|
20
43
|
settled?: boolean;
|
|
21
44
|
leafId?: string | null;
|
|
45
|
+
sessionStartedAt?: string;
|
|
46
|
+
origin?: "local_import";
|
|
22
47
|
}): Promise<NativeSyncStore | null>;
|
|
23
48
|
/** The existing Runtime supervisor retries receipts even after the original terminal has exited. */
|
|
24
49
|
export declare function nativeWebSocketTransport(spaceId: string, identity: string, send: (event: NativeRuntimeEvent) => Promise<unknown>): NativeSyncTransport;
|
|
@@ -1,14 +1,95 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { createClient } from "../client.js";
|
|
5
5
|
import { currentIdentityKey } from "../space.js";
|
|
6
6
|
import { canonicalRuntimeRoot, getRuntimeSpaceBinding } from "./space-binding.js";
|
|
7
|
-
import { readNativeTranscript } from "./native-transcript.js";
|
|
7
|
+
import { readNativeTranscript, readNativeTranscriptHeader } from "./native-transcript.js";
|
|
8
8
|
import { findRuntimeNativeSession } from "./session-store.js";
|
|
9
9
|
import { listNativeSyncStores, nativeIdentityHash, NativeSyncStore } from "./native-sync-store.js";
|
|
10
10
|
export const nativeRuntimeRoot = (spaceId) => join(homedir(), ".local", "state", "cohub", "runtime", spaceId);
|
|
11
11
|
export const nativeSyncConfigPath = (runtimeRoot, identity) => join(runtimeRoot, "native", nativeIdentityHash(identity), "config.json");
|
|
12
|
+
const piSessionDirectory = (cwd) => {
|
|
13
|
+
const custom = process.env.PI_CODING_AGENT_SESSION_DIR?.trim();
|
|
14
|
+
if (custom)
|
|
15
|
+
return custom;
|
|
16
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent");
|
|
17
|
+
const safePath = `--${cwd.replace(/^[/\\\\]/, "").replace(/[/\\\\:]/g, "-")}--`;
|
|
18
|
+
return join(agentDir, "sessions", safePath);
|
|
19
|
+
};
|
|
20
|
+
const codexSessionDirectory = () => join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "sessions");
|
|
21
|
+
const LARGE_TRANSCRIPT_BYTES = 64 * 1024 * 1024;
|
|
22
|
+
async function nativeTranscriptPaths(root, harness, signal) {
|
|
23
|
+
const pending = [harness === "pi" ? piSessionDirectory(root) : codexSessionDirectory()];
|
|
24
|
+
const paths = [];
|
|
25
|
+
const errors = [];
|
|
26
|
+
while (pending.length) {
|
|
27
|
+
signal?.throwIfAborted();
|
|
28
|
+
const directory = pending.pop();
|
|
29
|
+
if (!directory)
|
|
30
|
+
continue;
|
|
31
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
|
|
32
|
+
if (error.code === "ENOENT")
|
|
33
|
+
return [];
|
|
34
|
+
errors.push({ harness, path: directory, message: error instanceof Error ? error.message : String(error) });
|
|
35
|
+
return [];
|
|
36
|
+
});
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const path = join(directory, entry.name);
|
|
39
|
+
if (entry.isDirectory())
|
|
40
|
+
pending.push(path);
|
|
41
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
42
|
+
paths.push(path);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { paths, errors };
|
|
46
|
+
}
|
|
47
|
+
/** Find existing native transcripts for exactly one bound project. */
|
|
48
|
+
export async function discoverNativeImportCandidates(root, harnesses, options = {}) {
|
|
49
|
+
const candidates = [];
|
|
50
|
+
const errors = [];
|
|
51
|
+
for (const harness of harnesses) {
|
|
52
|
+
options.signal?.throwIfAborted();
|
|
53
|
+
const scanned = await nativeTranscriptPaths(root, harness, options.signal);
|
|
54
|
+
errors.push(...scanned.errors);
|
|
55
|
+
let start = 0;
|
|
56
|
+
while (start < scanned.paths.length) {
|
|
57
|
+
options.signal?.throwIfAborted();
|
|
58
|
+
const paths = scanned.paths.slice(start, start + 2);
|
|
59
|
+
const sizes = await Promise.all(paths.map((path) => stat(path).then((value) => value.size).catch(() => 0)));
|
|
60
|
+
const concurrency = sizes.some((size) => size > LARGE_TRANSCRIPT_BYTES) ? 1 : 2;
|
|
61
|
+
const batch = await Promise.all(paths.slice(0, concurrency).map(async (path) => {
|
|
62
|
+
try {
|
|
63
|
+
const header = await readNativeTranscriptHeader(path, harness, options.signal);
|
|
64
|
+
if (await canonicalRuntimeRoot(header.cwd) !== root)
|
|
65
|
+
return null;
|
|
66
|
+
const transcript = await readNativeTranscript(path, harness, { settled: true, signal: options.signal });
|
|
67
|
+
if (transcript.turns.length === 0)
|
|
68
|
+
return { error: { harness, path, message: "Transcript contains no importable Turns" } };
|
|
69
|
+
return { harness, path, nativeSessionId: transcript.nativeSessionId, turnCount: transcript.turns.length,
|
|
70
|
+
sessionStartedAt: transcript.turns.map((turn) => turn.startedAt).sort()[0] };
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return { error: { harness, path, message: error instanceof Error ? error.message : String(error) } };
|
|
74
|
+
}
|
|
75
|
+
}));
|
|
76
|
+
for (const result of batch) {
|
|
77
|
+
options.signal?.throwIfAborted();
|
|
78
|
+
if (!result)
|
|
79
|
+
continue;
|
|
80
|
+
if ("error" in result) {
|
|
81
|
+
if (result.error)
|
|
82
|
+
errors.push(result.error);
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
candidates.push(result);
|
|
86
|
+
options.onProgress?.({ harness, scanned: start + batch.length, candidates: candidates.length });
|
|
87
|
+
}
|
|
88
|
+
start += concurrency;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { candidates, errors };
|
|
92
|
+
}
|
|
12
93
|
export function nativeArchiveTransport(spaceId, identity) {
|
|
13
94
|
const client = createClient().space(spaceId);
|
|
14
95
|
const guard = async (task) => {
|
|
@@ -83,7 +164,7 @@ export async function captureNativeSession(input) {
|
|
|
83
164
|
nativeStores.delete(nativeStores.keys().next().value ?? "");
|
|
84
165
|
nativeStores.set(key, store);
|
|
85
166
|
}
|
|
86
|
-
await store.capture(path, transcript);
|
|
167
|
+
await store.capture(path, transcript, { sessionStartedAt: input.sessionStartedAt, origin: input.origin });
|
|
87
168
|
return store;
|
|
88
169
|
}
|
|
89
170
|
/** The existing Runtime supervisor retries receipts even after the original terminal has exited. */
|
|
@@ -20,8 +20,16 @@ export type NativeTranscript = {
|
|
|
20
20
|
turns: NativeTranscriptTurn[];
|
|
21
21
|
prefixes: ReadonlyMap<number, string>;
|
|
22
22
|
};
|
|
23
|
+
export declare const NATIVE_MAX_RECORD_BYTES: number;
|
|
24
|
+
export declare const NATIVE_MAX_TRANSCRIPT_BYTES: number;
|
|
25
|
+
/** Read only the session header before deciding whether a global Codex file belongs to this project. */
|
|
26
|
+
export declare function readNativeTranscriptHeader(path: string, harness: "pi" | "codex", signal?: AbortSignal): Promise<{
|
|
27
|
+
nativeSessionId: string;
|
|
28
|
+
cwd: string;
|
|
29
|
+
}>;
|
|
23
30
|
/** Partial trailing records are retried, never parsed or acknowledged as complete. */
|
|
24
31
|
export declare function readNativeTranscript(path: string, harness: "pi" | "codex", options?: {
|
|
25
32
|
settled?: boolean;
|
|
26
33
|
leafId?: string | null;
|
|
34
|
+
signal?: AbortSignal;
|
|
27
35
|
}): Promise<NativeTranscript>;
|
|
@@ -11,6 +11,35 @@ const iso = (value) => {
|
|
|
11
11
|
throw new Error("Invalid native timestamp");
|
|
12
12
|
return date.toISOString();
|
|
13
13
|
};
|
|
14
|
+
export const NATIVE_MAX_RECORD_BYTES = 256 * 1024 * 1024;
|
|
15
|
+
export const NATIVE_MAX_TRANSCRIPT_BYTES = 1024 * 1024 * 1024;
|
|
16
|
+
const NATIVE_HEADER_MAX_BYTES = 1024 * 1024;
|
|
17
|
+
/** Read only the session header before deciding whether a global Codex file belongs to this project. */
|
|
18
|
+
export async function readNativeTranscriptHeader(path, harness, signal) {
|
|
19
|
+
const stream = createReadStream(path, { signal });
|
|
20
|
+
let buffer = Buffer.alloc(0);
|
|
21
|
+
try {
|
|
22
|
+
for await (const chunk of stream) {
|
|
23
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
24
|
+
const newline = buffer.indexOf(10);
|
|
25
|
+
if (newline >= 0) {
|
|
26
|
+
const value = record(JSON.parse(buffer.subarray(0, newline).toString("utf8")));
|
|
27
|
+
const payload = record(value.payload);
|
|
28
|
+
const nativeSessionId = harness === "pi" ? text(value.id) : text(payload.id);
|
|
29
|
+
const cwd = harness === "pi" ? text(value.cwd) : text(payload.cwd);
|
|
30
|
+
if ((harness === "pi" && value.type !== "session") || (harness === "codex" && value.type !== "session_meta") || !nativeSessionId || !cwd)
|
|
31
|
+
throw new Error("Invalid native session header");
|
|
32
|
+
return { nativeSessionId, cwd };
|
|
33
|
+
}
|
|
34
|
+
if (buffer.length > NATIVE_HEADER_MAX_BYTES)
|
|
35
|
+
throw new Error("Native session header is too large");
|
|
36
|
+
}
|
|
37
|
+
throw new Error("Native transcript is empty");
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
stream.destroy();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
14
43
|
/** Partial trailing records are retried, never parsed or acknowledged as complete. */
|
|
15
44
|
export async function readNativeTranscript(path, harness, options = {}) {
|
|
16
45
|
const lines = [];
|
|
@@ -23,12 +52,12 @@ export async function readNativeTranscript(path, harness, options = {}) {
|
|
|
23
52
|
fragments.push(bytes);
|
|
24
53
|
pendingBytes += bytes.length;
|
|
25
54
|
checksum.update(bytes);
|
|
26
|
-
if (pendingBytes >
|
|
27
|
-
throw new Error("Native record
|
|
28
|
-
if (offset + pendingBytes >
|
|
29
|
-
throw new Error("Native transcript exceeds the
|
|
55
|
+
if (pendingBytes > NATIVE_MAX_RECORD_BYTES)
|
|
56
|
+
throw new Error("Native record exceeds the 256 MiB local parse limit; original retained");
|
|
57
|
+
if (offset + pendingBytes > NATIVE_MAX_TRANSCRIPT_BYTES)
|
|
58
|
+
throw new Error("Native transcript exceeds the 1 GiB local parse limit; original retained");
|
|
30
59
|
};
|
|
31
|
-
for await (const chunk of createReadStream(path)) {
|
|
60
|
+
for await (const chunk of createReadStream(path, { signal: options.signal })) {
|
|
32
61
|
let start = 0;
|
|
33
62
|
for (let end = chunk.indexOf(10); end >= 0; end = chunk.indexOf(10, start)) {
|
|
34
63
|
append(chunk.subarray(start, end + 1));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NativeSyncConfig } from "./native-sync.js";
|
|
1
2
|
import type { RuntimeDiagnostic, RuntimeDiagnosticLevel } from "./diagnostics.js";
|
|
2
3
|
export declare const diagnosticLevels: RuntimeDiagnosticLevel[];
|
|
3
4
|
export declare const atLeastLevel: (level: RuntimeDiagnosticLevel, minimum: RuntimeDiagnosticLevel) => boolean;
|
|
@@ -16,6 +17,14 @@ export type RuntimeSummary = {
|
|
|
16
17
|
workspaceConnected: boolean;
|
|
17
18
|
diagnosticsPath: string;
|
|
18
19
|
background: boolean;
|
|
19
|
-
nativeSync?: boolean;
|
|
20
20
|
};
|
|
21
21
|
export declare function printRuntimeSummary(summary: RuntimeSummary, json?: boolean, reused?: boolean): void;
|
|
22
|
+
export type NativeSessionStatus = {
|
|
23
|
+
harness: "pi" | "codex";
|
|
24
|
+
nativeSessionId: string;
|
|
25
|
+
sessionId: string | null;
|
|
26
|
+
pendingTurns: number;
|
|
27
|
+
pendingArchives: number;
|
|
28
|
+
};
|
|
29
|
+
/** `runtime status` native sync block: installation state plus per-Harness pending work. */
|
|
30
|
+
export declare function formatNativeSync(config: NativeSyncConfig | null, sessions: NativeSessionStatus[], error?: string | null): string;
|
|
@@ -76,3 +76,19 @@ export function printRuntimeSummary(summary, json = false, reused = false) {
|
|
|
76
76
|
process.stdout.write(" Ctrl+C to stop\n");
|
|
77
77
|
process.stdout.write("\n");
|
|
78
78
|
}
|
|
79
|
+
/** `runtime status` native sync block: installation state plus per-Harness pending work. */
|
|
80
|
+
export function formatNativeSync(config, sessions, error = null) {
|
|
81
|
+
if (error)
|
|
82
|
+
return `Native sync unknown — ${error} · fix or remove the config, then runtime up\n`;
|
|
83
|
+
if (!config?.harnesses.length)
|
|
84
|
+
return "Native sync off · run cohub runtime up to enable\n";
|
|
85
|
+
const lines = [`Native sync enabled · ${config.harnesses.join(", ")}`];
|
|
86
|
+
for (const session of sessions) {
|
|
87
|
+
lines.push(` ${session.harness.padEnd(5)} ${session.nativeSessionId.slice(0, 8)} ${session.pendingTurns} Turns · ${session.pendingArchives} archives pending`);
|
|
88
|
+
}
|
|
89
|
+
if (!sessions.length)
|
|
90
|
+
lines.push(" No native chats captured yet");
|
|
91
|
+
else if (sessions.every((session) => !session.pendingTurns && !session.pendingArchives))
|
|
92
|
+
lines.push(" Up to date");
|
|
93
|
+
return `${lines.join("\n")}\n`;
|
|
94
|
+
}
|
|
@@ -41,7 +41,6 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
|
|
|
41
41
|
pid: process.pid, harnesses: config.harnesses, background: config.background,
|
|
42
42
|
state: "starting", harnessConnected: false, workspaceConnected: false,
|
|
43
43
|
diagnosticsPath: diagnostics.directory,
|
|
44
|
-
nativeSync: true,
|
|
45
44
|
};
|
|
46
45
|
let hasBeenReady = false;
|
|
47
46
|
const update = (patch) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "8.0
|
|
3
|
+
"version": "8.1.0",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"commander": "^15.0.0",
|
|
23
23
|
"pixi.js": "^8.20.1",
|
|
24
24
|
"sharp": "^0.35.4",
|
|
25
|
-
"@neta-art/cohub": "8.21.
|
|
25
|
+
"@neta-art/cohub": "8.21.1"
|
|
26
26
|
},
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|