@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,9 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { setTimeout as delay } from "node:timers/promises";
3
- import { RUNTIME_MAX_FRAME_BYTES, RUNTIME_PROTOCOL_VERSION, runtimeCommandSchema, runtimeReadySchema, } from "@neta-art/cohub";
4
- import { executeCodex, executePi } from "./harness.js";
3
+ import { RUNTIME_MAX_FRAME_BYTES, RUNTIME_PROTOCOL_VERSION, runtimeCommandSchema, runtimeReadySchema, runtimeNativeStopSchema, } from "@neta-art/cohub";
4
+ import { executeTurn } from "./native/execution.js";
5
+ import { ContextRequiredError } from "./native/results.js";
5
6
  import { ProcessCleanupUncertainError } from "./process-group.js";
6
- import { ContextRequiredError } from "./session-store.js";
7
7
  import { serializeDiagnosticError, } from "./diagnostics.js";
8
8
  export async function serveRuntime(options) {
9
9
  const runtimeId = options.runtimeId ?? options.diagnostics?.runtimeId ?? randomUUID();
@@ -13,7 +13,7 @@ export async function serveRuntime(options) {
13
13
  const uploads = new AbortController();
14
14
  const uploadSignal = AbortSignal.any([options.signal, uploads.signal]);
15
15
  const log = (level, event, data, context) => options.diagnostics?.log(level, event, data, context);
16
- const flush = () => options.store.flushArchives(uploadSignal).catch((error) => {
16
+ const flush = () => options.executor.archives.flush(uploadSignal).catch((error) => {
17
17
  if (!uploadSignal.aborted) {
18
18
  log("warn", "archive.flush_failed", { error: serializeDiagnosticError(error) });
19
19
  // The diagnostic sink handles terminal presentation and throttling.
@@ -151,7 +151,8 @@ async function connect(options) {
151
151
  return;
152
152
  }
153
153
  const requestId = randomUUID();
154
- const timer = setTimeout(() => { nativePending.delete(requestId); reject(new Error("Native Runtime event timed out")); }, 30_000);
154
+ // An ingest batch writes each of its Turns durably; everything else answers from memory or one row.
155
+ const timer = setTimeout(() => { nativePending.delete(requestId); reject(new Error("Native Runtime event timed out")); }, event.type === "ingest" ? 120_000 : 30_000);
155
156
  nativePending.set(requestId, { resolve, reject, timer });
156
157
  try {
157
158
  send({ type: "runtime.native", requestId, event });
@@ -290,7 +291,7 @@ async function connect(options) {
290
291
  options.onNativeChannel?.(sendNative);
291
292
  options.onReady();
292
293
  let recoveryBatch = 0;
293
- for await (const executions of options.store.pendingExecutionBatches()) {
294
+ for await (const executions of options.executor.results.pendingBatches()) {
294
295
  recoveryBatch += 1;
295
296
  log("info", "runtime.recovery_batch_sent", {
296
297
  batch: recoveryBatch,
@@ -305,6 +306,16 @@ async function connect(options) {
305
306
  if (raw.type === "runtime.heartbeat") {
306
307
  return;
307
308
  }
309
+ if (raw.type === "runtime.native.stop") {
310
+ const stop = runtimeNativeStopSchema.safeParse(raw);
311
+ if (!stop.success) {
312
+ log("warn", "runtime.protocol.invalid_native_stop", { error: stop.error.message }, { connectionId });
313
+ return;
314
+ }
315
+ if (stop.data.spaceId === options.spaceId)
316
+ options.onNativeStop?.(stop.data);
317
+ return;
318
+ }
308
319
  if (raw.type === "runtime.native.result") {
309
320
  const result = raw;
310
321
  const pending = result.requestId ? nativePending.get(result.requestId) : null;
@@ -341,7 +352,7 @@ async function connect(options) {
341
352
  await execution.promise;
342
353
  if (!execution.result)
343
354
  throw new Error("Runtime result is not available");
344
- await options.store.acknowledge(execution.result.state, frame.turnId, frame.revision);
355
+ await options.executor.results.acknowledge(execution.sessionId, frame.turnId);
345
356
  }
346
357
  catch (error) {
347
358
  log("error", "runtime.turn_ack_failed", {
@@ -373,7 +384,7 @@ async function connect(options) {
373
384
  if (previous.sessionId !== identity.sessionId || previous.turnId !== identity.turnId || previous.harness !== identity.harness)
374
385
  throw new Error("Runtime recovery identity changed");
375
386
  await previous.promise;
376
- const saved = await options.store.recoverResult(identity);
387
+ const saved = await options.executor.results.recover(identity);
377
388
  if (saved)
378
389
  for (const event of saved.events)
379
390
  send({ type: "runtime.event", requestId: frame.requestId, event });
@@ -395,12 +406,12 @@ async function connect(options) {
395
406
  await running[1].promise;
396
407
  active.delete(running[0]);
397
408
  }
398
- const saved = await options.store.recoverResult(identity);
409
+ const saved = await options.executor.results.recover(identity);
399
410
  recovery.controller.signal.throwIfAborted();
400
411
  const last = saved?.events.at(-1);
401
412
  if (!saved || last?.type !== "turn.end")
402
413
  throw new Error("No confirmed result");
403
- recovery.result = { state: saved.state, event: last };
414
+ recovery.result = { session: saved.session, event: last };
404
415
  for (const event of saved.events)
405
416
  send({ type: "runtime.event", requestId: frame.requestId, event });
406
417
  }
@@ -432,7 +443,7 @@ async function connect(options) {
432
443
  if (previous.sessionId !== frame.input.sessionId || previous.turnId !== frame.input.turnId || previous.harness !== frame.input.harness)
433
444
  throw new Error("Runtime execution identity changed");
434
445
  await previous.promise;
435
- const saved = await options.store.recoverResult(frame.input, frame.requestId);
446
+ const saved = await options.executor.results.recover(frame.input, frame.requestId);
436
447
  if (saved)
437
448
  for (const event of saved.events)
438
449
  send({ type: "runtime.event", requestId: frame.requestId, event });
@@ -441,7 +452,7 @@ async function connect(options) {
441
452
  const requestContext = async (executionSignal) => {
442
453
  const signal = AbortSignal.any([executionSignal, disconnected.signal]);
443
454
  signal.throwIfAborted();
444
- const pendingTurnIds = await options.store.pendingTurnIds(frame.input.sessionId);
455
+ const pendingTurnIds = await options.executor.results.pendingTurnIds(frame.input.sessionId);
445
456
  log("debug", "runtime.context_requested", { pendingTurnCount: pendingTurnIds.length }, context);
446
457
  return new Promise((resolve, reject) => {
447
458
  const abort = () => {
@@ -521,12 +532,12 @@ async function connect(options) {
521
532
  };
522
533
  execution.promise = (async () => {
523
534
  try {
524
- const saved = await options.store.recoverResult(frame.input, frame.requestId);
535
+ const saved = await options.executor.results.recover(frame.input, frame.requestId);
525
536
  if (saved) {
526
537
  const last = saved.events.at(-1);
527
538
  if (last?.type !== "turn.end")
528
539
  throw new Error("Incomplete saved Runtime result");
529
- execution.result = { state: saved.state, event: last };
540
+ execution.result = { session: saved.session, event: last };
530
541
  for (const event of saved.events)
531
542
  emit(event);
532
543
  return;
@@ -536,7 +547,7 @@ async function connect(options) {
536
547
  active.delete(frame.requestId);
537
548
  return;
538
549
  }
539
- const run = () => (frame.input.harness === "pi" ? executePi : executeCodex)(frame.input, options.harnesses, options.cwd, options.store, emit, controller.signal, options.diagnostics, context);
550
+ const run = () => executeTurn(options.executor, frame.input, emit, controller.signal, frame.requestId, context);
540
551
  // Preparation can request context, then fall back once from native archive to DB.
541
552
  // These retries precede started(), so they never replay model or tool work.
542
553
  for (let retry = 0;; retry += 1) {
@@ -550,7 +561,7 @@ async function connect(options) {
550
561
  frame.input.context = await requestContext(controller.signal);
551
562
  }
552
563
  }
553
- await options.store.recordResult(execution.result.state, frame.requestId, [...durableEvents, execution.result.event], execution.result.uncertainCleanup ? { uncertainCleanup: execution.result.uncertainCleanup } : undefined);
564
+ await options.executor.results.record(execution.result.session, frame.input, frame.requestId, [...durableEvents, execution.result.event], execution.result.uncertainCleanup);
554
565
  if (execution.result.uncertainCleanup) {
555
566
  log("warn", "runtime.turn_cleanup_pending", { cleanup: execution.result.uncertainCleanup.error }, context);
556
567
  }
@@ -1,7 +1,5 @@
1
- import type { ContentBlock, RuntimeCapabilities, RuntimeExecutionEvent, RuntimeTurnInput } from "@neta-art/cohub";
1
+ import type { ContentBlock, RuntimeCapabilities } from "@neta-art/cohub";
2
2
  import { type JsonRecord } from "./json-rpc.js";
3
- import type { RuntimeSessionStore, NativeSession } from "./session-store.js";
4
- import { type RuntimeDiagnostics, type RuntimeDiagnosticContext } from "./diagnostics.js";
5
3
  export type HarnessOptions = {
6
4
  pi?: string;
7
5
  codex?: string;
@@ -9,19 +7,13 @@ export type HarnessOptions = {
9
7
  export declare function harnessExecutableCandidates(binary: string, cwd: string, path: string, platform?: NodeJS.Platform, pathExt?: string | undefined): string[];
10
8
  /** Discover installed executables only; authentication/capability errors stay explicit. */
11
9
  export declare function installedHarnesses(cwd: string, options: HarnessOptions, path?: string): Promise<("pi" | "codex")[]>;
12
- export type HarnessResult = {
13
- state: NativeSession;
14
- event: Extract<RuntimeExecutionEvent, {
15
- type: "turn.end";
16
- }>;
17
- uncertainCleanup?: {
18
- processGroupId: number;
19
- error: string;
20
- };
21
- };
22
10
  export declare function piContent(value: unknown): ContentBlock[];
23
- export declare function promptText(content: ContentBlock[]): string;
11
+ export declare function imageForPi(block: Extract<ContentBlock, {
12
+ type: "image";
13
+ }>): Promise<{
14
+ type: string;
15
+ data: string;
16
+ mimeType: string;
17
+ }>;
24
18
  export declare function discoverHarnesses(harnesses: ("pi" | "codex")[], options: HarnessOptions, cwd: string): Promise<RuntimeCapabilities>;
25
- export declare function executePi(input: RuntimeTurnInput, options: HarnessOptions, cwd: string, store: RuntimeSessionStore, emit: (event: RuntimeExecutionEvent) => void, signal: AbortSignal, diagnostics?: RuntimeDiagnostics, diagnosticContext?: RuntimeDiagnosticContext): Promise<HarnessResult>;
26
19
  export declare function codexItemContent(item: JsonRecord): ContentBlock[];
27
- export declare function executeCodex(input: RuntimeTurnInput, options: HarnessOptions, cwd: string, store: RuntimeSessionStore, emit: (event: RuntimeExecutionEvent) => void, signal: AbortSignal, diagnostics?: RuntimeDiagnostics, diagnosticContext?: RuntimeDiagnosticContext): Promise<HarnessResult>;
@@ -2,11 +2,8 @@ import { access, stat } from "node:fs/promises";
2
2
  import { constants } from "node:fs";
3
3
  import { delimiter, join, resolve, win32 } from "node:path";
4
4
  import { JsonRpcProcess, record } from "./json-rpc.js";
5
- import { ProcessCleanupUncertainError } from "./process-group.js";
6
5
  import { codexModelCatalog } from "./model-catalog.js";
7
- import { codexTokenTotals, codexUsage, subtractCodexTokens } from "./codex-usage.js";
8
6
  import { downloadPublicImage } from "../safe-remote-image.js";
9
- import { serializeDiagnosticError } from "./diagnostics.js";
10
7
  export function harnessExecutableCandidates(binary, cwd, path, platform = process.platform, pathExt = process.env.PATHEXT) {
11
8
  const windows = platform === "win32";
12
9
  const paths = windows ? win32 : { delimiter, join, resolve };
@@ -36,7 +33,6 @@ export async function installedHarnesses(cwd, options, path = process.env.PATH ?
36
33
  }));
37
34
  return found.filter((name) => name !== null);
38
35
  }
39
- const runtimeEnvironment = (input) => ({ COHUB_SPACE_ID: input.spaceId, COHUB_SESSION_ID: input.sessionId, COHUB_TURN_ID: input.turnId });
40
36
  const array = (value) => Array.isArray(value) ? value : [];
41
37
  const text = (value) => typeof value === "string" ? value : "";
42
38
  export function piContent(value) {
@@ -53,49 +49,16 @@ export function piContent(value) {
53
49
  return [{ type: "text", text: JSON.stringify(block) }];
54
50
  });
55
51
  }
56
- async function imageForPi(block) {
52
+ export async function imageForPi(block) {
57
53
  if (block.source.type === "base64")
58
54
  return { type: "image", data: block.source.data, mimeType: block.source.media_type };
59
55
  const image = await downloadPublicImage(block.source.url);
60
56
  return { type: "image", data: Buffer.from(image.bytes).toString("base64"), mimeType: image.mimeType };
61
57
  }
62
- export function promptText(content) {
63
- return content.filter((block) => block.type !== "image").map((block) => block.type === "text" ? block.text : JSON.stringify(block)).join("\n");
64
- }
65
58
  async function initializeCodex(rpc) {
66
59
  await rpc.request("initialize", { clientInfo: { name: "cohub", title: "Cohub", version: "1" }, capabilities: { experimentalApi: true } });
67
60
  rpc.write({ method: "initialized", params: {} });
68
61
  }
69
- /** Ask the native process to stop, then hard-close it if the request does not settle quickly. */
70
- function createAbortEscalation(rpc, signal, interrupt) {
71
- let timer = null;
72
- const abort = () => {
73
- interrupt();
74
- timer ??= setTimeout(() => { void rpc.close().catch(() => undefined); }, 5000);
75
- };
76
- signal.addEventListener("abort", abort, { once: true });
77
- return {
78
- abort,
79
- clear: () => { signal.removeEventListener("abort", abort); if (timer)
80
- clearTimeout(timer); },
81
- };
82
- }
83
- /** Native files stay authoritative; archival failure only degrades cross-host resume. */
84
- async function finishHarnessTurn(store, state, message, resume, turnId, diagnosticContext) {
85
- // The store records a redacted diagnostic; native files remain authoritative.
86
- const archive = await store.archive(state, turnId, diagnosticContext).catch(() => null);
87
- return { state, event: { type: "turn.end", message, resume, archive } };
88
- }
89
- /** Close the RPC process. An unconfirmable cleanup degrades the result to uncertain, never masks it. */
90
- async function closeHarness(rpc, executionError) {
91
- return rpc.close().then(() => undefined, (cleanupError) => {
92
- const processGroupId = rpc.processGroupId;
93
- if (!(cleanupError instanceof ProcessCleanupUncertainError) || processGroupId == null)
94
- throw cleanupError;
95
- const detail = executionError ? `; execution error: ${executionError}` : "";
96
- return { processGroupId, error: `${cleanupError.message}${detail}` };
97
- });
98
- }
99
62
  export async function discoverHarnesses(harnesses, options, cwd) {
100
63
  const models = [];
101
64
  await Promise.all(harnesses.map(async (harness) => {
@@ -133,132 +96,6 @@ export async function discoverHarnesses(harnesses, options, cwd) {
133
96
  }));
134
97
  return { harnesses, models };
135
98
  }
136
- export async function executePi(input, options, cwd, store, emit, signal, diagnostics, diagnosticContext) {
137
- if (input.accessMode === "read_only")
138
- throw new Error("Pi cannot enforce read-only access; select Cohub or Codex");
139
- signal.throwIfAborted();
140
- const { state, resume } = await store.prepare(input, cwd, signal);
141
- signal.throwIfAborted();
142
- const rpc = new JsonRpcProcess(options.pi || "pi", ["--mode", "rpc", "--session", state.path], cwd, "pi", runtimeEnvironment(input));
143
- let ordinal = -1;
144
- let currentContent = [];
145
- let last = { ordinal: 0, content: [] };
146
- const abortEscalation = createAbortEscalation(rpc, signal, () => { void rpc.request("abort").catch(() => undefined); });
147
- let uncertainCleanup;
148
- const stopFailureLogging = diagnostics
149
- ? rpc.onFailure((error) => diagnostics.log("error", "harness.rpc_process_failed", { error: serializeDiagnosticError(error) }, { ...diagnosticContext, component: "harness" }))
150
- : () => undefined;
151
- const stopTimeoutLogging = diagnostics
152
- ? rpc.onTimeout((method, timeoutMs) => diagnostics.log("error", "harness.rpc_timeout", { method, timeoutMs }, { ...diagnosticContext, component: "harness" }))
153
- : () => undefined;
154
- try {
155
- if (input.model) {
156
- let provider = input.provider;
157
- if (!provider) {
158
- const catalog = await rpc.request("get_available_models");
159
- const matches = array(catalog.models).map(record).filter((model) => model.id === input.model);
160
- if (matches.length !== 1)
161
- throw new Error("Select a provider for this local model");
162
- provider = text(matches[0]?.provider);
163
- }
164
- await rpc.request("set_model", { provider, modelId: input.model });
165
- }
166
- if (input.thinkingLevel)
167
- await rpc.request("set_thinking_level", { level: input.thinkingLevel });
168
- const stateResult = await rpc.request("get_state");
169
- state.nativeSessionId = text(stateResult.sessionId) || state.nativeSessionId;
170
- const model = record(stateResult.model);
171
- const content = input.messages.flatMap((message) => message.content);
172
- const images = await Promise.all(content.flatMap((block) => block.type === "image" ? [imageForPi(block)] : []));
173
- signal.throwIfAborted();
174
- await store.started(state, input.turnId);
175
- await new Promise((resolve, reject) => {
176
- const offFailure = rpc.onFailure(reject);
177
- const off = rpc.onEvent((event) => {
178
- try {
179
- if (event.type === "extension_ui_request") {
180
- if (["confirm", "select", "input", "editor"].includes(text(event.method)))
181
- rpc.write({ type: "extension_ui_response", id: event.id, cancelled: true });
182
- return;
183
- }
184
- if (event.type === "message_start" && record(event.message).role === "assistant") {
185
- ordinal++;
186
- currentContent = [];
187
- emit({ type: "message.start", ordinal });
188
- }
189
- if (event.type === "message_end" && record(event.message).role === "assistant") {
190
- currentContent = piContent(record(event.message).content);
191
- emit({ type: "content.replace", ordinal, content: currentContent });
192
- }
193
- if (event.type === "tool_execution_start") {
194
- const id = text(event.toolCallId);
195
- if (!currentContent.some((block) => block.type === "tool_use" && block.id === id))
196
- currentContent.push({ type: "tool_use", id, name: text(event.toolName), input: record(event.args), _meta: { toolStatus: "running" } });
197
- emit({ type: "content.replace", ordinal, content: [...currentContent] });
198
- }
199
- if (event.type === "tool_execution_update" || event.type === "tool_execution_end") {
200
- const id = text(event.toolCallId);
201
- const rawResult = event.type === "tool_execution_end" ? event.result : event.partialResult;
202
- const result = record(rawResult);
203
- const resultContent = typeof rawResult === "string" ? rawResult : piContent(result.content);
204
- currentContent = currentContent.filter((block) => block.type !== "tool_result" || block.tool_use_id !== id);
205
- currentContent.push({ type: "tool_result", tool_use_id: id, content: resultContent, is_error: Boolean(event.isError), _meta: { toolStatus: event.type === "tool_execution_end" ? "done" : "running" } });
206
- emit({ type: "content.replace", ordinal, content: [...currentContent] });
207
- }
208
- if (event.type === "message_update") {
209
- const delta = record(event.assistantMessageEvent);
210
- if (delta.type === "text_delta" || delta.type === "thinking_delta")
211
- emit({ type: "text.delta", ordinal, index: Number(delta.contentIndex ?? 0), kind: delta.type === "text_delta" ? "text" : "thinking", delta: text(delta.delta) });
212
- }
213
- if (event.type === "turn_end") {
214
- const message = record(event.message);
215
- const content = piContent(message.content);
216
- for (const resultValue of array(event.toolResults)) {
217
- const result = record(resultValue);
218
- content.push({ type: "tool_result", tool_use_id: text(result.toolCallId), content: typeof result.content === "string" ? result.content : piContent(result.content), is_error: Boolean(result.isError) });
219
- }
220
- last = { ordinal: Math.max(0, ordinal), content, provider: text(message.provider) || text(model.provider), model: text(message.model) || text(model.id), usage: message.usage, stopReason: text(message.stopReason) === "toolUse" ? "tool_use" : text(message.stopReason) || "stop", errorMessage: text(message.errorMessage) || null };
221
- emit({ type: "content.replace", ordinal: last.ordinal, content });
222
- if (last.stopReason === "tool_use")
223
- emit({ type: "message.commit", message: last });
224
- }
225
- if (event.type === "agent_settled" || event.type === "agent_end" && event.willRetry === undefined) {
226
- off();
227
- offFailure();
228
- resolve();
229
- }
230
- }
231
- catch (error) {
232
- off();
233
- offFailure();
234
- reject(error);
235
- }
236
- });
237
- void rpc.request("prompt", { message: promptText(content), images }).catch((error) => { off(); offFailure(); reject(error); });
238
- });
239
- await rpc.request("get_state");
240
- }
241
- catch (error) {
242
- if (!state.pendingTurnId)
243
- throw error;
244
- last = { ...last, stopReason: signal.aborted ? "aborted" : "error", errorMessage: signal.aborted ? null : error instanceof Error ? error.message : String(error) };
245
- }
246
- finally {
247
- abortEscalation.clear();
248
- stopFailureLogging();
249
- stopTimeoutLogging();
250
- uncertainCleanup = await closeHarness(rpc, last.errorMessage);
251
- }
252
- if (signal.aborted)
253
- last = { ...last, stopReason: "aborted" };
254
- return { ...await finishHarnessTurn(store, state, last, resume, input.turnId, {
255
- component: "harness",
256
- sessionId: input.sessionId,
257
- turnId: input.turnId,
258
- harness: "pi",
259
- traceContext: input.traceContext,
260
- }), ...(uncertainCleanup ? { uncertainCleanup } : {}) };
261
- }
262
99
  export function codexItemContent(item) {
263
100
  if (item.type === "agentMessage" || item.type === "plan")
264
101
  return [{ type: "text", text: text(item.text) }];
@@ -276,205 +113,3 @@ export function codexItemContent(item) {
276
113
  { type: "tool_result", tool_use_id: id, content: typeof item.aggregatedOutput === "string" ? item.aggregatedOutput : JSON.stringify(item.result ?? item.error ?? item.changes ?? item), is_error: item.status === "failed" || typeof item.exitCode === "number" && item.exitCode !== 0 },
277
114
  ];
278
115
  }
279
- export async function executeCodex(input, options, cwd, store, emit, signal, diagnostics, diagnosticContext) {
280
- signal.throwIfAborted();
281
- const { state, resume } = await store.prepare(input, cwd, signal);
282
- signal.throwIfAborted();
283
- const rpc = new JsonRpcProcess(options.codex || "codex", ["app-server", "--listen", "stdio://"], cwd, "codex", runtimeEnvironment(input));
284
- const stopFailureLogging = diagnostics
285
- ? rpc.onFailure((error) => diagnostics.log("error", "harness.rpc_process_failed", { error: serializeDiagnosticError(error) }, { ...diagnosticContext, component: "harness" }))
286
- : () => undefined;
287
- const stopTimeoutLogging = diagnostics
288
- ? rpc.onTimeout((method, timeoutMs) => diagnostics.log("error", "harness.rpc_timeout", { method, timeoutMs }, { ...diagnosticContext, component: "harness" }))
289
- : () => undefined;
290
- let nativeTurnId = null;
291
- let ordinal = -1;
292
- const ordinals = new Map();
293
- const items = new Map();
294
- const outputs = new Map();
295
- const completedItems = new Set();
296
- let pending = null;
297
- let final = { ordinal: 0, content: [] };
298
- let usage;
299
- let usageBaseline = state.codexTokenTotals;
300
- const latestMessage = () => {
301
- if (pending && pending.ordinal >= ordinal)
302
- return pending;
303
- const item = [...items.entries()].find(([id]) => ordinals.get(id) === ordinal)?.[1];
304
- return { ordinal: Math.max(0, ordinal), content: item ? codexItemContent(item) : [] };
305
- };
306
- const abortEscalation = createAbortEscalation(rpc, signal, () => {
307
- if (nativeTurnId)
308
- void rpc.request("turn/interrupt", { threadId: state.nativeSessionId, turnId: nativeTurnId }).catch(() => undefined);
309
- });
310
- let uncertainCleanup;
311
- try {
312
- await initializeCodex(rpc);
313
- const threadOptions = {
314
- cwd,
315
- ...(input.model ? { model: input.model } : {}),
316
- ...(input.provider && input.provider !== "codex" ? { modelProvider: input.provider } : {}),
317
- ...(input.accessMode === "read_only" ? { sandbox: "read-only" } : {}),
318
- };
319
- const opened = resume === "native"
320
- ? await rpc.request("thread/resume", { ...threadOptions, threadId: state.nativeSessionId, path: state.path, excludeTurns: true })
321
- : resume === "restored"
322
- ? await rpc.request("thread/fork", { ...threadOptions, threadId: state.nativeSessionId, path: state.path, excludeTurns: true })
323
- : resume === "handoff"
324
- ? await rpc.request("thread/resume", { ...threadOptions, threadId: state.nativeSessionId, path: state.path, excludeTurns: true })
325
- : await rpc.request("thread/start", threadOptions);
326
- const thread = record(opened.thread);
327
- if (typeof thread.id !== "string" || typeof thread.path !== "string")
328
- throw new Error("Codex did not provide a durable native thread");
329
- state.nativeSessionId = thread.id;
330
- state.path = thread.path;
331
- const provider = text(opened.modelProvider) || "codex";
332
- const model = text(opened.model) || input.model;
333
- const content = await Promise.all(input.messages.flatMap((message) => message.content).map(async (block) => {
334
- if (block.type === "image") {
335
- const image = await imageForPi(block);
336
- return { type: "image", url: `data:${image.mimeType};base64,${image.data}` };
337
- }
338
- return { type: "text", text: block.type === "text" ? block.text : JSON.stringify(block), text_elements: [] };
339
- }));
340
- signal.throwIfAborted();
341
- await store.started(state, input.turnId);
342
- await new Promise((resolve, reject) => {
343
- const offFailure = rpc.onFailure(reject);
344
- const off = rpc.onEvent((event) => {
345
- try {
346
- const method = text(event.method);
347
- const params = record(event.params);
348
- if (event.id != null) {
349
- // Keep native approval policy. An unattended client never grants an escalation.
350
- rpc.write({ id: event.id, error: { code: -32000, message: "Approval requires an interactive local client" } });
351
- return;
352
- }
353
- if (params.threadId !== state.nativeSessionId)
354
- return;
355
- if (method === "turn/started") {
356
- nativeTurnId = text(record(params.turn).id);
357
- if (signal.aborted)
358
- abortEscalation.abort();
359
- }
360
- if (method === "thread/tokenUsage/updated" && nativeTurnId && params.turnId === nativeTurnId) {
361
- const nativeUsage = record(params.tokenUsage);
362
- const total = codexTokenTotals(nativeUsage.total);
363
- usageBaseline ??= subtractCodexTokens(total, codexTokenTotals(nativeUsage.last));
364
- state.codexTokenTotals = total;
365
- usage = codexUsage(subtractCodexTokens(total, usageBaseline));
366
- }
367
- if (method === "item/started") {
368
- const item = record(params.item);
369
- if (item.type === "userMessage" || completedItems.has(text(item.id)))
370
- return;
371
- const existingOrdinal = ordinals.get(text(item.id));
372
- if (pending && (existingOrdinal == null || existingOrdinal > pending.ordinal)) {
373
- emit({ type: "message.commit", message: pending });
374
- pending = null;
375
- }
376
- if (existingOrdinal != null) {
377
- items.set(text(item.id), item);
378
- if (item.type === "agentMessage" && text(item.text))
379
- emit({ type: "content.replace", ordinal: existingOrdinal, content: codexItemContent(item) });
380
- return;
381
- }
382
- ordinal++;
383
- ordinals.set(text(item.id), ordinal);
384
- items.set(text(item.id), item);
385
- emit({ type: "message.start", ordinal });
386
- if (!["agentMessage", "reasoning", "plan"].includes(text(item.type)))
387
- emit({ type: "content.replace", ordinal, content: codexItemContent(item).filter((block) => block.type !== "tool_result") });
388
- }
389
- if (method === "item/agentMessage/delta" || method === "item/reasoning/textDelta" || method === "item/reasoning/summaryTextDelta") {
390
- const id = text(params.itemId);
391
- if (completedItems.has(id))
392
- return;
393
- const itemOrdinal = ordinals.get(id);
394
- const item = items.get(id);
395
- if (item) {
396
- if (method === "item/agentMessage/delta")
397
- item.text = text(item.text) + text(params.delta);
398
- else {
399
- const key = method.includes("summary") ? "summary" : "content";
400
- item[key] = [array(item[key]).map(text).join("\n") + text(params.delta)];
401
- }
402
- }
403
- if (itemOrdinal != null)
404
- emit({ type: "text.delta", ordinal: itemOrdinal, index: 0, kind: method === "item/agentMessage/delta" ? "text" : "thinking", delta: text(params.delta) });
405
- }
406
- if (method === "item/commandExecution/outputDelta") {
407
- const id = text(params.itemId);
408
- const item = items.get(id);
409
- const itemOrdinal = ordinals.get(id);
410
- if (item && itemOrdinal != null) {
411
- const output = (outputs.get(id) ?? "") + text(params.delta);
412
- outputs.set(id, output);
413
- item.aggregatedOutput = output;
414
- emit({ type: "content.replace", ordinal: itemOrdinal, content: codexItemContent(item) });
415
- }
416
- }
417
- if (method === "item/completed") {
418
- const item = record(params.item);
419
- if (item.type === "userMessage" || completedItems.has(text(item.id)))
420
- return;
421
- completedItems.add(text(item.id));
422
- const itemOrdinal = ordinals.get(text(item.id)) ?? ++ordinal;
423
- if (!ordinals.has(text(item.id))) {
424
- ordinals.set(text(item.id), itemOrdinal);
425
- emit({ type: "message.start", ordinal: itemOrdinal });
426
- }
427
- items.set(text(item.id), item);
428
- const message = { ordinal: itemOrdinal, content: codexItemContent(item), provider, model, stopReason: "stop" };
429
- emit({ type: "content.replace", ordinal: itemOrdinal, content: message.content });
430
- if (pending && pending.ordinal > itemOrdinal)
431
- emit({ type: "message.commit", message });
432
- else {
433
- if (pending && pending.ordinal !== itemOrdinal)
434
- emit({ type: "message.commit", message: pending });
435
- pending = message;
436
- }
437
- }
438
- if (method === "turn/completed") {
439
- const turn = record(params.turn);
440
- final = { ...latestMessage(), provider, model, stopReason: turn.status === "interrupted" ? "aborted" : turn.status === "failed" ? "error" : "stop", errorMessage: text(record(turn.error).message) || null };
441
- off();
442
- offFailure();
443
- resolve();
444
- }
445
- }
446
- catch (error) {
447
- off();
448
- offFailure();
449
- reject(error);
450
- }
451
- });
452
- void rpc.request("turn/start", { threadId: state.nativeSessionId, clientUserMessageId: input.userMessageId, input: content, ...(input.thinkingLevel ? { effort: input.thinkingLevel } : {}) })
453
- .then((result) => { nativeTurnId = text(record(result.turn).id); if (signal.aborted)
454
- abortEscalation.abort(); })
455
- .catch((error) => { off(); offFailure(); reject(error); });
456
- });
457
- }
458
- catch (error) {
459
- if (!state.pendingTurnId)
460
- throw error;
461
- final = { ...latestMessage(), stopReason: signal.aborted ? "aborted" : "error", errorMessage: signal.aborted ? null : error instanceof Error ? error.message : String(error) };
462
- }
463
- finally {
464
- abortEscalation.clear();
465
- stopFailureLogging();
466
- stopTimeoutLogging();
467
- uncertainCleanup = await closeHarness(rpc, final.errorMessage);
468
- }
469
- if (signal.aborted)
470
- final = { ...final, stopReason: "aborted" };
471
- if (usage)
472
- final = { ...final, usage };
473
- return { ...await finishHarnessTurn(store, state, final, resume, input.turnId, {
474
- component: "harness",
475
- sessionId: input.sessionId,
476
- turnId: input.turnId,
477
- harness: "codex",
478
- traceContext: input.traceContext,
479
- }), ...(uncertainCleanup ? { uncertainCleanup } : {}) };
480
- }
@@ -1,5 +1,12 @@
1
1
  import type { RuntimeSummary } from "./presentation.js";
2
2
  export declare function runtimeInstanceDirectory(identity: string, spaceId: string): string;
3
+ type Response = {
4
+ status: RuntimeSummary;
5
+ result?: unknown;
6
+ };
3
7
  export declare function requestRuntimeInstance(directory: string, action?: "status" | "stop", force?: boolean): Promise<RuntimeSummary | null>;
8
+ /** Send a control action to the running Runtime; `null` when none runs. */
9
+ export declare function controlRuntimeInstance(directory: string, action: string, payload?: Record<string, unknown>, timeoutMs?: number): Promise<Response | null>;
4
10
  /** Private local IPC is both the single-instance guard and the control surface. */
5
- export declare function ownRuntimeInstance(directory: string, status: () => RuntimeSummary, stop: (force: boolean) => Promise<void>): Promise<() => Promise<void>>;
11
+ export declare function ownRuntimeInstance(directory: string, status: () => RuntimeSummary, stop: (force: boolean) => Promise<void>, control?: (action: string, message: Record<string, unknown>) => Promise<unknown>): Promise<() => Promise<void>>;
12
+ export {};