@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.
- package/dist/commands/runtime.d.ts +0 -12
- package/dist/commands/runtime.js +129 -119
- 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/archive-store.d.ts +4 -0
- package/dist/runtime/archive-store.js +10 -6
- 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 +10 -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} +4 -3
- package/dist/runtime/{native-archive.js → native/archive.js} +7 -14
- package/dist/runtime/native/attach.d.ts +33 -0
- package/dist/runtime/native/attach.js +103 -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 +105 -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 +63 -0
- package/dist/runtime/native/transcript.js +594 -0
- 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 -86
- 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 -102
- package/dist/runtime/native-sync-store.js +0 -368
- package/dist/runtime/native-sync.d.ts +0 -50
- package/dist/runtime/native-sync.js +0 -221
- package/dist/runtime/native-transcript.d.ts +0 -35
- package/dist/runtime/native-transcript.js +0 -310
- package/dist/runtime/session-store.d.ts +0 -84
- package/dist/runtime/session-store.js +0 -587
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { codexItemContent, piContent } from "../harness.js";
|
|
2
|
+
import { codexTokenTotals, codexUsage, subtractCodexTokens } from "../codex-usage.js";
|
|
3
|
+
import { record } from "../json-rpc.js";
|
|
4
|
+
const text = (value) => typeof value === "string" ? value : "";
|
|
5
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
6
|
+
/** Pi emits the same event shapes to its extensions as over RPC. */
|
|
7
|
+
export function piTranslator(emit) {
|
|
8
|
+
let ordinal = -1;
|
|
9
|
+
let current = [];
|
|
10
|
+
let last = { ordinal: 0, content: [] };
|
|
11
|
+
return {
|
|
12
|
+
last: () => last,
|
|
13
|
+
push(event) {
|
|
14
|
+
const type = text(event.type);
|
|
15
|
+
const message = record(event.message);
|
|
16
|
+
if (type === "message_start" && message.role === "assistant") {
|
|
17
|
+
ordinal += 1;
|
|
18
|
+
current = [];
|
|
19
|
+
emit({ type: "message.start", ordinal });
|
|
20
|
+
}
|
|
21
|
+
if (type === "message_end" && message.role === "assistant") {
|
|
22
|
+
current = piContent(message.content);
|
|
23
|
+
emit({ type: "content.replace", ordinal, content: current });
|
|
24
|
+
}
|
|
25
|
+
if (type === "tool_execution_start") {
|
|
26
|
+
const id = text(event.toolCallId);
|
|
27
|
+
if (!current.some((block) => block.type === "tool_use" && block.id === id))
|
|
28
|
+
current.push({ type: "tool_use", id, name: text(event.toolName), input: record(event.args), _meta: { toolStatus: "running" } });
|
|
29
|
+
emit({ type: "content.replace", ordinal, content: [...current] });
|
|
30
|
+
}
|
|
31
|
+
if (type === "tool_execution_update" || type === "tool_execution_end") {
|
|
32
|
+
const id = text(event.toolCallId);
|
|
33
|
+
const raw = type === "tool_execution_end" ? event.result : event.partialResult;
|
|
34
|
+
const resultContent = typeof raw === "string" ? raw : piContent(record(raw).content);
|
|
35
|
+
current = current.filter((block) => block.type !== "tool_result" || block.tool_use_id !== id);
|
|
36
|
+
current.push({ type: "tool_result", tool_use_id: id, content: resultContent, is_error: Boolean(event.isError), _meta: { toolStatus: type === "tool_execution_end" ? "done" : "running" } });
|
|
37
|
+
emit({ type: "content.replace", ordinal, content: [...current] });
|
|
38
|
+
}
|
|
39
|
+
if (type === "message_update") {
|
|
40
|
+
const delta = record(event.assistantMessageEvent);
|
|
41
|
+
if (delta.type === "text_delta" || delta.type === "thinking_delta")
|
|
42
|
+
emit({ type: "text.delta", ordinal, index: Number(delta.contentIndex ?? 0), kind: delta.type === "text_delta" ? "text" : "thinking", delta: text(delta.delta) });
|
|
43
|
+
}
|
|
44
|
+
if (type === "turn_end") {
|
|
45
|
+
const content = piContent(message.content);
|
|
46
|
+
for (const value of list(event.toolResults)) {
|
|
47
|
+
const result = record(value);
|
|
48
|
+
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) });
|
|
49
|
+
}
|
|
50
|
+
const stopReason = text(message.stopReason);
|
|
51
|
+
last = { ordinal: Math.max(0, ordinal), content, provider: text(message.provider) || null, model: text(message.model) || null, usage: message.usage, stopReason: stopReason === "toolUse" ? "tool_use" : stopReason || "stop", errorMessage: text(message.errorMessage) || null };
|
|
52
|
+
emit({ type: "content.replace", ordinal: last.ordinal, content });
|
|
53
|
+
if (last.stopReason === "tool_use")
|
|
54
|
+
emit({ type: "message.commit", message: last });
|
|
55
|
+
}
|
|
56
|
+
// The run ends when Pi settles: an automatic retry or compaction continues after `agent_end`,
|
|
57
|
+
// and extensions are not told whether one follows.
|
|
58
|
+
return type === "agent_settled" || type === "agent_end" && event.willRetry === false;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Codex app-server notifications for one Turn of one thread. */
|
|
63
|
+
export function codexTranslator(emit, identity) {
|
|
64
|
+
let ordinal = -1;
|
|
65
|
+
const items = new Map();
|
|
66
|
+
const ordinals = new Map();
|
|
67
|
+
const outputs = new Map();
|
|
68
|
+
const completed = new Set();
|
|
69
|
+
let pending = null;
|
|
70
|
+
let final = { ordinal: 0, content: [] };
|
|
71
|
+
let usage;
|
|
72
|
+
let baseline;
|
|
73
|
+
const { provider, model } = identity;
|
|
74
|
+
const latest = () => {
|
|
75
|
+
if (pending && pending.ordinal >= ordinal)
|
|
76
|
+
return pending;
|
|
77
|
+
const item = [...items.entries()].find(([id]) => ordinals.get(id) === ordinal)?.[1];
|
|
78
|
+
return { ordinal: Math.max(0, ordinal), content: item ? codexItemContent(item) : [] };
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
last: () => usage ? { ...final, usage } : final,
|
|
82
|
+
push(event) {
|
|
83
|
+
const method = text(event.method);
|
|
84
|
+
const params = record(event.params);
|
|
85
|
+
if (method === "thread/tokenUsage/updated") {
|
|
86
|
+
const tokenUsage = record(params.tokenUsage);
|
|
87
|
+
const total = codexTokenTotals(tokenUsage.total);
|
|
88
|
+
baseline ??= subtractCodexTokens(total, codexTokenTotals(tokenUsage.last));
|
|
89
|
+
usage = codexUsage(subtractCodexTokens(total, baseline));
|
|
90
|
+
}
|
|
91
|
+
if (method === "item/started") {
|
|
92
|
+
const item = record(params.item);
|
|
93
|
+
if (item.type === "userMessage" || completed.has(text(item.id)))
|
|
94
|
+
return false;
|
|
95
|
+
const existing = ordinals.get(text(item.id));
|
|
96
|
+
if (pending && (existing == null || existing > pending.ordinal)) {
|
|
97
|
+
emit({ type: "message.commit", message: pending });
|
|
98
|
+
pending = null;
|
|
99
|
+
}
|
|
100
|
+
if (existing != null) {
|
|
101
|
+
items.set(text(item.id), item);
|
|
102
|
+
if (item.type === "agentMessage" && text(item.text))
|
|
103
|
+
emit({ type: "content.replace", ordinal: existing, content: codexItemContent(item) });
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
ordinal += 1;
|
|
107
|
+
ordinals.set(text(item.id), ordinal);
|
|
108
|
+
items.set(text(item.id), item);
|
|
109
|
+
emit({ type: "message.start", ordinal });
|
|
110
|
+
if (!["agentMessage", "reasoning", "plan"].includes(text(item.type)))
|
|
111
|
+
emit({ type: "content.replace", ordinal, content: codexItemContent(item).filter((block) => block.type !== "tool_result") });
|
|
112
|
+
}
|
|
113
|
+
if (method === "item/agentMessage/delta" || method === "item/reasoning/textDelta" || method === "item/reasoning/summaryTextDelta") {
|
|
114
|
+
const id = text(params.itemId);
|
|
115
|
+
if (completed.has(id))
|
|
116
|
+
return false;
|
|
117
|
+
const itemOrdinal = ordinals.get(id);
|
|
118
|
+
const item = items.get(id);
|
|
119
|
+
if (item) {
|
|
120
|
+
if (method === "item/agentMessage/delta")
|
|
121
|
+
item.text = text(item.text) + text(params.delta);
|
|
122
|
+
else {
|
|
123
|
+
const key = method.includes("summary") ? "summary" : "content";
|
|
124
|
+
item[key] = [list(item[key]).map(text).join("\n") + text(params.delta)];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (itemOrdinal != null)
|
|
128
|
+
emit({ type: "text.delta", ordinal: itemOrdinal, index: 0, kind: method === "item/agentMessage/delta" ? "text" : "thinking", delta: text(params.delta) });
|
|
129
|
+
}
|
|
130
|
+
if (method === "item/commandExecution/outputDelta") {
|
|
131
|
+
const id = text(params.itemId);
|
|
132
|
+
const item = items.get(id);
|
|
133
|
+
const itemOrdinal = ordinals.get(id);
|
|
134
|
+
if (item && itemOrdinal != null) {
|
|
135
|
+
const output = (outputs.get(id) ?? "") + text(params.delta);
|
|
136
|
+
outputs.set(id, output);
|
|
137
|
+
item.aggregatedOutput = output;
|
|
138
|
+
emit({ type: "content.replace", ordinal: itemOrdinal, content: codexItemContent(item) });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (method === "item/completed") {
|
|
142
|
+
const item = record(params.item);
|
|
143
|
+
if (item.type === "userMessage" || completed.has(text(item.id)))
|
|
144
|
+
return false;
|
|
145
|
+
completed.add(text(item.id));
|
|
146
|
+
const itemOrdinal = ordinals.get(text(item.id)) ?? ++ordinal;
|
|
147
|
+
if (!ordinals.has(text(item.id))) {
|
|
148
|
+
ordinals.set(text(item.id), itemOrdinal);
|
|
149
|
+
emit({ type: "message.start", ordinal: itemOrdinal });
|
|
150
|
+
}
|
|
151
|
+
items.set(text(item.id), item);
|
|
152
|
+
const message = { ordinal: itemOrdinal, content: codexItemContent(item), provider, model, stopReason: "stop" };
|
|
153
|
+
emit({ type: "content.replace", ordinal: itemOrdinal, content: message.content });
|
|
154
|
+
if (pending && pending.ordinal > itemOrdinal)
|
|
155
|
+
emit({ type: "message.commit", message });
|
|
156
|
+
else {
|
|
157
|
+
if (pending && pending.ordinal !== itemOrdinal)
|
|
158
|
+
emit({ type: "message.commit", message: pending });
|
|
159
|
+
pending = message;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (method === "turn/completed") {
|
|
163
|
+
const turn = record(params.turn);
|
|
164
|
+
final = { ...latest(), provider, model, stopReason: turn.status === "interrupted" ? "aborted" : turn.status === "failed" ? "error" : "stop", errorMessage: text(record(turn.error).message) || null };
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Revisions only grow, across every preview of this process: the server keeps the highest one, so a
|
|
173
|
+
* preview restarted within the same millisecond, or after the clock stepped back, still lands.
|
|
174
|
+
*/
|
|
175
|
+
let lastRevision = 0;
|
|
176
|
+
const nextRevision = () => (lastRevision = Math.max(Date.now(), lastRevision + 1));
|
|
177
|
+
/**
|
|
178
|
+
* The live preview of a Turn a terminal user runs: harness events folded into messages and sent as
|
|
179
|
+
* ephemeral progress, at most once per interval and only from the first message that changed.
|
|
180
|
+
*/
|
|
181
|
+
export class LiveProgress {
|
|
182
|
+
sink;
|
|
183
|
+
intervalMs;
|
|
184
|
+
messages = [];
|
|
185
|
+
dirtyFrom = Number.POSITIVE_INFINITY;
|
|
186
|
+
synced = false;
|
|
187
|
+
timer = null;
|
|
188
|
+
inFlight = false;
|
|
189
|
+
disposed = false;
|
|
190
|
+
constructor(sink, intervalMs) {
|
|
191
|
+
this.sink = sink;
|
|
192
|
+
this.intervalMs = intervalMs;
|
|
193
|
+
}
|
|
194
|
+
apply(event) {
|
|
195
|
+
const ordinal = event.type === "message.start" || event.type === "content.replace" || event.type === "text.delta" ? event.ordinal
|
|
196
|
+
: event.type === "message.commit" || event.type === "turn.end" ? event.message.ordinal : null;
|
|
197
|
+
if (ordinal === null)
|
|
198
|
+
return;
|
|
199
|
+
const target = this.at(ordinal);
|
|
200
|
+
if (event.type === "content.replace")
|
|
201
|
+
target.content = event.content;
|
|
202
|
+
if (event.type === "message.commit" || event.type === "turn.end")
|
|
203
|
+
Object.assign(target, { content: event.message.content, provider: event.message.provider ?? null, model: event.message.model ?? null });
|
|
204
|
+
if (event.type === "text.delta") {
|
|
205
|
+
const block = target.content[event.index];
|
|
206
|
+
if (event.kind === "text")
|
|
207
|
+
target.content[event.index] = block?.type === "text" ? { ...block, text: block.text + event.delta } : { type: "text", text: event.delta };
|
|
208
|
+
else
|
|
209
|
+
target.content[event.index] = block?.type === "thinking" ? { ...block, thinking: block.thinking + event.delta } : { type: "thinking", thinking: event.delta };
|
|
210
|
+
}
|
|
211
|
+
this.touched(ordinal);
|
|
212
|
+
}
|
|
213
|
+
replace(messages) {
|
|
214
|
+
const first = messages.findIndex((message, index) => JSON.stringify(message) !== JSON.stringify(this.messages[index]));
|
|
215
|
+
if (first < 0 && messages.length === this.messages.length)
|
|
216
|
+
return;
|
|
217
|
+
this.messages = messages.map((message) => ({ ...message, content: [...message.content] }));
|
|
218
|
+
this.touched(first < 0 ? messages.length : first);
|
|
219
|
+
}
|
|
220
|
+
dispose() {
|
|
221
|
+
this.disposed = true;
|
|
222
|
+
if (this.timer)
|
|
223
|
+
clearTimeout(this.timer);
|
|
224
|
+
this.timer = null;
|
|
225
|
+
}
|
|
226
|
+
touched(ordinal) {
|
|
227
|
+
this.dirtyFrom = Math.min(this.dirtyFrom, ordinal);
|
|
228
|
+
this.schedule();
|
|
229
|
+
}
|
|
230
|
+
schedule() {
|
|
231
|
+
if (this.disposed || this.inFlight)
|
|
232
|
+
return;
|
|
233
|
+
this.timer ??= setTimeout(() => { this.timer = null; this.flush(); }, this.intervalMs);
|
|
234
|
+
this.timer.unref?.();
|
|
235
|
+
}
|
|
236
|
+
flush() {
|
|
237
|
+
if (this.disposed || !Number.isFinite(this.dirtyFrom))
|
|
238
|
+
return;
|
|
239
|
+
const from = this.synced ? Math.min(this.dirtyFrom, this.messages.length) : 0;
|
|
240
|
+
const sent = this.sink({ revision: nextRevision(), from, messages: this.messages.slice(from) });
|
|
241
|
+
// Not yet recorded: stay dirty, the next event tries again.
|
|
242
|
+
if (!sent)
|
|
243
|
+
return;
|
|
244
|
+
this.dirtyFrom = Number.POSITIVE_INFINITY;
|
|
245
|
+
this.synced = true;
|
|
246
|
+
this.inFlight = true;
|
|
247
|
+
void sent.then((result) => {
|
|
248
|
+
const outcome = result;
|
|
249
|
+
if (outcome?.accepted === false || outcome?.resync)
|
|
250
|
+
this.synced = false;
|
|
251
|
+
}, () => { this.synced = false; }).finally(() => {
|
|
252
|
+
this.inFlight = false;
|
|
253
|
+
// Whatever changed meanwhile goes next; after a refusal, the next send is a full one.
|
|
254
|
+
if (Number.isFinite(this.dirtyFrom))
|
|
255
|
+
this.schedule();
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
at(ordinal) {
|
|
259
|
+
for (let index = this.messages.length; index <= ordinal; index += 1)
|
|
260
|
+
this.messages[index] = { content: [], provider: null, model: null };
|
|
261
|
+
return this.messages[ordinal];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { NativeRuntimeEvent } from "@neta-art/cohub";
|
|
2
|
+
import type { IngestTransport } from "./ingest.js";
|
|
3
|
+
/**
|
|
4
|
+
* Transcript ingest travels over the existing Runtime WebSocket: one connection, one credential, no
|
|
5
|
+
* second channel and no local receipts.
|
|
6
|
+
*/
|
|
7
|
+
export declare function nativeWebSocketTransport(send: (event: NativeRuntimeEvent) => Promise<unknown>): IngestTransport;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript ingest travels over the existing Runtime WebSocket: one connection, one credential, no
|
|
3
|
+
* second channel and no local receipts.
|
|
4
|
+
*/
|
|
5
|
+
export function nativeWebSocketTransport(send) {
|
|
6
|
+
const call = async (event, options) => {
|
|
7
|
+
options?.signal?.throwIfAborted();
|
|
8
|
+
return await send(event);
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
ingest: (event, options) => call(event, options),
|
|
12
|
+
known: async (turnIds, options) => (await call({ type: "known", input: { turnIds } }, options)).turns,
|
|
13
|
+
progress: (event, options) => call(event, options),
|
|
14
|
+
status: (sessionId, turnId, controllable, options) => call({ type: "status", sessionId, turnId, ...(controllable === undefined ? {} : { controllable }) }, options),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type Version = readonly [number, number, number];
|
|
2
|
+
/** A harness binary's own version, or `null` when it cannot say. */
|
|
3
|
+
export declare function harnessVersion(binary: string): Promise<Version | null>;
|
|
4
|
+
export declare function atLeast(version: Version | null, minimum: Version): boolean;
|
|
5
|
+
export declare const formatVersion: (version: Version | null) => string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
/** A harness binary's own version, or `null` when it cannot say. */
|
|
4
|
+
export async function harnessVersion(binary) {
|
|
5
|
+
const { stdout } = await promisify(execFile)(binary, ["--version"], { encoding: "utf8", timeout: 15_000 }).catch(() => ({ stdout: "" }));
|
|
6
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(stdout);
|
|
7
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
8
|
+
}
|
|
9
|
+
export function atLeast(version, minimum) {
|
|
10
|
+
if (!version)
|
|
11
|
+
return false;
|
|
12
|
+
for (let index = 0; index < 3; index += 1) {
|
|
13
|
+
if (version[index] !== minimum[index])
|
|
14
|
+
return (version[index] ?? 0) > (minimum[index] ?? 0);
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
export const formatVersion = (version) => version?.join(".") ?? "(unknown version)";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { NativeConfig } from "./native/config.js";
|
|
2
|
+
import type { NativeStatus } from "./native/daemon.js";
|
|
2
3
|
import type { RuntimeDiagnostic, RuntimeDiagnosticLevel } from "./diagnostics.js";
|
|
3
4
|
export declare const diagnosticLevels: RuntimeDiagnosticLevel[];
|
|
4
5
|
export declare const atLeastLevel: (level: RuntimeDiagnosticLevel, minimum: RuntimeDiagnosticLevel) => boolean;
|
|
@@ -17,14 +18,10 @@ export type RuntimeSummary = {
|
|
|
17
18
|
workspaceConnected: boolean;
|
|
18
19
|
diagnosticsPath: string;
|
|
19
20
|
background: boolean;
|
|
21
|
+
native?: NativeStatus;
|
|
20
22
|
};
|
|
21
23
|
export declare function printRuntimeSummary(summary: RuntimeSummary, json?: boolean, reused?: boolean): void;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
pendingTurns: number;
|
|
27
|
-
pendingArchives: number;
|
|
28
|
-
};
|
|
29
|
-
/** `runtime status` native sync block: installation state plus per-Harness pending work. */
|
|
30
|
-
export declare function formatNativeSync(config: NativeSyncConfig | null, sessions: NativeSessionStatus[], error?: string | null): string;
|
|
24
|
+
/** One line of import progress: files and bytes, with an estimate once enough is known. */
|
|
25
|
+
export declare function formatImportProgress(job: NativeStatus["import"], now?: number): string;
|
|
26
|
+
/** `runtime status` native block: what syncs, what can be driven, and backfill progress. */
|
|
27
|
+
export declare function formatNativeSync(config: NativeConfig | null, native: NativeStatus | undefined, error?: string | null): string;
|
|
@@ -17,7 +17,9 @@ const messages = {
|
|
|
17
17
|
"runtime.execution_transport_detached": "Execution continues locally; result replays after reconnect",
|
|
18
18
|
"runtime.execution_transport_invalidated": "Execution interrupted; outcome needs reconciliation",
|
|
19
19
|
"archive.upload_pending": "Archive upload pending; local data retained",
|
|
20
|
-
"native.sync_pending": "Native sync pending;
|
|
20
|
+
"native.sync_pending": "Native sync pending; the native file is the record and is retried",
|
|
21
|
+
"native.stop_failed": "Could not stop the native Turn; stop it in the terminal",
|
|
22
|
+
"native.import_failed": "Import did not start; run cohub runtime import",
|
|
21
23
|
"archive.capture_pending": "Archive capture pending",
|
|
22
24
|
"archive.capture_unavailable": "Archive unavailable; original receipt retained",
|
|
23
25
|
"archive.restore_failed": "Native restore unavailable; using saved history",
|
|
@@ -76,19 +78,42 @@ export function printRuntimeSummary(summary, json = false, reused = false) {
|
|
|
76
78
|
process.stdout.write(" Ctrl+C to stop\n");
|
|
77
79
|
process.stdout.write("\n");
|
|
78
80
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
const plural = (count, word) => `${count.toLocaleString("en-US")} ${word}${count === 1 ? "" : "s"}`;
|
|
82
|
+
const megabytes = (bytes) => `${(bytes / 1024 / 1024).toFixed(bytes < 10 * 1024 * 1024 ? 1 : 0)} MB`;
|
|
83
|
+
/** One line of import progress: files and bytes, with an estimate once enough is known. */
|
|
84
|
+
export function formatImportProgress(job, now = Date.now()) {
|
|
85
|
+
const parts = [`${job.done.toLocaleString("en-US")}/${plural(job.files, "conversation")}`, plural(job.turns, "Turn"), `${megabytes(job.bytes)}/${megabytes(job.totalBytes)}`];
|
|
86
|
+
const elapsed = job.startedAt ? now - Date.parse(job.startedAt) : 0;
|
|
87
|
+
if (job.state === "running" && job.bytes > 0 && elapsed > 2_000 && job.totalBytes > job.bytes) {
|
|
88
|
+
const seconds = Math.round((job.totalBytes - job.bytes) / (job.bytes / elapsed) / 1_000);
|
|
89
|
+
parts.push(`about ${seconds < 90 ? `${seconds}s` : `${Math.round(seconds / 60)}m`} left`);
|
|
90
|
+
}
|
|
91
|
+
return parts.join(" · ");
|
|
92
|
+
}
|
|
93
|
+
/** `runtime status` native block: what syncs, what can be driven, and backfill progress. */
|
|
94
|
+
export function formatNativeSync(config, native, error = null) {
|
|
81
95
|
if (error)
|
|
82
96
|
return `Native sync unknown — ${error} · fix or remove the config, then runtime up\n`;
|
|
83
97
|
if (!config?.harnesses.length)
|
|
84
98
|
return "Native sync off · run cohub runtime up to enable\n";
|
|
85
|
-
const lines = [`Native sync
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
const lines = [`Native sync ${config.harnesses.map((harness) => harness === "pi" ? "Pi" : "Codex").join(" · ")}`];
|
|
100
|
+
if (!native)
|
|
101
|
+
return `${lines[0]} · Runtime not running\n`;
|
|
102
|
+
if (native.pi) {
|
|
103
|
+
lines.push(native.pi.unavailable ? ` Pi read-only · ${native.pi.unavailable}`
|
|
104
|
+
: native.pi.extension === "installed"
|
|
105
|
+
? ` Pi ${plural(native.pi.connected, "session")} connected`
|
|
106
|
+
: " Pi read-only · install the extension with cohub runtime attach --harness pi, then /reload");
|
|
88
107
|
}
|
|
89
|
-
if (
|
|
90
|
-
lines.push("
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
if (native.codex)
|
|
109
|
+
lines.push(native.codex.control === "shared" ? " Codex shared app-server" : " Codex read-only in the terminal · Cohub Turns use a private app-server");
|
|
110
|
+
lines.push(` Files ${plural(native.transcripts, "transcript")}${native.running ? ` · ${plural(native.running, "Turn")} running` : ""}`);
|
|
111
|
+
const job = native.import;
|
|
112
|
+
if (job.state === "running")
|
|
113
|
+
lines.push(` Import ${formatImportProgress(job)}`);
|
|
114
|
+
else if (job.state === "paused")
|
|
115
|
+
lines.push(` Import paused at ${job.done}/${job.files} · run cohub runtime import to continue`);
|
|
116
|
+
if (job.failed.length)
|
|
117
|
+
lines.push(` ${plural(job.failed.length, "conversation")} failed to import · cohub runtime logs`);
|
|
93
118
|
return `${lines.join("\n")}\n`;
|
|
94
119
|
}
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { NativeProjection, ProjectionTarget } from "@neta-art/cohub";
|
|
2
2
|
import { type ProjectionSourceTurn, type SessionTurnProjectionClient } from "./turn-projection.js";
|
|
3
|
-
export type ProjectionCursorState = {
|
|
4
|
-
throughSequence: number | null;
|
|
5
|
-
throughTurnId: string | null;
|
|
6
|
-
sourceFingerprint: string | null;
|
|
7
|
-
};
|
|
8
3
|
export type ProjectionStoreInput = {
|
|
9
4
|
spaceId: string;
|
|
10
5
|
sessionId: string;
|
|
@@ -14,21 +9,19 @@ export type ProjectionStoreInput = {
|
|
|
14
9
|
provider?: string | null;
|
|
15
10
|
target: ProjectionTarget;
|
|
16
11
|
throughTurnId: string | null;
|
|
17
|
-
cursor: ProjectionCursorState | null;
|
|
18
12
|
};
|
|
19
13
|
export type ProjectionStoreResult = {
|
|
20
14
|
projection: NativeProjection;
|
|
21
15
|
turns: ProjectionSourceTurn[];
|
|
22
|
-
append: boolean;
|
|
23
|
-
cursor: ProjectionCursorState;
|
|
24
16
|
};
|
|
17
|
+
/** Give the projection's own records (the native header) the identity of the file it becomes. */
|
|
25
18
|
export declare function rebindProjectionNativeSession(result: ProjectionStoreResult, nativeSessionId: string): ProjectionStoreResult;
|
|
26
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Materializes a Session's durable Turns into one harness-specific native file. Cohub never appends
|
|
21
|
+
* to a native file it did not just write, so every projection is complete up to the Session head.
|
|
22
|
+
*/
|
|
27
23
|
export declare class ProjectionStore {
|
|
28
24
|
private readonly source;
|
|
29
25
|
constructor(source: SessionTurnProjectionClient);
|
|
30
26
|
project(input: ProjectionStoreInput, signal?: AbortSignal): Promise<ProjectionStoreResult>;
|
|
31
|
-
private sourceSequence;
|
|
32
|
-
cursorForTurn(sessionId: string, turnId: string, signal?: AbortSignal): Promise<ProjectionCursorState>;
|
|
33
|
-
private readTurns;
|
|
34
27
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { listSessionProjectionTurns, projectTurnBatch } from "./turn-projection.js";
|
|
2
|
+
/** Give the projection's own records (the native header) the identity of the file it becomes. */
|
|
3
3
|
export function rebindProjectionNativeSession(result, nativeSessionId) {
|
|
4
|
-
if (result.append)
|
|
5
|
-
return result;
|
|
6
4
|
const records = result.projection.records.map((entry) => {
|
|
7
5
|
if (entry.sourceTurnId !== null)
|
|
8
6
|
return entry;
|
|
@@ -17,87 +15,26 @@ export function rebindProjectionNativeSession(result, nativeSessionId) {
|
|
|
17
15
|
});
|
|
18
16
|
return { ...result, projection: { ...result.projection, records } };
|
|
19
17
|
}
|
|
20
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Materializes a Session's durable Turns into one harness-specific native file. Cohub never appends
|
|
20
|
+
* to a native file it did not just write, so every projection is complete up to the Session head.
|
|
21
|
+
*/
|
|
21
22
|
export class ProjectionStore {
|
|
22
23
|
source;
|
|
23
24
|
constructor(source) {
|
|
24
25
|
this.source = source;
|
|
25
26
|
}
|
|
26
27
|
async project(input, signal) {
|
|
27
|
-
const throughSequence = input.throughTurnId ? await this.
|
|
28
|
-
|
|
29
|
-
if (source.append && source.turns.some((turn) => turn.messages.some(isProjectionCompaction))) {
|
|
30
|
-
source = { turns: await listSessionProjectionTurns(this.source, input.sessionId, { throughSequence, excludeTurnId: input.turnId, signal }), append: false };
|
|
31
|
-
}
|
|
28
|
+
const throughSequence = input.throughTurnId ? (await this.source.session(input.sessionId).turns.get(input.throughTurnId, { signal })).turn.sequence : 0;
|
|
29
|
+
const turns = await listSessionProjectionTurns(this.source, input.sessionId, { throughSequence, excludeTurnId: input.turnId, signal });
|
|
32
30
|
const projection = projectTurnBatch({
|
|
33
31
|
spaceId: input.spaceId,
|
|
34
32
|
sessionId: input.sessionId,
|
|
35
33
|
nativeSessionId: input.nativeSessionId,
|
|
36
34
|
cwd: input.cwd,
|
|
37
35
|
provider: input.provider,
|
|
38
|
-
turns
|
|
39
|
-
}, input.target
|
|
40
|
-
|
|
41
|
-
const previous = source.append ? input.cursor : null;
|
|
42
|
-
return {
|
|
43
|
-
projection,
|
|
44
|
-
turns: source.turns,
|
|
45
|
-
append: source.append,
|
|
46
|
-
cursor: {
|
|
47
|
-
throughSequence: last?.sequence ?? previous?.throughSequence ?? null,
|
|
48
|
-
throughTurnId: last?.sourceTurnId ?? previous?.throughTurnId ?? null,
|
|
49
|
-
sourceFingerprint: last ? fingerprintProjectionTurns([last]) : previous?.sourceFingerprint ?? null,
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
async sourceSequence(sessionId, turnId, signal) {
|
|
54
|
-
const client = this.source.session(sessionId);
|
|
55
|
-
const response = await client.turns.get(turnId, { signal });
|
|
56
|
-
return response.turn.sequence;
|
|
57
|
-
}
|
|
58
|
-
async cursorForTurn(sessionId, turnId, signal) {
|
|
59
|
-
const turn = await getSessionProjectionTurn(this.source, sessionId, turnId, signal);
|
|
60
|
-
return {
|
|
61
|
-
throughSequence: turn.sequence,
|
|
62
|
-
throughTurnId: turn.sourceTurnId,
|
|
63
|
-
sourceFingerprint: fingerprintProjectionTurns([turn]),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
async readTurns(input, throughSequence, signal) {
|
|
67
|
-
const cursor = input.cursor;
|
|
68
|
-
if (cursor?.throughSequence != null && cursor.throughSequence > throughSequence) {
|
|
69
|
-
return { turns: await listSessionProjectionTurns(this.source, input.sessionId, { throughSequence, excludeTurnId: input.turnId, signal }), append: false };
|
|
70
|
-
}
|
|
71
|
-
if (cursor?.throughTurnId && cursor.throughSequence != null) {
|
|
72
|
-
let anchor;
|
|
73
|
-
try {
|
|
74
|
-
anchor = await getSessionProjectionTurn(this.source, input.sessionId, cursor.throughTurnId, signal);
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
if (!(error instanceof HttpError) || error.status !== 404)
|
|
78
|
-
throw error;
|
|
79
|
-
anchor = null;
|
|
80
|
-
}
|
|
81
|
-
const anchorFingerprint = anchor ? fingerprintProjectionTurns([anchor]) : null;
|
|
82
|
-
if (anchorFingerprint !== cursor.sourceFingerprint) {
|
|
83
|
-
return {
|
|
84
|
-
turns: await listSessionProjectionTurns(this.source, input.sessionId, { throughSequence, excludeTurnId: input.turnId, signal }),
|
|
85
|
-
append: false,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
return {
|
|
89
|
-
turns: await listSessionProjectionTurns(this.source, input.sessionId, {
|
|
90
|
-
afterSequence: Math.max(1, cursor.throughSequence),
|
|
91
|
-
throughSequence,
|
|
92
|
-
excludeTurnId: input.turnId,
|
|
93
|
-
signal,
|
|
94
|
-
}),
|
|
95
|
-
append: true,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
turns: await listSessionProjectionTurns(this.source, input.sessionId, { throughSequence, excludeTurnId: input.turnId, signal }),
|
|
100
|
-
append: false,
|
|
101
|
-
};
|
|
36
|
+
turns,
|
|
37
|
+
}, input.target);
|
|
38
|
+
return { projection, turns };
|
|
102
39
|
}
|
|
103
40
|
}
|
|
@@ -11,6 +11,8 @@ export type RuntimeLaunch = {
|
|
|
11
11
|
capabilities?: RuntimeCapabilities;
|
|
12
12
|
background: boolean;
|
|
13
13
|
verbose?: boolean;
|
|
14
|
+
/** Start importing earlier conversations as soon as the Runtime is connected. */
|
|
15
|
+
importHistory?: boolean;
|
|
14
16
|
};
|
|
15
17
|
export declare function sandboxOutputLevel(value: unknown, stream: "stdout" | "stderr"): RuntimeDiagnosticLevel;
|
|
16
18
|
export declare function runRuntime(config: RuntimeLaunch, onState: (status: RuntimeSummary) => void, externalSignal?: AbortSignal, onDiagnostic?: (event: RuntimeDiagnostic) => void): Promise<void>;
|