@neta-art/cohub-cli 8.1.1 → 8.3.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 (80) hide show
  1. package/dist/commands/runtime.d.ts +0 -12
  2. package/dist/commands/runtime.js +129 -119
  3. package/dist/commands/search.js +3 -9
  4. package/dist/commands/spaces.js +5 -2
  5. package/dist/output.d.ts +6 -0
  6. package/dist/output.js +11 -0
  7. package/dist/runtime/archive-store.d.ts +4 -0
  8. package/dist/runtime/archive-store.js +10 -6
  9. package/dist/runtime/codex-usage.d.ts +0 -2
  10. package/dist/runtime/codex-usage.js +0 -12
  11. package/dist/runtime/connection.d.ts +5 -5
  12. package/dist/runtime/connection.js +27 -16
  13. package/dist/runtime/harness.d.ts +8 -16
  14. package/dist/runtime/harness.js +1 -366
  15. package/dist/runtime/instance.d.ts +8 -1
  16. package/dist/runtime/instance.js +15 -8
  17. package/dist/runtime/launch.js +10 -5
  18. package/dist/runtime/native/adapters.d.ts +32 -0
  19. package/dist/runtime/native/adapters.js +79 -0
  20. package/dist/runtime/{native-archive.d.ts → native/archive.d.ts} +4 -3
  21. package/dist/runtime/{native-archive.js → native/archive.js} +7 -14
  22. package/dist/runtime/native/attach.d.ts +33 -0
  23. package/dist/runtime/native/attach.js +103 -0
  24. package/dist/runtime/native/codex-link.d.ts +40 -0
  25. package/dist/runtime/native/codex-link.js +249 -0
  26. package/dist/runtime/native/config.d.ts +16 -0
  27. package/dist/runtime/native/config.js +24 -0
  28. package/dist/runtime/native/daemon.d.ts +88 -0
  29. package/dist/runtime/native/daemon.js +367 -0
  30. package/dist/runtime/native/execution.d.ts +45 -0
  31. package/dist/runtime/native/execution.js +337 -0
  32. package/dist/runtime/native/identity.d.ts +9 -0
  33. package/dist/runtime/native/identity.js +15 -0
  34. package/dist/runtime/native/ingest.d.ts +241 -0
  35. package/dist/runtime/native/ingest.js +823 -0
  36. package/dist/runtime/native/install.d.ts +9 -0
  37. package/dist/runtime/native/install.js +105 -0
  38. package/dist/runtime/native/pi-extension.d.ts +35 -0
  39. package/dist/runtime/native/pi-extension.js +160 -0
  40. package/dist/runtime/native/pi-link.d.ts +67 -0
  41. package/dist/runtime/native/pi-link.js +238 -0
  42. package/dist/runtime/native/results.d.ts +41 -0
  43. package/dist/runtime/native/results.js +128 -0
  44. package/dist/runtime/native/sessions.d.ts +45 -0
  45. package/dist/runtime/native/sessions.js +92 -0
  46. package/dist/runtime/native/transcript.d.ts +63 -0
  47. package/dist/runtime/native/transcript.js +594 -0
  48. package/dist/runtime/native/translate.d.ts +44 -0
  49. package/dist/runtime/native/translate.js +263 -0
  50. package/dist/runtime/native/transport.d.ts +7 -0
  51. package/dist/runtime/native/transport.js +16 -0
  52. package/dist/runtime/native/version.d.ts +5 -0
  53. package/dist/runtime/native/version.js +18 -0
  54. package/dist/runtime/presentation.d.ts +7 -10
  55. package/dist/runtime/presentation.js +35 -10
  56. package/dist/runtime/projection-store.d.ts +6 -13
  57. package/dist/runtime/projection-store.js +11 -74
  58. package/dist/runtime/supervisor.d.ts +2 -0
  59. package/dist/runtime/supervisor.js +36 -86
  60. package/dist/runtime/turn-projection.d.ts +1 -3
  61. package/dist/runtime/turn-projection.js +3 -10
  62. package/package.json +4 -5
  63. package/dist/runtime/native-attach.d.ts +0 -28
  64. package/dist/runtime/native-attach.js +0 -47
  65. package/dist/runtime/native-codex-hook.d.ts +0 -1
  66. package/dist/runtime/native-codex-hook.js +0 -28
  67. package/dist/runtime/native-install.d.ts +0 -21
  68. package/dist/runtime/native-install.js +0 -130
  69. package/dist/runtime/native-ipc.d.ts +0 -37
  70. package/dist/runtime/native-ipc.js +0 -118
  71. package/dist/runtime/native-pi-extension.d.ts +0 -20
  72. package/dist/runtime/native-pi-extension.js +0 -47
  73. package/dist/runtime/native-sync-store.d.ts +0 -102
  74. package/dist/runtime/native-sync-store.js +0 -368
  75. package/dist/runtime/native-sync.d.ts +0 -50
  76. package/dist/runtime/native-sync.js +0 -221
  77. package/dist/runtime/native-transcript.d.ts +0 -35
  78. package/dist/runtime/native-transcript.js +0 -310
  79. package/dist/runtime/session-store.d.ts +0 -84
  80. package/dist/runtime/session-store.js +0 -587
@@ -1,15 +1,3 @@
1
1
  import type { Command } from "commander";
2
- import { type NativeIpcResponse } from "../runtime/native-ipc.js";
3
2
  export { resolveLocalSpaceName, parseRuntimeHarnesses } from "../runtime/launch.js";
4
- export declare function applyNativeImportResponse(result: {
5
- imported: number;
6
- failed: Array<{
7
- path: string;
8
- message: string;
9
- }>;
10
- skipped: Array<{
11
- path: string;
12
- message: string;
13
- }>;
14
- }, path: string, response: NativeIpcResponse): void;
15
3
  export declare function registerRuntime(program: Command): void;
@@ -4,28 +4,31 @@ import { json as outJson, jsonRequested } from "../output.js";
4
4
  import { currentIdentityKey } from "../space.js";
5
5
  import { resolveRuntimeTarget, runtimeUp, parseRuntimeHarnesses } from "../runtime/launch.js";
6
6
  import { canonicalRuntimeRoot, getRuntimeSpaceBinding } from "../runtime/space-binding.js";
7
- import { installNativeSync } from "../runtime/native-install.js";
8
- import { discoverNativeImportCandidates, nativeRuntimeRoot, readNativeSyncConfig } from "../runtime/native-sync.js";
9
- import { listNativeSyncStores } from "../runtime/native-sync-store.js";
10
- import { requestNativeDaemon } from "../runtime/native-ipc.js";
11
- import { requestRuntimeInstance, runtimeInstanceDirectory } from "../runtime/instance.js";
12
- import { atLeastLevel, diagnosticLevels, formatDiagnostic, formatNativeSync, printRuntimeSummary } from "../runtime/presentation.js";
13
- import { RuntimeSessionStore } from "../runtime/session-store.js";
7
+ import { join } from "node:path";
8
+ import { RuntimeArchiveStore } from "../runtime/archive-store.js";
9
+ import { readNativeConfig, runtimeStateRoot, writeNativeConfig } from "../runtime/native/config.js";
10
+ import { MAX_IMPORT_CONCURRENCY, DEFAULT_IMPORT_CONCURRENCY } from "../runtime/native/ingest.js";
11
+ import { installPiExtension, piExtensionState } from "../runtime/native/install.js";
12
+ import { controlRuntimeInstance, requestRuntimeInstance, runtimeInstanceDirectory } from "../runtime/instance.js";
13
+ import { atLeastLevel, diagnosticLevels, formatDiagnostic, formatImportProgress, formatNativeSync, printRuntimeSummary } from "../runtime/presentation.js";
14
14
  import { readRuntimeDiagnosticEvents, RuntimeDiagnosticReader, runtimeDiagnosticsDirectory, serializeDiagnosticError } from "../runtime/diagnostics.js";
15
15
  export { resolveLocalSpaceName, parseRuntimeHarnesses } from "../runtime/launch.js";
16
16
  const reportFailure = (cause) => {
17
17
  process.stderr.write(`Runtime failed: ${serializeDiagnosticError(cause).message}\n`);
18
18
  process.exitCode = 1;
19
19
  };
20
- export function applyNativeImportResponse(result, path, response) {
21
- if (!response.ok) {
22
- if (response.skipped) {
23
- result.skipped.push({ path, message: response.message });
24
- return;
25
- }
26
- throw new Error(response.message);
27
- }
28
- result.imported += 1;
20
+ const megabytes = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
21
+ /** The bound Space of a directory, with its running Runtime; import and attach act through it. */
22
+ async function boundRuntime(program, dir, space) {
23
+ const identity = currentIdentityKey();
24
+ if (!identity)
25
+ throw new Error("Sign in first");
26
+ const root = await canonicalRuntimeRoot(dir ?? process.cwd());
27
+ const spaceId = await resolveRuntimeTarget(program, space);
28
+ const binding = await getRuntimeSpaceBinding(root, identity);
29
+ if (!binding || binding.spaceId !== spaceId)
30
+ throw new Error("Bind this directory with runtime up first");
31
+ return { identity, root, spaceId, directory: runtimeInstanceDirectory(identity, spaceId) };
29
32
  }
30
33
  export function registerRuntime(program) {
31
34
  const runtime = program.command("runtime").description("Connect a local workspace");
@@ -49,37 +52,68 @@ export function registerRuntime(program) {
49
52
  reportFailure(cause);
50
53
  }
51
54
  });
52
- runtime.command("detach")
53
- .description("Pause native sync; retain all receipts")
55
+ runtime.command("attach")
56
+ .description("Connect native harnesses to Cohub: install the Pi extension")
54
57
  .option("-s, --space <id>", "Target Space")
55
- .option("--harness <name>", "Pi or Codex; repeatable", (value, previous) => [...previous, value], [])
58
+ .option("--harness <name>", "Pi; repeatable", (value, previous) => [...previous, value], [])
56
59
  .option("--json", "JSON output")
57
60
  .action(async (options) => {
58
61
  try {
59
- const spaceId = await resolveRuntimeTarget(program, options.space);
62
+ const harnesses = parseRuntimeHarnesses(options.harness.length ? options.harness : ["pi"]);
63
+ if (!harnesses.includes("pi"))
64
+ throw new Error("Codex needs no extension; runtime up offers its shared app-server");
65
+ const before = await piExtensionState();
66
+ if (before === "foreign")
67
+ throw new Error("A Pi extension named cohub exists and is not managed by Cohub; move it aside, then retry");
68
+ await installPiExtension();
69
+ const spaceId = await resolveRuntimeTarget(program, options.space).catch(() => null);
60
70
  const identity = currentIdentityKey();
61
- if (!identity)
62
- throw new Error("Sign in first");
63
- const root = await canonicalRuntimeRoot(process.cwd());
64
- if ((await getRuntimeSpaceBinding(root, identity))?.spaceId !== spaceId)
65
- throw new Error("Bind this directory with runtime up first");
66
- const instance = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
67
- const harnesses = parseRuntimeHarnesses(options.harness.length ? options.harness : instance?.harnesses ?? ["pi", "codex"]);
68
- const result = await installNativeSync({ root, spaceId, identity, harnesses, disabled: true });
71
+ if (spaceId && identity)
72
+ await controlRuntimeInstance(runtimeInstanceDirectory(identity, spaceId), "reload").catch(() => undefined);
69
73
  if (jsonRequested(options))
70
- outJson({ ...result, enabled: false });
74
+ outJson({ harness: "pi", extension: "installed", updated: before !== "installed" });
71
75
  else
72
- process.stdout.write("Native sync paused; all local records retained\n");
76
+ process.stdout.write(before === "installed" ? "Pi extension is up to date\n" : "Pi extension installed. Run /reload in open Pi sessions to connect them\n");
77
+ }
78
+ catch (cause) {
79
+ reportFailure(cause);
80
+ }
81
+ });
82
+ runtime.command("detach")
83
+ .description("Pause native sync; keep all data")
84
+ .option("-s, --space <id>", "Target Space")
85
+ .option("--harness <name>", "Pi or Codex; repeatable", (value, previous) => [...previous, value], [])
86
+ .option("--json", "JSON output")
87
+ .action(async (options) => {
88
+ try {
89
+ const { identity, spaceId, directory } = await boundRuntime(program, undefined, options.space);
90
+ const stateRoot = runtimeStateRoot(spaceId);
91
+ const config = await readNativeConfig(stateRoot, identity);
92
+ const paused = parseRuntimeHarnesses(options.harness.length ? options.harness : config?.harnesses ?? ["pi", "codex"]);
93
+ const harnesses = (config?.harnesses ?? []).filter((harness) => !paused.includes(harness));
94
+ if (config)
95
+ await writeNativeConfig(stateRoot, { ...config, harnesses, codexShared: harnesses.includes("codex") && config.codexShared });
96
+ await controlRuntimeInstance(directory, "reload").catch(() => undefined);
97
+ const codexWasShared = Boolean(config?.codexShared) && paused.includes("codex");
98
+ if (jsonRequested(options))
99
+ outJson({ spaceId, harnesses, paused, enabled: harnesses.length > 0 });
100
+ else {
101
+ process.stdout.write(harnesses.length ? `Native sync paused for ${paused.join(", ")}; still syncing ${harnesses.join(", ")}\n` : "Native sync paused; all data retained\n");
102
+ // Cohub never stops a server that terminal clients may be attached to.
103
+ if (codexWasShared)
104
+ process.stdout.write("Codex's shared app-server keeps running; stop it with codex app-server daemon stop\n");
105
+ }
73
106
  }
74
107
  catch (cause) {
75
108
  reportFailure(cause);
76
109
  }
77
110
  });
78
111
  runtime.command("import [dir]")
79
- .description("Import local conversations")
112
+ .description("Import earlier local conversations, newest first")
80
113
  .option("-s, --space <id>", "Target Space")
81
114
  .option("--harness <name>", "Filter by harness; repeatable", (value, previous) => [...previous, value], [])
82
115
  .option("--session <id>", "Filter by native session ID")
116
+ .option("--concurrency <count>", `Conversations read in parallel, 1 to ${MAX_IMPORT_CONCURRENCY}`, String(DEFAULT_IMPORT_CONCURRENCY))
83
117
  .option("--dry-run", "Preview without importing")
84
118
  .option("-y, --yes", "Skip confirmation")
85
119
  .option("--json", "Output as JSON")
@@ -88,66 +122,34 @@ export function registerRuntime(program) {
88
122
  const stop = () => controller.abort();
89
123
  process.once("SIGINT", stop);
90
124
  process.once("SIGTERM", stop);
125
+ const asJson = jsonRequested(options);
91
126
  try {
92
- const identity = currentIdentityKey();
93
- if (!identity)
94
- throw new Error("Sign in first");
95
- const root = await canonicalRuntimeRoot(dir ?? process.cwd());
96
- const spaceId = await resolveRuntimeTarget(program, options.space);
97
- const binding = await getRuntimeSpaceBinding(root, identity);
98
- if (!binding || binding.spaceId !== spaceId)
99
- throw new Error("Bind this directory with runtime up first");
100
- const local = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
101
- if (!local)
102
- throw new Error("Local Runtime is not running");
103
- const config = await readNativeSyncConfig(nativeRuntimeRoot(spaceId), identity);
104
- if (!config || config.root !== root)
105
- throw new Error("Native sync is not enabled for this directory");
106
- const harnesses = parseRuntimeHarnesses(options.harness.length ? options.harness : config.harnesses);
107
- let lastProgress = 0;
108
- const discovered = await discoverNativeImportCandidates(root, harnesses, {
109
- signal: controller.signal,
110
- onProgress: ({ harness, scanned, candidates: found }) => {
111
- if (jsonRequested(options) || scanned - lastProgress < 25)
112
- return;
113
- lastProgress = scanned;
114
- process.stderr.write(`Scanned ${scanned} ${harness} transcript${scanned === 1 ? "" : "s"}; found ${found}\n`);
115
- },
116
- });
117
- const candidates = discovered.candidates.filter((candidate) => !options.session || candidate.nativeSessionId === options.session);
118
- const result = {
119
- spaceId, root, harnesses, candidates, errors: discovered.errors,
120
- imported: 0, failed: [], skipped: [], dryRun: Boolean(options.dryRun),
121
- complete: discovered.errors.length === 0,
127
+ const concurrency = Number(options.concurrency);
128
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > MAX_IMPORT_CONCURRENCY)
129
+ throw new Error(`Use a concurrency from 1 to ${MAX_IMPORT_CONCURRENCY}`);
130
+ const { spaceId, root, directory } = await boundRuntime(program, dir, options.space);
131
+ // The Runtime does the work, so an import survives this command: Ctrl-C only pauses it.
132
+ const filter = { harnesses: options.harness.length ? parseRuntimeHarnesses(options.harness) : [], ...(options.session ? { nativeSessionId: options.session } : {}) };
133
+ const control = async (command, timeoutMs = 60_000) => {
134
+ const response = await controlRuntimeInstance(directory, "import", { ...filter, command, concurrency }, timeoutMs);
135
+ if (!response)
136
+ throw new Error("Local Runtime is not running; start it with cohub runtime up");
137
+ return response.result;
122
138
  };
123
- if (jsonRequested(options) && options.dryRun) {
124
- result.complete = result.complete && result.failed.length === 0;
125
- outJson(result);
126
- if (!result.complete)
127
- process.exitCode = 1;
128
- return;
129
- }
130
- if (!candidates.length) {
131
- if (!result.complete)
132
- process.exitCode = 1;
133
- if (jsonRequested(options))
134
- outJson(result);
135
- else
136
- process.stdout.write("No existing native conversations found\n");
137
- return;
138
- }
139
- if (options.dryRun) {
140
- if (jsonRequested(options))
141
- outJson(result);
139
+ const plan = await control("plan");
140
+ const summary = { spaceId, root, ...plan, dryRun: Boolean(options.dryRun) };
141
+ if (options.dryRun || !plan.files) {
142
+ if (asJson)
143
+ outJson(summary);
144
+ else if (!plan.files)
145
+ process.stdout.write("No earlier conversations left to import\n");
142
146
  else {
143
- process.stdout.write(`Found ${candidates.length} native conversation${candidates.length === 1 ? "" : "s"}\n`);
144
- for (const candidate of candidates)
145
- process.stdout.write(` ${candidate.harness} ${candidate.nativeSessionId} ${candidate.turnCount} Turn${candidate.turnCount === 1 ? "" : "s"} ${candidate.path}\n`);
146
- for (const error of discovered.errors)
147
- process.stdout.write(`Skipped ${error.path}: ${error.message}\n`);
147
+ process.stdout.write(`Found ${plan.files} conversation${plan.files === 1 ? "" : "s"} · ${megabytes(plan.bytes)}\n`);
148
+ for (const item of plan.sample)
149
+ process.stdout.write(` ${item.harness.padEnd(5)} ${new Date(item.mtimeMs).toISOString().slice(0, 16).replace("T", " ")} ${item.nativeSessionId ?? ""} ${item.path}\n`);
150
+ if (plan.files > plan.sample.length)
151
+ process.stdout.write(` … and ${plan.files - plan.sample.length} more\n`);
148
152
  }
149
- if (!result.complete)
150
- process.exitCode = 1;
151
153
  return;
152
154
  }
153
155
  if (!options.yes) {
@@ -156,10 +158,10 @@ export function registerRuntime(program) {
156
158
  const { createInterface } = await import("node:readline/promises");
157
159
  const rl = createInterface({ input: process.stdin, output: process.stderr });
158
160
  try {
159
- const answer = await rl.question(`Import ${candidates.length} native conversation${candidates.length === 1 ? "" : "s"} to Space ${spaceId}? [Y/n] `);
161
+ const answer = await rl.question(`Import ${plan.files} conversation${plan.files === 1 ? "" : "s"} (${megabytes(plan.bytes)}) to Space ${spaceId}? [Y/n] `);
160
162
  if (!/^(|y(es)?)$/i.test(answer.trim())) {
161
- if (jsonRequested(options))
162
- outJson({ ...result, cancelled: true });
163
+ if (asJson)
164
+ outJson({ ...summary, cancelled: true });
163
165
  else
164
166
  process.stdout.write("Import cancelled\n");
165
167
  return;
@@ -169,26 +171,31 @@ export function registerRuntime(program) {
169
171
  rl.close();
170
172
  }
171
173
  }
172
- for (const candidate of candidates) {
173
- try {
174
- const response = await requestNativeDaemon({ harness: candidate.harness, cwd: root, path: candidate.path, nativeSessionId: candidate.nativeSessionId,
175
- sessionStartedAt: candidate.sessionStartedAt, origin: "local_import", settled: true });
176
- applyNativeImportResponse(result, candidate.path, response);
177
- }
178
- catch (error) {
179
- result.failed.push({ path: candidate.path, message: error instanceof Error ? error.message : String(error) });
180
- }
174
+ let job = await control("start");
175
+ while (!controller.signal.aborted && job.state === "running") {
176
+ if (!asJson && process.stderr.isTTY)
177
+ process.stderr.write(`\r\x1b[2KImporting ${formatImportProgress(job)}`);
178
+ await delay(500, undefined, { signal: controller.signal }).catch(() => undefined);
179
+ const status = await requestRuntimeInstance(directory);
180
+ if (!status?.native)
181
+ throw new Error("Local Runtime stopped; the import resumes when it starts again");
182
+ job = status.native.import;
181
183
  }
182
- result.complete = result.complete && result.failed.length === 0;
183
- if (jsonRequested(options))
184
- outJson(result);
184
+ if (controller.signal.aborted)
185
+ job = await control("pause", 5_000);
186
+ const paused = job.state === "paused";
187
+ if (asJson)
188
+ outJson({ ...summary, ...job, paused });
185
189
  else {
186
- process.stdout.write(`Imported ${result.imported}/${candidates.length} native conversation${candidates.length === 1 ? "" : "s"}\n`);
187
- for (const error of [...discovered.errors, ...result.skipped, ...result.failed])
188
- process.stdout.write(`Skipped ${error.path}: ${error.message}\n`);
189
- process.stdout.write("Uploads continue in the local Runtime background\n");
190
+ if (process.stderr.isTTY)
191
+ process.stderr.write("\r\x1b[2K");
192
+ process.stdout.write(`${paused ? "Paused" : "Imported"} ${formatImportProgress(job)}\n`);
193
+ for (const failure of job.failed)
194
+ process.stdout.write(`Skipped ${failure.path}: ${failure.message}\n`);
195
+ if (paused)
196
+ process.stdout.write("Run cohub runtime import again to continue\n");
190
197
  }
191
- if (!result.complete)
198
+ if (job.failed.length)
192
199
  process.exitCode = 1;
193
200
  }
194
201
  catch (cause) {
@@ -208,16 +215,15 @@ export function registerRuntime(program) {
208
215
  const identity = currentIdentityKey();
209
216
  const local = identity ? await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId)) : null;
210
217
  const space = createClient().space(spaceId);
211
- const store = new RuntimeSessionStore(spaceId, { projectionSource: space, archiveTransport: null });
212
- const [remote, pendingLocalArchives, failedLocalArchives, nativeStores] = await Promise.all([
218
+ const stateRoot = runtimeStateRoot(spaceId);
219
+ const archives = new RuntimeArchiveStore(join(stateRoot, "archives"));
220
+ const [remote, pendingLocalArchives, failedLocalArchives] = await Promise.all([
213
221
  space.getRuntime(undefined, { signal: AbortSignal.timeout(5000) }).then((value) => ({ value, error: null })).catch((error) => ({ value: null, error: serializeDiagnosticError(error).message })),
214
- store.archives.pendingCount(), store.archives.failedCaptureCount(),
215
- identity ? listNativeSyncStores(store.root, spaceId, identity) : [],
222
+ archives.pendingCount(), archives.failedCaptureCount(),
216
223
  ]);
217
- const nativeSessions = await Promise.all(nativeStores.map((native) => native.status()));
218
224
  // A corrupt native sync config must never take down the whole status report.
219
- const nativeSync = identity ? await readNativeSyncConfig(store.root, identity).then((config) => ({ config, error: null }), (error) => ({ config: null, error: serializeDiagnosticError(error).message })) : { config: null, error: null };
220
- 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 };
225
+ const nativeSync = identity ? await readNativeConfig(stateRoot, identity).then((config) => ({ config, error: null }), (error) => ({ config: null, error: serializeDiagnosticError(error).message })) : { config: null, error: null };
226
+ const result = { ...remote.value, spaceId, local, remote: remote.value, remoteError: remote.error, diagnosticsPath: runtimeDiagnosticsDirectory(stateRoot), pendingLocalArchives, failedLocalArchives, nativeSync: nativeSync.config, nativeSyncError: nativeSync.error, native: local?.native ?? null };
221
227
  if (jsonRequested(options))
222
228
  outJson(result);
223
229
  else {
@@ -226,7 +232,7 @@ export function registerRuntime(program) {
226
232
  else
227
233
  process.stdout.write(`Local process Not running\nSpace ${spaceId}\nLogs ${result.diagnosticsPath}\n`);
228
234
  process.stdout.write(`Server ${remote.error ? `Unknown — ${remote.error}` : remote.value?.online ? "Harness connected" : "Offline"}\nArchives ${pendingLocalArchives} pending · ${failedLocalArchives} failed\n`);
229
- process.stdout.write(formatNativeSync(nativeSync.config, nativeSessions, nativeSync.error));
235
+ process.stdout.write(formatNativeSync(nativeSync.config, local?.native, nativeSync.error));
230
236
  }
231
237
  }
232
238
  catch (cause) {
@@ -260,8 +266,12 @@ export function registerRuntime(program) {
260
266
  throw new Error("Runtime is still stopping; inspect logs");
261
267
  if (jsonRequested(options))
262
268
  outJson({ spaceId, stopped: true });
263
- else
269
+ else {
264
270
  process.stdout.write("Runtime stopped; data retained\n");
271
+ // Terminal clients may still be attached to it, so it is never stopped for them.
272
+ if (local?.native?.codex?.control === "shared")
273
+ process.stdout.write("Codex's shared app-server keeps running; stop it with codex app-server daemon stop\n");
274
+ }
265
275
  }
266
276
  catch (cause) {
267
277
  reportFailure(cause);
@@ -278,18 +288,18 @@ export function registerRuntime(program) {
278
288
  const stop = () => controller.abort();
279
289
  try {
280
290
  const spaceId = await resolveRuntimeTarget(program, options.space);
281
- const store = new RuntimeSessionStore(spaceId, { projectionSource: createClient().space(spaceId), archiveTransport: null });
291
+ const stateRoot = runtimeStateRoot(spaceId);
282
292
  const limit = Number(options.limit);
283
293
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10_000)
284
294
  throw new Error("Use a limit from 1 to 10000");
285
295
  if (!diagnosticLevels.includes(options.level))
286
296
  throw new Error("Use debug, info, warn or error");
287
297
  const asJson = jsonRequested(options);
288
- const reader = new RuntimeDiagnosticReader(store.root);
298
+ const reader = new RuntimeDiagnosticReader(stateRoot);
289
299
  process.once("SIGINT", stop);
290
300
  process.once("SIGTERM", stop);
291
301
  do {
292
- const events = (options.follow ? await reader.read({ limit }) : await readRuntimeDiagnosticEvents(store.root, { limit }))
302
+ const events = (options.follow ? await reader.read({ limit }) : await readRuntimeDiagnosticEvents(stateRoot, { limit }))
293
303
  .filter((event) => atLeastLevel(event.level, options.level));
294
304
  if (asJson && !options.follow)
295
305
  outJson(events);
@@ -1,6 +1,6 @@
1
1
  import { isUuid } from "@neta-art/cohub";
2
2
  import { createClient } from "../client.js";
3
- import { formatLocalDateTime, table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
3
+ import { formatLocalDateTime, table, json as outJson, jsonRequested, error, handleHttp, truncateText } from "../output.js";
4
4
  const DEFAULT_LIMIT = 20;
5
5
  const MAX_TITLE_LENGTH = 72;
6
6
  const MAX_CONTEXT_LENGTH = 42;
@@ -11,12 +11,6 @@ function clampLimit(value) {
11
11
  return DEFAULT_LIMIT;
12
12
  return Math.min(Math.max(Math.floor(parsed), 1), 50);
13
13
  }
14
- function truncate(value, maxLength) {
15
- const text = (value ?? "").replace(/\s+/g, " ").trim();
16
- if (text.length <= maxLength)
17
- return text;
18
- return `${text.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
19
- }
20
14
  function contextFor(item) {
21
15
  const owner = item.ownerProfile;
22
16
  if (owner?.username)
@@ -47,8 +41,8 @@ function parseSearchInput(opts) {
47
41
  function rowsFor(items) {
48
42
  return items.map((item) => ({
49
43
  type: item.type,
50
- title: truncate(item.title, MAX_TITLE_LENGTH),
51
- context: truncate(contextFor(item), MAX_CONTEXT_LENGTH),
44
+ title: truncateText(item.title, MAX_TITLE_LENGTH),
45
+ context: truncateText(contextFor(item), MAX_CONTEXT_LENGTH),
52
46
  match: item.matchedField,
53
47
  updated: item.updatedAt ? formatLocalDateTime(item.updatedAt).slice(0, 10) : "",
54
48
  href: item.href,
@@ -5,7 +5,7 @@ import { resolveCohubEnvironment } from "@neta-art/cohub";
5
5
  import { uploadAvatarAsset, uploadChatImageAsset } from "../avatar.js";
6
6
  import { createClient } from "../client.js";
7
7
  import { putLocalFile } from "../http-put.js";
8
- import { table, json as outJson, jsonRequested, ok, error, handleHttp, formatEpochMs } from "../output.js";
8
+ import { table, json as outJson, jsonRequested, ok, error, handleHttp, formatEpochMs, truncateText } from "../output.js";
9
9
  import { resolveSpace } from "../space.js";
10
10
  import { registerSpaceCommerce } from "./space-commerce.js";
11
11
  import { registerSpaceActivity } from "./space-activity.js";
@@ -17,6 +17,8 @@ const defaultIdleTtlSeconds = cliEnv === "prod" ? 12 * 60 * 60 : 10 * 60;
17
17
  const SPACE_ROLES = ["host", "builder", "guest"];
18
18
  const SANDBOX_SPEC_IDS = ["standard", "boost", "ultra"];
19
19
  const LABEL_RESOURCE_TYPES = ["session", "checkpoint", "file"];
20
+ /** Table cell width for space descriptions; `--json` keeps the full value. */
21
+ const SPACE_DESCRIPTION_COLUMN_WIDTH = 40;
20
22
  function parseInteger(value, name, options = {}) {
21
23
  if (!/^-?\d+$/.test(value.trim()))
22
24
  return error(`Invalid ${name}`, `${name} must be an integer`);
@@ -476,6 +478,7 @@ export function registerSpaces(program) {
476
478
  table(filtered, [
477
479
  { key: "id", label: "ID" },
478
480
  { key: "name", label: "Name" },
481
+ { key: "description", label: "Description", format: (value) => truncateText(value, SPACE_DESCRIPTION_COLUMN_WIDTH) },
479
482
  { key: "isPinned", label: "Pinned" },
480
483
  { key: "createdAt", label: "Created" },
481
484
  ]);
@@ -500,7 +503,7 @@ export function registerSpaces(program) {
500
503
  { key: "id", label: "ID" },
501
504
  { key: "name", label: "Name" },
502
505
  { key: "slug", label: "Slug" },
503
- { key: "description", label: "Description" },
506
+ { key: "description", label: "Description", format: (value) => truncateText(value, SPACE_DESCRIPTION_COLUMN_WIDTH) },
504
507
  { key: "status", label: "Status" },
505
508
  { key: "createdAt", label: "Created" },
506
509
  ]);
package/dist/output.d.ts CHANGED
@@ -24,6 +24,12 @@ export declare function formatLocalDateTime(value: unknown): string;
24
24
  * Unix epoch for a timestamp the server never sent.
25
25
  */
26
26
  export declare function formatEpochMs(value: unknown): string;
27
+ /**
28
+ * Collapse whitespace and clip long text for a single-line table cell, so
29
+ * multi-line values cannot break the row layout. `--json` output keeps the
30
+ * raw value; only the human-facing table is shortened.
31
+ */
32
+ export declare function truncateText(value: unknown, maxLength: number): string;
27
33
  export declare function table(rows: Row[], columns: Column[]): void;
28
34
  export declare function json(data: unknown): void;
29
35
  export declare function jsonRequested(opts?: {
package/dist/output.js CHANGED
@@ -40,6 +40,17 @@ export function formatEpochMs(value) {
40
40
  return "";
41
41
  return formatLocalDateTime(ms);
42
42
  }
43
+ /**
44
+ * Collapse whitespace and clip long text for a single-line table cell, so
45
+ * multi-line values cannot break the row layout. `--json` output keeps the
46
+ * raw value; only the human-facing table is shortened.
47
+ */
48
+ export function truncateText(value, maxLength) {
49
+ const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
50
+ if (text.length <= maxLength)
51
+ return text;
52
+ return `${text.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
53
+ }
43
54
  function cellText(column, row) {
44
55
  const raw = row[column.key];
45
56
  if (column.format)
@@ -15,6 +15,10 @@ export type ArchiveTransport = {
15
15
  signal?: AbortSignal;
16
16
  }): Promise<RuntimeArchivePage>;
17
17
  };
18
+ /** Raised when local capture bytes fail integrity checks (checksum, truncation, boundary); retryable via cache rebuild. */
19
+ export declare class ArchiveCaptureIntegrityError extends Error {
20
+ constructor(message: string);
21
+ }
18
22
  export declare function checksumNativeFile(path: string): Promise<string>;
19
23
  export declare function atomicRuntimeJson(path: string, value: unknown): Promise<void>;
20
24
  /** Immutable local segments are the outbox. Model acknowledgements never remove them. */
@@ -4,6 +4,10 @@ import { mkdir, open, readFile, readdir, rename, rm, stat, link } from "node:fs/
4
4
  import { dirname, join } from "node:path";
5
5
  import { RUNTIME_ARCHIVE_SEGMENT_BYTES, harnessArchiveIndexSchema, validateArchiveBoundary, } from "@neta-art/cohub";
6
6
  const missing = (error) => error?.code === "ENOENT";
7
+ /** Raised when local capture bytes fail integrity checks (checksum, truncation, boundary); retryable via cache rebuild. */
8
+ export class ArchiveCaptureIntegrityError extends Error {
9
+ constructor(message) { super(message); this.name = "ArchiveCaptureIntegrityError"; }
10
+ }
7
11
  const hash = (bytes, algorithm = "sha256") => createHash(algorithm).update(bytes).digest("hex");
8
12
  export async function checksumNativeFile(path) {
9
13
  const digest = createHash("sha256");
@@ -118,7 +122,7 @@ export class RuntimeArchiveStore {
118
122
  if (saved.sessionId !== state.sessionId || saved.harness !== state.harness)
119
123
  throw new Error("Archive identity mismatch");
120
124
  if (state.expectedChecksum && state.expectedChecksum !== saved.sha256)
121
- throw new Error("Native Turn bytes changed; original archive retained");
125
+ throw new ArchiveCaptureIntegrityError("Native Turn bytes changed; original archive retained");
122
126
  const committed = await stat(join(this.root, "ready", `${turnId}.json`)).catch((error) => { if (missing(error))
123
127
  return null; throw error; });
124
128
  if (!committed)
@@ -133,10 +137,10 @@ export class RuntimeArchiveStore {
133
137
  try {
134
138
  const before = await file.stat();
135
139
  if (!before.isFile() || !before.size)
136
- throw new Error("Native archive is empty");
140
+ throw new ArchiveCaptureIntegrityError("Native archive is empty");
137
141
  const sizeBytes = state.sizeBytes ?? before.size;
138
142
  if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 1 || sizeBytes > before.size)
139
- throw new Error("Native archive boundary is unavailable");
143
+ throw new ArchiveCaptureIntegrityError("Native archive boundary is unavailable");
140
144
  const buffer = Buffer.alloc(RUNTIME_ARCHIVE_SEGMENT_BYTES);
141
145
  let offset = 0;
142
146
  let digest = createHash("sha256");
@@ -172,14 +176,14 @@ export class RuntimeArchiveStore {
172
176
  }
173
177
  const after = await stat(state.path);
174
178
  if (before.ino !== after.ino || after.size < sizeBytes || state.sizeBytes === undefined && (before.size !== after.size || before.mtimeMs !== after.mtimeMs))
175
- throw new Error("Native file changed during capture");
179
+ throw new ArchiveCaptureIntegrityError("Native file changed during capture");
176
180
  if (state.sizeBytes !== undefined) {
177
181
  // Native clients may append while an earlier Turn is captured. Validate the exact prefix twice.
178
182
  const verified = createHash("sha256");
179
183
  for await (const bytes of createReadStream(state.path, { end: sizeBytes - 1 }))
180
184
  verified.update(bytes);
181
185
  if (verified.digest("hex") !== digest.copy().digest("hex"))
182
- throw new Error("Native prefix changed during capture");
186
+ throw new ArchiveCaptureIntegrityError("Native prefix changed during capture");
183
187
  }
184
188
  if (process.platform !== "win32") {
185
189
  const directory = await open(join(this.root, "objects"), "r");
@@ -195,7 +199,7 @@ export class RuntimeArchiveStore {
195
199
  sizeBytes, sha256: digest.digest("hex"), segments });
196
200
  validateArchiveBoundary(index, parent);
197
201
  if (state.expectedChecksum && index.sha256 !== state.expectedChecksum)
198
- throw new Error("Native Turn bytes changed during capture; original retained");
202
+ throw new ArchiveCaptureIntegrityError("Native Turn bytes changed during capture; original retained");
199
203
  }
200
204
  finally {
201
205
  await file.close();
@@ -4,6 +4,4 @@ export type CodexTokenTotals = Record<typeof fields[number], number>;
4
4
  export declare const codexTokenTotals: (value: unknown) => CodexTokenTotals;
5
5
  export declare const subtractCodexTokens: (total: CodexTokenTotals, base: CodexTokenTotals) => CodexTokenTotals;
6
6
  export declare function codexUsage(total: CodexTokenTotals): NonNullable<RuntimeMessage["usage"]>;
7
- /** Seed portable imports from the original native counters, not a prior turn's `last`. */
8
- export declare function codexArchiveTotals(records: Record<string, unknown>[]): CodexTokenTotals | undefined;
9
7
  export {};
@@ -9,15 +9,3 @@ export const subtractCodexTokens = (total, base) => Object.fromEntries(fields.ma
9
9
  export function codexUsage(total) {
10
10
  return { input: Math.max(0, total.inputTokens - total.cachedInputTokens - total.cacheWriteInputTokens), output: total.outputTokens, cacheRead: total.cachedInputTokens, cacheWrite: total.cacheWriteInputTokens, totalTokens: total.totalTokens };
11
11
  }
12
- /** Seed portable imports from the original native counters, not a prior turn's `last`. */
13
- export function codexArchiveTotals(records) {
14
- for (let i = records.length - 1; i >= 0; i--) {
15
- const entry = records[i];
16
- if (entry?.type !== "token_usage_record")
17
- continue;
18
- const value = record(record(entry.payload).thread_token_usage);
19
- if (typeof value.total_tokens !== "number")
20
- continue;
21
- return codexTokenTotals({ inputTokens: value.input_tokens, outputTokens: value.output_tokens, cachedInputTokens: value.cached_input_tokens, cacheWriteInputTokens: value.cache_write_input_tokens, totalTokens: value.total_tokens });
22
- }
23
- }
@@ -1,21 +1,21 @@
1
- import { type RuntimeCapabilities, type NativeRuntimeEvent } from "@neta-art/cohub";
2
- import { type HarnessOptions } from "./harness.js";
3
- import { type RuntimeSessionStore } from "./session-store.js";
1
+ import { type RuntimeNativeStop, type RuntimeCapabilities, type NativeRuntimeEvent } from "@neta-art/cohub";
2
+ import { type Executor } from "./native/execution.js";
4
3
  import { type RuntimeDiagnostics } from "./diagnostics.js";
5
4
  export type RuntimeConnectionOptions = {
6
5
  spaceId: string;
7
6
  cwd: string;
8
7
  url: string;
9
8
  capabilities: RuntimeCapabilities;
10
- harnesses: HarnessOptions;
11
9
  token: (forceRefresh?: boolean) => Promise<string>;
12
10
  signal: AbortSignal;
13
- store: RuntimeSessionStore;
11
+ executor: Executor;
14
12
  onReady: () => void;
15
13
  onDisconnected?: () => void;
16
14
  runtimeId?: string;
17
15
  diagnostics?: RuntimeDiagnostics;
18
16
  leaseConflictTimeoutMs?: number;
19
17
  onNativeChannel?: (send: (event: NativeRuntimeEvent) => Promise<unknown>) => void;
18
+ /** The web asked to stop a Turn; one running in a native client is stopped there. */
19
+ onNativeStop?: (stop: RuntimeNativeStop) => void;
20
20
  };
21
21
  export declare function serveRuntime(options: RuntimeConnectionOptions): Promise<void>;