@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.
Files changed (36) hide show
  1. package/README.md +7 -16
  2. package/dist/commands/runtime.js +190 -87
  3. package/dist/commands/sandboxd-binary.d.ts +1 -1
  4. package/dist/commands/sandboxd-binary.js +8 -1
  5. package/dist/runtime/archive-store.d.ts +3 -1
  6. package/dist/runtime/archive-store.js +2 -0
  7. package/dist/runtime/connection.js +34 -22
  8. package/dist/runtime/diagnostics.js +8 -1
  9. package/dist/runtime/harness.d.ts +4 -0
  10. package/dist/runtime/harness.js +29 -16
  11. package/dist/runtime/instance.js +5 -5
  12. package/dist/runtime/json-rpc.d.ts +2 -0
  13. package/dist/runtime/json-rpc.js +2 -0
  14. package/dist/runtime/launch.js +29 -23
  15. package/dist/runtime/native-attach.d.ts +28 -0
  16. package/dist/runtime/native-attach.js +47 -0
  17. package/dist/runtime/native-codex-hook.js +3 -3
  18. package/dist/runtime/native-install.js +9 -9
  19. package/dist/runtime/native-ipc.d.ts +2 -0
  20. package/dist/runtime/native-ipc.js +7 -5
  21. package/dist/runtime/native-pi-extension.js +1 -1
  22. package/dist/runtime/native-sync-store.d.ts +6 -1
  23. package/dist/runtime/native-sync-store.js +24 -21
  24. package/dist/runtime/native-sync.d.ts +25 -0
  25. package/dist/runtime/native-sync.js +89 -8
  26. package/dist/runtime/native-transcript.d.ts +8 -0
  27. package/dist/runtime/native-transcript.js +45 -16
  28. package/dist/runtime/presentation.d.ts +10 -1
  29. package/dist/runtime/presentation.js +47 -29
  30. package/dist/runtime/process-group.d.ts +2 -0
  31. package/dist/runtime/process-group.js +124 -31
  32. package/dist/runtime/session-store.d.ts +15 -2
  33. package/dist/runtime/session-store.js +79 -5
  34. package/dist/runtime/space-binding.js +3 -3
  35. package/dist/runtime/supervisor.js +4 -5
  36. package/package.json +2 -2
@@ -54,7 +54,7 @@ export class NativeSyncStore {
54
54
  async binding() {
55
55
  const value = await readJson(this.bindingPath());
56
56
  if (value?.version !== 1 || value.identity !== this.options.identity || value.spaceId !== this.options.spaceId || value.nativeSessionId !== this.options.nativeSessionId || value.instanceKey !== this.options.instanceKey || value.harness !== this.options.harness)
57
- throw new Error("Native binding mismatch / 原生关联不匹配");
57
+ throw new Error("Native binding mismatch");
58
58
  return value;
59
59
  }
60
60
  async initialize(path, transcript) {
@@ -62,7 +62,7 @@ export class NativeSyncStore {
62
62
  if (existing) {
63
63
  const binding = await this.binding();
64
64
  if (binding.path !== path)
65
- throw new Error("Native path changed; original binding retained / 原生路径已变化,原关联已保留");
65
+ throw new Error("Native path changed; original binding retained");
66
66
  return binding;
67
67
  }
68
68
  const managed = await findRuntimeNativeSession(this.options.runtimeRoot, this.options.harness, transcript.nativeSessionId, path);
@@ -70,7 +70,7 @@ export class NativeSyncStore {
70
70
  const anchors = [];
71
71
  if (managed) {
72
72
  if (managed.pendingTurnId)
73
- throw new Error("Reconcile the managed Turn before native continuation / 请先确认 Runtime 中尚未确认的 Turn");
73
+ throw new Error("Reconcile the managed Turn before native continuation");
74
74
  if (managed.throughTurnId) {
75
75
  const index = await readJson(join(this.options.runtimeRoot, "archives", "versions", `${managed.throughTurnId}.json`));
76
76
  if (index) {
@@ -79,7 +79,7 @@ export class NativeSyncStore {
79
79
  for await (const bytes of createReadStream(path, { end: parsed.sizeBytes - 1 }))
80
80
  checksum.update(bytes);
81
81
  if (checksum.digest("hex") !== parsed.sha256)
82
- throw new Error("Runtime history prefix changed / Runtime 历史前缀已变化");
82
+ throw new Error("Runtime history prefix changed");
83
83
  throughBytes = parsed.sizeBytes;
84
84
  }
85
85
  else {
@@ -87,7 +87,7 @@ export class NativeSyncStore {
87
87
  for await (const bytes of createReadStream(path))
88
88
  checksum.update(bytes);
89
89
  if (checksum.digest("hex") !== managed.checksum)
90
- throw new Error("Cannot identify the last complete Runtime Turn / 无法识别 Runtime 最后一个完整 Turn");
90
+ throw new Error("Cannot identify the last complete Runtime Turn");
91
91
  throughBytes = (await stat(path)).size;
92
92
  anchors.push({ turnId: managed.throughTurnId, sizeBytes: throughBytes, sha256: managed.checksum });
93
93
  }
@@ -112,15 +112,15 @@ 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
- throw new Error("Native session identity mismatch / 原生会话身份不匹配");
117
+ throw new Error("Native session identity mismatch");
118
118
  await withRuntimeSpaceBindingsLock(async () => {
119
119
  const binding = await this.initialize(path, transcript);
120
120
  if (binding.throughBytes > 0) {
121
121
  const anchor = binding.anchors.find((entry) => entry.sizeBytes === binding.throughBytes);
122
122
  if (!anchor || transcript.prefixes.get(binding.throughBytes) !== anchor.sha256)
123
- throw new Error("Runtime history prefix changed; original binding retained / Runtime 历史前缀已变化,原关联已保留");
123
+ throw new Error("Runtime history prefix changed; original binding retained");
124
124
  }
125
125
  let parentKey = null;
126
126
  let parentCloudTurnId = binding.throughTurnId;
@@ -130,7 +130,7 @@ export class NativeSyncStore {
130
130
  // Native offsets only validate whole-Turn archive checkpoints; they never become cloud fork anchors.
131
131
  const matches = (binding.anchors ?? []).filter((anchor) => anchor.sizeBytes >= turn.contentEndBytes && turn.boundaries[anchor.sizeBytes] === anchor.sha256);
132
132
  if (new Set(matches.map((anchor) => anchor.turnId)).size > 1)
133
- throw new Error("Ambiguous Runtime Turn boundary / Runtime Turn 边界不明确");
133
+ throw new Error("Ambiguous Runtime Turn boundary");
134
134
  parentCloudTurnId = matches[0]?.turnId ?? null;
135
135
  knownBoundary = matches.length > 0;
136
136
  parentKey = null;
@@ -143,12 +143,12 @@ export class NativeSyncStore {
143
143
  continue;
144
144
  }
145
145
  if (!parentKey && !knownBoundary)
146
- throw new Error("Native continuation is not at a complete Runtime Turn boundary / 原生续聊不在完整 Runtime Turn 边界上");
146
+ throw new Error("Native continuation is not at a complete Runtime Turn boundary");
147
147
  const turnId = this.turnId(turn.key);
148
148
  const old = await readJson(this.receiptPath(turnId));
149
149
  if (old?.result) {
150
150
  if (turn.contentEndBytes < (old.contentEndBytes ?? old.endBytes) || JSON.stringify(turn.userContent) !== JSON.stringify(old.userContent) || turn.result && JSON.stringify(nativeTurnCompleteSchema.parse(turn.result)) !== JSON.stringify(old.result)) {
151
- throw new Error("Native branch is inside a settled Turn; only whole-Turn forks are supported / 原生分支位于已结束的 Turn 内,仅支持完整 Turn 分支");
151
+ throw new Error("Native branch is inside a settled Turn; only whole-Turn forks are supported");
152
152
  }
153
153
  parentKey = turn.key;
154
154
  continue;
@@ -156,9 +156,11 @@ 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
- throw new Error("Native Turn changed; original receipt retained / 原生 Turn 已变化,原回执已保留");
163
+ throw new Error("Native Turn changed; original receipt retained");
162
164
  // Capture immutable native bytes before publishing the completed receipt. Subsequent Turns may change the source.
163
165
  if (result)
164
166
  await this.archives.stage({ sessionId: binding.originSessionId, harness: binding.harness, nativeSessionId: binding.nativeSessionId, path, sizeBytes: turn.endBytes, expectedChecksum: turn.sha256 }, turnId);
@@ -186,7 +188,7 @@ export class NativeSyncStore {
186
188
  continue;
187
189
  }
188
190
  if (receipt?.version !== 1 || receipt.turnId !== this.turnId(receipt.key))
189
- throw new Error("Native receipt is corrupt; original retained / 原生回执损坏,原件已保留");
191
+ throw new Error("Native receipt is corrupt; original retained");
190
192
  receipts.push(receipt);
191
193
  }
192
194
  return receipts.sort((a, b) => a.endBytes - b.endBytes || a.turnId.localeCompare(b.turnId));
@@ -233,21 +235,22 @@ export class NativeSyncStore {
233
235
  parent = await readJson(this.acknowledgementPath(parentId));
234
236
  }
235
237
  if (!parent)
236
- throw new Error("Parent binding is missing / 父 Turn 关联缺失");
238
+ throw new Error("Parent binding is missing");
237
239
  }
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));
245
248
  if (!remote) {
246
249
  if (!transport.startNativeTurn)
247
- throw new Error("Native Runtime WS is unavailable / 原生 Runtime WS 不可用");
250
+ throw new Error("Native Runtime WS is unavailable");
248
251
  remote = await transport.startNativeTurn(request, { signal });
249
252
  if (remote.turnId !== receipt.turnId)
250
- throw new Error("Server Turn identity mismatch / 服务端 Turn 身份不匹配");
253
+ throw new Error("Server Turn identity mismatch");
251
254
  await atomicRuntimeJson(this.cloudBindingPath(receipt.turnId), remote);
252
255
  }
253
256
  if (!receipt.result) {
@@ -267,7 +270,7 @@ export class NativeSyncStore {
267
270
  return false;
268
271
  }
269
272
  if (!transport.completeNativeTurn)
270
- throw new Error("Native Runtime WS is unavailable / 原生 Runtime WS 不可用");
273
+ throw new Error("Native Runtime WS is unavailable");
271
274
  // Artifact retries back off: the terminal state is durable, so hammering the completion
272
275
  // endpoint every flush cycle (5s) while object storage is down only adds load.
273
276
  const backoffPath = join(this.root, "backoff", `${receipt.turnId}.json`);
@@ -279,7 +282,7 @@ export class NativeSyncStore {
279
282
  // Terminal state is durable; only the artifact snapshot is missing. Keep the receipt pending
280
283
  // and retry on the next flush cycle (>= 30s) until artifacts persist.
281
284
  await atomicRuntimeJson(backoffPath, { at: Date.now() });
282
- throw new Error("Native artifacts are pending; completion replays later / 原生产物待生成,稍后重放完成请求");
285
+ throw new Error("Native artifacts are pending; completion replays later");
283
286
  }
284
287
  await rm(backoffPath, { force: true });
285
288
  await atomicRuntimeJson(this.acknowledgementPath(receipt.turnId), remote);
@@ -327,7 +330,7 @@ export class NativeSyncStore {
327
330
  async cloudArchive(index) {
328
331
  const binding = await readJson(this.acknowledgementPath(index.turnId));
329
332
  if (!binding)
330
- throw new Error("Native Turn result is not confirmed / 原生 Turn 结果尚未确认");
333
+ throw new Error("Native Turn result is not confirmed");
331
334
  if (index.parentTurnId) {
332
335
  const parent = await readJson(this.acknowledgementPath(index.parentTurnId));
333
336
  if (parent?.sessionId === binding.sessionId)
@@ -338,7 +341,7 @@ export class NativeSyncStore {
338
341
  let parentId = index.parentTurnId;
339
342
  while (parentId) {
340
343
  if (visited.has(parentId))
341
- throw new Error("Cyclic native archive / 原生归档存在循环");
344
+ throw new Error("Cyclic native archive");
342
345
  visited.add(parentId);
343
346
  const previous = harnessArchiveIndexSchema.parse(await readJson(join(this.archives.root, "versions", `${parentId}.json`)));
344
347
  segments.unshift(...previous.segments);
@@ -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,22 +1,103 @@
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) => {
15
96
  if (currentIdentityKey() !== identity)
16
- throw new Error("Native sync account changed / 原生同步账号已变化");
97
+ throw new Error("Native sync account changed");
17
98
  const result = await task();
18
99
  if (currentIdentityKey() !== identity)
19
- throw new Error("Native sync account changed / 原生同步账号已变化");
100
+ throw new Error("Native sync account changed");
20
101
  return result;
21
102
  };
22
103
  return {
@@ -29,7 +110,7 @@ export async function readNativeSyncConfig(runtimeRoot, identity) {
29
110
  try {
30
111
  const config = JSON.parse(await readFile(nativeSyncConfigPath(runtimeRoot, identity), "utf8"));
31
112
  if (config.version !== 1 || config.identity !== identity || !Array.isArray(config.harnesses))
32
- throw new Error("Invalid native sync configuration / 原生同步配置无效");
113
+ throw new Error("Invalid native sync configuration");
33
114
  return config;
34
115
  }
35
116
  catch (error) {
@@ -57,7 +138,7 @@ export async function captureNativeSession(input) {
57
138
  const path = await canonicalRuntimeRoot(input.path);
58
139
  const transcript = await readNativeTranscript(path, input.harness, input);
59
140
  if (await canonicalRuntimeRoot(transcript.cwd) !== root || input.nativeSessionId && transcript.nativeSessionId !== input.nativeSessionId)
60
- throw new Error("Native transcript belongs to another project or Session / 原生记录属于其他项目或会话");
141
+ throw new Error("Native transcript belongs to another project or Session");
61
142
  const key = JSON.stringify([identity, space.spaceId, input.harness, transcript.nativeSessionId, path]);
62
143
  let store = nativeStores.get(key);
63
144
  if (!store) {
@@ -74,7 +155,7 @@ export async function captureNativeSession(input) {
74
155
  const managedPath = managed ? await canonicalRuntimeRoot(managed.path).catch((error) => { if (error.code === "ENOENT")
75
156
  return null; throw error; }) : null;
76
157
  if (candidates.length && managedPath !== path)
77
- throw new Error("Native path changed; original bindings retained / 原生路径已变化,原关联已保留");
158
+ throw new Error("Native path changed; original bindings retained");
78
159
  // Restored Pi working copies can share a native ID. Existing Cohub sidecars disambiguate them.
79
160
  store = new NativeSyncStore({ runtimeRoot, spaceId: space.spaceId, identity, harness: input.harness, nativeSessionId: transcript.nativeSessionId,
80
161
  instanceKey: managedPath === path ? path : undefined, transport });
@@ -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>;
@@ -8,9 +8,38 @@ const list = (value) => Array.isArray(value) ? value : [];
8
8
  const iso = (value) => {
9
9
  const date = new Date(typeof value === "number" || typeof value === "string" ? value : 0);
10
10
  if (!Number.isFinite(date.getTime()))
11
- throw new Error("Invalid native timestamp / 原生时间无效");
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 > 32 * 1024 * 1024)
27
- throw new Error("Native record is too large / 原生记录过大");
28
- if (offset + pendingBytes > 128 * 1024 * 1024)
29
- throw new Error("Native transcript exceeds the capture limit; original retained / 原生记录超出采集上限,原件已保留");
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));
@@ -47,7 +76,7 @@ export async function readNativeTranscript(path, harness, options = {}) {
47
76
  value = record(JSON.parse(line.toString("utf8")));
48
77
  }
49
78
  catch {
50
- throw new Error("Invalid native JSON record; original retained / 原生 JSON 记录无效,原件已保留");
79
+ throw new Error("Invalid native JSON record; original retained");
51
80
  }
52
81
  lines.push({ value, startBytes, endBytes: offset, sha256 });
53
82
  }
@@ -56,24 +85,24 @@ export async function readNativeTranscript(path, harness, options = {}) {
56
85
  append(chunk.subarray(start));
57
86
  }
58
87
  if (!lines.length)
59
- throw new Error("Native transcript is empty / 原生记录为空");
88
+ throw new Error("Native transcript is empty");
60
89
  return { ...(harness === "pi" ? parsePiTranscript(lines, options) : parseCodexTranscript(lines)), prefixes };
61
90
  }
62
91
  function parsePiTranscript(lines, options) {
63
92
  const header = lines[0]?.value ?? {};
64
93
  if (header.type !== "session" || !text(header.id))
65
- throw new Error("Invalid Pi session / Pi 会话无效");
94
+ throw new Error("Invalid Pi session");
66
95
  const entries = new Map(lines.slice(1).filter((line) => text(line.value.id)).map((line) => [text(line.value.id), line]));
67
96
  const branch = [];
68
97
  let leaf = options.leafId ?? (lines.length > 1 ? text(lines.at(-1)?.value.id) : "");
69
98
  const visited = new Set();
70
99
  while (leaf) {
71
100
  if (visited.has(leaf))
72
- throw new Error("Cyclic Pi history / Pi 历史存在循环");
101
+ throw new Error("Cyclic Pi history");
73
102
  visited.add(leaf);
74
103
  const entry = entries.get(leaf);
75
104
  if (!entry)
76
- throw new Error("Pi parent history is missing / Pi 父历史缺失");
105
+ throw new Error("Pi parent history is missing");
77
106
  branch.push(entry);
78
107
  leaf = text(entry.value.parentId);
79
108
  }
@@ -128,7 +157,7 @@ function parsePiTranscript(lines, options) {
128
157
  else if (current && message.role === "toolResult") {
129
158
  const assistant = messages.at(-1);
130
159
  if (!assistant)
131
- throw new Error("Pi tool result has no assistant Turn / Pi 工具结果缺少所属 Turn");
160
+ throw new Error("Pi tool result has no assistant Turn");
132
161
  assistant.content.push({ type: "tool_result", tool_use_id: text(message.toolCallId), content: typeof message.content === "string" ? message.content : piContent(message.content), is_error: Boolean(message.isError) });
133
162
  }
134
163
  if (current) {
@@ -158,9 +187,9 @@ function parseCodexTranscript(lines) {
158
187
  const header = lines[0]?.value ?? {};
159
188
  const metadata = record(header.payload);
160
189
  if (header.type !== "session_meta" || !text(metadata.id))
161
- throw new Error("Invalid Codex session / Codex 会话无效");
190
+ throw new Error("Invalid Codex session");
162
191
  if (metadata.history_base || metadata.fork_source)
163
- throw new Error("Codex history references another rollout; retain the original and materialize its full history first / Codex 历史引用其他记录,请保留原件并先导出完整历史");
192
+ throw new Error("Codex history references another rollout; retain the original and materialize its full history first");
164
193
  const turns = [];
165
194
  let current = null;
166
195
  let messages = [];
@@ -188,7 +217,7 @@ function parseCodexTranscript(lines) {
188
217
  turns.push(current);
189
218
  current = { key: text(payload.turn_id), parentKey: turns.at(-1)?.key ?? null, userContent: [], messages: [], startedAt: iso(entry.timestamp), startBytes: line.startBytes, endBytes: line.endBytes, contentEndBytes: line.endBytes, boundaries: {}, sha256: line.sha256, result: null };
190
219
  if (!current.key)
191
- throw new Error("Codex Turn identity is missing / Codex Turn 身份缺失");
220
+ throw new Error("Codex Turn identity is missing");
192
221
  messages = current.messages;
193
222
  userFromResponse = false;
194
223
  }
@@ -255,7 +284,7 @@ function parseCodexTranscript(lines) {
255
284
  }
256
285
  if (entry.type === "event_msg" && ["turn_complete", "task_complete", "turn_aborted"].includes(text(payload.type))) {
257
286
  if (payload.turn_id && payload.turn_id !== current.key)
258
- throw new Error("Codex Turn boundary mismatch / Codex Turn 边界不匹配");
287
+ throw new Error("Codex Turn boundary mismatch");
259
288
  if (!messages.length && text(payload.last_agent_message))
260
289
  messages.push({ content: [{ type: "text", text: text(payload.last_agent_message) }], model, provider });
261
290
  const errorMessage = text(record(payload.error).message);
@@ -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;
@@ -3,26 +3,28 @@ export const diagnosticLevels = ["debug", "info", "warn", "error"];
3
3
  export const atLeastLevel = (level, minimum) => diagnosticLevels.indexOf(level) >= diagnosticLevels.indexOf(minimum);
4
4
  export const runtimeWebUrl = (spaceId) => `https://${resolveCohubEnvironment() === "prod" ? "" : "dev."}cohub.live/spaces/${spaceId}`;
5
5
  const messages = {
6
- "runtime.ready": "Harness connected / Harness 已连接",
7
- "runtime.available": "Runtime ready / Runtime 已就绪",
8
- "runtime.websocket.closed": "Connection lost; reconnecting / 连接中断,正在重连",
9
- "runtime.heartbeat_timeout": "Connection timed out; reconnecting / 连接超时,正在重连",
10
- "runtime.auth_token_failed": "Cannot obtain credentials; retrying / 暂时无法获取凭证,正在重试",
11
- "runtime.auth_required": "Sign in with cohub auth login / 请运行 cohub auth login 登录",
12
- "runtime.stopped": "Runtime stopped / Runtime 已停止",
13
- "runtime.failed": "Runtime needs attention / Runtime 需要处理",
14
- "runtime.turn_failed": "Turn failed; local files retained / 执行失败,本地文件已保留",
15
- "runtime.connection_failed": "Connection attempt failed; retrying / 连接失败,将继续重试",
16
- "runtime.execution_transport_lost": "Execution disconnected; outcome needs reconciliation / 执行连接中断,结果待确认",
17
- "archive.upload_pending": "Archive upload pending; local data retained / 归档待上传,本地数据已保留",
18
- "native.sync_pending": "Native sync pending; local records retained / 原生同步待处理,本地记录已保留",
19
- "archive.capture_pending": "Archive capture pending / 归档待处理",
20
- "archive.capture_unavailable": "Archive unavailable; original receipt retained / 归档不可用,原始回执已保留",
21
- "archive.restore_failed": "Native restore unavailable; using saved history / 原生恢复不可用,使用已保存历史",
22
- "sandboxd.process_exit": "File bridge stopped; restarting / 文件桥接已退出,正在重启",
23
- "sandboxd.download": "Preparing file bridge / 正在准备文件桥接",
24
- "sandboxd.connected": "File bridge connected / 文件桥接已连接",
25
- "sandboxd.disconnected": "File bridge disconnected; reconnecting / 文件桥接已断开,正在重连",
6
+ "runtime.ready": "Harness connected",
7
+ "runtime.available": "Runtime ready",
8
+ "runtime.websocket.closed": "Connection lost; reconnecting",
9
+ "runtime.heartbeat_timeout": "Connection timed out; reconnecting",
10
+ "runtime.auth_token_failed": "Cannot obtain credentials; retrying",
11
+ "runtime.auth_required": "Sign in with cohub auth login",
12
+ "runtime.stopped": "Runtime stopped",
13
+ "runtime.failed": "Runtime needs attention",
14
+ "runtime.turn_failed": "Turn failed; local files retained",
15
+ "runtime.turn_cleanup_pending": "Turn result saved; tool cleanup still unconfirmed",
16
+ "runtime.connection_failed": "Connection attempt failed; retrying",
17
+ "runtime.execution_transport_detached": "Execution continues locally; result replays after reconnect",
18
+ "runtime.execution_transport_invalidated": "Execution interrupted; outcome needs reconciliation",
19
+ "archive.upload_pending": "Archive upload pending; local data retained",
20
+ "native.sync_pending": "Native sync pending; local records retained",
21
+ "archive.capture_pending": "Archive capture pending",
22
+ "archive.capture_unavailable": "Archive unavailable; original receipt retained",
23
+ "archive.restore_failed": "Native restore unavailable; using saved history",
24
+ "sandboxd.process_exit": "File bridge stopped; restarting",
25
+ "sandboxd.download": "Preparing file bridge",
26
+ "sandboxd.connected": "File bridge connected",
27
+ "sandboxd.disconnected": "File bridge disconnected; reconnecting",
26
28
  };
27
29
  export function formatDiagnostic(event, verbose = false) {
28
30
  const text = messages[event.event] ?? (typeof event.data?.message === "string" ? event.data.message : event.event);
@@ -46,7 +48,7 @@ export function createDiagnosticConsole(verbose = false, write = (line) => proce
46
48
  if (last.size >= 256)
47
49
  last.delete(last.keys().next().value ?? "");
48
50
  last.set(key, { at: now, suppressed: 0 });
49
- const repeated = previous?.suppressed ? ` (+${previous.suppressed} repeated / 重复)` : "";
51
+ const repeated = previous?.suppressed ? ` (+${previous.suppressed} repeated)` : "";
50
52
  write(`${formatDiagnostic(event, verbose).trimEnd()}${repeated}\n`);
51
53
  };
52
54
  }
@@ -56,21 +58,37 @@ export function printRuntimeSummary(summary, json = false, reused = false) {
56
58
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
57
59
  return;
58
60
  }
59
- const label = summary.state === "ready" ? "Runtime ready / Runtime 已就绪" : `Runtime ${summary.state} / Runtime 尚未就绪`;
60
- process.stdout.write(`\n${label}${reused ? " · reused / 已复用" : ""}\n\n`);
61
+ const label = summary.state === "ready" ? "Runtime ready" : `Runtime ${summary.state}`;
62
+ process.stdout.write(`\n${label}${reused ? " · reused" : ""}\n\n`);
61
63
  const rows = [
62
- ["Space / 空间", summary.spaceId],
63
- ["URL / 链接", value.url],
64
- ["Directory / 目录", summary.root],
64
+ ["Space", summary.spaceId],
65
+ ["URL", value.url],
66
+ ["Directory", summary.root],
65
67
  ["Harness", summary.harnesses.join(" · ")],
66
- ["Mode / 模式", summary.background ? "Background / 后台" : "Foreground / 前台"],
68
+ ["Mode", summary.background ? "Background" : "Foreground"],
67
69
  ["PID", String(summary.pid)],
68
- ["Logs / 日志", summary.diagnosticsPath],
70
+ ["Logs", summary.diagnosticsPath],
69
71
  ];
70
72
  for (const [name, text] of rows)
71
73
  process.stdout.write(` ${name} ${text}\n`);
72
74
  process.stdout.write(`\n cohub runtime logs --space ${summary.spaceId} --follow\n cohub runtime down --space ${summary.spaceId}\n`);
73
75
  if (!summary.background && !reused)
74
- process.stdout.write(" Ctrl+C to stop / 按 Ctrl+C 停止\n");
76
+ process.stdout.write(" Ctrl+C to stop\n");
75
77
  process.stdout.write("\n");
76
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
+ }
@@ -1,3 +1,5 @@
1
1
  export declare class ProcessCleanupUncertainError extends Error {
2
2
  }
3
+ /** True only when the process group is gone or holds nothing that can execute. */
4
+ export declare function confirmQuiescentProcessGroup(pid: number): Promise<boolean>;
3
5
  export declare function stopProcessGroup(pid: number): Promise<void>;