gnhf 0.1.12 → 0.1.13
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 +2 -4
- package/dist/cli.mjs +710 -69
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -192,11 +192,9 @@ When sleep prevention is enabled, `gnhf` uses the native mechanism for your OS:
|
|
|
192
192
|
|
|
193
193
|
## Debug Logs
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
Every run writes a JSONL debug log to `.gnhf/runs/<runId>/gnhf.log` alongside `notes.md`. Lifecycle events for the orchestrator, agent, and HTTP requests are captured with elapsed timings and (for failures) the full `error.cause` chain — which is what you need to tell a bare `TypeError: fetch failed` apart from an undici `UND_ERR_HEADERS_TIMEOUT`. The agent's own streaming output still goes to the per-iteration `iteration-<n>.jsonl` file next to it.
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
GNHF_DEBUG_LOG_PATH=/tmp/gnhf-debug.jsonl gnhf "ship it"
|
|
199
|
-
```
|
|
197
|
+
Including a snippet of `gnhf.log` is the single most useful thing you can attach when filing an issue.
|
|
200
198
|
|
|
201
199
|
## Agents
|
|
202
200
|
|
package/dist/cli.mjs
CHANGED
|
@@ -135,18 +135,111 @@ function loadConfig(overrides) {
|
|
|
135
135
|
}
|
|
136
136
|
//#endregion
|
|
137
137
|
//#region src/core/debug-log.ts
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
const PRE_INIT_BUFFER_CAPACITY = 1e3;
|
|
139
|
+
const STACK_LINE_LIMIT = 12;
|
|
140
|
+
const CAUSE_DEPTH_LIMIT = 6;
|
|
141
|
+
let logPath = null;
|
|
142
|
+
let preInitBuffer = [];
|
|
143
|
+
let preInitDroppedCount = 0;
|
|
144
|
+
function formatLine(event, details) {
|
|
145
|
+
const base = {
|
|
146
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
147
|
+
pid: process.pid,
|
|
148
|
+
event
|
|
149
|
+
};
|
|
141
150
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
pid: process.pid,
|
|
145
|
-
event,
|
|
151
|
+
return `${JSON.stringify({
|
|
152
|
+
...base,
|
|
146
153
|
...details
|
|
147
|
-
})}\n
|
|
154
|
+
})}\n`;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return `${JSON.stringify({
|
|
157
|
+
...base,
|
|
158
|
+
logError: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
|
|
159
|
+
detailsKeys: Object.keys(details)
|
|
160
|
+
})}\n`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function initDebugLog(path) {
|
|
164
|
+
logPath = path;
|
|
165
|
+
try {
|
|
166
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
167
|
+
} catch {}
|
|
168
|
+
if (preInitBuffer.length === 0 && preInitDroppedCount === 0) return;
|
|
169
|
+
const flushed = (preInitDroppedCount > 0 ? formatLine("debug-log:pre-init-overflow", {
|
|
170
|
+
droppedCount: preInitDroppedCount,
|
|
171
|
+
bufferCapacity: PRE_INIT_BUFFER_CAPACITY
|
|
172
|
+
}) : "") + preInitBuffer.join("");
|
|
173
|
+
preInitBuffer = [];
|
|
174
|
+
preInitDroppedCount = 0;
|
|
175
|
+
try {
|
|
176
|
+
appendFileSync(path, flushed, "utf-8");
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
179
|
+
function appendDebugLog(event, details = {}) {
|
|
180
|
+
const line = formatLine(event, details);
|
|
181
|
+
if (logPath === null) {
|
|
182
|
+
preInitBuffer.push(line);
|
|
183
|
+
if (preInitBuffer.length > PRE_INIT_BUFFER_CAPACITY) {
|
|
184
|
+
preInitBuffer.shift();
|
|
185
|
+
preInitDroppedCount += 1;
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
appendFileSync(logPath, line, "utf-8");
|
|
148
191
|
} catch {}
|
|
149
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Serialize an error (including its cause chain) to a plain object that's
|
|
195
|
+
* safe to embed in a JSONL log line. This is critical for diagnosing
|
|
196
|
+
* fetch failures, where the surface message is often just "fetch failed"
|
|
197
|
+
* but `err.cause` holds the real undici error (e.g. UND_ERR_HEADERS_TIMEOUT).
|
|
198
|
+
*
|
|
199
|
+
* Contract: must never throw. Callers invoke this inside catch blocks and
|
|
200
|
+
* timer/EventEmitter handlers, and a throw here would mask the original
|
|
201
|
+
* error or crash the process.
|
|
202
|
+
*/
|
|
203
|
+
function serializeError(error, depth = 0) {
|
|
204
|
+
try {
|
|
205
|
+
return serializeErrorUnsafe(error, depth);
|
|
206
|
+
} catch (serializationError) {
|
|
207
|
+
return {
|
|
208
|
+
value: "[serialization failed]",
|
|
209
|
+
serializationError: serializationError instanceof Error ? `${serializationError.name}: ${serializationError.message}` : String(serializationError)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function tryRead(read) {
|
|
214
|
+
try {
|
|
215
|
+
return read();
|
|
216
|
+
} catch {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function serializeErrorUnsafe(error, depth) {
|
|
221
|
+
if (depth > CAUSE_DEPTH_LIMIT) return { value: "[cause chain truncated]" };
|
|
222
|
+
if (error instanceof Error) {
|
|
223
|
+
const result = {
|
|
224
|
+
name: tryRead(() => error.name) ?? "Error",
|
|
225
|
+
message: tryRead(() => error.message) ?? ""
|
|
226
|
+
};
|
|
227
|
+
const code = tryRead(() => error.code);
|
|
228
|
+
if (typeof code === "string" || typeof code === "number") result.code = code;
|
|
229
|
+
const stack = tryRead(() => error.stack);
|
|
230
|
+
if (typeof stack === "string") result.stack = stack.split("\n").slice(0, STACK_LINE_LIMIT).join("\n");
|
|
231
|
+
const cause = tryRead(() => "cause" in error ? error.cause : void 0);
|
|
232
|
+
if (cause !== void 0) result.cause = serializeError(cause, depth + 1);
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
if (error === null || error === void 0) return { value: String(error) };
|
|
236
|
+
if (typeof error === "object") try {
|
|
237
|
+
return { value: JSON.parse(JSON.stringify(error)) };
|
|
238
|
+
} catch {
|
|
239
|
+
return { value: String(error) };
|
|
240
|
+
}
|
|
241
|
+
return { value: String(error) };
|
|
242
|
+
}
|
|
150
243
|
//#endregion
|
|
151
244
|
//#region src/core/git.ts
|
|
152
245
|
const NOT_GIT_REPOSITORY_MESSAGE = "This command must be run inside a Git repository. Change into a repo or run \"git init\" first.";
|
|
@@ -255,6 +348,7 @@ const AGENT_OUTPUT_SCHEMA = {
|
|
|
255
348
|
};
|
|
256
349
|
//#endregion
|
|
257
350
|
//#region src/core/run.ts
|
|
351
|
+
const LOG_FILENAME = "gnhf.log";
|
|
258
352
|
function writeSchemaFile(schemaPath) {
|
|
259
353
|
writeFileSync(schemaPath, JSON.stringify(AGENT_OUTPUT_SCHEMA, null, 2), "utf-8");
|
|
260
354
|
}
|
|
@@ -286,6 +380,7 @@ function setupRun(runId, prompt, baseCommit, cwd) {
|
|
|
286
380
|
writeFileSync(notesPath, `# gnhf run: ${runId}\n\nObjective: ${prompt}\n\n## Iteration Log\n`, "utf-8");
|
|
287
381
|
const schemaPath = join(runDir, "output-schema.json");
|
|
288
382
|
writeSchemaFile(schemaPath);
|
|
383
|
+
const logPath = join(runDir, LOG_FILENAME);
|
|
289
384
|
const baseCommitPath = join(runDir, "base-commit");
|
|
290
385
|
const hasStoredBaseCommit = existsSync(baseCommitPath);
|
|
291
386
|
const resolvedBaseCommit = hasStoredBaseCommit ? readFileSync(baseCommitPath, "utf-8").trim() : baseCommit;
|
|
@@ -296,6 +391,7 @@ function setupRun(runId, prompt, baseCommit, cwd) {
|
|
|
296
391
|
promptPath,
|
|
297
392
|
notesPath,
|
|
298
393
|
schemaPath,
|
|
394
|
+
logPath,
|
|
299
395
|
baseCommit: resolvedBaseCommit,
|
|
300
396
|
baseCommitPath
|
|
301
397
|
};
|
|
@@ -307,6 +403,7 @@ function resumeRun(runId, cwd) {
|
|
|
307
403
|
const notesPath = join(runDir, "notes.md");
|
|
308
404
|
const schemaPath = join(runDir, "output-schema.json");
|
|
309
405
|
writeSchemaFile(schemaPath);
|
|
406
|
+
const logPath = join(runDir, LOG_FILENAME);
|
|
310
407
|
const baseCommitPath = join(runDir, "base-commit");
|
|
311
408
|
return {
|
|
312
409
|
runId,
|
|
@@ -314,6 +411,7 @@ function resumeRun(runId, cwd) {
|
|
|
314
411
|
promptPath,
|
|
315
412
|
notesPath,
|
|
316
413
|
schemaPath,
|
|
414
|
+
logPath,
|
|
317
415
|
baseCommit: existsSync(baseCommitPath) ? readFileSync(baseCommitPath, "utf-8").trim() : backfillLegacyBaseCommit(runId, baseCommitPath, cwd),
|
|
318
416
|
baseCommitPath
|
|
319
417
|
};
|
|
@@ -1179,20 +1277,48 @@ var OpenCodeAgent = class {
|
|
|
1179
1277
|
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
1180
1278
|
const runController = new AbortController();
|
|
1181
1279
|
let sessionId = null;
|
|
1280
|
+
const runStartedAt = Date.now();
|
|
1281
|
+
appendDebugLog("opencode:run:start", {
|
|
1282
|
+
cwd,
|
|
1283
|
+
promptLength: prompt.length,
|
|
1284
|
+
hasLogPath: logPath !== void 0
|
|
1285
|
+
});
|
|
1182
1286
|
const onAbort = () => {
|
|
1183
1287
|
runController.abort();
|
|
1184
1288
|
};
|
|
1185
1289
|
if (signal?.aborted) {
|
|
1186
1290
|
logStream?.end();
|
|
1291
|
+
appendDebugLog("opencode:run:aborted-early", {});
|
|
1187
1292
|
throw createAbortError$1();
|
|
1188
1293
|
}
|
|
1189
1294
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1190
1295
|
try {
|
|
1191
1296
|
const server = await this.ensureServer(cwd, runController.signal);
|
|
1192
1297
|
sessionId = await this.createSession(server, cwd, runController.signal);
|
|
1193
|
-
|
|
1298
|
+
const result = await this.streamMessage(server, sessionId, buildPrompt(prompt), runController.signal, logStream, onUsage, onMessage);
|
|
1299
|
+
appendDebugLog("opencode:run:end", {
|
|
1300
|
+
sessionId,
|
|
1301
|
+
elapsedMs: Date.now() - runStartedAt,
|
|
1302
|
+
inputTokens: result.usage.inputTokens,
|
|
1303
|
+
outputTokens: result.usage.outputTokens
|
|
1304
|
+
});
|
|
1305
|
+
return result;
|
|
1194
1306
|
} catch (error) {
|
|
1195
|
-
if (runController.signal.aborted || isAbortError$1(error))
|
|
1307
|
+
if (runController.signal.aborted || isAbortError$1(error)) {
|
|
1308
|
+
appendDebugLog("opencode:run:aborted", {
|
|
1309
|
+
sessionId,
|
|
1310
|
+
elapsedMs: Date.now() - runStartedAt
|
|
1311
|
+
});
|
|
1312
|
+
throw createAbortError$1();
|
|
1313
|
+
}
|
|
1314
|
+
appendDebugLog("opencode:run:error", {
|
|
1315
|
+
sessionId,
|
|
1316
|
+
elapsedMs: Date.now() - runStartedAt,
|
|
1317
|
+
error: serializeError(error),
|
|
1318
|
+
serverStderr: this.server?.stderr.slice(-2048),
|
|
1319
|
+
serverStdout: this.server?.stdout.slice(-2048),
|
|
1320
|
+
serverClosed: this.server?.closed ?? true
|
|
1321
|
+
});
|
|
1196
1322
|
throw error;
|
|
1197
1323
|
} finally {
|
|
1198
1324
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -1249,25 +1375,82 @@ var OpenCodeAgent = class {
|
|
|
1249
1375
|
stdout: ""
|
|
1250
1376
|
};
|
|
1251
1377
|
const maxOutput = 64 * 1024;
|
|
1378
|
+
const maxMirroredLineLength = 2048;
|
|
1379
|
+
const maxMirroredLinesPerRun = 500;
|
|
1380
|
+
let mirroredLineCount = 0;
|
|
1381
|
+
let mirroredSuppressionLogged = false;
|
|
1382
|
+
const stderrLineBuffer = { tail: "" };
|
|
1383
|
+
const mirrorStderrChunk = (chunk) => {
|
|
1384
|
+
stderrLineBuffer.tail += chunk;
|
|
1385
|
+
let newlineIndex = stderrLineBuffer.tail.indexOf("\n");
|
|
1386
|
+
while (newlineIndex !== -1) {
|
|
1387
|
+
const rawLine = stderrLineBuffer.tail.slice(0, newlineIndex);
|
|
1388
|
+
stderrLineBuffer.tail = stderrLineBuffer.tail.slice(newlineIndex + 1);
|
|
1389
|
+
const line = rawLine.replace(/\r$/, "");
|
|
1390
|
+
if (line.length > 0) if (mirroredLineCount >= maxMirroredLinesPerRun) {
|
|
1391
|
+
if (!mirroredSuppressionLogged) {
|
|
1392
|
+
appendDebugLog("opencode:server:stderr:suppressed", {
|
|
1393
|
+
port: server.port,
|
|
1394
|
+
cap: maxMirroredLinesPerRun
|
|
1395
|
+
});
|
|
1396
|
+
mirroredSuppressionLogged = true;
|
|
1397
|
+
}
|
|
1398
|
+
} else {
|
|
1399
|
+
mirroredLineCount += 1;
|
|
1400
|
+
appendDebugLog("opencode:server:stderr", {
|
|
1401
|
+
port: server.port,
|
|
1402
|
+
line: line.length > maxMirroredLineLength ? `${line.slice(0, maxMirroredLineLength)}…` : line,
|
|
1403
|
+
truncated: line.length > maxMirroredLineLength
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
newlineIndex = stderrLineBuffer.tail.indexOf("\n");
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1252
1409
|
child.stdout.on("data", (data) => {
|
|
1253
1410
|
server.stdout += data.toString();
|
|
1254
1411
|
if (server.stdout.length > maxOutput) server.stdout = server.stdout.slice(-maxOutput);
|
|
1255
1412
|
});
|
|
1256
1413
|
child.stderr.on("data", (data) => {
|
|
1257
|
-
|
|
1414
|
+
const text = data.toString();
|
|
1415
|
+
server.stderr += text;
|
|
1258
1416
|
if (server.stderr.length > maxOutput) server.stderr = server.stderr.slice(-maxOutput);
|
|
1417
|
+
mirrorStderrChunk(text);
|
|
1259
1418
|
});
|
|
1260
|
-
child.on("close", () => {
|
|
1419
|
+
child.on("close", (code, closeSignal) => {
|
|
1261
1420
|
server.closed = true;
|
|
1421
|
+
if (stderrLineBuffer.tail.length > 0) mirrorStderrChunk("\n");
|
|
1422
|
+
appendDebugLog("opencode:server:close", {
|
|
1423
|
+
cwd: server.cwd,
|
|
1424
|
+
port: server.port,
|
|
1425
|
+
code,
|
|
1426
|
+
signal: closeSignal,
|
|
1427
|
+
stderr: server.stderr.slice(-2048),
|
|
1428
|
+
stdout: server.stdout.slice(-2048)
|
|
1429
|
+
});
|
|
1262
1430
|
if (this.server === server) this.server = null;
|
|
1263
1431
|
});
|
|
1264
1432
|
this.server = server;
|
|
1433
|
+
const spawnedAt = Date.now();
|
|
1265
1434
|
appendDebugLog("opencode:spawn", {
|
|
1266
1435
|
cwd,
|
|
1267
1436
|
port,
|
|
1268
|
-
detached
|
|
1437
|
+
detached,
|
|
1438
|
+
pid: child.pid,
|
|
1439
|
+
bin: this.bin
|
|
1269
1440
|
});
|
|
1270
|
-
server.readyPromise = this.waitForHealthy(server, signal).
|
|
1441
|
+
server.readyPromise = this.waitForHealthy(server, signal).then(() => {
|
|
1442
|
+
appendDebugLog("opencode:server:ready", {
|
|
1443
|
+
port,
|
|
1444
|
+
elapsedMs: Date.now() - spawnedAt
|
|
1445
|
+
});
|
|
1446
|
+
}).catch(async (error) => {
|
|
1447
|
+
appendDebugLog("opencode:server:ready-failed", {
|
|
1448
|
+
port,
|
|
1449
|
+
elapsedMs: Date.now() - spawnedAt,
|
|
1450
|
+
error: serializeError(error),
|
|
1451
|
+
stderr: server.stderr.slice(-2048),
|
|
1452
|
+
stdout: server.stdout.slice(-2048)
|
|
1453
|
+
});
|
|
1271
1454
|
await this.shutdownServer();
|
|
1272
1455
|
throw error;
|
|
1273
1456
|
});
|
|
@@ -1300,44 +1483,70 @@ var OpenCodeAgent = class {
|
|
|
1300
1483
|
throw new Error(`Timed out waiting for opencode serve to become ready on port ${server.port}`);
|
|
1301
1484
|
}
|
|
1302
1485
|
async createSession(server, cwd, signal) {
|
|
1303
|
-
|
|
1486
|
+
const response = await this.requestJSON(server, "/session", {
|
|
1304
1487
|
method: "POST",
|
|
1305
1488
|
body: {
|
|
1306
1489
|
directory: cwd,
|
|
1307
1490
|
permission: BLANKET_PERMISSION_RULESET
|
|
1308
1491
|
},
|
|
1309
1492
|
signal
|
|
1310
|
-
})
|
|
1493
|
+
});
|
|
1494
|
+
appendDebugLog("opencode:session:create", { sessionId: response.id });
|
|
1495
|
+
return response.id;
|
|
1311
1496
|
}
|
|
1312
1497
|
async streamMessage(server, sessionId, prompt, signal, logStream, onUsage, onMessage) {
|
|
1313
1498
|
const streamAbortController = new AbortController();
|
|
1314
1499
|
const streamSignal = AbortSignal.any([signal, streamAbortController.signal]);
|
|
1500
|
+
const streamStartedAt = Date.now();
|
|
1501
|
+
appendDebugLog("opencode:stream:start", { sessionId });
|
|
1315
1502
|
const eventResponse = await this.request(server, "/global/event", {
|
|
1316
1503
|
method: "GET",
|
|
1317
1504
|
headers: { accept: "text/event-stream" },
|
|
1318
1505
|
signal: streamSignal
|
|
1319
1506
|
});
|
|
1320
|
-
if (!eventResponse.body)
|
|
1507
|
+
if (!eventResponse.body) {
|
|
1508
|
+
appendDebugLog("opencode:stream:no-body", { sessionId });
|
|
1509
|
+
throw new Error("opencode returned no event stream body");
|
|
1510
|
+
}
|
|
1511
|
+
const messagePostStartedAt = Date.now();
|
|
1512
|
+
appendDebugLog("opencode:message-post:start", {
|
|
1513
|
+
sessionId,
|
|
1514
|
+
promptLength: prompt.length
|
|
1515
|
+
});
|
|
1321
1516
|
let messageRequestError = null;
|
|
1322
1517
|
const messageRequest = (async () => {
|
|
1323
1518
|
try {
|
|
1519
|
+
const body = await this.requestText(server, `/session/${sessionId}/message`, {
|
|
1520
|
+
method: "POST",
|
|
1521
|
+
body: {
|
|
1522
|
+
role: "user",
|
|
1523
|
+
parts: [{
|
|
1524
|
+
type: "text",
|
|
1525
|
+
text: prompt
|
|
1526
|
+
}],
|
|
1527
|
+
format: STRUCTURED_OUTPUT_FORMAT
|
|
1528
|
+
},
|
|
1529
|
+
signal
|
|
1530
|
+
});
|
|
1531
|
+
appendDebugLog("opencode:message-post:end", {
|
|
1532
|
+
sessionId,
|
|
1533
|
+
elapsedMs: Date.now() - messagePostStartedAt,
|
|
1534
|
+
bodyLength: body.length
|
|
1535
|
+
});
|
|
1324
1536
|
return {
|
|
1325
1537
|
ok: true,
|
|
1326
|
-
body
|
|
1327
|
-
method: "POST",
|
|
1328
|
-
body: {
|
|
1329
|
-
role: "user",
|
|
1330
|
-
parts: [{
|
|
1331
|
-
type: "text",
|
|
1332
|
-
text: prompt
|
|
1333
|
-
}],
|
|
1334
|
-
format: STRUCTURED_OUTPUT_FORMAT
|
|
1335
|
-
},
|
|
1336
|
-
signal
|
|
1337
|
-
})
|
|
1538
|
+
body
|
|
1338
1539
|
};
|
|
1339
1540
|
} catch (error) {
|
|
1340
1541
|
messageRequestError = error;
|
|
1542
|
+
appendDebugLog("opencode:message-post:error", {
|
|
1543
|
+
sessionId,
|
|
1544
|
+
elapsedMs: Date.now() - messagePostStartedAt,
|
|
1545
|
+
error: serializeError(error),
|
|
1546
|
+
serverClosed: server.closed,
|
|
1547
|
+
serverStderr: server.stderr.slice(-2048),
|
|
1548
|
+
streamTelemetry: buildTelemetry()
|
|
1549
|
+
});
|
|
1341
1550
|
streamAbortController.abort();
|
|
1342
1551
|
return {
|
|
1343
1552
|
ok: false,
|
|
@@ -1356,6 +1565,66 @@ var OpenCodeAgent = class {
|
|
|
1356
1565
|
let lastText = null;
|
|
1357
1566
|
let lastFinalAnswerText = null;
|
|
1358
1567
|
let lastUsageSignature = "0:0:0:0";
|
|
1568
|
+
const eventCounts = {};
|
|
1569
|
+
let firstEventAtMs = null;
|
|
1570
|
+
let lastEventAtMs = null;
|
|
1571
|
+
let lastHeartbeatAtMs = null;
|
|
1572
|
+
const phaseTransitions = [];
|
|
1573
|
+
let currentPhase = null;
|
|
1574
|
+
const noteEvent = (type) => {
|
|
1575
|
+
const key = type ?? "unknown";
|
|
1576
|
+
eventCounts[key] = (eventCounts[key] ?? 0) + 1;
|
|
1577
|
+
const nowMs = Date.now() - streamStartedAt;
|
|
1578
|
+
if (type === "server.heartbeat") {
|
|
1579
|
+
lastHeartbeatAtMs = nowMs;
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
if (firstEventAtMs === null) firstEventAtMs = nowMs;
|
|
1583
|
+
lastEventAtMs = nowMs;
|
|
1584
|
+
};
|
|
1585
|
+
const notePhase = (phase) => {
|
|
1586
|
+
if (!phase || phase === currentPhase) return;
|
|
1587
|
+
currentPhase = phase;
|
|
1588
|
+
phaseTransitions.push({
|
|
1589
|
+
phase,
|
|
1590
|
+
atMs: Date.now() - streamStartedAt
|
|
1591
|
+
});
|
|
1592
|
+
};
|
|
1593
|
+
const buildTelemetry = () => ({
|
|
1594
|
+
eventCounts: { ...eventCounts },
|
|
1595
|
+
firstEventAtMs,
|
|
1596
|
+
lastEventAtMs,
|
|
1597
|
+
lastHeartbeatAtMs,
|
|
1598
|
+
msSinceLastEvent: lastEventAtMs === null ? null : Date.now() - streamStartedAt - lastEventAtMs,
|
|
1599
|
+
phaseTransitions: [...phaseTransitions],
|
|
1600
|
+
currentPhase,
|
|
1601
|
+
sawSessionIdle: (eventCounts["session.idle"] ?? 0) > 0
|
|
1602
|
+
});
|
|
1603
|
+
const STALL_THRESHOLDS_MS = [
|
|
1604
|
+
6e4,
|
|
1605
|
+
12e4,
|
|
1606
|
+
24e4,
|
|
1607
|
+
48e4
|
|
1608
|
+
];
|
|
1609
|
+
let nextStallThresholdIndex = 0;
|
|
1610
|
+
const stallTimer = setInterval(() => {
|
|
1611
|
+
if (nextStallThresholdIndex >= STALL_THRESHOLDS_MS.length) return;
|
|
1612
|
+
const threshold = STALL_THRESHOLDS_MS[nextStallThresholdIndex];
|
|
1613
|
+
const referencePointMs = lastEventAtMs ?? firstEventAtMs ?? 0;
|
|
1614
|
+
const silenceMs = Date.now() - streamStartedAt - referencePointMs;
|
|
1615
|
+
if (silenceMs < threshold) return;
|
|
1616
|
+
nextStallThresholdIndex += 1;
|
|
1617
|
+
appendDebugLog("opencode:stream:stall", {
|
|
1618
|
+
sessionId,
|
|
1619
|
+
thresholdMs: threshold,
|
|
1620
|
+
silenceMs,
|
|
1621
|
+
currentPhase,
|
|
1622
|
+
lastEventAtMs,
|
|
1623
|
+
lastHeartbeatAtMs,
|
|
1624
|
+
eventCounts: { ...eventCounts }
|
|
1625
|
+
});
|
|
1626
|
+
}, 15e3);
|
|
1627
|
+
stallTimer.unref?.();
|
|
1359
1628
|
const updateUsage = (messageId, tokens) => {
|
|
1360
1629
|
if (!messageId || !tokens) return;
|
|
1361
1630
|
usageByMessageId.set(messageId, toUsage(tokens));
|
|
@@ -1390,6 +1659,7 @@ var OpenCodeAgent = class {
|
|
|
1390
1659
|
text: nextText,
|
|
1391
1660
|
phase
|
|
1392
1661
|
});
|
|
1662
|
+
notePhase(phase);
|
|
1393
1663
|
if (!trimmed) return;
|
|
1394
1664
|
lastText = nextText;
|
|
1395
1665
|
if (phase === "final_answer") lastFinalAnswerText = nextText;
|
|
@@ -1432,7 +1702,9 @@ var OpenCodeAgent = class {
|
|
|
1432
1702
|
const dataLines = rawEvent.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart());
|
|
1433
1703
|
if (dataLines.length === 0) return;
|
|
1434
1704
|
try {
|
|
1435
|
-
|
|
1705
|
+
const event = JSON.parse(dataLines.join("\n"));
|
|
1706
|
+
noteEvent(event.payload?.type);
|
|
1707
|
+
if (handleEvent(event)) sawSessionIdle = true;
|
|
1436
1708
|
} catch {}
|
|
1437
1709
|
};
|
|
1438
1710
|
const processBufferedEvents = (flushRemainder = false) => {
|
|
@@ -1458,6 +1730,7 @@ var OpenCodeAgent = class {
|
|
|
1458
1730
|
buffer = "";
|
|
1459
1731
|
}
|
|
1460
1732
|
};
|
|
1733
|
+
let bytesRead = 0;
|
|
1461
1734
|
try {
|
|
1462
1735
|
while (!sawSessionIdle) {
|
|
1463
1736
|
let readResult;
|
|
@@ -1465,9 +1738,27 @@ var OpenCodeAgent = class {
|
|
|
1465
1738
|
readResult = await reader.read();
|
|
1466
1739
|
} catch (error) {
|
|
1467
1740
|
if (messageRequestError) {
|
|
1741
|
+
appendDebugLog("opencode:stream:error", {
|
|
1742
|
+
sessionId,
|
|
1743
|
+
elapsedMs: Date.now() - streamStartedAt,
|
|
1744
|
+
bytesRead,
|
|
1745
|
+
reason: "message-post-failed",
|
|
1746
|
+
error: serializeError(messageRequestError),
|
|
1747
|
+
telemetry: buildTelemetry()
|
|
1748
|
+
});
|
|
1468
1749
|
if (isAbortError$1(messageRequestError) || isAgentAbortError(messageRequestError)) throw createAbortError$1();
|
|
1469
1750
|
throw messageRequestError;
|
|
1470
1751
|
}
|
|
1752
|
+
appendDebugLog("opencode:stream:error", {
|
|
1753
|
+
sessionId,
|
|
1754
|
+
elapsedMs: Date.now() - streamStartedAt,
|
|
1755
|
+
bytesRead,
|
|
1756
|
+
reason: "reader-read-failed",
|
|
1757
|
+
error: serializeError(error),
|
|
1758
|
+
serverClosed: server.closed,
|
|
1759
|
+
serverStderr: server.stderr.slice(-2048),
|
|
1760
|
+
telemetry: buildTelemetry()
|
|
1761
|
+
});
|
|
1471
1762
|
if (isAbortError$1(error)) throw createAbortError$1();
|
|
1472
1763
|
throw error;
|
|
1473
1764
|
}
|
|
@@ -1476,6 +1767,7 @@ var OpenCodeAgent = class {
|
|
|
1476
1767
|
if (tail) {
|
|
1477
1768
|
logStream?.write(tail);
|
|
1478
1769
|
buffer += tail;
|
|
1770
|
+
bytesRead += tail.length;
|
|
1479
1771
|
}
|
|
1480
1772
|
processBufferedEvents(true);
|
|
1481
1773
|
break;
|
|
@@ -1483,12 +1775,21 @@ var OpenCodeAgent = class {
|
|
|
1483
1775
|
const chunk = decoder.decode(readResult.value, { stream: true });
|
|
1484
1776
|
logStream?.write(chunk);
|
|
1485
1777
|
buffer += chunk;
|
|
1778
|
+
bytesRead += chunk.length;
|
|
1486
1779
|
processBufferedEvents();
|
|
1487
1780
|
}
|
|
1488
1781
|
} finally {
|
|
1782
|
+
clearInterval(stallTimer);
|
|
1489
1783
|
streamAbortController.abort();
|
|
1490
1784
|
await reader.cancel().catch(() => void 0);
|
|
1491
1785
|
}
|
|
1786
|
+
appendDebugLog("opencode:stream:end", {
|
|
1787
|
+
sessionId,
|
|
1788
|
+
elapsedMs: Date.now() - streamStartedAt,
|
|
1789
|
+
bytesRead,
|
|
1790
|
+
sawSessionIdle,
|
|
1791
|
+
telemetry: buildTelemetry()
|
|
1792
|
+
});
|
|
1492
1793
|
const messageResult = await messageRequest;
|
|
1493
1794
|
if (!messageResult.ok) {
|
|
1494
1795
|
if (isAbortError$1(messageResult.error) || isAgentAbortError(messageResult.error)) throw createAbortError$1();
|
|
@@ -1499,6 +1800,12 @@ var OpenCodeAgent = class {
|
|
|
1499
1800
|
try {
|
|
1500
1801
|
response = JSON.parse(body);
|
|
1501
1802
|
} catch (error) {
|
|
1803
|
+
appendDebugLog("opencode:response:parse-error", {
|
|
1804
|
+
sessionId,
|
|
1805
|
+
bodyLength: body.length,
|
|
1806
|
+
bodySample: body.slice(0, 512),
|
|
1807
|
+
error: serializeError(error)
|
|
1808
|
+
});
|
|
1502
1809
|
throw new Error(`Failed to parse opencode response: ${error instanceof Error ? error.message : String(error)}`);
|
|
1503
1810
|
}
|
|
1504
1811
|
if (response.info?.role === "assistant") updateUsage(response.info.id, response.info.tokens);
|
|
@@ -1508,18 +1815,43 @@ var OpenCodeAgent = class {
|
|
|
1508
1815
|
lastText = part.text;
|
|
1509
1816
|
if (part.metadata?.openai?.phase === "final_answer") lastFinalAnswerText = part.text;
|
|
1510
1817
|
}
|
|
1511
|
-
if (response.info?.structured)
|
|
1512
|
-
output:
|
|
1513
|
-
|
|
1514
|
-
|
|
1818
|
+
if (response.info?.structured) {
|
|
1819
|
+
appendDebugLog("opencode:output:structured", {
|
|
1820
|
+
sessionId,
|
|
1821
|
+
source: "response.info.structured"
|
|
1822
|
+
});
|
|
1823
|
+
return {
|
|
1824
|
+
output: response.info.structured,
|
|
1825
|
+
usage
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1515
1828
|
const outputText = lastFinalAnswerText ?? lastText;
|
|
1516
|
-
if (!outputText)
|
|
1829
|
+
if (!outputText) {
|
|
1830
|
+
appendDebugLog("opencode:output:missing", {
|
|
1831
|
+
sessionId,
|
|
1832
|
+
hasInfo: response.info !== void 0,
|
|
1833
|
+
partCount: response.parts?.length ?? 0
|
|
1834
|
+
});
|
|
1835
|
+
throw new Error("opencode returned no text output");
|
|
1836
|
+
}
|
|
1517
1837
|
try {
|
|
1838
|
+
const output = JSON.parse(outputText);
|
|
1839
|
+
appendDebugLog("opencode:output:structured", {
|
|
1840
|
+
sessionId,
|
|
1841
|
+
source: lastFinalAnswerText ? "final_answer" : "last_text",
|
|
1842
|
+
outputTextLength: outputText.length
|
|
1843
|
+
});
|
|
1518
1844
|
return {
|
|
1519
|
-
output
|
|
1845
|
+
output,
|
|
1520
1846
|
usage
|
|
1521
1847
|
};
|
|
1522
1848
|
} catch (error) {
|
|
1849
|
+
appendDebugLog("opencode:output:parse-error", {
|
|
1850
|
+
sessionId,
|
|
1851
|
+
outputTextLength: outputText.length,
|
|
1852
|
+
outputTextSample: outputText.slice(0, 512),
|
|
1853
|
+
error: serializeError(error)
|
|
1854
|
+
});
|
|
1523
1855
|
throw new Error(`Failed to parse opencode output: ${error instanceof Error ? error.message : String(error)}`);
|
|
1524
1856
|
}
|
|
1525
1857
|
}
|
|
@@ -1529,7 +1861,13 @@ var OpenCodeAgent = class {
|
|
|
1529
1861
|
method: "DELETE",
|
|
1530
1862
|
timeoutMs: 1e3
|
|
1531
1863
|
});
|
|
1532
|
-
|
|
1864
|
+
appendDebugLog("opencode:session:delete", { sessionId });
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
appendDebugLog("opencode:session:delete-failed", {
|
|
1867
|
+
sessionId,
|
|
1868
|
+
error: serializeError(error)
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1533
1871
|
}
|
|
1534
1872
|
async abortSession(server, sessionId) {
|
|
1535
1873
|
try {
|
|
@@ -1537,7 +1875,13 @@ var OpenCodeAgent = class {
|
|
|
1537
1875
|
method: "POST",
|
|
1538
1876
|
timeoutMs: 1e3
|
|
1539
1877
|
});
|
|
1540
|
-
|
|
1878
|
+
appendDebugLog("opencode:session:abort", { sessionId });
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
appendDebugLog("opencode:session:abort-failed", {
|
|
1881
|
+
sessionId,
|
|
1882
|
+
error: serializeError(error)
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1541
1885
|
}
|
|
1542
1886
|
async shutdownServer() {
|
|
1543
1887
|
if (!this.server || this.server.closed) {
|
|
@@ -1549,9 +1893,11 @@ var OpenCodeAgent = class {
|
|
|
1549
1893
|
return;
|
|
1550
1894
|
}
|
|
1551
1895
|
const server = this.server;
|
|
1896
|
+
const shutdownStartedAt = Date.now();
|
|
1552
1897
|
appendDebugLog("opencode:shutdown", {
|
|
1553
1898
|
cwd: server.cwd,
|
|
1554
|
-
port: server.port
|
|
1899
|
+
port: server.port,
|
|
1900
|
+
pid: server.child.pid
|
|
1555
1901
|
});
|
|
1556
1902
|
this.closingPromise = (this.platform === "win32" && server.child.pid ? killWindowsProcessTree(server.child.pid) : shutdownChildProcess(server.child, {
|
|
1557
1903
|
detached: server.detached,
|
|
@@ -1560,6 +1906,10 @@ var OpenCodeAgent = class {
|
|
|
1560
1906
|
})).finally(() => {
|
|
1561
1907
|
if (this.server === server) this.server = null;
|
|
1562
1908
|
this.closingPromise = null;
|
|
1909
|
+
appendDebugLog("opencode:shutdown:done", {
|
|
1910
|
+
port: server.port,
|
|
1911
|
+
elapsedMs: Date.now() - shutdownStartedAt
|
|
1912
|
+
});
|
|
1563
1913
|
});
|
|
1564
1914
|
await this.closingPromise;
|
|
1565
1915
|
}
|
|
@@ -1574,14 +1924,35 @@ var OpenCodeAgent = class {
|
|
|
1574
1924
|
const headers = new Headers(options.headers);
|
|
1575
1925
|
if (options.body !== void 0) headers.set("content-type", "application/json");
|
|
1576
1926
|
const signal = withTimeoutSignal$1(options.signal, options.timeoutMs);
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1927
|
+
const startedAt = Date.now();
|
|
1928
|
+
let response;
|
|
1929
|
+
try {
|
|
1930
|
+
response = await this.fetchFn(`${server.baseUrl}${path}`, {
|
|
1931
|
+
method: options.method,
|
|
1932
|
+
headers,
|
|
1933
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
|
|
1934
|
+
signal
|
|
1935
|
+
});
|
|
1936
|
+
} catch (error) {
|
|
1937
|
+
appendDebugLog("opencode:request:error", {
|
|
1938
|
+
method: options.method,
|
|
1939
|
+
path,
|
|
1940
|
+
elapsedMs: Date.now() - startedAt,
|
|
1941
|
+
timeoutMs: options.timeoutMs,
|
|
1942
|
+
error: serializeError(error),
|
|
1943
|
+
serverClosed: server.closed
|
|
1944
|
+
});
|
|
1945
|
+
throw error;
|
|
1946
|
+
}
|
|
1583
1947
|
if (!response.ok) {
|
|
1584
1948
|
const body = await response.text();
|
|
1949
|
+
appendDebugLog("opencode:request:non-ok", {
|
|
1950
|
+
method: options.method,
|
|
1951
|
+
path,
|
|
1952
|
+
status: response.status,
|
|
1953
|
+
elapsedMs: Date.now() - startedAt,
|
|
1954
|
+
bodySample: body.slice(0, 1024)
|
|
1955
|
+
});
|
|
1585
1956
|
throw new Error(`opencode ${options.method} ${path} failed with ${response.status}: ${body}`);
|
|
1586
1957
|
}
|
|
1587
1958
|
return response;
|
|
@@ -1706,11 +2077,18 @@ var RovoDevAgent = class {
|
|
|
1706
2077
|
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
1707
2078
|
const runController = new AbortController();
|
|
1708
2079
|
let sessionId = null;
|
|
2080
|
+
const runStartedAt = Date.now();
|
|
2081
|
+
appendDebugLog("rovodev:run:start", {
|
|
2082
|
+
cwd,
|
|
2083
|
+
promptLength: prompt.length,
|
|
2084
|
+
hasLogPath: logPath !== void 0
|
|
2085
|
+
});
|
|
1709
2086
|
const onAbort = () => {
|
|
1710
2087
|
runController.abort();
|
|
1711
2088
|
};
|
|
1712
2089
|
if (signal?.aborted) {
|
|
1713
2090
|
logStream?.end();
|
|
2091
|
+
appendDebugLog("rovodev:run:aborted-early", {});
|
|
1714
2092
|
throw createAbortError();
|
|
1715
2093
|
}
|
|
1716
2094
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
@@ -1719,9 +2097,30 @@ var RovoDevAgent = class {
|
|
|
1719
2097
|
sessionId = await this.createSession(server, runController.signal);
|
|
1720
2098
|
await this.setInlineSystemPrompt(server, sessionId, runController.signal);
|
|
1721
2099
|
await this.setChatMessage(server, sessionId, prompt, runController.signal);
|
|
1722
|
-
|
|
2100
|
+
const result = await this.streamChat(server, sessionId, runController.signal, logStream, onUsage, onMessage);
|
|
2101
|
+
appendDebugLog("rovodev:run:end", {
|
|
2102
|
+
sessionId,
|
|
2103
|
+
elapsedMs: Date.now() - runStartedAt,
|
|
2104
|
+
inputTokens: result.usage.inputTokens,
|
|
2105
|
+
outputTokens: result.usage.outputTokens
|
|
2106
|
+
});
|
|
2107
|
+
return result;
|
|
1723
2108
|
} catch (error) {
|
|
1724
|
-
if (runController.signal.aborted || isAbortError(error))
|
|
2109
|
+
if (runController.signal.aborted || isAbortError(error)) {
|
|
2110
|
+
appendDebugLog("rovodev:run:aborted", {
|
|
2111
|
+
sessionId,
|
|
2112
|
+
elapsedMs: Date.now() - runStartedAt
|
|
2113
|
+
});
|
|
2114
|
+
throw createAbortError();
|
|
2115
|
+
}
|
|
2116
|
+
appendDebugLog("rovodev:run:error", {
|
|
2117
|
+
sessionId,
|
|
2118
|
+
elapsedMs: Date.now() - runStartedAt,
|
|
2119
|
+
error: serializeError(error),
|
|
2120
|
+
serverStderr: this.server?.stderr.slice(-2048),
|
|
2121
|
+
serverStdout: this.server?.stdout.slice(-2048),
|
|
2122
|
+
serverClosed: this.server?.closed ?? true
|
|
2123
|
+
});
|
|
1725
2124
|
throw error;
|
|
1726
2125
|
} finally {
|
|
1727
2126
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -1779,17 +2178,40 @@ var RovoDevAgent = class {
|
|
|
1779
2178
|
server.stderr += data.toString();
|
|
1780
2179
|
if (server.stderr.length > MAX_OUTPUT) server.stderr = server.stderr.slice(-MAX_OUTPUT);
|
|
1781
2180
|
});
|
|
1782
|
-
child.on("close", () => {
|
|
2181
|
+
child.on("close", (code, closeSignal) => {
|
|
1783
2182
|
server.closed = true;
|
|
2183
|
+
appendDebugLog("rovodev:server:close", {
|
|
2184
|
+
cwd: server.cwd,
|
|
2185
|
+
port: server.port,
|
|
2186
|
+
code,
|
|
2187
|
+
signal: closeSignal,
|
|
2188
|
+
stderr: server.stderr.slice(-2048),
|
|
2189
|
+
stdout: server.stdout.slice(-2048)
|
|
2190
|
+
});
|
|
1784
2191
|
if (this.server === server) this.server = null;
|
|
1785
2192
|
});
|
|
1786
2193
|
this.server = server;
|
|
2194
|
+
const spawnedAt = Date.now();
|
|
1787
2195
|
appendDebugLog("rovodev:spawn", {
|
|
1788
2196
|
cwd,
|
|
1789
2197
|
port,
|
|
1790
|
-
detached
|
|
2198
|
+
detached,
|
|
2199
|
+
pid: child.pid,
|
|
2200
|
+
bin: this.bin
|
|
1791
2201
|
});
|
|
1792
|
-
server.readyPromise = this.waitForHealthy(server, signal).
|
|
2202
|
+
server.readyPromise = this.waitForHealthy(server, signal).then(() => {
|
|
2203
|
+
appendDebugLog("rovodev:server:ready", {
|
|
2204
|
+
port,
|
|
2205
|
+
elapsedMs: Date.now() - spawnedAt
|
|
2206
|
+
});
|
|
2207
|
+
}).catch(async (error) => {
|
|
2208
|
+
appendDebugLog("rovodev:server:ready-failed", {
|
|
2209
|
+
port,
|
|
2210
|
+
elapsedMs: Date.now() - spawnedAt,
|
|
2211
|
+
error: serializeError(error),
|
|
2212
|
+
stderr: server.stderr.slice(-2048),
|
|
2213
|
+
stdout: server.stdout.slice(-2048)
|
|
2214
|
+
});
|
|
1793
2215
|
await this.shutdownServer();
|
|
1794
2216
|
throw error;
|
|
1795
2217
|
});
|
|
@@ -1822,11 +2244,13 @@ var RovoDevAgent = class {
|
|
|
1822
2244
|
throw new Error(`Timed out waiting for rovodev serve to become ready on port ${server.port}`);
|
|
1823
2245
|
}
|
|
1824
2246
|
async createSession(server, signal) {
|
|
1825
|
-
|
|
2247
|
+
const response = await this.requestJSON(server, "/v3/sessions/create", {
|
|
1826
2248
|
method: "POST",
|
|
1827
2249
|
body: { custom_title: "gnhf" },
|
|
1828
2250
|
signal
|
|
1829
|
-
})
|
|
2251
|
+
});
|
|
2252
|
+
appendDebugLog("rovodev:session:create", { sessionId: response.session_id });
|
|
2253
|
+
return response.session_id;
|
|
1830
2254
|
}
|
|
1831
2255
|
async setInlineSystemPrompt(server, sessionId, signal) {
|
|
1832
2256
|
const schema = readFileSync(this.schemaPath, "utf-8").trim();
|
|
@@ -1852,7 +2276,13 @@ var RovoDevAgent = class {
|
|
|
1852
2276
|
sessionId,
|
|
1853
2277
|
timeoutMs: 1e3
|
|
1854
2278
|
});
|
|
1855
|
-
|
|
2279
|
+
appendDebugLog("rovodev:session:cancel", { sessionId });
|
|
2280
|
+
} catch (error) {
|
|
2281
|
+
appendDebugLog("rovodev:session:cancel-failed", {
|
|
2282
|
+
sessionId,
|
|
2283
|
+
error: serializeError(error)
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
1856
2286
|
}
|
|
1857
2287
|
async deleteSession(server, sessionId) {
|
|
1858
2288
|
try {
|
|
@@ -1861,16 +2291,27 @@ var RovoDevAgent = class {
|
|
|
1861
2291
|
sessionId,
|
|
1862
2292
|
timeoutMs: 1e3
|
|
1863
2293
|
});
|
|
1864
|
-
|
|
2294
|
+
appendDebugLog("rovodev:session:delete", { sessionId });
|
|
2295
|
+
} catch (error) {
|
|
2296
|
+
appendDebugLog("rovodev:session:delete-failed", {
|
|
2297
|
+
sessionId,
|
|
2298
|
+
error: serializeError(error)
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
1865
2301
|
}
|
|
1866
2302
|
async streamChat(server, sessionId, signal, logStream, onUsage, onMessage) {
|
|
2303
|
+
const streamStartedAt = Date.now();
|
|
2304
|
+
appendDebugLog("rovodev:stream:start", { sessionId });
|
|
1867
2305
|
const response = await this.request(server, "/v3/stream_chat", {
|
|
1868
2306
|
method: "GET",
|
|
1869
2307
|
sessionId,
|
|
1870
2308
|
headers: { accept: "text/event-stream" },
|
|
1871
2309
|
signal
|
|
1872
2310
|
});
|
|
1873
|
-
if (!response.body)
|
|
2311
|
+
if (!response.body) {
|
|
2312
|
+
appendDebugLog("rovodev:stream:no-body", { sessionId });
|
|
2313
|
+
throw new Error("rovodev returned no response body");
|
|
2314
|
+
}
|
|
1874
2315
|
const usage = {
|
|
1875
2316
|
inputTokens: 0,
|
|
1876
2317
|
outputTokens: 0,
|
|
@@ -1959,11 +2400,20 @@ var RovoDevAgent = class {
|
|
|
1959
2400
|
}
|
|
1960
2401
|
}
|
|
1961
2402
|
};
|
|
2403
|
+
let bytesRead = 0;
|
|
1962
2404
|
while (true) {
|
|
1963
2405
|
let readResult;
|
|
1964
2406
|
try {
|
|
1965
2407
|
readResult = await reader.read();
|
|
1966
2408
|
} catch (error) {
|
|
2409
|
+
appendDebugLog("rovodev:stream:error", {
|
|
2410
|
+
sessionId,
|
|
2411
|
+
elapsedMs: Date.now() - streamStartedAt,
|
|
2412
|
+
bytesRead,
|
|
2413
|
+
error: serializeError(error),
|
|
2414
|
+
serverClosed: server.closed,
|
|
2415
|
+
serverStderr: server.stderr.slice(-2048)
|
|
2416
|
+
});
|
|
1967
2417
|
if (isAbortError(error)) throw createAbortError();
|
|
1968
2418
|
throw error;
|
|
1969
2419
|
}
|
|
@@ -1971,6 +2421,7 @@ var RovoDevAgent = class {
|
|
|
1971
2421
|
const chunk = decoder.decode(readResult.value, { stream: true });
|
|
1972
2422
|
logStream?.write(chunk);
|
|
1973
2423
|
buffer += chunk;
|
|
2424
|
+
bytesRead += chunk.length;
|
|
1974
2425
|
while (true) {
|
|
1975
2426
|
const lfBoundary = buffer.indexOf("\n\n");
|
|
1976
2427
|
const crlfBoundary = buffer.indexOf("\r\n\r\n");
|
|
@@ -1991,14 +2442,33 @@ var RovoDevAgent = class {
|
|
|
1991
2442
|
}
|
|
1992
2443
|
buffer += decoder.decode();
|
|
1993
2444
|
if (buffer.trim()) handleEvent(buffer);
|
|
2445
|
+
appendDebugLog("rovodev:stream:end", {
|
|
2446
|
+
sessionId,
|
|
2447
|
+
elapsedMs: Date.now() - streamStartedAt,
|
|
2448
|
+
bytesRead
|
|
2449
|
+
});
|
|
1994
2450
|
const finalText = latestTextSegment.trim();
|
|
1995
|
-
if (!finalText)
|
|
2451
|
+
if (!finalText) {
|
|
2452
|
+
appendDebugLog("rovodev:output:missing", { sessionId });
|
|
2453
|
+
throw new Error("rovodev returned no text output");
|
|
2454
|
+
}
|
|
1996
2455
|
try {
|
|
2456
|
+
const output = JSON.parse(finalText);
|
|
2457
|
+
appendDebugLog("rovodev:output:parsed", {
|
|
2458
|
+
sessionId,
|
|
2459
|
+
outputTextLength: finalText.length
|
|
2460
|
+
});
|
|
1997
2461
|
return {
|
|
1998
|
-
output
|
|
2462
|
+
output,
|
|
1999
2463
|
usage
|
|
2000
2464
|
};
|
|
2001
2465
|
} catch (error) {
|
|
2466
|
+
appendDebugLog("rovodev:output:parse-error", {
|
|
2467
|
+
sessionId,
|
|
2468
|
+
outputTextLength: finalText.length,
|
|
2469
|
+
outputTextSample: finalText.slice(0, 512),
|
|
2470
|
+
error: serializeError(error)
|
|
2471
|
+
});
|
|
2002
2472
|
throw new Error(`Failed to parse rovodev output: ${error instanceof Error ? error.message : String(error)}`);
|
|
2003
2473
|
}
|
|
2004
2474
|
}
|
|
@@ -2012,9 +2482,11 @@ var RovoDevAgent = class {
|
|
|
2012
2482
|
return;
|
|
2013
2483
|
}
|
|
2014
2484
|
const server = this.server;
|
|
2485
|
+
const shutdownStartedAt = Date.now();
|
|
2015
2486
|
appendDebugLog("rovodev:shutdown", {
|
|
2016
2487
|
cwd: server.cwd,
|
|
2017
|
-
port: server.port
|
|
2488
|
+
port: server.port,
|
|
2489
|
+
pid: server.child.pid
|
|
2018
2490
|
});
|
|
2019
2491
|
this.closingPromise = this.platform === "win32" ? new Promise((resolve) => {
|
|
2020
2492
|
const handleClose = () => {
|
|
@@ -2041,6 +2513,10 @@ var RovoDevAgent = class {
|
|
|
2041
2513
|
this.closingPromise = this.closingPromise.finally(() => {
|
|
2042
2514
|
if (this.server === server) this.server = null;
|
|
2043
2515
|
this.closingPromise = null;
|
|
2516
|
+
appendDebugLog("rovodev:shutdown:done", {
|
|
2517
|
+
port: server.port,
|
|
2518
|
+
elapsedMs: Date.now() - shutdownStartedAt
|
|
2519
|
+
});
|
|
2044
2520
|
});
|
|
2045
2521
|
await this.closingPromise;
|
|
2046
2522
|
}
|
|
@@ -2052,14 +2528,35 @@ var RovoDevAgent = class {
|
|
|
2052
2528
|
if (options.sessionId) headers.set("x-session-id", options.sessionId);
|
|
2053
2529
|
if (options.body !== void 0 && !headers.has("content-type")) headers.set("content-type", "application/json");
|
|
2054
2530
|
const signal = withTimeoutSignal(options.signal, options.timeoutMs);
|
|
2055
|
-
const
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2531
|
+
const startedAt = Date.now();
|
|
2532
|
+
let response;
|
|
2533
|
+
try {
|
|
2534
|
+
response = await this.fetchFn(`${server.baseUrl}${path}`, {
|
|
2535
|
+
method: options.method,
|
|
2536
|
+
headers,
|
|
2537
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
|
|
2538
|
+
signal
|
|
2539
|
+
});
|
|
2540
|
+
} catch (error) {
|
|
2541
|
+
appendDebugLog("rovodev:request:error", {
|
|
2542
|
+
method: options.method,
|
|
2543
|
+
path,
|
|
2544
|
+
elapsedMs: Date.now() - startedAt,
|
|
2545
|
+
timeoutMs: options.timeoutMs,
|
|
2546
|
+
error: serializeError(error),
|
|
2547
|
+
serverClosed: server.closed
|
|
2548
|
+
});
|
|
2549
|
+
throw error;
|
|
2550
|
+
}
|
|
2061
2551
|
if (!response.ok) {
|
|
2062
2552
|
const body = await response.text();
|
|
2553
|
+
appendDebugLog("rovodev:request:non-ok", {
|
|
2554
|
+
method: options.method,
|
|
2555
|
+
path,
|
|
2556
|
+
status: response.status,
|
|
2557
|
+
elapsedMs: Date.now() - startedAt,
|
|
2558
|
+
bodySample: body.slice(0, 1024)
|
|
2559
|
+
});
|
|
2063
2560
|
throw new Error(`rovodev ${options.method} ${path} failed with ${response.status}: ${body}`);
|
|
2064
2561
|
}
|
|
2065
2562
|
return response;
|
|
@@ -2151,6 +2648,11 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2151
2648
|
}
|
|
2152
2649
|
stop() {
|
|
2153
2650
|
this.stopRequested = true;
|
|
2651
|
+
appendDebugLog("orchestrator:stop-requested", {
|
|
2652
|
+
iteration: this.state.currentIteration,
|
|
2653
|
+
hasActiveIteration: this.activeIterationPromise !== null,
|
|
2654
|
+
loopDone: this.loopDone
|
|
2655
|
+
});
|
|
2154
2656
|
this.activeAbortController?.abort();
|
|
2155
2657
|
if (this.loopDone) {
|
|
2156
2658
|
this.emit("stopped");
|
|
@@ -2185,6 +2687,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2185
2687
|
this.state.startTime = /* @__PURE__ */ new Date();
|
|
2186
2688
|
this.state.status = "running";
|
|
2187
2689
|
this.emit("state", this.getState());
|
|
2690
|
+
appendDebugLog("orchestrator:start", {
|
|
2691
|
+
agent: this.agent.name,
|
|
2692
|
+
runId: this.runInfo.runId,
|
|
2693
|
+
startIteration: this.state.currentIteration,
|
|
2694
|
+
maxIterations: this.limits.maxIterations,
|
|
2695
|
+
maxTokens: this.limits.maxTokens,
|
|
2696
|
+
maxConsecutiveFailures: this.config.maxConsecutiveFailures,
|
|
2697
|
+
baseCommit: this.runInfo.baseCommit,
|
|
2698
|
+
initialCommitCount: this.state.commitCount
|
|
2699
|
+
});
|
|
2188
2700
|
try {
|
|
2189
2701
|
while (!this.stopRequested) {
|
|
2190
2702
|
const preIterationAbortReason = this.getPreIterationAbortReason();
|
|
@@ -2201,11 +2713,32 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2201
2713
|
runId: this.runInfo.runId,
|
|
2202
2714
|
prompt: this.prompt
|
|
2203
2715
|
});
|
|
2716
|
+
appendDebugLog("iteration:start", {
|
|
2717
|
+
iteration: this.state.currentIteration,
|
|
2718
|
+
promptLength: iterationPrompt.length,
|
|
2719
|
+
consecutiveFailures: this.state.consecutiveFailures,
|
|
2720
|
+
totalInputTokens: this.state.totalInputTokens,
|
|
2721
|
+
totalOutputTokens: this.state.totalOutputTokens,
|
|
2722
|
+
git: this.snapshotGitState()
|
|
2723
|
+
});
|
|
2724
|
+
const iterationStartedAt = Date.now();
|
|
2204
2725
|
this.activeIterationPromise = this.runIteration(iterationPrompt);
|
|
2205
2726
|
const result = await this.activeIterationPromise;
|
|
2206
2727
|
this.activeIterationPromise = null;
|
|
2207
|
-
|
|
2728
|
+
const iterationElapsedMs = Date.now() - iterationStartedAt;
|
|
2729
|
+
if (result.type === "stopped") {
|
|
2730
|
+
appendDebugLog("iteration:stopped", {
|
|
2731
|
+
iteration: this.state.currentIteration,
|
|
2732
|
+
elapsedMs: iterationElapsedMs
|
|
2733
|
+
});
|
|
2734
|
+
break;
|
|
2735
|
+
}
|
|
2208
2736
|
if (result.type === "aborted") {
|
|
2737
|
+
appendDebugLog("iteration:aborted", {
|
|
2738
|
+
iteration: this.state.currentIteration,
|
|
2739
|
+
elapsedMs: iterationElapsedMs,
|
|
2740
|
+
reason: result.reason
|
|
2741
|
+
});
|
|
2209
2742
|
this.abort(result.reason);
|
|
2210
2743
|
break;
|
|
2211
2744
|
}
|
|
@@ -2213,6 +2746,18 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2213
2746
|
this.state.iterations.push(record);
|
|
2214
2747
|
this.emit("iteration:end", record);
|
|
2215
2748
|
this.emit("state", this.getState());
|
|
2749
|
+
appendDebugLog("iteration:end", {
|
|
2750
|
+
iteration: record.number,
|
|
2751
|
+
elapsedMs: iterationElapsedMs,
|
|
2752
|
+
success: record.success,
|
|
2753
|
+
summary: record.summary,
|
|
2754
|
+
keyChanges: record.keyChanges.length,
|
|
2755
|
+
keyLearnings: record.keyLearnings.length,
|
|
2756
|
+
consecutiveFailures: this.state.consecutiveFailures,
|
|
2757
|
+
totalInputTokens: this.state.totalInputTokens,
|
|
2758
|
+
totalOutputTokens: this.state.totalOutputTokens,
|
|
2759
|
+
commitCount: this.state.commitCount
|
|
2760
|
+
});
|
|
2216
2761
|
const postIterationAbortReason = this.getPostIterationAbortReason();
|
|
2217
2762
|
if (postIterationAbortReason) {
|
|
2218
2763
|
this.abort(postIterationAbortReason);
|
|
@@ -2227,7 +2772,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2227
2772
|
this.state.status = "waiting";
|
|
2228
2773
|
this.state.waitingUntil = new Date(Date.now() + backoffMs);
|
|
2229
2774
|
this.emit("state", this.getState());
|
|
2775
|
+
appendDebugLog("backoff:start", {
|
|
2776
|
+
iteration: this.state.currentIteration,
|
|
2777
|
+
consecutiveFailures: this.state.consecutiveFailures,
|
|
2778
|
+
backoffMs
|
|
2779
|
+
});
|
|
2230
2780
|
await this.interruptibleSleep(backoffMs);
|
|
2781
|
+
appendDebugLog("backoff:end", {
|
|
2782
|
+
iteration: this.state.currentIteration,
|
|
2783
|
+
stopRequested: this.stopRequested
|
|
2784
|
+
});
|
|
2231
2785
|
this.state.waitingUntil = null;
|
|
2232
2786
|
if (!this.stopRequested) {
|
|
2233
2787
|
this.state.status = "running";
|
|
@@ -2240,6 +2794,15 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2240
2794
|
if (this.stopPromise) await this.stopPromise;
|
|
2241
2795
|
else await this.closeAgent();
|
|
2242
2796
|
this.loopDone = true;
|
|
2797
|
+
appendDebugLog("orchestrator:end", {
|
|
2798
|
+
status: this.state.status,
|
|
2799
|
+
iterations: this.state.currentIteration,
|
|
2800
|
+
successCount: this.state.successCount,
|
|
2801
|
+
failCount: this.state.failCount,
|
|
2802
|
+
totalInputTokens: this.state.totalInputTokens,
|
|
2803
|
+
totalOutputTokens: this.state.totalOutputTokens,
|
|
2804
|
+
commitCount: this.state.commitCount
|
|
2805
|
+
});
|
|
2243
2806
|
}
|
|
2244
2807
|
}
|
|
2245
2808
|
async runIteration(prompt) {
|
|
@@ -2262,6 +2825,12 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2262
2825
|
this.emit("state", this.getState());
|
|
2263
2826
|
};
|
|
2264
2827
|
const logPath = join(this.runInfo.runDir, `iteration-${this.state.currentIteration}.jsonl`);
|
|
2828
|
+
const agentStartedAt = Date.now();
|
|
2829
|
+
appendDebugLog("agent:run:start", {
|
|
2830
|
+
iteration: this.state.currentIteration,
|
|
2831
|
+
agent: this.agent.name,
|
|
2832
|
+
logPath
|
|
2833
|
+
});
|
|
2265
2834
|
try {
|
|
2266
2835
|
const result = await this.agent.run(prompt, this.cwd, {
|
|
2267
2836
|
onUsage,
|
|
@@ -2269,6 +2838,15 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2269
2838
|
signal: this.activeAbortController.signal,
|
|
2270
2839
|
logPath
|
|
2271
2840
|
});
|
|
2841
|
+
appendDebugLog("agent:run:end", {
|
|
2842
|
+
iteration: this.state.currentIteration,
|
|
2843
|
+
elapsedMs: Date.now() - agentStartedAt,
|
|
2844
|
+
success: result.output.success,
|
|
2845
|
+
inputTokens: result.usage.inputTokens,
|
|
2846
|
+
outputTokens: result.usage.outputTokens,
|
|
2847
|
+
cacheReadTokens: result.usage.cacheReadTokens,
|
|
2848
|
+
cacheCreationTokens: result.usage.cacheCreationTokens
|
|
2849
|
+
});
|
|
2272
2850
|
if (this.stopRequested) return { type: "stopped" };
|
|
2273
2851
|
if (result.output.success) return {
|
|
2274
2852
|
type: "completed",
|
|
@@ -2279,14 +2857,31 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2279
2857
|
record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, result.output.key_learnings)
|
|
2280
2858
|
};
|
|
2281
2859
|
} catch (err) {
|
|
2860
|
+
const elapsedMs = Date.now() - agentStartedAt;
|
|
2282
2861
|
if (this.pendingAbortReason && err instanceof Error && err.message === "Agent was aborted") {
|
|
2862
|
+
appendDebugLog("agent:run:aborted", {
|
|
2863
|
+
iteration: this.state.currentIteration,
|
|
2864
|
+
elapsedMs,
|
|
2865
|
+
reason: this.pendingAbortReason
|
|
2866
|
+
});
|
|
2283
2867
|
resetHard(this.cwd);
|
|
2284
2868
|
return {
|
|
2285
2869
|
type: "aborted",
|
|
2286
2870
|
reason: this.pendingAbortReason
|
|
2287
2871
|
};
|
|
2288
2872
|
}
|
|
2289
|
-
if (this.stopRequested)
|
|
2873
|
+
if (this.stopRequested) {
|
|
2874
|
+
appendDebugLog("agent:run:stopped", {
|
|
2875
|
+
iteration: this.state.currentIteration,
|
|
2876
|
+
elapsedMs
|
|
2877
|
+
});
|
|
2878
|
+
return { type: "stopped" };
|
|
2879
|
+
}
|
|
2880
|
+
appendDebugLog("agent:run:error", {
|
|
2881
|
+
iteration: this.state.currentIteration,
|
|
2882
|
+
elapsedMs,
|
|
2883
|
+
error: serializeError(err)
|
|
2884
|
+
});
|
|
2290
2885
|
const summary = err instanceof Error ? err.message : String(err);
|
|
2291
2886
|
return {
|
|
2292
2887
|
type: "completed",
|
|
@@ -2358,13 +2953,31 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2358
2953
|
this.state.status = "aborted";
|
|
2359
2954
|
this.state.lastMessage = reason;
|
|
2360
2955
|
this.state.waitingUntil = null;
|
|
2956
|
+
appendDebugLog("orchestrator:abort", {
|
|
2957
|
+
reason,
|
|
2958
|
+
iteration: this.state.currentIteration,
|
|
2959
|
+
consecutiveFailures: this.state.consecutiveFailures
|
|
2960
|
+
});
|
|
2361
2961
|
this.emit("abort", reason);
|
|
2362
2962
|
this.emit("state", this.getState());
|
|
2363
2963
|
}
|
|
2364
2964
|
async closeAgent() {
|
|
2365
2965
|
try {
|
|
2366
2966
|
await this.agent.close?.();
|
|
2367
|
-
} catch {
|
|
2967
|
+
} catch (err) {
|
|
2968
|
+
appendDebugLog("agent:close:error", { error: serializeError(err) });
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
snapshotGitState() {
|
|
2972
|
+
try {
|
|
2973
|
+
return {
|
|
2974
|
+
head: getHeadCommit(this.cwd),
|
|
2975
|
+
branch: getCurrentBranch(this.cwd),
|
|
2976
|
+
commitCount: this.state.commitCount
|
|
2977
|
+
};
|
|
2978
|
+
} catch (err) {
|
|
2979
|
+
return { error: serializeError(err) };
|
|
2980
|
+
}
|
|
2368
2981
|
}
|
|
2369
2982
|
};
|
|
2370
2983
|
//#endregion
|
|
@@ -3285,7 +3898,22 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
3285
3898
|
if (!reexeced) persistedPrompt?.cleanup();
|
|
3286
3899
|
}
|
|
3287
3900
|
}
|
|
3288
|
-
|
|
3901
|
+
initDebugLog(runInfo.logPath);
|
|
3902
|
+
appendDebugLog("run:start", {
|
|
3903
|
+
args: process$1.argv.slice(2),
|
|
3904
|
+
runId: runInfo.runId,
|
|
3905
|
+
runDir: runInfo.runDir,
|
|
3906
|
+
agent: config.agent,
|
|
3907
|
+
promptLength: prompt.length,
|
|
3908
|
+
promptFromStdin,
|
|
3909
|
+
startIteration,
|
|
3910
|
+
maxIterations: options.maxIterations,
|
|
3911
|
+
maxTokens: options.maxTokens,
|
|
3912
|
+
preventSleep: config.preventSleep,
|
|
3913
|
+
platform: process$1.platform,
|
|
3914
|
+
nodeVersion: process$1.version,
|
|
3915
|
+
gnhfVersion: packageVersion
|
|
3916
|
+
});
|
|
3289
3917
|
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent]), runInfo, prompt, cwd, startIteration, {
|
|
3290
3918
|
maxIterations: options.maxIterations,
|
|
3291
3919
|
maxTokens: options.maxTokens
|
|
@@ -3308,6 +3936,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
3308
3936
|
const orchestratorPromise = orchestrator.start().finally(() => {
|
|
3309
3937
|
if (!(orchestrator.getState().status === "aborted" && process$1.stdin.isTTY)) renderer.stop();
|
|
3310
3938
|
}).catch((err) => {
|
|
3939
|
+
appendDebugLog("orchestrator:fatal", { error: serializeError(err) });
|
|
3311
3940
|
exitAltScreen();
|
|
3312
3941
|
die(err instanceof Error ? err.message : String(err));
|
|
3313
3942
|
});
|
|
@@ -3329,7 +3958,19 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
3329
3958
|
process$1.off("SIGTERM", handleSigTerm);
|
|
3330
3959
|
await sleepPreventionCleanup?.();
|
|
3331
3960
|
}
|
|
3332
|
-
|
|
3961
|
+
{
|
|
3962
|
+
const finalState = orchestrator.getState();
|
|
3963
|
+
appendDebugLog("run:complete", {
|
|
3964
|
+
signal: shutdownSignal,
|
|
3965
|
+
status: finalState.status,
|
|
3966
|
+
iterations: finalState.currentIteration,
|
|
3967
|
+
successCount: finalState.successCount,
|
|
3968
|
+
failCount: finalState.failCount,
|
|
3969
|
+
totalInputTokens: finalState.totalInputTokens,
|
|
3970
|
+
totalOutputTokens: finalState.totalOutputTokens,
|
|
3971
|
+
commitCount: finalState.commitCount
|
|
3972
|
+
});
|
|
3973
|
+
}
|
|
3333
3974
|
if (shutdownSignal) process$1.exit(getSignalExitCode(shutdownSignal));
|
|
3334
3975
|
});
|
|
3335
3976
|
function enterAltScreen() {
|