@neta-art/cohub-cli 7.0.1 → 7.1.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/README.md +13 -1
- package/dist/commands/desktop.d.ts +9 -0
- package/dist/commands/desktop.js +13 -5
- package/dist/commands/run.d.ts +11 -0
- package/dist/commands/run.js +4 -4
- package/dist/commands/runtime.js +254 -43
- package/dist/commands/sandboxd-binary.d.ts +2 -0
- package/dist/commands/sandboxd-binary.js +37 -16
- package/dist/index.js +4 -2
- package/dist/runtime/archive-store.d.ts +2 -0
- package/dist/runtime/archive-store.js +7 -1
- package/dist/runtime/connection.d.ts +3 -0
- package/dist/runtime/connection.js +242 -36
- package/dist/runtime/diagnostics.d.ts +104 -0
- package/dist/runtime/diagnostics.js +382 -0
- package/dist/runtime/harness.d.ts +3 -2
- package/dist/runtime/harness.js +39 -8
- package/dist/runtime/json-rpc.d.ts +2 -0
- package/dist/runtime/json-rpc.js +12 -1
- package/dist/runtime/projection-store.d.ts +34 -0
- package/dist/runtime/projection-store.js +103 -0
- package/dist/runtime/session-store.d.ts +26 -4
- package/dist/runtime/session-store.js +238 -60
- package/dist/runtime/space-binding.d.ts +45 -0
- package/dist/runtime/space-binding.js +305 -0
- package/dist/runtime/turn-projection.d.ts +43 -0
- package/dist/runtime/turn-projection.js +127 -0
- package/dist/space.d.ts +11 -3
- package/dist/space.js +18 -6
- package/package.json +3 -2
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { mkdir, open, readdir, rename, rm, stat } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { StringDecoder } from "node:string_decoder";
|
|
6
|
+
const DEFAULT_LOG_FLUSH_INTERVAL_MS = 250;
|
|
7
|
+
const MAX_PENDING_LOG_BYTES = 4 * 1024 * 1024;
|
|
8
|
+
const DEFAULT_MAX_LOG_FILE_BYTES = 8 * 1024 * 1024;
|
|
9
|
+
const DEFAULT_MAX_TOTAL_LOG_BYTES = 64 * 1024 * 1024;
|
|
10
|
+
const DEFAULT_MAX_LOG_FILE_AGE_MS = 60 * 60 * 1_000;
|
|
11
|
+
const MAX_VALUE_STRING_LENGTH = 4_096;
|
|
12
|
+
const MAX_EVENT_BYTES = 48 * 1024;
|
|
13
|
+
const SENSITIVE_KEY = /authorization|cookie|password|secret|token|access[_-]?key|refresh[_-]?token/i;
|
|
14
|
+
const TRACEPARENT = /^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/i;
|
|
15
|
+
const diagnosticsDirectoryName = "diagnostics";
|
|
16
|
+
export const runtimeDiagnosticsDirectory = (root) => join(root, diagnosticsDirectoryName);
|
|
17
|
+
export const runtimeDiagnosticsPath = (root, runtimeId) => join(runtimeDiagnosticsDirectory(root), `${runtimeId}.jsonl`);
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
20
|
+
}
|
|
21
|
+
function redactUrl(value) {
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(value);
|
|
24
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
25
|
+
if (SENSITIVE_KEY.test(key))
|
|
26
|
+
url.searchParams.set(key, "[REDACTED]");
|
|
27
|
+
}
|
|
28
|
+
return url.toString();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function redactDiagnosticString(value) {
|
|
35
|
+
const shortened = value.length > MAX_VALUE_STRING_LENGTH
|
|
36
|
+
? `${value.slice(0, MAX_VALUE_STRING_LENGTH)}…`
|
|
37
|
+
: value;
|
|
38
|
+
return shortened
|
|
39
|
+
.replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [REDACTED]")
|
|
40
|
+
.replace(/\b(rediss?|https?|wss?):\/\/[^\s]+/gi, (url) => redactUrl(url))
|
|
41
|
+
.replace(/\b(authorization|password|secret|token|access[_-]?key|refresh[_-]?token)([\s:=]+)([^\s,;]+)/gi, "$1$2[REDACTED]");
|
|
42
|
+
}
|
|
43
|
+
function redactValue(value, seen = new WeakSet(), depth = 0) {
|
|
44
|
+
if (value == null || typeof value === "boolean" || typeof value === "number")
|
|
45
|
+
return value;
|
|
46
|
+
if (typeof value === "string")
|
|
47
|
+
return redactDiagnosticString(value);
|
|
48
|
+
if (depth > 5)
|
|
49
|
+
return "[Truncated]";
|
|
50
|
+
if (typeof value !== "object")
|
|
51
|
+
return String(value);
|
|
52
|
+
if (seen.has(value))
|
|
53
|
+
return "[Circular]";
|
|
54
|
+
seen.add(value);
|
|
55
|
+
if (Array.isArray(value))
|
|
56
|
+
return value.slice(0, 64).map((item) => redactValue(item, seen, depth + 1));
|
|
57
|
+
const output = {};
|
|
58
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
59
|
+
output[key] = SENSITIVE_KEY.test(key) ? "[REDACTED]" : redactValue(nested, seen, depth + 1);
|
|
60
|
+
}
|
|
61
|
+
return output;
|
|
62
|
+
}
|
|
63
|
+
function normalizeDiagnosticTraceContext(value) {
|
|
64
|
+
if (!isRecord(value))
|
|
65
|
+
return undefined;
|
|
66
|
+
const requestId = typeof value.requestId === "string" && /^[a-zA-Z0-9._:-]{1,128}$/.test(value.requestId)
|
|
67
|
+
? value.requestId
|
|
68
|
+
: undefined;
|
|
69
|
+
const traceId = typeof value.traceId === "string" && /^[0-9a-f]{32}$/i.test(value.traceId) ? value.traceId : undefined;
|
|
70
|
+
const spanId = typeof value.spanId === "string" && /^[0-9a-f]{16}$/i.test(value.spanId) ? value.spanId : undefined;
|
|
71
|
+
const traceparent = typeof value.traceparent === "string" && TRACEPARENT.test(value.traceparent) ? value.traceparent : undefined;
|
|
72
|
+
if (!requestId && !traceId && !spanId && !traceparent)
|
|
73
|
+
return undefined;
|
|
74
|
+
return {
|
|
75
|
+
...(requestId ? { requestId } : {}),
|
|
76
|
+
...(traceId ? { traceId } : {}),
|
|
77
|
+
...(spanId ? { spanId } : {}),
|
|
78
|
+
...(traceparent ? { traceparent } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export function serializeDiagnosticError(error) {
|
|
82
|
+
if (!(error instanceof Error))
|
|
83
|
+
return { message: redactDiagnosticString(String(error)) };
|
|
84
|
+
const source = error;
|
|
85
|
+
const result = {
|
|
86
|
+
message: error.message || error.name || "Unknown error",
|
|
87
|
+
...(error.name ? { name: error.name } : {}),
|
|
88
|
+
...(error.stack ? { stack: error.stack } : {}),
|
|
89
|
+
};
|
|
90
|
+
for (const key of ["code", "status", "syscall", "hostname"]) {
|
|
91
|
+
const value = source[key];
|
|
92
|
+
if (typeof value === "string" || typeof value === "number")
|
|
93
|
+
result[key] = value;
|
|
94
|
+
}
|
|
95
|
+
const cause = isRecord(source.cause) ? source.cause : null;
|
|
96
|
+
const traceContext = normalizeDiagnosticTraceContext(source.traceContext);
|
|
97
|
+
if (traceContext)
|
|
98
|
+
result.traceContext = traceContext;
|
|
99
|
+
if (typeof cause?.code === "string")
|
|
100
|
+
result.causeCode = cause.code;
|
|
101
|
+
if (typeof cause?.message === "string")
|
|
102
|
+
result.causeMessage = cause.message;
|
|
103
|
+
if (typeof cause?.syscall === "string")
|
|
104
|
+
result.causeSyscall = cause.syscall;
|
|
105
|
+
if (typeof cause?.hostname === "string")
|
|
106
|
+
result.causeHostname = cause.hostname;
|
|
107
|
+
return redactValue(result);
|
|
108
|
+
}
|
|
109
|
+
function compactEvent(event) {
|
|
110
|
+
let result = redactValue(event);
|
|
111
|
+
if (Buffer.byteLength(JSON.stringify(result), "utf8") <= MAX_EVENT_BYTES)
|
|
112
|
+
return result;
|
|
113
|
+
result = {
|
|
114
|
+
...result,
|
|
115
|
+
data: { truncated: true },
|
|
116
|
+
...(result.error ? { error: { ...result.error, stack: undefined } } : {}),
|
|
117
|
+
};
|
|
118
|
+
if (Buffer.byteLength(JSON.stringify(result), "utf8") <= MAX_EVENT_BYTES)
|
|
119
|
+
return result;
|
|
120
|
+
return {
|
|
121
|
+
...result,
|
|
122
|
+
data: undefined,
|
|
123
|
+
error: result.error ? { message: result.error.message, name: result.error.name } : undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function compareDiagnosticEvents(left, right) {
|
|
127
|
+
if (left.runtimeId === right.runtimeId)
|
|
128
|
+
return left.sequence - right.sequence;
|
|
129
|
+
return left.timestamp.localeCompare(right.timestamp) || left.runtimeId.localeCompare(right.runtimeId) || left.sequence - right.sequence;
|
|
130
|
+
}
|
|
131
|
+
export class RuntimeDiagnostics {
|
|
132
|
+
runtimeId;
|
|
133
|
+
directory;
|
|
134
|
+
logPath;
|
|
135
|
+
spaceId;
|
|
136
|
+
component;
|
|
137
|
+
logFlushIntervalMs;
|
|
138
|
+
maxLogFileBytes;
|
|
139
|
+
maxTotalLogBytes;
|
|
140
|
+
startedAtMs = Date.now();
|
|
141
|
+
ready;
|
|
142
|
+
logFile = null;
|
|
143
|
+
logFileSize = 0;
|
|
144
|
+
logFileStartedAt = 0;
|
|
145
|
+
writeChain = Promise.resolve();
|
|
146
|
+
writeBuffer = [];
|
|
147
|
+
writeBufferBytes = 0;
|
|
148
|
+
droppedWriteEvents = 0;
|
|
149
|
+
writeTimer = null;
|
|
150
|
+
sequence = 0;
|
|
151
|
+
closed = false;
|
|
152
|
+
constructor(options) {
|
|
153
|
+
this.runtimeId = options.runtimeId ?? randomUUID();
|
|
154
|
+
this.spaceId = options.spaceId;
|
|
155
|
+
this.component = options.component ?? "runtime";
|
|
156
|
+
this.logFlushIntervalMs = options.logFlushIntervalMs ?? DEFAULT_LOG_FLUSH_INTERVAL_MS;
|
|
157
|
+
this.maxLogFileBytes = options.maxLogFileBytes ?? DEFAULT_MAX_LOG_FILE_BYTES;
|
|
158
|
+
this.maxTotalLogBytes = options.maxTotalLogBytes ?? DEFAULT_MAX_TOTAL_LOG_BYTES;
|
|
159
|
+
this.directory = runtimeDiagnosticsDirectory(options.root);
|
|
160
|
+
this.logPath = runtimeDiagnosticsPath(options.root, this.runtimeId);
|
|
161
|
+
this.ready = mkdir(this.directory, { recursive: true, mode: 0o700 }).then(() => undefined);
|
|
162
|
+
}
|
|
163
|
+
log(level, event, data, context = {}) {
|
|
164
|
+
if (this.closed)
|
|
165
|
+
return;
|
|
166
|
+
const traceContext = normalizeDiagnosticTraceContext(context.traceContext ?? (context.requestId ? { requestId: context.requestId } : undefined));
|
|
167
|
+
const rawError = data?.error;
|
|
168
|
+
const diagnosticError = rawError instanceof Error
|
|
169
|
+
? serializeDiagnosticError(rawError)
|
|
170
|
+
: isRecord(rawError) && typeof rawError.message === "string"
|
|
171
|
+
? redactValue(rawError)
|
|
172
|
+
: typeof rawError === "string"
|
|
173
|
+
? { message: redactDiagnosticString(rawError) }
|
|
174
|
+
: undefined;
|
|
175
|
+
const eventTraceContext = traceContext ?? normalizeDiagnosticTraceContext(diagnosticError?.traceContext);
|
|
176
|
+
const eventData = data ? Object.fromEntries(Object.entries(data).filter(([key]) => key !== "error")) : undefined;
|
|
177
|
+
const value = compactEvent({
|
|
178
|
+
schemaVersion: 1,
|
|
179
|
+
sequence: this.sequence++,
|
|
180
|
+
timestamp: new Date().toISOString(),
|
|
181
|
+
elapsedMs: Date.now() - this.startedAtMs,
|
|
182
|
+
level,
|
|
183
|
+
component: context.component ?? this.component,
|
|
184
|
+
event,
|
|
185
|
+
runtimeId: this.runtimeId,
|
|
186
|
+
...(context.connectionId ? { connectionId: context.connectionId } : {}),
|
|
187
|
+
spaceId: context.spaceId ?? this.spaceId,
|
|
188
|
+
...(context.sessionId ? { sessionId: context.sessionId } : {}),
|
|
189
|
+
...(context.turnId ? { turnId: context.turnId } : {}),
|
|
190
|
+
...(context.harness ? { harness: context.harness } : {}),
|
|
191
|
+
...(eventTraceContext ? { traceContext: eventTraceContext } : {}),
|
|
192
|
+
...(eventData && Object.keys(eventData).length > 0 ? { data: eventData } : {}),
|
|
193
|
+
...(diagnosticError ? { error: diagnosticError } : {}),
|
|
194
|
+
});
|
|
195
|
+
this.enqueueWrite(value);
|
|
196
|
+
}
|
|
197
|
+
async close() {
|
|
198
|
+
this.closed = true;
|
|
199
|
+
if (this.writeTimer)
|
|
200
|
+
clearTimeout(this.writeTimer);
|
|
201
|
+
this.writeTimer = null;
|
|
202
|
+
await this.flushLogBuffer();
|
|
203
|
+
await this.writeChain;
|
|
204
|
+
if (this.logFile) {
|
|
205
|
+
const file = this.logFile;
|
|
206
|
+
this.logFile = null;
|
|
207
|
+
await file.sync().catch(() => undefined);
|
|
208
|
+
await file.close().catch(() => undefined);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
enqueueWrite(event) {
|
|
212
|
+
let nextEvent = event;
|
|
213
|
+
if (this.droppedWriteEvents > 0) {
|
|
214
|
+
nextEvent = { ...event, data: { ...(event.data ?? {}), droppedDiagnosticsBeforeWrite: this.droppedWriteEvents } };
|
|
215
|
+
this.droppedWriteEvents = 0;
|
|
216
|
+
}
|
|
217
|
+
const line = `${JSON.stringify(nextEvent)}\n`;
|
|
218
|
+
const bytes = Buffer.byteLength(line, "utf8");
|
|
219
|
+
while (this.writeBufferBytes + bytes > MAX_PENDING_LOG_BYTES && this.writeBuffer.length > 0) {
|
|
220
|
+
const index = this.writeBuffer.findIndex(({ event: queued }) => queued.level === "debug" || queued.level === "info");
|
|
221
|
+
const removed = this.writeBuffer.splice(index >= 0 ? index : 0, 1)[0];
|
|
222
|
+
if (removed) {
|
|
223
|
+
this.writeBufferBytes -= removed.bytes;
|
|
224
|
+
this.droppedWriteEvents += 1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (this.writeBufferBytes + bytes > MAX_PENDING_LOG_BYTES && nextEvent.level !== "error") {
|
|
228
|
+
this.droppedWriteEvents += 1;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.writeBuffer.push({ event: nextEvent, line, bytes });
|
|
232
|
+
this.writeBufferBytes += bytes;
|
|
233
|
+
if (nextEvent.level === "error" || this.writeBufferBytes >= 64 * 1024)
|
|
234
|
+
void this.flushLogBuffer();
|
|
235
|
+
else
|
|
236
|
+
this.scheduleLogFlush();
|
|
237
|
+
}
|
|
238
|
+
scheduleLogFlush() {
|
|
239
|
+
if (this.writeTimer || this.closed)
|
|
240
|
+
return;
|
|
241
|
+
this.writeTimer = setTimeout(() => {
|
|
242
|
+
this.writeTimer = null;
|
|
243
|
+
void this.flushLogBuffer();
|
|
244
|
+
}, this.logFlushIntervalMs);
|
|
245
|
+
}
|
|
246
|
+
async flushLogBuffer() {
|
|
247
|
+
if (this.writeBuffer.length === 0)
|
|
248
|
+
return this.writeChain;
|
|
249
|
+
const entries = this.writeBuffer;
|
|
250
|
+
this.writeBuffer = [];
|
|
251
|
+
this.writeBufferBytes = 0;
|
|
252
|
+
this.writeChain = this.writeChain.then(() => this.writeEntries(entries)).catch(() => {
|
|
253
|
+
// Local diagnostics must never take down the Runtime.
|
|
254
|
+
});
|
|
255
|
+
return this.writeChain;
|
|
256
|
+
}
|
|
257
|
+
async writeEntries(entries) {
|
|
258
|
+
await this.ready;
|
|
259
|
+
for (const entry of entries) {
|
|
260
|
+
await this.ensureLogFile();
|
|
261
|
+
const shouldRotate = this.logFileSize > 0 && (this.logFileSize + entry.bytes > this.maxLogFileBytes ||
|
|
262
|
+
Date.now() - this.logFileStartedAt >= DEFAULT_MAX_LOG_FILE_AGE_MS);
|
|
263
|
+
if (shouldRotate) {
|
|
264
|
+
await this.rotateLog(entry.event.sequence);
|
|
265
|
+
await this.ensureLogFile();
|
|
266
|
+
}
|
|
267
|
+
await this.logFile?.write(entry.line);
|
|
268
|
+
this.logFileSize += entry.bytes;
|
|
269
|
+
}
|
|
270
|
+
await this.logFile?.sync();
|
|
271
|
+
}
|
|
272
|
+
async ensureLogFile() {
|
|
273
|
+
if (this.logFile)
|
|
274
|
+
return;
|
|
275
|
+
this.logFile = await open(this.logPath, "a", 0o600);
|
|
276
|
+
await this.logFile.chmod(0o600).catch(() => undefined);
|
|
277
|
+
this.logFileSize = (await stat(this.logPath)).size;
|
|
278
|
+
this.logFileStartedAt = Date.now();
|
|
279
|
+
}
|
|
280
|
+
async rotateLog(nextSequence) {
|
|
281
|
+
const file = this.logFile;
|
|
282
|
+
this.logFile = null;
|
|
283
|
+
if (file) {
|
|
284
|
+
await file.sync();
|
|
285
|
+
await file.close();
|
|
286
|
+
}
|
|
287
|
+
await rename(this.logPath, join(this.directory, `${this.runtimeId}-${nextSequence}-${Date.now()}.jsonl`));
|
|
288
|
+
this.logFileSize = 0;
|
|
289
|
+
this.logFileStartedAt = 0;
|
|
290
|
+
await this.enforceLogCapacity();
|
|
291
|
+
}
|
|
292
|
+
async enforceLogCapacity() {
|
|
293
|
+
const names = await readdir(this.directory).catch((error) => {
|
|
294
|
+
if (error.code === "ENOENT")
|
|
295
|
+
return [];
|
|
296
|
+
throw error;
|
|
297
|
+
});
|
|
298
|
+
const files = await Promise.all(names.filter((name) => name.endsWith(".jsonl") && name !== `${this.runtimeId}.jsonl`).map(async (name) => {
|
|
299
|
+
const path = join(this.directory, name);
|
|
300
|
+
const info = await stat(path).catch(() => null);
|
|
301
|
+
return info ? { path, size: info.size, mtimeMs: info.mtimeMs } : null;
|
|
302
|
+
}));
|
|
303
|
+
let total = this.logFileSize + files.reduce((sum, file) => sum + (file?.size ?? 0), 0);
|
|
304
|
+
for (const file of files.filter((value) => value !== null).sort((left, right) => left.mtimeMs - right.mtimeMs)) {
|
|
305
|
+
if (total <= this.maxTotalLogBytes)
|
|
306
|
+
break;
|
|
307
|
+
await rm(file.path, { force: true });
|
|
308
|
+
total -= file.size;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
export class RuntimeDiagnosticReader {
|
|
313
|
+
root;
|
|
314
|
+
files = new Map();
|
|
315
|
+
lastSequenceByRuntime = new Map();
|
|
316
|
+
initialized = false;
|
|
317
|
+
constructor(root) {
|
|
318
|
+
this.root = root;
|
|
319
|
+
}
|
|
320
|
+
async read(options = {}) {
|
|
321
|
+
const directory = runtimeDiagnosticsDirectory(this.root);
|
|
322
|
+
const names = await readdir(directory).catch((error) => {
|
|
323
|
+
if (error.code === "ENOENT")
|
|
324
|
+
return [];
|
|
325
|
+
throw error;
|
|
326
|
+
});
|
|
327
|
+
const fileEntries = await Promise.all(names.filter((value) => value.endsWith(".jsonl")).map(async (name) => {
|
|
328
|
+
const path = join(directory, name);
|
|
329
|
+
const info = await stat(path).catch(() => null);
|
|
330
|
+
return info ? { path, size: info.size, mtimeMs: info.mtimeMs } : null;
|
|
331
|
+
}));
|
|
332
|
+
const events = [];
|
|
333
|
+
for (const file of fileEntries.filter((value) => value !== null).sort((left, right) => left.mtimeMs - right.mtimeMs)) {
|
|
334
|
+
const state = this.files.get(file.path) ?? { offset: 0, partial: "", decoder: new StringDecoder("utf8") };
|
|
335
|
+
if (file.size < state.offset) {
|
|
336
|
+
state.offset = 0;
|
|
337
|
+
state.partial = "";
|
|
338
|
+
state.decoder = new StringDecoder("utf8");
|
|
339
|
+
}
|
|
340
|
+
const stream = createReadStream(file.path, { start: state.offset });
|
|
341
|
+
let bytesRead = 0;
|
|
342
|
+
let text = state.partial;
|
|
343
|
+
for await (const chunk of stream) {
|
|
344
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
345
|
+
bytesRead += buffer.length;
|
|
346
|
+
text += state.decoder.write(buffer);
|
|
347
|
+
}
|
|
348
|
+
state.offset += bytesRead;
|
|
349
|
+
const lines = text.split("\n");
|
|
350
|
+
state.partial = lines.pop() ?? "";
|
|
351
|
+
this.files.set(file.path, state);
|
|
352
|
+
for (const line of lines) {
|
|
353
|
+
if (!line.trim())
|
|
354
|
+
continue;
|
|
355
|
+
try {
|
|
356
|
+
const value = JSON.parse(line);
|
|
357
|
+
if (isRecord(value) && value.schemaVersion === 1 && typeof value.sequence === "number")
|
|
358
|
+
events.push(value);
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
// Ignore only the damaged line; source files remain untouched.
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
events.sort(compareDiagnosticEvents);
|
|
366
|
+
const fresh = events.filter((event) => event.sequence > (this.lastSequenceByRuntime.get(event.runtimeId) ?? -1));
|
|
367
|
+
for (const event of fresh)
|
|
368
|
+
this.lastSequenceByRuntime.set(event.runtimeId, event.sequence);
|
|
369
|
+
if (!this.initialized) {
|
|
370
|
+
this.initialized = true;
|
|
371
|
+
const limit = options.limit ?? 100;
|
|
372
|
+
return limit > 0 ? fresh.slice(-limit) : fresh;
|
|
373
|
+
}
|
|
374
|
+
return fresh;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
export async function readRuntimeDiagnosticEvents(root, options = {}) {
|
|
378
|
+
return new RuntimeDiagnosticReader(root).read(options);
|
|
379
|
+
}
|
|
380
|
+
export function isValidRuntimeTraceparent(value) {
|
|
381
|
+
return typeof value === "string" && TRACEPARENT.test(value);
|
|
382
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ContentBlock, RuntimeCapabilities, RuntimeExecutionEvent, RuntimeTurnInput } from "@neta-art/cohub";
|
|
2
2
|
import { type JsonRecord } from "./json-rpc.js";
|
|
3
3
|
import type { RuntimeSessionStore, NativeSession } from "./session-store.js";
|
|
4
|
+
import { type RuntimeDiagnostics, type RuntimeDiagnosticContext } from "./diagnostics.js";
|
|
4
5
|
export type HarnessOptions = {
|
|
5
6
|
pi?: string;
|
|
6
7
|
codex?: string;
|
|
@@ -14,6 +15,6 @@ export type HarnessResult = {
|
|
|
14
15
|
export declare function piContent(value: unknown): ContentBlock[];
|
|
15
16
|
export declare function promptText(content: ContentBlock[]): string;
|
|
16
17
|
export declare function discoverHarnesses(harnesses: ("pi" | "codex")[], options: HarnessOptions, cwd: string): Promise<RuntimeCapabilities>;
|
|
17
|
-
export declare function executePi(input: RuntimeTurnInput, options: HarnessOptions, cwd: string, store: RuntimeSessionStore, emit: (event: RuntimeExecutionEvent) => void, signal: AbortSignal): Promise<HarnessResult>;
|
|
18
|
+
export declare function executePi(input: RuntimeTurnInput, options: HarnessOptions, cwd: string, store: RuntimeSessionStore, emit: (event: RuntimeExecutionEvent) => void, signal: AbortSignal, diagnostics?: RuntimeDiagnostics, diagnosticContext?: RuntimeDiagnosticContext): Promise<HarnessResult>;
|
|
18
19
|
export declare function codexItemContent(item: JsonRecord): ContentBlock[];
|
|
19
|
-
export declare function executeCodex(input: RuntimeTurnInput, options: HarnessOptions, cwd: string, store: RuntimeSessionStore, emit: (event: RuntimeExecutionEvent) => void, signal: AbortSignal): Promise<HarnessResult>;
|
|
20
|
+
export declare function executeCodex(input: RuntimeTurnInput, options: HarnessOptions, cwd: string, store: RuntimeSessionStore, emit: (event: RuntimeExecutionEvent) => void, signal: AbortSignal, diagnostics?: RuntimeDiagnostics, diagnosticContext?: RuntimeDiagnosticContext): Promise<HarnessResult>;
|
package/dist/runtime/harness.js
CHANGED
|
@@ -2,6 +2,7 @@ import { JsonRpcProcess, record } from "./json-rpc.js";
|
|
|
2
2
|
import { codexModelCatalog } from "./model-catalog.js";
|
|
3
3
|
import { codexTokenTotals, codexUsage, subtractCodexTokens } from "./codex-usage.js";
|
|
4
4
|
import { downloadPublicImage } from "../safe-remote-image.js";
|
|
5
|
+
import { serializeDiagnosticError } from "./diagnostics.js";
|
|
5
6
|
const runtimeEnvironment = (input) => ({ COHUB_SPACE_ID: input.spaceId, COHUB_SESSION_ID: input.sessionId, COHUB_TURN_ID: input.turnId });
|
|
6
7
|
const array = (value) => Array.isArray(value) ? value : [];
|
|
7
8
|
const text = (value) => typeof value === "string" ? value : "";
|
|
@@ -47,8 +48,8 @@ function createAbortEscalation(rpc, signal, interrupt) {
|
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
/** Native files stay authoritative; archival failure only degrades cross-host resume. */
|
|
50
|
-
async function finishHarnessTurn(store, state, message, resume, turnId) {
|
|
51
|
-
const archive = await store.archive(state, turnId).catch((error) => { console.error("Native archive unavailable; local files retained:", error); return null; });
|
|
51
|
+
async function finishHarnessTurn(store, state, message, resume, turnId, diagnosticContext) {
|
|
52
|
+
const archive = await store.archive(state, turnId, diagnosticContext).catch((error) => { console.error("Native archive unavailable; local files retained:", error); return null; });
|
|
52
53
|
return { state, event: { type: "turn.end", message, resume, archive } };
|
|
53
54
|
}
|
|
54
55
|
export async function discoverHarnesses(harnesses, options, cwd) {
|
|
@@ -88,7 +89,7 @@ export async function discoverHarnesses(harnesses, options, cwd) {
|
|
|
88
89
|
}));
|
|
89
90
|
return { harnesses, models };
|
|
90
91
|
}
|
|
91
|
-
export async function executePi(input, options, cwd, store, emit, signal) {
|
|
92
|
+
export async function executePi(input, options, cwd, store, emit, signal, diagnostics, diagnosticContext) {
|
|
92
93
|
if (input.accessMode === "read_only")
|
|
93
94
|
throw new Error("Pi cannot enforce read-only access; select Cohub or Codex");
|
|
94
95
|
signal.throwIfAborted();
|
|
@@ -99,6 +100,12 @@ export async function executePi(input, options, cwd, store, emit, signal) {
|
|
|
99
100
|
let currentContent = [];
|
|
100
101
|
let last = { ordinal: 0, content: [] };
|
|
101
102
|
const abortEscalation = createAbortEscalation(rpc, signal, () => { void rpc.request("abort").catch(() => undefined); });
|
|
103
|
+
const stopFailureLogging = diagnostics
|
|
104
|
+
? rpc.onFailure((error) => diagnostics.log("error", "harness.rpc_process_failed", { error: serializeDiagnosticError(error) }, { ...diagnosticContext, component: "harness" }))
|
|
105
|
+
: () => undefined;
|
|
106
|
+
const stopTimeoutLogging = diagnostics
|
|
107
|
+
? rpc.onTimeout((method, timeoutMs) => diagnostics.log("error", "harness.rpc_timeout", { method, timeoutMs }, { ...diagnosticContext, component: "harness" }))
|
|
108
|
+
: () => undefined;
|
|
102
109
|
try {
|
|
103
110
|
if (input.model) {
|
|
104
111
|
let provider = input.provider;
|
|
@@ -193,11 +200,19 @@ export async function executePi(input, options, cwd, store, emit, signal) {
|
|
|
193
200
|
}
|
|
194
201
|
finally {
|
|
195
202
|
abortEscalation.clear();
|
|
203
|
+
stopFailureLogging();
|
|
204
|
+
stopTimeoutLogging();
|
|
196
205
|
await rpc.close();
|
|
197
206
|
}
|
|
198
207
|
if (signal.aborted)
|
|
199
208
|
last = { ...last, stopReason: "aborted" };
|
|
200
|
-
return finishHarnessTurn(store, state, last, resume, input.turnId
|
|
209
|
+
return finishHarnessTurn(store, state, last, resume, input.turnId, {
|
|
210
|
+
component: "harness",
|
|
211
|
+
sessionId: input.sessionId,
|
|
212
|
+
turnId: input.turnId,
|
|
213
|
+
harness: "pi",
|
|
214
|
+
traceContext: input.traceContext,
|
|
215
|
+
});
|
|
201
216
|
}
|
|
202
217
|
export function codexItemContent(item) {
|
|
203
218
|
if (item.type === "agentMessage" || item.type === "plan")
|
|
@@ -216,11 +231,17 @@ export function codexItemContent(item) {
|
|
|
216
231
|
{ 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 },
|
|
217
232
|
];
|
|
218
233
|
}
|
|
219
|
-
export async function executeCodex(input, options, cwd, store, emit, signal) {
|
|
234
|
+
export async function executeCodex(input, options, cwd, store, emit, signal, diagnostics, diagnosticContext) {
|
|
220
235
|
signal.throwIfAborted();
|
|
221
236
|
const { state, resume } = await store.prepare(input, cwd, signal);
|
|
222
237
|
signal.throwIfAborted();
|
|
223
238
|
const rpc = new JsonRpcProcess(options.codex || "codex", ["app-server", "--listen", "stdio://"], cwd, "codex", runtimeEnvironment(input));
|
|
239
|
+
const stopFailureLogging = diagnostics
|
|
240
|
+
? rpc.onFailure((error) => diagnostics.log("error", "harness.rpc_process_failed", { error: serializeDiagnosticError(error) }, { ...diagnosticContext, component: "harness" }))
|
|
241
|
+
: () => undefined;
|
|
242
|
+
const stopTimeoutLogging = diagnostics
|
|
243
|
+
? rpc.onTimeout((method, timeoutMs) => diagnostics.log("error", "harness.rpc_timeout", { method, timeoutMs }, { ...diagnosticContext, component: "harness" }))
|
|
244
|
+
: () => undefined;
|
|
224
245
|
let nativeTurnId = null;
|
|
225
246
|
let ordinal = -1;
|
|
226
247
|
const ordinals = new Map();
|
|
@@ -250,10 +271,12 @@ export async function executeCodex(input, options, cwd, store, emit, signal) {
|
|
|
250
271
|
...(input.accessMode === "read_only" ? { sandbox: "read-only" } : {}),
|
|
251
272
|
};
|
|
252
273
|
const opened = resume === "native"
|
|
253
|
-
? await rpc.request("thread/resume", { ...threadOptions, threadId: state.nativeSessionId, excludeTurns: true })
|
|
274
|
+
? await rpc.request("thread/resume", { ...threadOptions, threadId: state.nativeSessionId, path: state.path, excludeTurns: true })
|
|
254
275
|
: resume === "restored"
|
|
255
276
|
? await rpc.request("thread/fork", { ...threadOptions, threadId: state.nativeSessionId, path: state.path, excludeTurns: true })
|
|
256
|
-
:
|
|
277
|
+
: resume === "handoff"
|
|
278
|
+
? await rpc.request("thread/resume", { ...threadOptions, threadId: state.nativeSessionId, path: state.path, excludeTurns: true })
|
|
279
|
+
: await rpc.request("thread/start", threadOptions);
|
|
257
280
|
const thread = record(opened.thread);
|
|
258
281
|
if (typeof thread.id !== "string" || typeof thread.path !== "string")
|
|
259
282
|
throw new Error("Codex did not provide a durable native thread");
|
|
@@ -393,11 +416,19 @@ export async function executeCodex(input, options, cwd, store, emit, signal) {
|
|
|
393
416
|
}
|
|
394
417
|
finally {
|
|
395
418
|
abortEscalation.clear();
|
|
419
|
+
stopFailureLogging();
|
|
420
|
+
stopTimeoutLogging();
|
|
396
421
|
await rpc.close();
|
|
397
422
|
}
|
|
398
423
|
if (signal.aborted)
|
|
399
424
|
final = { ...final, stopReason: "aborted" };
|
|
400
425
|
if (usage)
|
|
401
426
|
final = { ...final, usage };
|
|
402
|
-
return finishHarnessTurn(store, state, final, resume, input.turnId
|
|
427
|
+
return finishHarnessTurn(store, state, final, resume, input.turnId, {
|
|
428
|
+
component: "harness",
|
|
429
|
+
sessionId: input.sessionId,
|
|
430
|
+
turnId: input.turnId,
|
|
431
|
+
harness: "codex",
|
|
432
|
+
traceContext: input.traceContext,
|
|
433
|
+
});
|
|
403
434
|
}
|
|
@@ -20,6 +20,7 @@ export declare class JsonRpcProcess {
|
|
|
20
20
|
private pending;
|
|
21
21
|
private listeners;
|
|
22
22
|
private failureListeners;
|
|
23
|
+
private timeoutListeners;
|
|
23
24
|
private failure;
|
|
24
25
|
private nextId;
|
|
25
26
|
private stderr;
|
|
@@ -32,6 +33,7 @@ export declare class JsonRpcProcess {
|
|
|
32
33
|
request(method: string, params?: JsonRecord, timeoutMs?: number): Promise<JsonRecord>;
|
|
33
34
|
onEvent(listener: (event: JsonRecord) => void): () => boolean;
|
|
34
35
|
onFailure(listener: (error: Error) => void): () => boolean;
|
|
36
|
+
onTimeout(listener: (method: string, timeoutMs: number) => void): () => boolean;
|
|
35
37
|
close(): Promise<void>;
|
|
36
38
|
private closeProcessGroup;
|
|
37
39
|
}
|
package/dist/runtime/json-rpc.js
CHANGED
|
@@ -53,6 +53,7 @@ export class JsonRpcProcess {
|
|
|
53
53
|
pending = new Map();
|
|
54
54
|
listeners = new Set();
|
|
55
55
|
failureListeners = new Set();
|
|
56
|
+
timeoutListeners = new Set();
|
|
56
57
|
failure = null;
|
|
57
58
|
nextId = 0;
|
|
58
59
|
stderr = "";
|
|
@@ -123,7 +124,12 @@ export class JsonRpcProcess {
|
|
|
123
124
|
return Promise.reject(this.failure);
|
|
124
125
|
const id = String(++this.nextId);
|
|
125
126
|
return new Promise((resolve, reject) => {
|
|
126
|
-
const timer = setTimeout(() => {
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
this.pending.delete(id);
|
|
129
|
+
for (const listener of this.timeoutListeners)
|
|
130
|
+
listener(method, timeoutMs);
|
|
131
|
+
reject(new Error(`${method} timed out`));
|
|
132
|
+
}, timeoutMs);
|
|
127
133
|
this.pending.set(id, { resolve, reject, timer });
|
|
128
134
|
try {
|
|
129
135
|
this.write(this.mode === "pi" ? { ...params, id, type: method } : { id, method, params });
|
|
@@ -143,6 +149,10 @@ export class JsonRpcProcess {
|
|
|
143
149
|
listener(this.failure); });
|
|
144
150
|
return () => this.failureListeners.delete(listener);
|
|
145
151
|
}
|
|
152
|
+
onTimeout(listener) {
|
|
153
|
+
this.timeoutListeners.add(listener);
|
|
154
|
+
return () => this.timeoutListeners.delete(listener);
|
|
155
|
+
}
|
|
146
156
|
close() {
|
|
147
157
|
this.closing ??= this.closeProcessGroup();
|
|
148
158
|
return this.closing;
|
|
@@ -159,6 +169,7 @@ export class JsonRpcProcess {
|
|
|
159
169
|
this.fail(new Error("RPC process closed"));
|
|
160
170
|
this.listeners.clear();
|
|
161
171
|
this.failureListeners.clear();
|
|
172
|
+
this.timeoutListeners.clear();
|
|
162
173
|
}
|
|
163
174
|
await this.closed;
|
|
164
175
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type NativeProjection, type ProjectionTarget } from "@neta-art/cohub";
|
|
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
|
+
export type ProjectionStoreInput = {
|
|
9
|
+
spaceId: string;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
turnId: string;
|
|
12
|
+
nativeSessionId: string;
|
|
13
|
+
cwd: string;
|
|
14
|
+
provider?: string | null;
|
|
15
|
+
target: ProjectionTarget;
|
|
16
|
+
throughTurnId: string | null;
|
|
17
|
+
cursor: ProjectionCursorState | null;
|
|
18
|
+
};
|
|
19
|
+
export type ProjectionStoreResult = {
|
|
20
|
+
projection: NativeProjection;
|
|
21
|
+
turns: ProjectionSourceTurn[];
|
|
22
|
+
append: boolean;
|
|
23
|
+
cursor: ProjectionCursorState;
|
|
24
|
+
};
|
|
25
|
+
export declare function rebindProjectionNativeSession(result: ProjectionStoreResult, nativeSessionId: string): ProjectionStoreResult;
|
|
26
|
+
/** Reads durable Cohub turns and materializes one harness-specific native projection batch. */
|
|
27
|
+
export declare class ProjectionStore {
|
|
28
|
+
private readonly source;
|
|
29
|
+
constructor(source: SessionTurnProjectionClient);
|
|
30
|
+
project(input: ProjectionStoreInput, signal?: AbortSignal): Promise<ProjectionStoreResult>;
|
|
31
|
+
private sourceSequence;
|
|
32
|
+
cursorForTurn(sessionId: string, turnId: string, signal?: AbortSignal): Promise<ProjectionCursorState>;
|
|
33
|
+
private readTurns;
|
|
34
|
+
}
|