@neta-art/cohub-cli 7.0.0 → 7.0.1

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 CHANGED
@@ -79,26 +79,22 @@ cohub -s <spaceId> spaces prompt "message" --json
79
79
  COHUB_SPACE_ID=<spaceId> cohub spaces prompt "message" --json
80
80
  ```
81
81
 
82
- ## Local Runtime / 本地 Runtime
82
+ ## Local Runtime
83
83
 
84
84
  Connect one local workspace to a Space and select Pi or Codex per turn.
85
- 将一个本地工作区连接到 Space,每轮可选择 Pi 或 Codex。
86
85
 
87
86
  ```bash
88
87
  cohub runtime up ./project --harness pi --harness codex
89
88
  cohub runtime up ./project --space <spaceId> --harness codex
90
- cohub -s <spaceId> spaces prompt "Continue / 继续" --harness codex
89
+ cohub -s <spaceId> spaces prompt "Continue" --harness codex
91
90
  cohub runtime status --space <spaceId>
92
91
  ```
93
92
 
94
93
  The Runtime uses WebSockets, native Pi RPC / Codex app-server, and existing cloud
95
94
  streaming. Local executables and credentials are required. `sandbox up` has been
96
95
  removed. Reconnection and result reconciliation are automatic; no recovery command is needed.
97
- See [Runtime details](../../docs/local-runtime.md).
98
-
99
- Runtime 使用 WebSocket、原生 Pi RPC / Codex app-server 和现有云端流式链路。
100
- 需要本机已安装并登录对应程序。Runtime 断线重连和结果核对自动完成,无需恢复命令。
101
- 仅在无法确定结果时,由 Space 页头提供异常确认。旧 `sandbox up` 命令已移除。
96
+ When a result genuinely cannot be determined, the affected Chat offers an explicit stop
97
+ confirmation. See [Runtime details](../../docs/local-runtime.md).
102
98
 
103
99
  ## Chats and prompts
104
100
 
@@ -378,7 +374,7 @@ legacy bare targets. A bare target checks the current Space for a file first, th
378
374
  falls back to the same App references as `apps get`. Showing a preview is
379
375
  idempotent: repeating it re-activates the same tab and refreshes any launch state
380
376
  carried by the reference. With `--call`, the command waits for the App to announce readiness, invokes the method,
381
- and waits for the App to complete the same UI command with `client.ui.reportResult()`.
377
+ and waits for the App to complete the same UI command with `client.desktop.reportResult()`.
382
378
 
383
379
  App authors decide what is callable by registering handlers inside the App:
384
380
 
@@ -508,7 +508,7 @@ export function registerApps(program) {
508
508
  .option("--app-scope <scope>", "Scope granted directly to the app runtime (space.view, session.view, file.view, file.edit, taskrun.view, session.prompt.readonly, session.prompt.fullaccess, command.execute)", collectOption, [])
509
509
  .option("--viewer-scope <scope>", "Deprecated: viewer grants are no longer gated by the app configuration", collectOption, [])
510
510
  .option("--clear-app-scopes", "Clear app runtime scopes")
511
- .option("--clear-viewer-scopes", "Deprecated: clear legacy scope metadata / 已废弃:清除旧权限元数据")
511
+ .option("--clear-viewer-scopes", "Deprecated: clear legacy scope metadata")
512
512
  .option("--meta <json>", "App metadata as a JSON object")
513
513
  .option("--hide-cohub-bar", "Hide the Cohub footer bar on the public app page")
514
514
  .option("--show-cohub-bar", "Show the Cohub footer bar on the public app page")
@@ -728,7 +728,7 @@ export function registerApps(program) {
728
728
  .description("Grant an app scopes as the current user")
729
729
  .requiredOption("--scope <scope>", "Scope to grant (repeatable)", collectOption, [])
730
730
  .option("--space <spaceId>", "Target space; defaults to the app's own space")
731
- .option("--extend", "Add scopes without replacing active grants / 增加权限,保留有效授权")
731
+ .option("--extend", "Add scopes without replacing active grants")
732
732
  .option("--json", "Output as JSON")
733
733
  .action(async (appRef, opts) => {
734
734
  const client = createClient();
@@ -15,20 +15,20 @@ export const resolveLocalSpaceName = (root, name) => name?.trim() || basename(ro
15
15
  export function parseRuntimeHarnesses(values) {
16
16
  const names = values.flatMap((value) => value.split(",")).map((name) => name.trim()).filter(Boolean);
17
17
  if (names.some((name) => !isLocalHarness(name)))
18
- throw new Error("Harness must be pi or codex / Harness 必须是 pi 或 codex");
18
+ throw new Error("Harness must be pi or codex");
19
19
  return [...new Set(names.length ? names : ["pi"])];
20
20
  }
21
21
  export function registerRuntime(program) {
22
- const runtime = program.command("runtime").description("Connect a local workspace / 连接本地工作区");
22
+ const runtime = program.command("runtime").description("Connect a local workspace");
23
23
  runtime.command("up [dir]")
24
- .description("Connect local Harnesses and files / 连接本地 Harness 与文件")
25
- .option("-s, --space <id>", "Target Space / 目标 Space")
26
- .option("-n, --name <name>", "New Space name / 新 Space 名称")
27
- .option("--harness <name>", "Pi or Codex; repeatable / Pi 或 Codex,可重复", (value, previous) => [...previous, value], [])
28
- .option("--pi <path>", "Pi executable / Pi 可执行文件")
29
- .option("--codex <path>", "Codex executable / Codex 可执行文件")
30
- .option("-y, --yes", "Accept local execution access / 同意本机执行权限")
31
- .option("--json", "JSON output / JSON 输出")
24
+ .description("Connect local Harnesses and files")
25
+ .option("-s, --space <id>", "Target Space")
26
+ .option("-n, --name <name>", "New Space name")
27
+ .option("--harness <name>", "Pi or Codex; repeatable", (value, previous) => [...previous, value], [])
28
+ .option("--pi <path>", "Pi executable")
29
+ .option("--codex <path>", "Codex executable")
30
+ .option("-y, --yes", "Accept local execution access")
31
+ .option("--json", "JSON output")
32
32
  .action(async (dir, options) => {
33
33
  const controller = new AbortController();
34
34
  const stop = () => controller.abort();
@@ -37,14 +37,14 @@ export function registerRuntime(program) {
37
37
  try {
38
38
  const root = resolve(dir ?? process.cwd());
39
39
  if (!(await stat(root)).isDirectory())
40
- throw new Error("Workspace is not a directory / 工作区不是目录");
40
+ throw new Error("Workspace is not a directory");
41
41
  const harnesses = parseRuntimeHarnesses(options.harness);
42
42
  if (!options.yes) {
43
43
  if (!process.stdin.isTTY)
44
- throw new Error("Use --yes to authorize local execution / 请使用 --yes 授权本机执行");
44
+ throw new Error("Use --yes to authorize local execution");
45
45
  const rl = createInterface({ input: process.stdin, output: process.stderr });
46
46
  try {
47
- const answer = await rl.question(`Connect ${root}? Space collaborators can run commands as your OS user, beyond this folder.\n连接此目录?Space 协作者可使用当前系统用户执行命令,权限不限于此目录。 [y/N] `);
47
+ const answer = await rl.question(`Connect ${root}? Space collaborators can run commands as your OS user, beyond this folder. [y/N] `);
48
48
  if (!/^y(es)?$/i.test(answer.trim()))
49
49
  return;
50
50
  }
@@ -58,7 +58,7 @@ export function registerRuntime(program) {
58
58
  const spaceId = requested || (await client.spaces.create({ name: resolveLocalSpaceName(root, options.name), config: { sandbox: { provider: "local" } } })).space.id;
59
59
  const sandbox = (await client.space(spaceId).sandbox.get()).sandbox;
60
60
  if (sandbox?.provider !== "local")
61
- throw new Error("Space does not have a local Runtime / Space 不是本地 Runtime");
61
+ throw new Error("Space does not have a local Runtime");
62
62
  const binary = await ensureSandboxdBinary();
63
63
  const wsBase = resolveWebsocketUrl({ url: process.env.COHUB_WS_URL });
64
64
  const url = new URL(wsBase);
@@ -83,7 +83,7 @@ export function registerRuntime(program) {
83
83
  if (jsonRequested(options))
84
84
  outJson({ spaceId, root, harnesses, url: webUrl });
85
85
  else
86
- console.error(`Runtime connected / Runtime 已连接: ${webUrl}`);
86
+ console.error(`Runtime connected: ${webUrl}`);
87
87
  },
88
88
  });
89
89
  }
@@ -99,14 +99,14 @@ export function registerRuntime(program) {
99
99
  }
100
100
  catch (cause) {
101
101
  if (!controller.signal.aborted)
102
- error("Runtime failed / Runtime 失败", cause instanceof Error ? cause.message : String(cause));
102
+ error("Runtime failed", cause instanceof Error ? cause.message : String(cause));
103
103
  }
104
104
  finally {
105
105
  process.removeListener("SIGINT", stop);
106
106
  process.removeListener("SIGTERM", stop);
107
107
  }
108
108
  });
109
- runtime.command("status").description("Runtime status / Runtime 状态").option("-s, --space <id>", "Target Space / 目标 Space").action(async (options) => {
109
+ runtime.command("status").description("Runtime status").option("-s, --space <id>", "Target Space").action(async (options) => {
110
110
  const spaceId = options.space?.trim() || await resolveSpace(program);
111
111
  const { archives } = new RuntimeSessionStore(spaceId);
112
112
  const [status, pendingLocalArchives, failedLocalArchives] = await Promise.all([
@@ -110,7 +110,7 @@ export class RuntimeArchiveStore {
110
110
  const saved = await this.readIndex(this.version(turnId));
111
111
  if (saved) {
112
112
  if (saved.sessionId !== state.sessionId || saved.harness !== state.harness)
113
- throw new Error("Archive identity mismatch / 归档身份不匹配");
113
+ throw new Error("Archive identity mismatch");
114
114
  const committed = await stat(join(this.root, "ready", `${turnId}.json`)).catch((error) => { if (missing(error))
115
115
  return null; throw error; });
116
116
  if (!committed)
@@ -125,7 +125,7 @@ export class RuntimeArchiveStore {
125
125
  try {
126
126
  const before = await file.stat();
127
127
  if (!before.isFile() || !before.size)
128
- throw new Error("Native archive is empty / 原生归档为空");
128
+ throw new Error("Native archive is empty");
129
129
  const buffer = Buffer.alloc(RUNTIME_ARCHIVE_SEGMENT_BYTES);
130
130
  let offset = 0;
131
131
  let digest = createHash("sha256");
@@ -135,7 +135,7 @@ export class RuntimeArchiveStore {
135
135
  while (offset < previous.sizeBytes) {
136
136
  const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, previous.sizeBytes - offset), offset);
137
137
  if (!bytesRead)
138
- throw new Error("Native file changed during capture / 归档时文件发生变化");
138
+ throw new Error("Native file changed during capture");
139
139
  digest.update(buffer.subarray(0, bytesRead));
140
140
  offset += bytesRead;
141
141
  }
@@ -151,7 +151,7 @@ export class RuntimeArchiveStore {
151
151
  while (offset < before.size) {
152
152
  const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, before.size - offset), offset);
153
153
  if (!bytesRead)
154
- throw new Error("Native file changed during capture / 归档时文件发生变化");
154
+ throw new Error("Native file changed during capture");
155
155
  const bytes = buffer.subarray(0, bytesRead);
156
156
  digest.update(bytes);
157
157
  const segment = { offset, sizeBytes: bytesRead, sha256: hash(bytes), md5: hash(bytes, "md5") };
@@ -161,7 +161,7 @@ export class RuntimeArchiveStore {
161
161
  }
162
162
  const after = await stat(state.path);
163
163
  if (before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs)
164
- throw new Error("Native file changed during capture / 归档时文件发生变化");
164
+ throw new Error("Native file changed during capture");
165
165
  if (process.platform !== "win32") {
166
166
  const directory = await open(join(this.root, "objects"), "r");
167
167
  try {
@@ -219,7 +219,7 @@ export class RuntimeArchiveStore {
219
219
  queue.push(index);
220
220
  }
221
221
  if (pending.size && !queue.length)
222
- throw new Error("Cyclic archive outbox / 归档队列引用循环");
222
+ throw new Error("Cyclic archive outbox");
223
223
  for (let cursor = 0; cursor < queue.length; cursor++) {
224
224
  signal.throwIfAborted();
225
225
  const index = queue[cursor];
@@ -228,19 +228,19 @@ export class RuntimeArchiveStore {
228
228
  try {
229
229
  const { uploads } = await transport.prepareRuntimeArchive(index, { signal: AbortSignal.any([signal, AbortSignal.timeout(30_000)]) });
230
230
  if (uploads.length > index.segments.length)
231
- throw new Error("Upload plan mismatch / 上传计划不匹配");
231
+ throw new Error("Upload plan mismatch");
232
232
  const expected = new Set(index.segments.map((segment) => JSON.stringify(segment)));
233
233
  for (const { segment, uploadUrl, headers } of uploads) {
234
234
  if (!expected.has(JSON.stringify(segment)))
235
- throw new Error("Upload segment mismatch / 上传分段不匹配");
235
+ throw new Error("Upload segment mismatch");
236
236
  signal.throwIfAborted();
237
237
  const bytes = await readFile(this.blob(segment.sha256));
238
238
  if (bytes.length !== segment.sizeBytes || hash(bytes) !== segment.sha256)
239
- throw new Error("Local archive segment is corrupt / 本地归档分段已损坏");
239
+ throw new Error("Local archive segment is corrupt");
240
240
  const response = await (transport.fetchObject ?? fetch)(uploadUrl, { method: "PUT", headers, body: bytes, redirect: "error", signal: AbortSignal.any([signal, AbortSignal.timeout(60_000)]) });
241
241
  // An immutable segment may already exist after a lost acknowledgement. Commit verifies it.
242
242
  if (!response.ok && ![409, 412].includes(response.status))
243
- throw new Error(`Archive upload failed / 归档上传失败: ${response.status}`);
243
+ throw new Error(`Archive upload failed: ${response.status}`);
244
244
  }
245
245
  await transport.commitRuntimeArchive(index, { signal: AbortSignal.any([signal, AbortSignal.timeout(30_000)]) });
246
246
  await atomicRuntimeJson(join(this.root, "ready", `${index.turnId}.json`), { turnId: index.turnId, sha256: index.sha256 });
@@ -249,13 +249,13 @@ export class RuntimeArchiveStore {
249
249
  }
250
250
  catch (error) {
251
251
  if (!signal.aborted)
252
- console.error("Archive pending; native segments retained / 归档待重试,原始分段已保留:", error);
252
+ console.error("Archive pending; native segments retained:", error);
253
253
  }
254
254
  }
255
255
  }
256
256
  async restore(reference, target, signal) {
257
257
  if (!this.transport)
258
- throw new Error("Archive transport unavailable / 归档传输不可用");
258
+ throw new Error("Archive transport unavailable");
259
259
  const timeout = (ms) => signal ? AbortSignal.any([signal, AbortSignal.timeout(ms)]) : AbortSignal.timeout(ms);
260
260
  const pages = [];
261
261
  const visited = new Set();
@@ -263,18 +263,18 @@ export class RuntimeArchiveStore {
263
263
  while (turnId) {
264
264
  signal?.throwIfAborted();
265
265
  if (visited.has(turnId))
266
- throw new Error("Cyclic archive / 归档引用循环");
266
+ throw new Error("Cyclic archive");
267
267
  visited.add(turnId);
268
268
  const page = await this.transport.getRuntimeArchive(reference.sessionId, turnId, { signal: timeout(30_000) });
269
269
  const index = harnessArchiveIndexSchema.parse(page.index);
270
270
  if (index.turnId !== turnId || index.sessionId !== reference.sessionId || index.harness !== reference.harness)
271
- throw new Error("Archive identity mismatch / 归档身份不匹配");
271
+ throw new Error("Archive identity mismatch");
272
272
  pages.push({ ...page, index });
273
273
  turnId = index.parentTurnId;
274
274
  }
275
275
  const head = pages[0]?.index;
276
276
  if (!head)
277
- throw new Error("Archive missing / 归档不存在");
277
+ throw new Error("Archive missing");
278
278
  await mkdir(dirname(target), { recursive: true, mode: 0o700 });
279
279
  const temporary = `${target}.${randomUUID()}.restoring`;
280
280
  const file = await open(temporary, "wx", 0o600);
@@ -284,53 +284,53 @@ export class RuntimeArchiveStore {
284
284
  for (const page of pages.reverse()) {
285
285
  validateArchiveBoundary(page.index, parent);
286
286
  if (page.segments.length !== page.index.segments.length)
287
- throw new Error("Missing archive segments / 归档分段缺失");
287
+ throw new Error("Missing archive segments");
288
288
  for (const [ordinal, expected] of page.index.segments.entries()) {
289
289
  signal?.throwIfAborted();
290
290
  const cached = await readFile(this.blob(expected.sha256)).catch((error) => { if (missing(error))
291
291
  return null; throw error; });
292
292
  if (cached) {
293
293
  if (cached.length !== expected.sizeBytes || hash(cached) !== expected.sha256)
294
- throw new Error("Cached archive segment is corrupt / 缓存归档分段已损坏");
294
+ throw new Error("Cached archive segment is corrupt");
295
295
  digest.update(cached);
296
296
  await file.writeFile(cached);
297
297
  continue;
298
298
  }
299
299
  let link = page.segments[ordinal];
300
300
  if (!link || JSON.stringify(link.segment) !== JSON.stringify(expected))
301
- throw new Error("Archive segment identity mismatch / 归档分段标识不匹配");
301
+ throw new Error("Archive segment identity mismatch");
302
302
  let response = await (this.transport.fetchObject ?? fetch)(link.downloadUrl, { redirect: "error", signal: timeout(60_000) });
303
303
  if ([401, 403].includes(response.status)) {
304
304
  const refreshed = await this.transport.getRuntimeArchive(reference.sessionId, page.index.turnId, { signal: timeout(30_000) });
305
305
  link = refreshed.segments[ordinal];
306
306
  if (!link || JSON.stringify(link.segment) !== JSON.stringify(expected))
307
- throw new Error("Archive segment missing / 归档分段缺失");
307
+ throw new Error("Archive segment missing");
308
308
  response = await (this.transport.fetchObject ?? fetch)(link.downloadUrl, { redirect: "error", signal: timeout(60_000) });
309
309
  }
310
310
  if (!response.ok || !response.body)
311
- throw new Error(`Archive download failed / 归档下载失败: ${response.status}`);
311
+ throw new Error(`Archive download failed: ${response.status}`);
312
312
  const segmentHash = createHash("sha256");
313
313
  let size = 0;
314
314
  const chunks = [];
315
315
  for await (const chunk of response.body) {
316
316
  size += chunk.length;
317
317
  if (size > expected.sizeBytes)
318
- throw new Error("Archive size mismatch / 归档大小不匹配");
318
+ throw new Error("Archive size mismatch");
319
319
  chunks.push(chunk);
320
320
  segmentHash.update(chunk);
321
321
  digest.update(chunk);
322
322
  await file.writeFile(chunk);
323
323
  }
324
324
  if (size !== expected.sizeBytes || segmentHash.digest("hex") !== expected.sha256)
325
- throw new Error("Archive checksum mismatch / 归档校验失败");
325
+ throw new Error("Archive checksum mismatch");
326
326
  await this.saveBlob(expected.sha256, Buffer.concat(chunks));
327
327
  }
328
328
  if (digest.copy().digest("hex") !== page.index.sha256)
329
- throw new Error("Archive version checksum mismatch / 归档版本校验失败");
329
+ throw new Error("Archive version checksum mismatch");
330
330
  parent = page.index;
331
331
  }
332
332
  if ((await file.stat()).size !== head.sizeBytes)
333
- throw new Error("Archive length mismatch / 归档长度不匹配");
333
+ throw new Error("Archive length mismatch");
334
334
  await file.sync();
335
335
  await file.close();
336
336
  await link(temporary, target);
@@ -10,7 +10,7 @@ export async function serveRuntime(options) {
10
10
  const uploadSignal = AbortSignal.any([options.signal, uploads.signal]);
11
11
  const flush = () => options.store.flushArchives(uploadSignal).catch((error) => {
12
12
  if (!uploadSignal.aborted)
13
- console.error("Archive pending / 归档待重试:", error);
13
+ console.error("Archive pending:", error);
14
14
  });
15
15
  const timer = setInterval(() => { void flush(); }, 10_000);
16
16
  void flush();
@@ -20,11 +20,11 @@ export async function serveRuntime(options) {
20
20
  if (options.signal.aborted)
21
21
  return;
22
22
  if (outcome === "fatal")
23
- throw new Error("Runtime connection rejected / Runtime 连接被拒绝");
23
+ throw new Error("Runtime connection rejected");
24
24
  if (outcome === "conflict") {
25
25
  conflictSince ??= Date.now();
26
26
  if (Date.now() - conflictSince >= (options.leaseConflictTimeoutMs ?? 90_000))
27
- throw new Error("Space is already connected to another Runtime / Space 已连接其他 Runtime");
27
+ throw new Error("Space is already connected to another Runtime");
28
28
  }
29
29
  await delay(backoff, undefined, { signal: options.signal }).catch(() => undefined);
30
30
  backoff = Math.min(10_000, backoff * 2);
@@ -37,7 +37,7 @@ export async function serveRuntime(options) {
37
37
  }
38
38
  }
39
39
  async function connect(options) {
40
- let token = await options.token();
40
+ let currentToken = await options.token();
41
41
  const socket = new WebSocket(options.url);
42
42
  const active = new Map();
43
43
  const seen = new Set();
@@ -69,9 +69,9 @@ async function connect(options) {
69
69
  catch {
70
70
  stop();
71
71
  }
72
- void options.token().then((next) => { if (next !== token) {
72
+ void options.token().then((next) => { if (next !== currentToken) {
73
73
  send({ type: "runtime.auth", token: next });
74
- token = next;
74
+ currentToken = next;
75
75
  } }).catch(stop);
76
76
  }
77
77
  }, 10_000);
@@ -90,7 +90,7 @@ async function connect(options) {
90
90
  socket.addEventListener("error", () => socket.close());
91
91
  socket.addEventListener("open", () => {
92
92
  try {
93
- send({ type: "runtime.hello", version: RUNTIME_PROTOCOL_VERSION, spaceId: options.spaceId, token, capabilities: options.capabilities });
93
+ send({ type: "runtime.hello", version: RUNTIME_PROTOCOL_VERSION, spaceId: options.spaceId, token: currentToken, capabilities: options.capabilities });
94
94
  }
95
95
  catch {
96
96
  stop();
@@ -108,6 +108,9 @@ async function connect(options) {
108
108
  connectionId = frame.connectionId;
109
109
  clearTimeout(readyTimer);
110
110
  options.onReady();
111
+ for await (const executions of options.store.pendingExecutionBatches()) {
112
+ send({ type: "runtime.recovery", executions });
113
+ }
111
114
  return;
112
115
  }
113
116
  if (!connectionId)
@@ -139,7 +142,7 @@ async function connect(options) {
139
142
  catch (error) {
140
143
  console.error("Runtime acknowledgement failed; result retained:", error);
141
144
  active.delete(frame.requestId);
142
- send({ type: "runtime.event", requestId: frame.requestId, event: { type: "turn.error", message: "Local acknowledgement failed; result retained / 本地确认失败,结果已保留" } });
145
+ send({ type: "runtime.event", requestId: frame.requestId, event: { type: "turn.error", message: "Local acknowledgement failed; result retained" } });
143
146
  return;
144
147
  }
145
148
  active.delete(frame.requestId);
@@ -184,7 +187,7 @@ async function connect(options) {
184
187
  if (!recovery.controller.signal.aborted) {
185
188
  console.error("Runtime recovery failed; original files retained:", error);
186
189
  try {
187
- send({ type: "runtime.event", requestId: frame.requestId, event: { type: "turn.error", uncertain: true, message: "Result unavailable; files retained / 结果不可用,原始文件已保留" } });
190
+ send({ type: "runtime.event", requestId: frame.requestId, event: { type: "turn.error", uncertain: true, message: "Result unavailable; files retained" } });
188
191
  }
189
192
  catch { /* The next connection can read the same result. */ }
190
193
  }
@@ -240,7 +243,7 @@ async function connect(options) {
240
243
  active.delete(requestId);
241
244
  }
242
245
  if ([...active.values()].some((entry) => entry.sessionId === frame.input.sessionId) || active.size >= 8) {
243
- send({ type: "runtime.event", requestId: frame.requestId, event: { type: "turn.error", message: "Local Runtime is busy / 本地 Runtime 繁忙" } });
246
+ send({ type: "runtime.event", requestId: frame.requestId, event: { type: "turn.error", message: "Local Runtime is busy" } });
244
247
  return;
245
248
  }
246
249
  seen.add(frame.requestId);
@@ -28,13 +28,13 @@ export async function importNativeArchive(input) {
28
28
  const header = JSON.parse(Buffer.concat(fragments).toString("utf8"));
29
29
  if (input.harness === "pi") {
30
30
  if (header?.type !== "session" || header.id !== input.nativeSessionId)
31
- throw new Error("Pi archive identity mismatch / Pi 归档身份不匹配");
31
+ throw new Error("Pi archive identity mismatch");
32
32
  header.cwd = input.cwd;
33
33
  delete header.parentSession;
34
34
  }
35
35
  else {
36
36
  if (header?.type !== "session_meta" || header.payload?.id !== input.nativeSessionId)
37
- throw new Error("Codex archive identity mismatch / Codex 归档身份不匹配");
37
+ throw new Error("Codex archive identity mismatch");
38
38
  header.payload.id = input.id;
39
39
  if (header.payload.session_id != null)
40
40
  header.payload.session_id = input.id;
@@ -56,7 +56,7 @@ export async function importNativeArchive(input) {
56
56
  const prefix = newline < 0 ? bytes : bytes.subarray(0, newline);
57
57
  headerBytes += prefix.length;
58
58
  if (headerBytes > RUNTIME_MAX_FRAME_BYTES)
59
- throw new Error("Native header is too large / 原生文件头过大");
59
+ throw new Error("Native header is too large");
60
60
  fragments.push(prefix);
61
61
  if (newline >= 0) {
62
62
  await writeHeader();
@@ -63,6 +63,6 @@ export async function stopProcessGroup(pid) {
63
63
  }
64
64
  }
65
65
  catch (cause) {
66
- throw new ProcessCleanupUncertainError("Tool process cleanup could not be confirmed; execution remains unresolved / 无法确认工具进程已停止,执行结果保持未确认", { cause });
66
+ throw new ProcessCleanupUncertainError("Tool process cleanup could not be confirmed; execution remains unresolved", { cause });
67
67
  }
68
68
  }
@@ -1,4 +1,4 @@
1
- import { type RuntimeExecutionEvent, type HarnessArchive, type RuntimeTurnInput } from "@neta-art/cohub";
1
+ import { type RuntimeExecutionEvent, type HarnessArchive, type RuntimePendingExecution, type RuntimeTurnInput } from "@neta-art/cohub";
2
2
  import { RuntimeArchiveStore, type ArchiveTransport } from "./archive-store.js";
3
3
  import type { CodexTokenTotals } from "./codex-usage.js";
4
4
  export declare class ContextRequiredError extends Error {
@@ -27,6 +27,7 @@ export declare class RuntimeSessionStore {
27
27
  private archiveFlush;
28
28
  constructor(spaceId: string, stateRoot?: string, transport?: ArchiveTransport);
29
29
  private statePath;
30
+ pendingExecutionBatches(): AsyncGenerator<RuntimePendingExecution[]>;
30
31
  flushArchives(signal: AbortSignal): Promise<void>;
31
32
  private flushArchiveOutbox;
32
33
  pendingTurnIds(sessionId: string): Promise<string[]>;
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { mkdir, open, readFile, readdir, rm } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
- import { contextToPiMessages, selectRuntimeContextMessages, runtimeEventSchema } from "@neta-art/cohub";
5
+ import { contextToPiMessages, RUNTIME_RECOVERY_BATCH_SIZE, selectRuntimeContextMessages, runtimeEventSchema } from "@neta-art/cohub";
6
6
  import { RuntimeArchiveStore, checksumNativeFile, atomicRuntimeJson as atomicJson } from "./archive-store.js";
7
7
  import { importNativeArchive, readCodexArchiveTotals } from "./native-archive.js";
8
8
  export class ContextRequiredError extends Error {
@@ -26,6 +26,33 @@ export class RuntimeSessionStore {
26
26
  this.archives = new RuntimeArchiveStore(join(this.root, "archives"), transport);
27
27
  }
28
28
  statePath(input) { return join(this.root, input.harness, `${input.sessionId}.json`); }
29
+ async *pendingExecutionBatches() {
30
+ let batch = [];
31
+ for (const harness of ["pi", "codex"]) {
32
+ const directory = join(this.root, harness);
33
+ const names = await readdir(directory).catch((error) => { if (missing(error))
34
+ return []; throw error; });
35
+ for (const name of names) {
36
+ if (!name.endsWith(".json"))
37
+ continue;
38
+ try {
39
+ const state = JSON.parse(await readFile(join(directory, name), "utf8"));
40
+ if (state.version !== 1 || state.harness !== harness || !state.sessionId || !state.pendingTurnId)
41
+ continue;
42
+ batch.push({ sessionId: state.sessionId, turnId: state.pendingTurnId, harness });
43
+ if (batch.length >= RUNTIME_RECOVERY_BATCH_SIZE) {
44
+ yield batch;
45
+ batch = [];
46
+ }
47
+ }
48
+ catch (error) {
49
+ console.error(`Runtime session state unreadable: ${join(directory, name)}`, error);
50
+ }
51
+ }
52
+ }
53
+ if (batch.length)
54
+ yield batch;
55
+ }
29
56
  async flushArchives(signal) {
30
57
  this.archiveFlush ??= this.flushArchiveOutbox(signal).finally(() => { this.archiveFlush = null; });
31
58
  return this.archiveFlush;
@@ -46,20 +73,20 @@ export class RuntimeSessionStore {
46
73
  state = JSON.parse(receipt);
47
74
  }
48
75
  catch {
49
- throw new CaptureUnavailableError("Invalid capture receipt / 归档捕获记录无效");
76
+ throw new CaptureUnavailableError("Invalid capture receipt");
50
77
  }
51
78
  const turnId = state?.archivePendingTurnId;
52
79
  if (typeof turnId !== "string" || typeof state?.path !== "string" || typeof state.resultChecksum !== "string") {
53
- throw new CaptureUnavailableError("Invalid capture receipt / 归档捕获记录无效");
80
+ throw new CaptureUnavailableError("Invalid capture receipt");
54
81
  }
55
82
  if (!await this.archives.hasCapture(turnId)) {
56
83
  const digest = await checksumNativeFile(state.path).catch((error) => {
57
84
  if (missing(error))
58
- throw new CaptureUnavailableError("Native session missing / 原生会话文件不存在");
85
+ throw new CaptureUnavailableError("Native session missing");
59
86
  throw error;
60
87
  });
61
88
  if (digest !== state.resultChecksum)
62
- throw new CaptureUnavailableError("Native session changed; original files retained / 原生会话已变化,原文件已保留");
89
+ throw new CaptureUnavailableError("Native session changed; original files retained");
63
90
  signal.throwIfAborted();
64
91
  await this.archives.stage(state, turnId);
65
92
  }
@@ -73,10 +100,10 @@ export class RuntimeSessionStore {
73
100
  receipt, reason: error.message, failedAt: new Date().toISOString(),
74
101
  });
75
102
  await rm(join(captures, name), { force: true });
76
- console.error("Archive capture unavailable; receipt retained / 归档捕获不可恢复,记录已保留:", error.message);
103
+ console.error("Archive capture unavailable; receipt retained:", error.message);
77
104
  }
78
105
  else
79
- console.error("Archive capture pending / 归档捕获待重试:", error);
106
+ console.error("Archive capture pending:", error);
80
107
  }
81
108
  }
82
109
  await this.archives.flush(signal);
@@ -182,7 +209,7 @@ export class RuntimeSessionStore {
182
209
  }
183
210
  catch (error) {
184
211
  signal?.throwIfAborted();
185
- console.error("Native archive unavailable; rebuilding from durable history / 原生归档不可用,将从持久历史重建:", error);
212
+ console.error("Native archive unavailable; rebuilding from durable history:", error);
186
213
  }
187
214
  }
188
215
  if (input.context.complete === false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "7.0.0",
3
+ "version": "7.0.1",
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.18.0"
25
+ "@neta-art/cohub": "8.19.0"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"