gnhf 0.1.11 → 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 +3 -5
- package/dist/cli.mjs +731 -82
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,7 +128,7 @@ npm link
|
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
- **Incremental commits** — each successful iteration is a separate git commit, so you can cherry-pick or revert individual changes
|
|
131
|
-
- **Runtime caps** — `--max-iterations` stops before the next iteration begins, while `--max-tokens` can abort mid-iteration once reported usage reaches the cap; uncommitted work is rolled back in either case
|
|
131
|
+
- **Runtime caps** — `--max-iterations` stops before the next iteration begins, while `--max-tokens` can abort mid-iteration once reported usage reaches the cap; uncommitted work is rolled back in either case, and in the interactive TUI the final state remains visible until you press Ctrl+C to exit
|
|
132
132
|
- **Shared memory** — the agent reads `notes.md` (built up from prior iterations) to communicate across iterations
|
|
133
133
|
- **Local run metadata** — gnhf stores prompt, notes, and resume metadata under `.gnhf/runs/` and ignores it locally, so your branch only contains intentional work
|
|
134
134
|
- **Resume support** — run `gnhf` while on an existing `gnhf/` branch to pick up where a previous run left off
|
|
@@ -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;
|
|
@@ -2083,23 +2580,23 @@ function createAgent(name, runInfo, pathOverride) {
|
|
|
2083
2580
|
//#endregion
|
|
2084
2581
|
//#region src/templates/iteration-prompt.ts
|
|
2085
2582
|
function buildIterationPrompt(params) {
|
|
2086
|
-
return `You are working autonomously
|
|
2087
|
-
This is iteration ${params.n}
|
|
2583
|
+
return `You are working autonomously towards an objective given below.
|
|
2584
|
+
This is iteration ${params.n}. Each iteration aims to make an incremental step forward, not to complete the entire objective.
|
|
2088
2585
|
|
|
2089
2586
|
## Instructions
|
|
2090
2587
|
|
|
2091
|
-
1. Read .gnhf/runs/${params.runId}/notes.md first to understand what has been done in previous iterations
|
|
2092
|
-
2.
|
|
2093
|
-
3. If you
|
|
2094
|
-
4. Do NOT make any git commits
|
|
2095
|
-
|
|
2588
|
+
1. Read .gnhf/runs/${params.runId}/notes.md first to understand what has been done in previous iterations
|
|
2589
|
+
2. Identify the next smallest logical unit of work that's individually verifiable and would make incremental progress towards the objective, and treat that as the scope of this iteration
|
|
2590
|
+
3. If you attempted a solution and it didn't end up moving the needle on the objective, document learnings and record success=false, then conclude the iteration rather than continuously pivoting
|
|
2591
|
+
4. If you made code changes, run build/tests/linters/formatters if available to validate your work. Do NOT make any git commits - that will be handled automatically by the gnhf orchestrator
|
|
2592
|
+
6. Finally, respond with a JSON object according to the provided schema
|
|
2096
2593
|
|
|
2097
2594
|
## Output
|
|
2098
2595
|
|
|
2099
|
-
- success: whether you were able to
|
|
2596
|
+
- success: whether you were able to make a meaningful contribution that got us closer towards the objective. setting this to false means any code change you made should be discarded
|
|
2100
2597
|
- summary: a concise one-sentence summary of the accomplishment in this iteration
|
|
2101
2598
|
- key_changes_made: an array of descriptions for key changes you made. don't group this by file - group by logical units of work. don't describe activities - describe material outcomes
|
|
2102
|
-
- key_learnings: an array of new learnings that were surprising
|
|
2599
|
+
- key_learnings: an array of new learnings that were surprising, weren't captured by previous notes and would be informative for future iterations
|
|
2103
2600
|
|
|
2104
2601
|
## Objective
|
|
2105
2602
|
|
|
@@ -2120,6 +2617,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2120
2617
|
activeIterationPromise = null;
|
|
2121
2618
|
activeAbortController = null;
|
|
2122
2619
|
pendingAbortReason = null;
|
|
2620
|
+
loopDone = false;
|
|
2123
2621
|
state = {
|
|
2124
2622
|
status: "running",
|
|
2125
2623
|
currentIteration: 0,
|
|
@@ -2150,7 +2648,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2150
2648
|
}
|
|
2151
2649
|
stop() {
|
|
2152
2650
|
this.stopRequested = true;
|
|
2651
|
+
appendDebugLog("orchestrator:stop-requested", {
|
|
2652
|
+
iteration: this.state.currentIteration,
|
|
2653
|
+
hasActiveIteration: this.activeIterationPromise !== null,
|
|
2654
|
+
loopDone: this.loopDone
|
|
2655
|
+
});
|
|
2153
2656
|
this.activeAbortController?.abort();
|
|
2657
|
+
if (this.loopDone) {
|
|
2658
|
+
this.emit("stopped");
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2154
2661
|
if (this.stopPromise) return;
|
|
2155
2662
|
this.stopPromise = (async () => {
|
|
2156
2663
|
if (this.activeIterationPromise) {
|
|
@@ -2180,6 +2687,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2180
2687
|
this.state.startTime = /* @__PURE__ */ new Date();
|
|
2181
2688
|
this.state.status = "running";
|
|
2182
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
|
+
});
|
|
2183
2700
|
try {
|
|
2184
2701
|
while (!this.stopRequested) {
|
|
2185
2702
|
const preIterationAbortReason = this.getPreIterationAbortReason();
|
|
@@ -2196,11 +2713,32 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2196
2713
|
runId: this.runInfo.runId,
|
|
2197
2714
|
prompt: this.prompt
|
|
2198
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();
|
|
2199
2725
|
this.activeIterationPromise = this.runIteration(iterationPrompt);
|
|
2200
2726
|
const result = await this.activeIterationPromise;
|
|
2201
2727
|
this.activeIterationPromise = null;
|
|
2202
|
-
|
|
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
|
+
}
|
|
2203
2736
|
if (result.type === "aborted") {
|
|
2737
|
+
appendDebugLog("iteration:aborted", {
|
|
2738
|
+
iteration: this.state.currentIteration,
|
|
2739
|
+
elapsedMs: iterationElapsedMs,
|
|
2740
|
+
reason: result.reason
|
|
2741
|
+
});
|
|
2204
2742
|
this.abort(result.reason);
|
|
2205
2743
|
break;
|
|
2206
2744
|
}
|
|
@@ -2208,6 +2746,18 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2208
2746
|
this.state.iterations.push(record);
|
|
2209
2747
|
this.emit("iteration:end", record);
|
|
2210
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
|
+
});
|
|
2211
2761
|
const postIterationAbortReason = this.getPostIterationAbortReason();
|
|
2212
2762
|
if (postIterationAbortReason) {
|
|
2213
2763
|
this.abort(postIterationAbortReason);
|
|
@@ -2222,7 +2772,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2222
2772
|
this.state.status = "waiting";
|
|
2223
2773
|
this.state.waitingUntil = new Date(Date.now() + backoffMs);
|
|
2224
2774
|
this.emit("state", this.getState());
|
|
2775
|
+
appendDebugLog("backoff:start", {
|
|
2776
|
+
iteration: this.state.currentIteration,
|
|
2777
|
+
consecutiveFailures: this.state.consecutiveFailures,
|
|
2778
|
+
backoffMs
|
|
2779
|
+
});
|
|
2225
2780
|
await this.interruptibleSleep(backoffMs);
|
|
2781
|
+
appendDebugLog("backoff:end", {
|
|
2782
|
+
iteration: this.state.currentIteration,
|
|
2783
|
+
stopRequested: this.stopRequested
|
|
2784
|
+
});
|
|
2226
2785
|
this.state.waitingUntil = null;
|
|
2227
2786
|
if (!this.stopRequested) {
|
|
2228
2787
|
this.state.status = "running";
|
|
@@ -2234,6 +2793,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2234
2793
|
this.activeIterationPromise = null;
|
|
2235
2794
|
if (this.stopPromise) await this.stopPromise;
|
|
2236
2795
|
else await this.closeAgent();
|
|
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
|
+
});
|
|
2237
2806
|
}
|
|
2238
2807
|
}
|
|
2239
2808
|
async runIteration(prompt) {
|
|
@@ -2256,6 +2825,12 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2256
2825
|
this.emit("state", this.getState());
|
|
2257
2826
|
};
|
|
2258
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
|
+
});
|
|
2259
2834
|
try {
|
|
2260
2835
|
const result = await this.agent.run(prompt, this.cwd, {
|
|
2261
2836
|
onUsage,
|
|
@@ -2263,6 +2838,15 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2263
2838
|
signal: this.activeAbortController.signal,
|
|
2264
2839
|
logPath
|
|
2265
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
|
+
});
|
|
2266
2850
|
if (this.stopRequested) return { type: "stopped" };
|
|
2267
2851
|
if (result.output.success) return {
|
|
2268
2852
|
type: "completed",
|
|
@@ -2273,14 +2857,31 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2273
2857
|
record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, result.output.key_learnings)
|
|
2274
2858
|
};
|
|
2275
2859
|
} catch (err) {
|
|
2860
|
+
const elapsedMs = Date.now() - agentStartedAt;
|
|
2276
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
|
+
});
|
|
2277
2867
|
resetHard(this.cwd);
|
|
2278
2868
|
return {
|
|
2279
2869
|
type: "aborted",
|
|
2280
2870
|
reason: this.pendingAbortReason
|
|
2281
2871
|
};
|
|
2282
2872
|
}
|
|
2283
|
-
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
|
+
});
|
|
2284
2885
|
const summary = err instanceof Error ? err.message : String(err);
|
|
2285
2886
|
return {
|
|
2286
2887
|
type: "completed",
|
|
@@ -2352,13 +2953,31 @@ var Orchestrator = class extends EventEmitter {
|
|
|
2352
2953
|
this.state.status = "aborted";
|
|
2353
2954
|
this.state.lastMessage = reason;
|
|
2354
2955
|
this.state.waitingUntil = null;
|
|
2956
|
+
appendDebugLog("orchestrator:abort", {
|
|
2957
|
+
reason,
|
|
2958
|
+
iteration: this.state.currentIteration,
|
|
2959
|
+
consecutiveFailures: this.state.consecutiveFailures
|
|
2960
|
+
});
|
|
2355
2961
|
this.emit("abort", reason);
|
|
2356
2962
|
this.emit("state", this.getState());
|
|
2357
2963
|
}
|
|
2358
2964
|
async closeAgent() {
|
|
2359
2965
|
try {
|
|
2360
2966
|
await this.agent.close?.();
|
|
2361
|
-
} 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
|
+
}
|
|
2362
2981
|
}
|
|
2363
2982
|
};
|
|
2364
2983
|
//#endregion
|
|
@@ -2758,6 +3377,7 @@ const MOON_PHASE_PERIOD = 1600;
|
|
|
2758
3377
|
const MAX_MSG_LINES = 3;
|
|
2759
3378
|
const MAX_MSG_LINE_LEN = CONTENT_WIDTH;
|
|
2760
3379
|
const RESUME_HINT = "[ctrl+c to stop, gnhf again to resume]";
|
|
3380
|
+
const DONE_HINT = "[ctrl+c to exit]";
|
|
2761
3381
|
function spacedLabel(text) {
|
|
2762
3382
|
return text.split("").join(" ");
|
|
2763
3383
|
}
|
|
@@ -2878,8 +3498,8 @@ function centerLineCells(content, width) {
|
|
|
2878
3498
|
...emptyCells(rightPad)
|
|
2879
3499
|
];
|
|
2880
3500
|
}
|
|
2881
|
-
function renderResumeHintCells(width) {
|
|
2882
|
-
return centerLineCells(textToCells(RESUME_HINT, "dim"), width);
|
|
3501
|
+
function renderResumeHintCells(width, done) {
|
|
3502
|
+
return centerLineCells(textToCells(done ? DONE_HINT : RESUME_HINT, "dim"), width);
|
|
2883
3503
|
}
|
|
2884
3504
|
/**
|
|
2885
3505
|
* Builds the centered content viewport for the renderer.
|
|
@@ -2987,7 +3607,8 @@ function buildFrameCells(prompt, agentName, state, topStars, bottomStars, sideSt
|
|
|
2987
3607
|
]);
|
|
2988
3608
|
}
|
|
2989
3609
|
for (let y = 0; y < bottomHeight; y++) frame.push(renderStarLineCells(bottomStars, terminalWidth, y, now));
|
|
2990
|
-
|
|
3610
|
+
const isDone = state.status === "aborted";
|
|
3611
|
+
frame.push(renderResumeHintCells(terminalWidth, isDone));
|
|
2991
3612
|
frame.push(emptyCells(terminalWidth));
|
|
2992
3613
|
return frame;
|
|
2993
3614
|
}
|
|
@@ -3277,7 +3898,22 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
3277
3898
|
if (!reexeced) persistedPrompt?.cleanup();
|
|
3278
3899
|
}
|
|
3279
3900
|
}
|
|
3280
|
-
|
|
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
|
+
});
|
|
3281
3917
|
const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent]), runInfo, prompt, cwd, startIteration, {
|
|
3282
3918
|
maxIterations: options.maxIterations,
|
|
3283
3919
|
maxTokens: options.maxTokens
|
|
@@ -3298,8 +3934,9 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
3298
3934
|
process$1.on("SIGINT", handleSigInt);
|
|
3299
3935
|
process$1.on("SIGTERM", handleSigTerm);
|
|
3300
3936
|
const orchestratorPromise = orchestrator.start().finally(() => {
|
|
3301
|
-
renderer.stop();
|
|
3937
|
+
if (!(orchestrator.getState().status === "aborted" && process$1.stdin.isTTY)) renderer.stop();
|
|
3302
3938
|
}).catch((err) => {
|
|
3939
|
+
appendDebugLog("orchestrator:fatal", { error: serializeError(err) });
|
|
3303
3940
|
exitAltScreen();
|
|
3304
3941
|
die(err instanceof Error ? err.message : String(err));
|
|
3305
3942
|
});
|
|
@@ -3321,7 +3958,19 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
|
|
|
3321
3958
|
process$1.off("SIGTERM", handleSigTerm);
|
|
3322
3959
|
await sleepPreventionCleanup?.();
|
|
3323
3960
|
}
|
|
3324
|
-
|
|
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
|
+
}
|
|
3325
3974
|
if (shutdownSignal) process$1.exit(getSignalExitCode(shutdownSignal));
|
|
3326
3975
|
});
|
|
3327
3976
|
function enterAltScreen() {
|