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.
Files changed (3) hide show
  1. package/README.md +2 -4
  2. package/dist/cli.mjs +710 -69
  3. 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
- Set `GNHF_DEBUG_LOG_PATH` to capture lifecycle events as JSONL while debugging a run:
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
- ```sh
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
- function appendDebugLog(event, details = {}) {
139
- const logPath = process.env.GNHF_DEBUG_LOG_PATH;
140
- if (!logPath) return;
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
- appendFileSync(logPath, `${JSON.stringify({
143
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
144
- pid: process.pid,
145
- event,
151
+ return `${JSON.stringify({
152
+ ...base,
146
153
  ...details
147
- })}\n`, "utf-8");
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
- return await this.streamMessage(server, sessionId, buildPrompt(prompt), runController.signal, logStream, onUsage, onMessage);
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)) throw createAbortError$1();
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
- server.stderr += data.toString();
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).catch(async (error) => {
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
- return (await this.requestJSON(server, "/session", {
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
- })).id;
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) throw new Error("opencode returned no event stream 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: await this.requestText(server, `/session/${sessionId}/message`, {
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
- if (handleEvent(JSON.parse(dataLines.join("\n")))) sawSessionIdle = true;
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) return {
1512
- output: response.info.structured,
1513
- usage
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) throw new Error("opencode returned no text output");
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: JSON.parse(outputText),
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
- } catch {}
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
- } catch {}
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 response = await this.fetchFn(`${server.baseUrl}${path}`, {
1578
- method: options.method,
1579
- headers,
1580
- body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
1581
- signal
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
- return await this.streamChat(server, sessionId, runController.signal, logStream, onUsage, onMessage);
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)) throw createAbortError();
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).catch(async (error) => {
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
- return (await this.requestJSON(server, "/v3/sessions/create", {
2247
+ const response = await this.requestJSON(server, "/v3/sessions/create", {
1826
2248
  method: "POST",
1827
2249
  body: { custom_title: "gnhf" },
1828
2250
  signal
1829
- })).session_id;
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
- } catch {}
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
- } catch {}
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) throw new Error("rovodev returned no 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) throw new Error("rovodev returned no text output");
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: JSON.parse(finalText),
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 response = await this.fetchFn(`${server.baseUrl}${path}`, {
2056
- method: options.method,
2057
- headers,
2058
- body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
2059
- signal
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
- if (result.type === "stopped") break;
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) return { type: "stopped" };
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
- appendDebugLog("run:start", { args: process$1.argv.slice(2) });
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
- appendDebugLog("run:complete", { signal: shutdownSignal });
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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gnhf",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Before I go to bed, I tell my agents: good night, have fun",
5
5
  "type": "module",
6
6
  "bin": {