@neta-art/cohub-cli 8.2.0 → 8.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/runtime.d.ts +0 -12
- package/dist/commands/runtime.js +129 -144
- package/dist/commands/search.js +3 -9
- package/dist/commands/spaces.js +5 -2
- package/dist/output.d.ts +6 -0
- package/dist/output.js +11 -0
- package/dist/runtime/codex-usage.d.ts +0 -2
- package/dist/runtime/codex-usage.js +0 -12
- package/dist/runtime/connection.d.ts +5 -5
- package/dist/runtime/connection.js +27 -16
- package/dist/runtime/harness.d.ts +8 -16
- package/dist/runtime/harness.js +1 -366
- package/dist/runtime/instance.d.ts +8 -1
- package/dist/runtime/instance.js +15 -8
- package/dist/runtime/launch.js +11 -5
- package/dist/runtime/native/adapters.d.ts +32 -0
- package/dist/runtime/native/adapters.js +79 -0
- package/dist/runtime/{native-archive.d.ts → native/archive.d.ts} +0 -3
- package/dist/runtime/{native-archive.js → native/archive.js} +2 -17
- package/dist/runtime/native/attach.d.ts +38 -0
- package/dist/runtime/native/attach.js +113 -0
- package/dist/runtime/native/codex-link.d.ts +40 -0
- package/dist/runtime/native/codex-link.js +249 -0
- package/dist/runtime/native/config.d.ts +16 -0
- package/dist/runtime/native/config.js +24 -0
- package/dist/runtime/native/daemon.d.ts +88 -0
- package/dist/runtime/native/daemon.js +367 -0
- package/dist/runtime/native/execution.d.ts +45 -0
- package/dist/runtime/native/execution.js +337 -0
- package/dist/runtime/native/identity.d.ts +9 -0
- package/dist/runtime/native/identity.js +15 -0
- package/dist/runtime/native/ingest.d.ts +241 -0
- package/dist/runtime/native/ingest.js +823 -0
- package/dist/runtime/native/install.d.ts +9 -0
- package/dist/runtime/native/install.js +113 -0
- package/dist/runtime/native/pi-extension.d.ts +35 -0
- package/dist/runtime/native/pi-extension.js +160 -0
- package/dist/runtime/native/pi-link.d.ts +67 -0
- package/dist/runtime/native/pi-link.js +238 -0
- package/dist/runtime/native/results.d.ts +41 -0
- package/dist/runtime/native/results.js +128 -0
- package/dist/runtime/native/sessions.d.ts +45 -0
- package/dist/runtime/native/sessions.js +92 -0
- package/dist/runtime/{native-transcript.d.ts → native/transcript.d.ts} +5 -5
- package/dist/runtime/{native-transcript.js → native/transcript.js} +37 -11
- package/dist/runtime/native/translate.d.ts +44 -0
- package/dist/runtime/native/translate.js +263 -0
- package/dist/runtime/native/transport.d.ts +7 -0
- package/dist/runtime/native/transport.js +16 -0
- package/dist/runtime/native/version.d.ts +5 -0
- package/dist/runtime/native/version.js +18 -0
- package/dist/runtime/presentation.d.ts +7 -10
- package/dist/runtime/presentation.js +35 -10
- package/dist/runtime/projection-store.d.ts +6 -13
- package/dist/runtime/projection-store.js +11 -74
- package/dist/runtime/supervisor.d.ts +2 -0
- package/dist/runtime/supervisor.js +36 -110
- package/dist/runtime/turn-projection.d.ts +1 -3
- package/dist/runtime/turn-projection.js +3 -10
- package/package.json +4 -5
- package/dist/runtime/native-attach.d.ts +0 -28
- package/dist/runtime/native-attach.js +0 -47
- package/dist/runtime/native-codex-hook.d.ts +0 -1
- package/dist/runtime/native-codex-hook.js +0 -28
- package/dist/runtime/native-install.d.ts +0 -21
- package/dist/runtime/native-install.js +0 -130
- package/dist/runtime/native-ipc.d.ts +0 -37
- package/dist/runtime/native-ipc.js +0 -118
- package/dist/runtime/native-pi-extension.d.ts +0 -20
- package/dist/runtime/native-pi-extension.js +0 -47
- package/dist/runtime/native-sync-store.d.ts +0 -116
- package/dist/runtime/native-sync-store.js +0 -495
- package/dist/runtime/native-sync.d.ts +0 -65
- package/dist/runtime/native-sync.js +0 -326
- package/dist/runtime/session-store.d.ts +0 -84
- package/dist/runtime/session-store.js +0 -601
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { serializeDiagnosticError } from "../diagnostics.js";
|
|
2
|
+
import { record } from "../json-rpc.js";
|
|
3
|
+
import { readTranscript } from "./adapters.js";
|
|
4
|
+
import { CodexShared } from "./codex-link.js";
|
|
5
|
+
import { readNativeConfig } from "./config.js";
|
|
6
|
+
import { DEFAULT_IMPORT_CONCURRENCY, NativeIngest } from "./ingest.js";
|
|
7
|
+
import { piExtensionSource, piExtensionState } from "./install.js";
|
|
8
|
+
import { controlSocketPath } from "./pi-extension.js";
|
|
9
|
+
import { PI_MIN_VERSION, PiLink } from "./pi-link.js";
|
|
10
|
+
import { atLeast, formatVersion, harnessVersion } from "./version.js";
|
|
11
|
+
import { ExecutionResults } from "./results.js";
|
|
12
|
+
import { NativeSessions } from "./sessions.js";
|
|
13
|
+
import { codexTranslator, LiveProgress, piTranslator } from "./translate.js";
|
|
14
|
+
import { nativeWebSocketTransport } from "./transport.js";
|
|
15
|
+
import { readRuntimeSpaceBindings } from "../space-binding.js";
|
|
16
|
+
const text = (value) => typeof value === "string" ? value : "";
|
|
17
|
+
/** Streamed previews reach the web at most this often. */
|
|
18
|
+
const STREAM_INTERVAL_MS = 250;
|
|
19
|
+
/** A client Cohub cannot stream is previewed from its file, at most this often. */
|
|
20
|
+
const FILE_PREVIEW_INTERVAL_MS = 2_000;
|
|
21
|
+
/** A file preview waits at least this many times as long as its last read took. */
|
|
22
|
+
const FILE_PREVIEW_COST_FACTOR = 20;
|
|
23
|
+
/**
|
|
24
|
+
* Everything the Runtime does with native sessions: the ingest engine (data plane), the Pi and
|
|
25
|
+
* Codex links (control plane), and the executor that runs Cohub Turns through them.
|
|
26
|
+
*/
|
|
27
|
+
export class NativeRuntime {
|
|
28
|
+
options;
|
|
29
|
+
ingest;
|
|
30
|
+
executor;
|
|
31
|
+
pi;
|
|
32
|
+
codex;
|
|
33
|
+
streams = new Map();
|
|
34
|
+
filePreviews = new Map();
|
|
35
|
+
watched = new Map();
|
|
36
|
+
active = new Set();
|
|
37
|
+
send = null;
|
|
38
|
+
extension = "missing";
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.options = options;
|
|
41
|
+
this.pi = new PiLink({
|
|
42
|
+
socketPath: controlSocketPath(options.root), owns: (cwd) => this.ingest.owns(cwd),
|
|
43
|
+
onEvent: (link, event) => this.onPiEvent(link, event),
|
|
44
|
+
onConnect: (link) => this.ingest.controlChanged("pi", link.sessionId),
|
|
45
|
+
onDisconnect: (link) => {
|
|
46
|
+
this.endStream(`pi:${link.sessionId}`);
|
|
47
|
+
this.ingest.controlChanged("pi", link.sessionId);
|
|
48
|
+
if (link.sessionFile)
|
|
49
|
+
this.ingest.touch(link.sessionFile);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
this.codex = new CodexShared(options.executables.codex || "codex", options.root, Boolean(options.config?.codexShared) && options.harnesses.includes("codex"));
|
|
53
|
+
this.codex.onConnect((channel) => this.observeCodex(channel));
|
|
54
|
+
this.ingest = new NativeIngest({
|
|
55
|
+
spaceId: options.spaceId,
|
|
56
|
+
cwd: options.root,
|
|
57
|
+
harnesses: this.synced,
|
|
58
|
+
enabledAt: options.config?.enabledAt ?? new Date().toISOString(),
|
|
59
|
+
root: options.stateRoot,
|
|
60
|
+
archives: options.archives,
|
|
61
|
+
hooks: {
|
|
62
|
+
piSettled: (nativeSessionId, mtimeMs) => {
|
|
63
|
+
const link = this.pi.find(nativeSessionId);
|
|
64
|
+
if (!link)
|
|
65
|
+
return null;
|
|
66
|
+
if (!link.idle)
|
|
67
|
+
return false;
|
|
68
|
+
// Idle and nothing written since it settled: done. A newer write lets the file decide.
|
|
69
|
+
return link.settledAt >= mtimeMs ? true : null;
|
|
70
|
+
},
|
|
71
|
+
controllable: (state) => state.harness === "pi"
|
|
72
|
+
? this.pi.find(state.nativeSessionId) !== null
|
|
73
|
+
: this.codex.enabled && (this.active.has(state.nativeSessionId) || this.watched.has(state.nativeSessionId) || this.codex.driving.has(state.nativeSessionId)),
|
|
74
|
+
abortTurn: (state) => {
|
|
75
|
+
const stop = state.harness === "pi"
|
|
76
|
+
? this.pi.abort(state.nativeSessionId)
|
|
77
|
+
: state.runningTurnKey ? this.codex.interrupt(state.nativeSessionId, state.runningTurnKey) : Promise.resolve(false);
|
|
78
|
+
void stop.catch((error) => options.diagnostics.log("warn", "native.stop_failed", { error: serializeDiagnosticError(error) }, { harness: state.harness, sessionId: state.sessionId ?? undefined }));
|
|
79
|
+
},
|
|
80
|
+
onGrowth: (state) => this.onGrowth(state),
|
|
81
|
+
onRunning: (state, turnId) => this.onRunning(state, turnId),
|
|
82
|
+
onError: (error, context) => options.diagnostics.log("warn", "native.sync_pending", { path: context.path, error: serializeDiagnosticError(error) }, { component: "native", harness: context.harness }),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const results = new ExecutionResults(options.stateRoot);
|
|
86
|
+
this.executor = {
|
|
87
|
+
cwd: options.root,
|
|
88
|
+
executables: options.executables,
|
|
89
|
+
sessions: new NativeSessions({
|
|
90
|
+
root: options.stateRoot, spaceId: options.spaceId, projectionSource: options.projectionSource, archives: options.archives, ingest: this.ingest,
|
|
91
|
+
onRestoreFailed: (error, context) => options.diagnostics.log("warn", "native.restore_failed", { error: serializeDiagnosticError(error), unrestorable: context.unrestorable }, { component: "archive", harness: context.harness, sessionId: context.sessionId }),
|
|
92
|
+
}),
|
|
93
|
+
results,
|
|
94
|
+
ingest: this.ingest,
|
|
95
|
+
archives: options.archives,
|
|
96
|
+
pi: this.pi,
|
|
97
|
+
codex: this.codex,
|
|
98
|
+
piExtension: piExtensionSource(),
|
|
99
|
+
diagnostics: options.diagnostics,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
get synced() {
|
|
103
|
+
const { config, root, harnesses } = this.options;
|
|
104
|
+
return config?.root === root ? config.harnesses.filter((harness) => harnesses.includes(harness)) : [];
|
|
105
|
+
}
|
|
106
|
+
async start(signal) {
|
|
107
|
+
const degraded = (event) => (error) => this.options.diagnostics.log("warn", event, { error: serializeDiagnosticError(error) }, { component: "native" });
|
|
108
|
+
await this.ingest.load().catch(degraded("native.ledger_unreadable"));
|
|
109
|
+
await this.refreshScope();
|
|
110
|
+
if (this.options.harnesses.includes("pi")) {
|
|
111
|
+
const version = await harnessVersion(this.options.executables.pi || "pi");
|
|
112
|
+
if (!atLeast(version, PI_MIN_VERSION))
|
|
113
|
+
this.pi.unavailable = `Pi ${formatVersion(version)} predates the Cohub extension (${PI_MIN_VERSION.join(".")}+); upgrade Pi, then restart the Runtime`;
|
|
114
|
+
else
|
|
115
|
+
await this.pi.listen(signal).catch((error) => {
|
|
116
|
+
this.pi.unavailable = `Pi control socket unavailable (${serializeDiagnosticError(error).message}); restart the Runtime`;
|
|
117
|
+
degraded("native.pi_socket_unavailable")(error);
|
|
118
|
+
});
|
|
119
|
+
this.extension = await piExtensionState().catch(() => "missing");
|
|
120
|
+
}
|
|
121
|
+
await this.ingest.watch(signal).catch(degraded("native.watch_unavailable"));
|
|
122
|
+
// Connecting early lets a terminal Codex find the server before the first Cohub Turn needs it.
|
|
123
|
+
if (this.codex.enabled)
|
|
124
|
+
void this.codex.get();
|
|
125
|
+
}
|
|
126
|
+
connect(send) {
|
|
127
|
+
this.send = send;
|
|
128
|
+
this.ingest.connect(send ? nativeWebSocketTransport(send) : null);
|
|
129
|
+
}
|
|
130
|
+
stop(sessionId, turnId) {
|
|
131
|
+
return this.ingest.stop(sessionId, turnId);
|
|
132
|
+
}
|
|
133
|
+
status() {
|
|
134
|
+
const ingest = this.ingest.status();
|
|
135
|
+
return {
|
|
136
|
+
synced: this.synced,
|
|
137
|
+
transcripts: ingest.transcripts,
|
|
138
|
+
running: ingest.running,
|
|
139
|
+
pi: this.options.harnesses.includes("pi") ? { extension: this.extension, connected: this.pi.list().length, unavailable: this.pi.unavailable } : null,
|
|
140
|
+
codex: this.options.harnesses.includes("codex") ? { control: this.codex.enabled ? "shared" : "private", watching: this.watched.size } : null,
|
|
141
|
+
import: ingest.import,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async control(action, message) {
|
|
145
|
+
if (action === "reload")
|
|
146
|
+
return await this.reload();
|
|
147
|
+
if (action !== "import")
|
|
148
|
+
throw new Error("Unknown Runtime control request");
|
|
149
|
+
if (!this.synced.length)
|
|
150
|
+
throw new Error("Native sync is not enabled for this directory; run cohub runtime up");
|
|
151
|
+
const filter = {
|
|
152
|
+
...(Array.isArray(message.harnesses) && message.harnesses.length ? { harnesses: message.harnesses } : {}),
|
|
153
|
+
...(typeof message.nativeSessionId === "string" && message.nativeSessionId ? { nativeSessionId: message.nativeSessionId } : {}),
|
|
154
|
+
};
|
|
155
|
+
if (message.command === "pause")
|
|
156
|
+
return this.ingest.pauseImport();
|
|
157
|
+
if (message.command === "start")
|
|
158
|
+
return await this.ingest.startImport(filter, typeof message.concurrency === "number" ? message.concurrency : DEFAULT_IMPORT_CONCURRENCY);
|
|
159
|
+
const plan = await this.ingest.plan(filter);
|
|
160
|
+
return {
|
|
161
|
+
files: plan.length,
|
|
162
|
+
bytes: plan.reduce((sum, candidate) => sum + candidate.size, 0),
|
|
163
|
+
// Enough to show what an import would bring in; the full list stays in the Runtime.
|
|
164
|
+
sample: plan.slice(0, 20).map(({ harness, path, nativeSessionId, mtimeMs, size }) => ({ harness, path, nativeSessionId, mtimeMs, size })),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async reload() {
|
|
168
|
+
await this.refreshScope();
|
|
169
|
+
this.options.config = await readNativeConfig(this.options.stateRoot, this.options.identity);
|
|
170
|
+
this.ingest.configure(this.synced, this.options.config?.enabledAt ?? new Date().toISOString());
|
|
171
|
+
this.codex.enabled = Boolean(this.options.config?.codexShared) && this.options.harnesses.includes("codex");
|
|
172
|
+
if (this.codex.enabled)
|
|
173
|
+
void this.codex.get();
|
|
174
|
+
if (this.options.harnesses.includes("pi"))
|
|
175
|
+
this.extension = await piExtensionState().catch(() => "missing");
|
|
176
|
+
return this.status();
|
|
177
|
+
}
|
|
178
|
+
async refreshScope() {
|
|
179
|
+
const { bindings } = await readRuntimeSpaceBindings().catch(() => ({ bindings: [] }));
|
|
180
|
+
this.ingest.exclude(bindings.filter((binding) => binding.key === this.options.identity && binding.spaceId !== this.options.spaceId).map((binding) => binding.root));
|
|
181
|
+
}
|
|
182
|
+
async close(signal) {
|
|
183
|
+
for (const stream of this.streams.values())
|
|
184
|
+
stream.progress.dispose();
|
|
185
|
+
for (const preview of this.filePreviews.values()) {
|
|
186
|
+
preview.progress.dispose();
|
|
187
|
+
if (preview.timer)
|
|
188
|
+
clearTimeout(preview.timer);
|
|
189
|
+
}
|
|
190
|
+
await Promise.allSettled([this.pi.close(), this.codex.close(), this.ingest.close(signal)]);
|
|
191
|
+
}
|
|
192
|
+
sink(harness, nativeSessionId) {
|
|
193
|
+
return (progress) => {
|
|
194
|
+
const running = this.ingest.running(harness, nativeSessionId);
|
|
195
|
+
if (!running?.sessionId || !running.runningTurnId || !this.send)
|
|
196
|
+
return null;
|
|
197
|
+
return this.send({ type: "progress", sessionId: running.sessionId, turnId: running.runningTurnId, progress }).catch(() => ({ accepted: false }));
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* A connected Pi. A user prompt starting and the agent settling are the two moments its file has
|
|
202
|
+
* something to persist; everything between streams as a preview of a Turn someone runs there.
|
|
203
|
+
*/
|
|
204
|
+
onPiEvent(link, event) {
|
|
205
|
+
const key = `pi:${link.sessionId}`;
|
|
206
|
+
const type = text(event.type);
|
|
207
|
+
const userPrompt = type === "message_end" && record(event.message).role === "user";
|
|
208
|
+
if (userPrompt || type === "agent_settled") {
|
|
209
|
+
if (link.sessionFile)
|
|
210
|
+
this.ingest.touch(link.sessionFile);
|
|
211
|
+
}
|
|
212
|
+
if (type === "agent_start")
|
|
213
|
+
this.endStream(key);
|
|
214
|
+
// An automatic retry follows `agent_end` within the same Turn; only settling ends it.
|
|
215
|
+
if (type === "agent_settled") {
|
|
216
|
+
this.endStream(key);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.ingest.alive("pi", link.sessionId);
|
|
220
|
+
// A Turn Cohub runs streams through its own execution; this is only a terminal user's Turn.
|
|
221
|
+
if (!this.ingest.running("pi", link.sessionId) && !userPrompt && !this.streams.has(key))
|
|
222
|
+
return;
|
|
223
|
+
this.stream(key, "pi", link.sessionId).translator.push(event);
|
|
224
|
+
}
|
|
225
|
+
stream(key, harness, nativeSessionId) {
|
|
226
|
+
let stream = this.streams.get(key);
|
|
227
|
+
if (!stream) {
|
|
228
|
+
const progress = new LiveProgress(this.sink(harness, nativeSessionId), STREAM_INTERVAL_MS);
|
|
229
|
+
const emit = (update) => progress.apply(update);
|
|
230
|
+
stream = { progress, translator: harness === "pi" ? piTranslator(emit) : codexTranslator(emit, { provider: null, model: null }) };
|
|
231
|
+
this.streams.set(key, stream);
|
|
232
|
+
}
|
|
233
|
+
return stream;
|
|
234
|
+
}
|
|
235
|
+
endStream(key) {
|
|
236
|
+
this.streams.get(key)?.progress.dispose();
|
|
237
|
+
this.streams.delete(key);
|
|
238
|
+
}
|
|
239
|
+
endFilePreview(path) {
|
|
240
|
+
const preview = this.filePreviews.get(path);
|
|
241
|
+
if (!preview)
|
|
242
|
+
return;
|
|
243
|
+
preview.progress.dispose();
|
|
244
|
+
if (preview.timer)
|
|
245
|
+
clearTimeout(preview.timer);
|
|
246
|
+
this.filePreviews.delete(path);
|
|
247
|
+
}
|
|
248
|
+
observeCodex(channel) {
|
|
249
|
+
const offEvent = channel.onEvent((event) => this.onCodexEvent(event));
|
|
250
|
+
const offClose = channel.onClose(() => {
|
|
251
|
+
offEvent();
|
|
252
|
+
offClose();
|
|
253
|
+
// Subscriptions die with the connection; the next activity in a rollout reconnects.
|
|
254
|
+
const lost = new Set([...this.watched.keys(), ...this.active]);
|
|
255
|
+
this.watched.clear();
|
|
256
|
+
this.active.clear();
|
|
257
|
+
for (const key of [...this.streams.keys()])
|
|
258
|
+
if (key.startsWith("codex:"))
|
|
259
|
+
this.endStream(key);
|
|
260
|
+
for (const threadId of lost)
|
|
261
|
+
this.ingest.controlChanged("codex", threadId);
|
|
262
|
+
});
|
|
263
|
+
void channel.request("thread/loaded/list", {}).then((result) => {
|
|
264
|
+
for (const threadId of Array.isArray(result.data) ? result.data : [])
|
|
265
|
+
if (typeof threadId === "string")
|
|
266
|
+
void this.watchCodexThread(channel, threadId, false);
|
|
267
|
+
}).catch(() => undefined);
|
|
268
|
+
}
|
|
269
|
+
async watchCodexThread(channel, threadId, busy) {
|
|
270
|
+
if (this.watched.has(threadId))
|
|
271
|
+
return;
|
|
272
|
+
const read = await channel.request("thread/read", { threadId }).catch(() => null);
|
|
273
|
+
const thread = record(read?.thread);
|
|
274
|
+
if (!busy && record(thread.status).type !== "active")
|
|
275
|
+
return;
|
|
276
|
+
// Only this project's conversations; sub-agents and ephemeral threads are not chats.
|
|
277
|
+
if (!this.ingest.owns(text(thread.cwd)) || thread.parentThreadId || thread.ephemeral === true)
|
|
278
|
+
return;
|
|
279
|
+
this.active.add(threadId);
|
|
280
|
+
if (this.codex.driving.has(threadId))
|
|
281
|
+
return;
|
|
282
|
+
this.watched.set(threadId, { path: text(thread.path) || null });
|
|
283
|
+
this.ingest.controlChanged("codex", threadId);
|
|
284
|
+
await this.codex.join(threadId).catch((error) => {
|
|
285
|
+
this.watched.delete(threadId);
|
|
286
|
+
this.options.diagnostics.log("debug", "native.codex_join_failed", { error: serializeDiagnosticError(error) }, { harness: "codex" });
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
onCodexEvent(event) {
|
|
290
|
+
// Approvals of other clients' Turns are theirs to answer.
|
|
291
|
+
if (event.id != null)
|
|
292
|
+
return;
|
|
293
|
+
const threadId = text(event.params.threadId);
|
|
294
|
+
if (!threadId)
|
|
295
|
+
return;
|
|
296
|
+
if (event.method === "thread/status/changed") {
|
|
297
|
+
if (record(event.params.status).type === "active")
|
|
298
|
+
void this.codex.get().then((channel) => channel && this.watchCodexThread(channel, threadId, true));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const watched = this.watched.get(threadId);
|
|
302
|
+
// A Turn Cohub runs streams through its own execution.
|
|
303
|
+
if (!watched || this.codex.driving.has(threadId))
|
|
304
|
+
return;
|
|
305
|
+
this.ingest.alive("codex", threadId);
|
|
306
|
+
const key = `codex:${threadId}`;
|
|
307
|
+
if (event.method === "turn/started")
|
|
308
|
+
this.endStream(key);
|
|
309
|
+
if (event.method === "turn/completed") {
|
|
310
|
+
this.endStream(key);
|
|
311
|
+
if (watched.path)
|
|
312
|
+
this.ingest.touch(watched.path);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
this.stream(key, "codex", threadId).translator.push(event);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* A terminal Pi runs many Turns in one process; once the ledger knows which one is running, its
|
|
319
|
+
* tools are told, so a `cohub` call from a tool names the right Space, Session and Turn.
|
|
320
|
+
*/
|
|
321
|
+
onRunning(state, turnId) {
|
|
322
|
+
if (!turnId)
|
|
323
|
+
this.endFilePreview(state.path);
|
|
324
|
+
if (state.harness !== "pi" || !state.sessionId || !this.pi.find(state.nativeSessionId))
|
|
325
|
+
return;
|
|
326
|
+
void this.pi.context(state.nativeSessionId, { COHUB_SPACE_ID: this.options.spaceId, COHUB_SESSION_ID: state.sessionId, COHUB_TURN_ID: turnId }).catch(() => undefined);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* A running Turn's file grew. A connected client streams it already; otherwise the preview is
|
|
330
|
+
* read from the file, at a pace set by a timer, not by every write.
|
|
331
|
+
*/
|
|
332
|
+
onGrowth(state) {
|
|
333
|
+
this.ingest.alive(state.harness, state.nativeSessionId);
|
|
334
|
+
if (state.harness === "codex" && this.codex.enabled && !this.watched.has(state.nativeSessionId))
|
|
335
|
+
void this.codex.get();
|
|
336
|
+
const streamed = state.harness === "pi" ? this.pi.find(state.nativeSessionId) !== null : this.watched.has(state.nativeSessionId);
|
|
337
|
+
if (streamed)
|
|
338
|
+
return;
|
|
339
|
+
let preview = this.filePreviews.get(state.path);
|
|
340
|
+
if (!preview) {
|
|
341
|
+
preview = { progress: new LiveProgress(this.sink(state.harness, state.nativeSessionId), 0), timer: null, readMs: 0 };
|
|
342
|
+
this.filePreviews.set(state.path, preview);
|
|
343
|
+
}
|
|
344
|
+
const entry = preview;
|
|
345
|
+
// A preview re-reads the file; a long transcript is previewed less often, so it costs a bounded share of time.
|
|
346
|
+
entry.timer ??= setTimeout(() => {
|
|
347
|
+
if (!state.runningTurnId) {
|
|
348
|
+
this.endFilePreview(state.path);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const startedAt = performance.now();
|
|
352
|
+
void readTranscript(state.path, state.harness).then((transcript) => {
|
|
353
|
+
if (!state.runningTurnId) {
|
|
354
|
+
this.endFilePreview(state.path);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const tail = transcript.turns.at(-1);
|
|
358
|
+
entry.progress.replace((tail?.messages ?? []).map((message) => ({ content: message.content, provider: message.provider ?? null, model: message.model ?? null })));
|
|
359
|
+
}).catch(() => undefined).finally(() => {
|
|
360
|
+
entry.readMs = performance.now() - startedAt;
|
|
361
|
+
// Growth during the read schedules the next one only now: reads never overlap.
|
|
362
|
+
entry.timer = null;
|
|
363
|
+
});
|
|
364
|
+
}, Math.max(FILE_PREVIEW_INTERVAL_MS, FILE_PREVIEW_COST_FACTOR * entry.readMs));
|
|
365
|
+
entry.timer.unref?.();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { RuntimeExecutionEvent, RuntimeTurnInput } from "@neta-art/cohub";
|
|
2
|
+
import { type HarnessOptions } from "../harness.js";
|
|
3
|
+
import type { RuntimeArchiveStore } from "../archive-store.js";
|
|
4
|
+
import { type RuntimeDiagnosticContext, type RuntimeDiagnostics } from "../diagnostics.js";
|
|
5
|
+
import { type CodexShared } from "./codex-link.js";
|
|
6
|
+
import type { NativeIngest } from "./ingest.js";
|
|
7
|
+
import { type PiLink } from "./pi-link.js";
|
|
8
|
+
import type { ExecutionResults } from "./results.js";
|
|
9
|
+
import type { NativeSession, NativeSessions } from "./sessions.js";
|
|
10
|
+
export type Executor = {
|
|
11
|
+
cwd: string;
|
|
12
|
+
executables: HarnessOptions;
|
|
13
|
+
sessions: NativeSessions;
|
|
14
|
+
results: ExecutionResults;
|
|
15
|
+
ingest: NativeIngest;
|
|
16
|
+
archives: RuntimeArchiveStore;
|
|
17
|
+
pi: PiLink;
|
|
18
|
+
codex: CodexShared;
|
|
19
|
+
piExtension: string;
|
|
20
|
+
diagnostics?: RuntimeDiagnostics;
|
|
21
|
+
};
|
|
22
|
+
export type HarnessResult = {
|
|
23
|
+
session: NativeSession;
|
|
24
|
+
event: Extract<RuntimeExecutionEvent, {
|
|
25
|
+
type: "turn.end";
|
|
26
|
+
}>;
|
|
27
|
+
uncertainCleanup?: {
|
|
28
|
+
processGroupId: number;
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
type Turn = {
|
|
33
|
+
input: RuntimeTurnInput;
|
|
34
|
+
emit: (event: RuntimeExecutionEvent) => void;
|
|
35
|
+
signal: AbortSignal;
|
|
36
|
+
diverged: string | null;
|
|
37
|
+
started: (session: NativeSession) => Promise<void>;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Run one Cohub Turn in a local harness. Both harnesses are driven through the same interface a
|
|
41
|
+
* terminal user shares: Pi through the Cohub extension inside the Pi process, Codex through its
|
|
42
|
+
* app-server.
|
|
43
|
+
*/
|
|
44
|
+
export declare function executeTurn(executor: Executor, input: RuntimeTurnInput, emit: Turn["emit"], signal: AbortSignal, requestId: string | null, context?: RuntimeDiagnosticContext): Promise<HarnessResult>;
|
|
45
|
+
export {};
|