@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
@@ -0,0 +1,337 @@
1
+ import { imageForPi } from "../harness.js";
2
+ import { JsonRpcProcess, record } from "../json-rpc.js";
3
+ import { ProcessCleanupUncertainError } from "../process-group.js";
4
+ import { serializeDiagnosticError } from "../diagnostics.js";
5
+ import { openPrivateServer } from "./codex-link.js";
6
+ import { PI_DISCONNECTED } from "./pi-link.js";
7
+ import { codexTranslator, piTranslator } from "./translate.js";
8
+ const text = (value) => typeof value === "string" ? value : "";
9
+ /** A stop request that the harness does not honour within this long ends its private process. */
10
+ const ABORT_ESCALATION_MS = 5_000;
11
+ /** Pause before asking a busy Pi again, so a Turn that is starting has reported itself. */
12
+ const PI_BUSY_RETRY_MS = 100;
13
+ /**
14
+ * Run one Cohub Turn in a local harness. Both harnesses are driven through the same interface a
15
+ * terminal user shares: Pi through the Cohub extension inside the Pi process, Codex through its
16
+ * app-server.
17
+ */
18
+ export async function executeTurn(executor, input, emit, signal, requestId, context) {
19
+ signal.throwIfAborted();
20
+ const diverged = await executor.results.admit(input);
21
+ const turn = { input, emit, signal, diverged, started: (session) => executor.results.start(session, input, requestId) };
22
+ const run = await (input.harness === "pi" ? runPi : runCodex)(executor, turn, context);
23
+ let uncertainCleanup;
24
+ if (run.host)
25
+ uncertainCleanup = await closeHost(run.host, run.message.errorMessage);
26
+ const message = signal.aborted ? { ...run.message, stopReason: "aborted" } : run.message;
27
+ const archive = await executor.archives.stage({ sessionId: input.sessionId, harness: input.harness, nativeSessionId: run.session.nativeSessionId, path: run.session.path }, input.turnId).catch((error) => {
28
+ // A missing archive only costs cross-machine native resume; the file itself is intact.
29
+ executor.diagnostics?.log("warn", "archive.capture_failed", { error: serializeDiagnosticError(error) }, { ...context, component: "archive" });
30
+ return null;
31
+ });
32
+ executor.ingest.advance(run.session.path, input.turnId);
33
+ return { session: run.session, event: { type: "turn.end", message, resume: run.resume, archive }, ...(uncertainCleanup ? { uncertainCleanup } : {}) };
34
+ }
35
+ /** Variables a Turn's tools see, so a `cohub` call inside a tool knows where it runs. */
36
+ const turnEnvironment = (input) => ({ COHUB_SPACE_ID: input.spaceId, COHUB_SESSION_ID: input.sessionId, COHUB_TURN_ID: input.turnId });
37
+ async function runPi(executor, turn, context) {
38
+ const { input, signal } = turn;
39
+ if (input.accessMode === "read_only")
40
+ throw new Error("Pi cannot enforce read-only access; select Cohub or Codex");
41
+ // Cohub drives every Pi through its extension, which needs the control socket and a recent Pi.
42
+ if (executor.pi.unavailable)
43
+ throw new Error(executor.pi.unavailable);
44
+ const prepared = await executor.sessions.prepare({
45
+ sessionId: input.sessionId, turnId: input.turnId, harness: "pi", cwd: executor.cwd,
46
+ provider: input.provider, context: input.context, signal,
47
+ // A terminal Pi is continued only through its extension; without one it is read-only.
48
+ resumable: (transcript) => transcript.path !== turn.diverged && (transcript.origin === "cohub" || executor.pi.find(transcript.nativeSessionId) !== null),
49
+ });
50
+ let session = prepared.session;
51
+ let link = executor.pi.find(session.nativeSessionId);
52
+ let host = null;
53
+ let message = { ordinal: 0, content: [] };
54
+ let begun = false;
55
+ try {
56
+ if (!link) {
57
+ host = new JsonRpcProcess(executor.executables.pi || "pi", ["--mode", "rpc", "--session", session.path, "-e", executor.piExtension], executor.cwd, "pi", turnEnvironment(input));
58
+ watchHost(host, executor.diagnostics, context);
59
+ const rpc = host;
60
+ // Nobody is at this Pi's terminal to answer an extension's question.
61
+ rpc.onEvent((event) => {
62
+ if (event.type === "extension_ui_request" && ["confirm", "select", "input", "editor"].includes(text(event.method)))
63
+ rpc.write({ type: "extension_ui_response", id: event.id, cancelled: true });
64
+ });
65
+ await configurePiHost(host, input);
66
+ link = await Promise.race([
67
+ executor.pi.waitFor({ sessionId: session.nativeSessionId, sessionFile: session.path }, signal),
68
+ new Promise((_, reject) => host?.onFailure(reject)),
69
+ ]);
70
+ }
71
+ const piSession = link;
72
+ session = { ...session, nativeSessionId: piSession.sessionId };
73
+ // Never interleave with a Turn a terminal user is running; follow it instead.
74
+ await executor.pi.idle(piSession.sessionId, signal);
75
+ const content = await piPrompt(input);
76
+ signal.throwIfAborted();
77
+ await turn.started(session);
78
+ begun = true;
79
+ const translator = piTranslator(turn.emit);
80
+ const model = !host && input.model && input.provider ? { provider: input.provider, id: input.model } : null;
81
+ await new Promise((resolve, reject) => {
82
+ let escalation;
83
+ /** Events held until Pi takes the prompt; until then they belong to a terminal Turn. */
84
+ let held = [];
85
+ let done = false;
86
+ const finish = (error) => {
87
+ if (done)
88
+ return;
89
+ done = true;
90
+ off();
91
+ signal.removeEventListener("abort", abort);
92
+ clearTimeout(escalation);
93
+ if (error)
94
+ reject(error);
95
+ else
96
+ resolve();
97
+ };
98
+ const feed = (event) => {
99
+ try {
100
+ if (translator.push(event))
101
+ finish();
102
+ }
103
+ catch (error) {
104
+ finish(error);
105
+ }
106
+ };
107
+ const off = executor.pi.subscribe(piSession.sessionId, (event) => {
108
+ if (event.type === PI_DISCONNECTED) {
109
+ finish(new Error("Pi exited during the Turn"));
110
+ return;
111
+ }
112
+ if (held)
113
+ held.push(event);
114
+ else
115
+ feed(event);
116
+ });
117
+ const abort = () => {
118
+ // Not taken yet: the Pi is running someone else's Turn, which is not Cohub's to stop.
119
+ if (held) {
120
+ finish(signal.reason ?? new Error("Aborted"));
121
+ return;
122
+ }
123
+ void executor.pi.abort(piSession.sessionId).catch(() => undefined);
124
+ if (host)
125
+ escalation = setTimeout(() => void host?.close().catch(() => undefined), ABORT_ESCALATION_MS);
126
+ };
127
+ signal.addEventListener("abort", abort, { once: true });
128
+ void (async () => {
129
+ for (;;) {
130
+ // Only what follows this request can be this Turn's.
131
+ held = [];
132
+ const { accepted } = await executor.pi.prompt(piSession.sessionId, { turnId: input.turnId, content, model, thinkingLevel: host ? null : input.thinkingLevel ?? null, env: turnEnvironment(input) });
133
+ if (accepted)
134
+ break;
135
+ // The terminal started a Turn first: wait for it to settle, then ask again.
136
+ await new Promise((resolve) => setTimeout(resolve, PI_BUSY_RETRY_MS));
137
+ await executor.pi.idle(piSession.sessionId, signal);
138
+ if (done)
139
+ return;
140
+ }
141
+ const events = held ?? [];
142
+ held = null;
143
+ if (done) {
144
+ void executor.pi.abort(piSession.sessionId).catch(() => undefined);
145
+ return;
146
+ }
147
+ for (const event of events)
148
+ feed(event);
149
+ if (signal.aborted)
150
+ abort();
151
+ })().catch(finish);
152
+ if (signal.aborted)
153
+ abort();
154
+ });
155
+ message = translator.last();
156
+ // The Pi keeps running for its terminal user; its tools no longer work for this Turn.
157
+ if (!host)
158
+ void executor.pi.context(piSession.sessionId, { COHUB_TURN_ID: null }).catch(() => undefined);
159
+ }
160
+ catch (error) {
161
+ // Before the prompt, nothing ran and the error is the outcome.
162
+ if (!begun) {
163
+ if (host)
164
+ await closeHost(host, null).catch(() => undefined);
165
+ throw error;
166
+ }
167
+ message = { ...message, stopReason: signal.aborted ? "aborted" : "error", errorMessage: signal.aborted ? null : error instanceof Error ? error.message : String(error) };
168
+ }
169
+ return { session, resume: prepared.resume, message, host };
170
+ }
171
+ /** A Pi that Cohub starts is configured through its RPC before the extension takes the prompt. */
172
+ async function configurePiHost(host, input) {
173
+ if (input.model) {
174
+ let provider = input.provider;
175
+ if (!provider) {
176
+ const catalog = await host.request("get_available_models");
177
+ const matches = (Array.isArray(catalog.models) ? catalog.models : []).map(record).filter((model) => model.id === input.model);
178
+ if (matches.length !== 1)
179
+ throw new Error("Select a provider for this local model");
180
+ provider = text(matches[0]?.provider);
181
+ }
182
+ await host.request("set_model", { provider, modelId: input.model });
183
+ }
184
+ if (input.thinkingLevel)
185
+ await host.request("set_thinking_level", { level: input.thinkingLevel });
186
+ }
187
+ async function piPrompt(input) {
188
+ const blocks = input.messages.flatMap((message) => message.content);
189
+ return await Promise.all(blocks.map(async (block) => {
190
+ if (block.type === "image")
191
+ return await imageForPi(block);
192
+ return { type: "text", text: block.type === "text" ? block.text : JSON.stringify(block) };
193
+ }));
194
+ }
195
+ async function runCodex(executor, turn, context) {
196
+ const { input, signal } = turn;
197
+ const shared = await executor.codex.get();
198
+ const prepared = await executor.sessions.prepare({
199
+ sessionId: input.sessionId, turnId: input.turnId, harness: "codex", cwd: executor.cwd,
200
+ provider: input.provider, context: input.context, signal,
201
+ // Only the shared server coordinates with terminal clients; a private one never opens their files.
202
+ resumable: (transcript) => transcript.path !== turn.diverged && (transcript.origin === "cohub" || shared !== null),
203
+ });
204
+ let session = prepared.session;
205
+ let host = null;
206
+ let channel = shared;
207
+ let message = { ordinal: 0, content: [] };
208
+ let begun = false;
209
+ try {
210
+ if (!channel) {
211
+ host = new JsonRpcProcess(executor.executables.codex || "codex", ["app-server", "--listen", "stdio://"], executor.cwd, "codex", turnEnvironment(input));
212
+ watchHost(host, executor.diagnostics, context);
213
+ channel = await openPrivateServer(host);
214
+ }
215
+ const codex = channel;
216
+ const threadOptions = {
217
+ cwd: executor.cwd,
218
+ ...(input.model ? { model: input.model } : {}),
219
+ ...(input.provider && input.provider !== "codex" ? { modelProvider: input.provider } : {}),
220
+ ...(input.accessMode === "read_only" ? { sandbox: "read-only" } : {}),
221
+ // Tools of a shared server otherwise see the environment of whoever started that server.
222
+ config: { "shell_environment_policy.set.COHUB_SPACE_ID": input.spaceId, "shell_environment_policy.set.COHUB_SESSION_ID": input.sessionId },
223
+ };
224
+ const existing = { ...threadOptions, threadId: session.nativeSessionId, path: session.path, excludeTurns: true };
225
+ const opened = prepared.resume === "new" ? await codex.request("thread/start", threadOptions)
226
+ : prepared.resume === "restored" ? await codex.request("thread/fork", existing)
227
+ : await codex.request("thread/resume", existing);
228
+ const thread = record(opened.thread);
229
+ if (typeof thread.id !== "string" || typeof thread.path !== "string")
230
+ throw new Error("Codex did not provide a durable native thread");
231
+ const threadId = thread.id;
232
+ if (thread.path !== session.path || threadId !== session.nativeSessionId) {
233
+ session = { ...session, nativeSessionId: threadId, path: thread.path };
234
+ executor.ingest.adopt(session, input.sessionId);
235
+ }
236
+ const translator = codexTranslator(turn.emit, { provider: text(opened.modelProvider) || "codex", model: text(opened.model) || input.model || null });
237
+ const content = await codexPrompt(input);
238
+ signal.throwIfAborted();
239
+ await turn.started(session);
240
+ begun = true;
241
+ let nativeTurnId = null;
242
+ executor.codex.driving.add(threadId);
243
+ await new Promise((resolve, reject) => {
244
+ let escalation;
245
+ let done = false;
246
+ const finish = (error) => {
247
+ if (done)
248
+ return;
249
+ done = true;
250
+ off();
251
+ offClose();
252
+ signal.removeEventListener("abort", abort);
253
+ clearTimeout(escalation);
254
+ if (error)
255
+ reject(error);
256
+ else
257
+ resolve();
258
+ };
259
+ const interrupt = () => { if (nativeTurnId)
260
+ void codex.request("turn/interrupt", { threadId, turnId: nativeTurnId }).catch(() => undefined); };
261
+ const off = codex.onEvent((event) => {
262
+ const params = event.params;
263
+ if (event.id != null) {
264
+ // An unattended Cohub Turn never grants an escalation.
265
+ if (!codex.shared || params.threadId === threadId && (!nativeTurnId || params.turnId === nativeTurnId))
266
+ codex.refuse(event.id, "Approval requires an interactive local client");
267
+ return;
268
+ }
269
+ if (params.threadId !== threadId)
270
+ return;
271
+ // A resumed thread replays its last usage; only this Turn's counts belong to it.
272
+ if (event.method === "thread/tokenUsage/updated" && params.turnId !== nativeTurnId)
273
+ return;
274
+ if (event.method === "turn/started" && !nativeTurnId) {
275
+ nativeTurnId = text(record(params.turn).id) || null;
276
+ if (signal.aborted)
277
+ interrupt();
278
+ }
279
+ if (event.method === "turn/completed" && nativeTurnId && text(record(params.turn).id) !== nativeTurnId)
280
+ return;
281
+ try {
282
+ if (translator.push(event))
283
+ finish();
284
+ }
285
+ catch (error) {
286
+ finish(error);
287
+ }
288
+ });
289
+ const offClose = codex.onClose((error) => finish(error));
290
+ const abort = () => {
291
+ interrupt();
292
+ if (host)
293
+ escalation = setTimeout(() => void host?.close().catch(() => undefined), ABORT_ESCALATION_MS);
294
+ };
295
+ signal.addEventListener("abort", abort, { once: true });
296
+ codex.request("turn/start", { threadId, clientUserMessageId: input.turnId, input: content, ...(input.thinkingLevel ? { effort: input.thinkingLevel } : {}) })
297
+ .then((result) => { nativeTurnId ??= text(record(result.turn).id) || null; if (signal.aborted)
298
+ interrupt(); })
299
+ .catch(finish);
300
+ }).finally(() => executor.codex.driving.delete(threadId));
301
+ message = translator.last();
302
+ }
303
+ catch (error) {
304
+ if (!begun) {
305
+ if (host)
306
+ await closeHost(host, null).catch(() => undefined);
307
+ throw error;
308
+ }
309
+ message = { ...message, stopReason: signal.aborted ? "aborted" : "error", errorMessage: signal.aborted ? null : error instanceof Error ? error.message : String(error) };
310
+ }
311
+ return { session, resume: prepared.resume, message, host };
312
+ }
313
+ async function codexPrompt(input) {
314
+ return await Promise.all(input.messages.flatMap((message) => message.content).map(async (block) => {
315
+ if (block.type === "image") {
316
+ const image = await imageForPi(block);
317
+ return { type: "image", url: `data:${image.mimeType};base64,${image.data}` };
318
+ }
319
+ return { type: "text", text: block.type === "text" ? block.text : JSON.stringify(block), text_elements: [] };
320
+ }));
321
+ }
322
+ function watchHost(host, diagnostics, context) {
323
+ if (!diagnostics)
324
+ return;
325
+ host.onFailure((error) => diagnostics.log("error", "harness.rpc_process_failed", { error: serializeDiagnosticError(error) }, { ...context, component: "harness" }));
326
+ host.onTimeout((method, timeoutMs) => diagnostics.log("error", "harness.rpc_timeout", { method, timeoutMs }, { ...context, component: "harness" }));
327
+ }
328
+ /** Close a harness process Cohub started. An unconfirmable cleanup marks the result uncertain; it never masks it. */
329
+ async function closeHost(host, executionError) {
330
+ return await host.close().then(() => undefined, (cleanupError) => {
331
+ const processGroupId = host.processGroupId;
332
+ if (!(cleanupError instanceof ProcessCleanupUncertainError) || processGroupId == null)
333
+ throw cleanupError;
334
+ const detail = executionError ? `; execution error: ${executionError}` : "";
335
+ return { processGroupId, error: `${cleanupError.message}${detail}` };
336
+ });
337
+ }
@@ -0,0 +1,9 @@
1
+ /** Deterministic UUID-shaped id. A native Turn always names the same cloud Turn. */
2
+ export declare function nativeStableId(value: string): string;
3
+ /**
4
+ * Cloud Turn id of a native Turn. A transcript can reach more than one Space (a directory rebound
5
+ * to another Space, or another account), so the Space is part of the identity; within it, Codex
6
+ * Turn ids are globally unique (a fork shares its history's Turns) and Pi entry ids are per
7
+ * session.
8
+ */
9
+ export declare function nativeTurnId(spaceId: string, harness: "pi" | "codex", nativeSessionId: string, turnKey: string): string;
@@ -0,0 +1,15 @@
1
+ import { createHash } from "node:crypto";
2
+ /** Deterministic UUID-shaped id. A native Turn always names the same cloud Turn. */
3
+ export function nativeStableId(value) {
4
+ const hex = createHash("sha256").update(value).digest("hex");
5
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
6
+ }
7
+ /**
8
+ * Cloud Turn id of a native Turn. A transcript can reach more than one Space (a directory rebound
9
+ * to another Space, or another account), so the Space is part of the identity; within it, Codex
10
+ * Turn ids are globally unique (a fork shares its history's Turns) and Pi entry ids are per
11
+ * session.
12
+ */
13
+ export function nativeTurnId(spaceId, harness, nativeSessionId, turnKey) {
14
+ return nativeStableId(JSON.stringify(harness === "pi" ? ["native", spaceId, "pi", nativeSessionId, turnKey] : ["native", spaceId, "codex", turnKey]));
15
+ }
@@ -0,0 +1,241 @@
1
+ import { type NativeIngestResult, type NativeRuntimeEvent } from "@neta-art/cohub";
2
+ import { type Harness } from "./adapters.js";
3
+ import type { RuntimeArchiveStore } from "../archive-store.js";
4
+ export declare const DEFAULT_IMPORT_CONCURRENCY = 4;
5
+ export declare const MAX_IMPORT_CONCURRENCY = 8;
6
+ export type TranscriptState = {
7
+ harness: Harness;
8
+ path: string;
9
+ nativeSessionId: string;
10
+ cwd: string;
11
+ threadKind: "user" | "internal" | "spawned";
12
+ size: number;
13
+ mtimeMs: number;
14
+ ino: number;
15
+ scannedBytes: number;
16
+ throughTurnKey: string | null;
17
+ throughTurnId: string | null;
18
+ sessionId: string | null;
19
+ runningTurnId: string | null;
20
+ runningTurnKey: string | null;
21
+ headTurnId: string | null;
22
+ origin: "cohub" | "native";
23
+ controllable?: boolean;
24
+ };
25
+ export type ImportFilter = {
26
+ harnesses?: Harness[];
27
+ nativeSessionId?: string;
28
+ };
29
+ /** A backfill of conversations that predate sync. Persisted so a restart resumes it. */
30
+ export type ImportJob = {
31
+ state: "idle" | "running" | "paused" | "done";
32
+ filter: ImportFilter;
33
+ concurrency: number;
34
+ files: number;
35
+ done: number;
36
+ turns: number;
37
+ bytes: number;
38
+ totalBytes: number;
39
+ startedAt: string | null;
40
+ failed: Array<{
41
+ path: string;
42
+ message: string;
43
+ }>;
44
+ };
45
+ export type IngestTransport = {
46
+ ingest(event: Extract<NativeRuntimeEvent, {
47
+ type: "ingest";
48
+ }>, options?: {
49
+ signal?: AbortSignal;
50
+ }): Promise<NativeIngestResult>;
51
+ known(turnIds: string[], options?: {
52
+ signal?: AbortSignal;
53
+ }): Promise<Array<{
54
+ turnId: string;
55
+ sessionId: string;
56
+ settled: boolean;
57
+ }>>;
58
+ progress(event: Extract<NativeRuntimeEvent, {
59
+ type: "progress";
60
+ }>, options?: {
61
+ signal?: AbortSignal;
62
+ }): Promise<{
63
+ accepted?: boolean;
64
+ resync?: boolean;
65
+ } | unknown>;
66
+ status(sessionId: string, turnId: string, controllable?: boolean, options?: {
67
+ signal?: AbortSignal;
68
+ }): Promise<{
69
+ abortRequested: boolean;
70
+ status?: string;
71
+ }>;
72
+ };
73
+ export type IngestHooks = {
74
+ /**
75
+ * Control-plane certainty about a Pi session's file as last written at `mtimeMs`: `true` when the
76
+ * agent settled after that write, `false` while it runs, `null` when the control plane cannot
77
+ * tell.
78
+ */
79
+ piSettled?: (nativeSessionId: string, mtimeMs: number) => boolean | null;
80
+ controllable?: (state: TranscriptState) => boolean;
81
+ abortTurn?: (state: TranscriptState) => void;
82
+ onGrowth?: (state: TranscriptState) => void;
83
+ onRunning?: (state: TranscriptState, turnId: string | null) => void;
84
+ onError?: (error: unknown, context: {
85
+ path: string;
86
+ harness: Harness;
87
+ }) => void;
88
+ };
89
+ /** A transcript on disk that may belong to the bound directory. */
90
+ export type NativeCandidate = {
91
+ harness: Harness;
92
+ path: string;
93
+ size: number;
94
+ mtimeMs: number;
95
+ ino: number;
96
+ };
97
+ /** Whether a session in `cwd` belongs to the directory bound at `root`. */
98
+ export declare const owns: (root: string, cwd: string, nested?: readonly string[]) => boolean;
99
+ /** Every transcript of the bound directory and its subdirectories, newest first. */
100
+ export declare function discoverTranscripts(cwd: string, harnesses: Harness[], options?: {
101
+ headers?: boolean;
102
+ nested?: readonly string[];
103
+ signal?: AbortSignal;
104
+ }): Promise<Array<NativeCandidate & {
105
+ nativeSessionId?: string;
106
+ }>>;
107
+ /**
108
+ * The daemon's data plane. A harness's transcript is both the durable record of a conversation and
109
+ * the local outbox.
110
+ */
111
+ export declare class NativeIngest {
112
+ private readonly options;
113
+ private readonly states;
114
+ private readonly live;
115
+ private readonly busy;
116
+ private readonly reads;
117
+ private readonly timers;
118
+ private readonly failures;
119
+ private readonly aliveAt;
120
+ private readonly runningPaths;
121
+ private writing;
122
+ private readonly foreign;
123
+ private importQueue;
124
+ private runningLive;
125
+ private runningImport;
126
+ private job;
127
+ private stopped;
128
+ private readonly dirty;
129
+ private jobDirty;
130
+ private journalLines;
131
+ private writeTimer;
132
+ private readonly watchers;
133
+ private transport;
134
+ private harnesses;
135
+ private enabledAt;
136
+ private watching;
137
+ private nested;
138
+ constructor(options: {
139
+ spaceId: string;
140
+ cwd: string;
141
+ harnesses: Harness[];
142
+ enabledAt: string;
143
+ root: string;
144
+ archives: RuntimeArchiveStore;
145
+ hooks?: IngestHooks;
146
+ });
147
+ owns(cwd: string): boolean;
148
+ exclude(nested: string[]): void;
149
+ configure(harnesses: Harness[], enabledAt: string): void;
150
+ private get ledgerPath();
151
+ load(): Promise<void>;
152
+ connect(transport: IngestTransport | null): void;
153
+ /**
154
+ * Record that Cohub created or chose this transcript for a Session, before any Turn of it runs.
155
+ */
156
+ adopt(session: {
157
+ harness: Harness;
158
+ path: string;
159
+ nativeSessionId: string;
160
+ cwd: string;
161
+ }, sessionId: string, through?: {
162
+ turnKey: string | null;
163
+ turnId: string | null;
164
+ }): void;
165
+ advance(path: string, turnId: string): void;
166
+ touch(path: string): void;
167
+ running(harness: Harness, nativeSessionId: string): TranscriptState | null;
168
+ stop(sessionId: string, turnId: string): boolean;
169
+ alive(harness: Harness, nativeSessionId: string): void;
170
+ /**
171
+ * A native client came or went, so whether Cohub can stop this session's running Turn may have
172
+ * changed.
173
+ */
174
+ controlChanged(harness: Harness, nativeSessionId: string): void;
175
+ status(): {
176
+ transcripts: number;
177
+ live: number;
178
+ running: number;
179
+ import: {
180
+ state: "idle" | "running" | "paused" | "done";
181
+ filter: ImportFilter;
182
+ concurrency: number;
183
+ files: number;
184
+ done: number;
185
+ turns: number;
186
+ bytes: number;
187
+ totalBytes: number;
188
+ startedAt: string | null;
189
+ failed: Array<{
190
+ path: string;
191
+ message: string;
192
+ }>;
193
+ queued: number;
194
+ running: number;
195
+ };
196
+ };
197
+ transcriptFor(sessionId: string, harness: Harness, headTurnId: string | null): TranscriptState | null;
198
+ scan(): Promise<void>;
199
+ plan(filter?: ImportFilter, signal?: AbortSignal): Promise<Array<NativeCandidate & {
200
+ nativeSessionId?: string;
201
+ }>>;
202
+ /**
203
+ * Start or resume a backfill. What was imported is tracked, so resuming simply plans again; the
204
+ * server recognizes any Turn it already has, so an interrupted batch is never duplicated.
205
+ */
206
+ startImport(filter?: ImportFilter, concurrency?: number): Promise<ImportJob>;
207
+ pauseImport(): ImportJob;
208
+ watch(signal: AbortSignal): Promise<void>;
209
+ private watchHarness;
210
+ private resync;
211
+ private reportControl;
212
+ close(signal: AbortSignal): Promise<void>;
213
+ private unchanged;
214
+ private drainLive;
215
+ private retryLater;
216
+ private drainImport;
217
+ private report;
218
+ private sync;
219
+ private read;
220
+ private crossesBoundary;
221
+ private blank;
222
+ private harnessFor;
223
+ private tailSettled;
224
+ private recheck;
225
+ private known;
226
+ private apply;
227
+ private markRunning;
228
+ private endRunning;
229
+ private cutOff;
230
+ private imported;
231
+ private archive;
232
+ private message;
233
+ private complete;
234
+ private markDirty;
235
+ private markJobDirty;
236
+ private scheduleWrite;
237
+ private write;
238
+ private append;
239
+ private overgrown;
240
+ private compact;
241
+ }