@neta-art/cohub-cli 10.8.2 → 10.8.3

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.
@@ -32153,6 +32153,10 @@ async function loadCanvasModule() {
32153
32153
  throw new Error("Board image export needs the optional '@napi-rs/canvas' package. Install it with: npm i @napi-rs/canvas", { cause });
32154
32154
  }
32155
32155
  }
32156
+ function installDOMMatrix(canvasModule) {
32157
+ const globals = globalThis;
32158
+ globals.DOMMatrix ??= canvasModule.DOMMatrix;
32159
+ }
32156
32160
  function installAnimationFrameShim() {
32157
32161
  const globals = globalThis;
32158
32162
  if (globals.requestAnimationFrame) return () => {};
@@ -32207,6 +32211,7 @@ async function createNodeBoardRenderer(options = {}) {
32207
32211
  throw new Error("XML parsing is not available in Node board export.");
32208
32212
  }
32209
32213
  });
32214
+ installDOMMatrix(canvasModule);
32210
32215
  installBoardTextMeasurement();
32211
32216
  const restoreAnimationFrame = installAnimationFrameShim();
32212
32217
  const renderer = new CanvasRenderer();
@@ -29,22 +29,21 @@ export async function ensureNativeSync(input, log = (line) => process.stderr.wri
29
29
  return { enabled: false, harnesses: [], codexShared: false, importHistory: false, note: "unsupported" };
30
30
  const stateRoot = runtimeStateRoot(input.spaceId);
31
31
  const previous = await readNativeConfig(stateRoot, input.identity).catch(() => null);
32
- if (previous && nativeSyncSatisfied(previous, input)) {
33
- await maintain(previous, log, input.executables?.pi);
34
- return { enabled: true, harnesses: previous.harnesses, codexShared: previous.codexShared, importHistory: false };
35
- }
32
+ const alreadySynced = previous && nativeSyncSatisfied(previous, input);
36
33
  const interactive = process.stdin.isTTY === true;
37
34
  const confirm = async (question) => input.yes ? true : interactive ? await ask(question) : false;
38
- if (!input.yes && !interactive) {
39
- log("Native sync off · rerun interactively or with --yes to enable\n");
40
- return { enabled: false, harnesses: input.harnesses, codexShared: false, importHistory: false, note: "non-interactive" };
41
- }
42
- const names = input.harnesses.map((harness) => LOCAL_HARNESS_SPECS[harness].label).join(" and ");
43
- if (!await confirm(`Sync ${names} chats in this folder to this Space so they continue on the web? Their history may contain secrets. [Y/n] `)) {
44
- return { enabled: false, harnesses: input.harnesses, codexShared: false, importHistory: false, note: "declined" };
35
+ if (!alreadySynced) {
36
+ if (!input.yes && !interactive) {
37
+ log("Native sync off · rerun interactively or with --yes to enable\n");
38
+ return { enabled: false, harnesses: input.harnesses, codexShared: false, importHistory: false, note: "non-interactive" };
39
+ }
40
+ const names = input.harnesses.map((harness) => LOCAL_HARNESS_SPECS[harness].label).join(" and ");
41
+ if (!await confirm(`Sync ${names} chats in this folder to this Space so they continue on the web? Their history may contain secrets. [Y/n] `)) {
42
+ return { enabled: false, harnesses: input.harnesses, codexShared: false, importHistory: false, note: "declined" };
43
+ }
45
44
  }
46
45
  let codexShared = previous?.codexShared ?? false;
47
- if (input.harnesses.includes("codex") && !codexShared) {
46
+ if (input.harnesses.includes("codex") && !codexShared && (input.yes || interactive)) {
48
47
  const version = await harnessVersion(input.executables?.codex || "codex");
49
48
  if (!sharedServerAvailable) {
50
49
  log("Shared Codex sessions are unavailable on Windows; terminal Codex chats sync read-only\n");
@@ -56,6 +55,16 @@ export async function ensureNativeSync(input, log = (line) => process.stderr.wri
56
55
  codexShared = await confirm("Let terminal Codex and the web drive the same live sessions? Cohub starts Codex's shared app-server, which keeps running in the background. [Y/n] ");
57
56
  }
58
57
  }
58
+ if (previous && alreadySynced) {
59
+ if (codexShared !== previous.codexShared) {
60
+ await writeNativeConfig(stateRoot, { ...previous, codexShared }).catch((error) => {
61
+ log(`Shared Codex sessions unavailable; Runtime continues without them — ${serializeDiagnosticError(error).message}\n`);
62
+ codexShared = previous.codexShared;
63
+ });
64
+ }
65
+ await maintain(previous, log, input.executables?.pi);
66
+ return { enabled: true, harnesses: previous.harnesses, codexShared, importHistory: false };
67
+ }
59
68
  const harnesses = [...new Set([...(previous?.root === input.root ? previous.harnesses : []), ...input.harnesses])];
60
69
  const config = {
61
70
  version: 2, identity: input.identity, spaceId: input.spaceId, root: input.root, harnesses,
@@ -158,11 +158,22 @@ async function scanCodexRollouts(signal) {
158
158
  codexRolloutIndexCache = { at: Date.now(), home, index };
159
159
  return index;
160
160
  }
161
+ async function* compressedCodexRolloutBytes(path, signal) {
162
+ const decoder = createZstdDecompress();
163
+ // pipeline keeps error listeners attached, so stopping at a header or prefix cannot crash with an unhandled decoder error.
164
+ const pumping = pipeline(createReadStream(path), decoder, { signal }).catch(() => { });
165
+ try {
166
+ yield* decoder;
167
+ }
168
+ finally {
169
+ decoder.destroy();
170
+ await pumping;
171
+ }
172
+ }
161
173
  /** Raw JSONL bytes of a rollout, transparently decompressing `.zst` and honoring a lineage cutoff. */
162
174
  export async function* codexRolloutBytes(path, endBytes, signal) {
163
- // compose (not pipe) so a source error propagates and early exit destroys both streams.
164
175
  const stream = path.endsWith(".zst")
165
- ? createReadStream(path, { signal }).compose(createZstdDecompress())
176
+ ? compressedCodexRolloutBytes(path, signal)
166
177
  : createReadStream(path, endBytes != null ? { end: endBytes - 1, signal } : { signal });
167
178
  let total = 0, last = -1;
168
179
  for await (const chunk of stream) {
@@ -522,9 +533,10 @@ function parseCodexLines(lines, segment) {
522
533
  calls.get(id)?.content.push({ type: "tool_result", tool_use_id: id, content: typeof payload.output === "string" ? payload.output : JSON.stringify(payload.output ?? null) });
523
534
  }
524
535
  }
536
+ // Cohub tags a Turn it starts with its own cloud Turn id (`client_id`), so the file itself records
537
+ // which Turns already belong to a cloud Turn and must not be ingested twice. Older Codex writes it
538
+ // on `item_completed`'s UserMessage, newer Codex on `user_message`.
525
539
  if (entry.type === "event_msg" && payload.type === "item_completed") {
526
- // Cohub tags a Turn it starts with its own cloud Turn id, so the file itself records which
527
- // Turns already belong to a cloud Turn (and must not be ingested twice).
528
540
  const item = record(payload.item);
529
541
  if (item.type === "UserMessage") {
530
542
  const clientId = text(item.client_id);
@@ -540,6 +552,9 @@ function parseCodexLines(lines, segment) {
540
552
  }
541
553
  }
542
554
  if (entry.type === "event_msg" && payload.type === "user_message") {
555
+ const clientId = text(payload.client_id);
556
+ if (clientId)
557
+ current.cloudTurnId = clientId;
543
558
  if (!userFromResponse)
544
559
  current.userContent = [{ type: "text", text: text(payload.message) }];
545
560
  userFromResponse = true;
@@ -152,11 +152,23 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
152
152
  });
153
153
  bridge.stdin?.on("error", () => undefined);
154
154
  const closed = new Promise((resolve) => {
155
+ let drainTimer;
155
156
  bridge.once("error", (error) => diagnostics.log("error", "sandboxd.process_error", { error: serializeDiagnosticError(error) }));
156
- bridge.once("close", (code, exitSignal) => {
157
+ bridge.once("exit", (code, exitSignal) => {
157
158
  childStopped = true;
158
159
  if (!signal.aborted)
159
160
  diagnostics.log("warn", "sandboxd.process_exit", { code, signal: exitSignal });
161
+ // Descendants may still hold inherited pipes. After draining buffered
162
+ // output, close only our ends so supervision can continue.
163
+ drainTimer = setTimeout(() => {
164
+ diagnostics.log("warn", "sandboxd.stdio_drain_timeout");
165
+ for (const stream of bridge.stdio)
166
+ stream?.destroy();
167
+ }, 1000);
168
+ });
169
+ bridge.once("close", () => {
170
+ childStopped = true;
171
+ clearTimeout(drainTimer);
160
172
  resolve();
161
173
  });
162
174
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "10.8.2",
3
+ "version": "10.8.3",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -18,7 +18,7 @@
18
18
  "NOTICE"
19
19
  ],
20
20
  "dependencies": {
21
- "@neta-art/cohub": "12.1.0",
21
+ "@neta-art/cohub": "12.1.1",
22
22
  "@neta-art/generation": "^0.1.40",
23
23
  "commander": "^15.0.0",
24
24
  "sharp": "^0.35.4",