gnhf 0.1.12 → 0.1.14

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 +713 -81
  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,69 @@ 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
+ await this.request(server, `/session/${sessionId}/prompt_async`, {
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
+ });
1324
1535
  return {
1325
1536
  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
- })
1537
+ body: ""
1338
1538
  };
1339
1539
  } catch (error) {
1340
1540
  messageRequestError = error;
1541
+ appendDebugLog("opencode:message-post:error", {
1542
+ sessionId,
1543
+ elapsedMs: Date.now() - messagePostStartedAt,
1544
+ error: serializeError(error),
1545
+ serverClosed: server.closed,
1546
+ serverStderr: server.stderr.slice(-2048),
1547
+ streamTelemetry: buildTelemetry()
1548
+ });
1341
1549
  streamAbortController.abort();
1342
1550
  return {
1343
1551
  ok: false,
@@ -1356,6 +1564,67 @@ var OpenCodeAgent = class {
1356
1564
  let lastText = null;
1357
1565
  let lastFinalAnswerText = null;
1358
1566
  let lastUsageSignature = "0:0:0:0";
1567
+ let structuredOutputFromSSE = null;
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;
@@ -1419,6 +1689,7 @@ var OpenCodeAgent = class {
1419
1689
  }
1420
1690
  if (payload?.type === "message.updated") {
1421
1691
  if (properties.info?.role === "assistant") updateUsage(properties.info.id, properties.info.tokens);
1692
+ if (properties.info?.structured) structuredOutputFromSSE = properties.info.structured;
1422
1693
  return false;
1423
1694
  }
1424
1695
  return payload?.type === "session.idle";
@@ -1432,7 +1703,9 @@ var OpenCodeAgent = class {
1432
1703
  const dataLines = rawEvent.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart());
1433
1704
  if (dataLines.length === 0) return;
1434
1705
  try {
1435
- if (handleEvent(JSON.parse(dataLines.join("\n")))) sawSessionIdle = true;
1706
+ const event = JSON.parse(dataLines.join("\n"));
1707
+ noteEvent(event.payload?.type);
1708
+ if (handleEvent(event)) sawSessionIdle = true;
1436
1709
  } catch {}
1437
1710
  };
1438
1711
  const processBufferedEvents = (flushRemainder = false) => {
@@ -1458,6 +1731,7 @@ var OpenCodeAgent = class {
1458
1731
  buffer = "";
1459
1732
  }
1460
1733
  };
1734
+ let bytesRead = 0;
1461
1735
  try {
1462
1736
  while (!sawSessionIdle) {
1463
1737
  let readResult;
@@ -1465,9 +1739,27 @@ var OpenCodeAgent = class {
1465
1739
  readResult = await reader.read();
1466
1740
  } catch (error) {
1467
1741
  if (messageRequestError) {
1742
+ appendDebugLog("opencode:stream:error", {
1743
+ sessionId,
1744
+ elapsedMs: Date.now() - streamStartedAt,
1745
+ bytesRead,
1746
+ reason: "message-post-failed",
1747
+ error: serializeError(messageRequestError),
1748
+ telemetry: buildTelemetry()
1749
+ });
1468
1750
  if (isAbortError$1(messageRequestError) || isAgentAbortError(messageRequestError)) throw createAbortError$1();
1469
1751
  throw messageRequestError;
1470
1752
  }
1753
+ appendDebugLog("opencode:stream:error", {
1754
+ sessionId,
1755
+ elapsedMs: Date.now() - streamStartedAt,
1756
+ bytesRead,
1757
+ reason: "reader-read-failed",
1758
+ error: serializeError(error),
1759
+ serverClosed: server.closed,
1760
+ serverStderr: server.stderr.slice(-2048),
1761
+ telemetry: buildTelemetry()
1762
+ });
1471
1763
  if (isAbortError$1(error)) throw createAbortError$1();
1472
1764
  throw error;
1473
1765
  }
@@ -1476,6 +1768,7 @@ var OpenCodeAgent = class {
1476
1768
  if (tail) {
1477
1769
  logStream?.write(tail);
1478
1770
  buffer += tail;
1771
+ bytesRead += tail.length;
1479
1772
  }
1480
1773
  processBufferedEvents(true);
1481
1774
  break;
@@ -1483,43 +1776,73 @@ var OpenCodeAgent = class {
1483
1776
  const chunk = decoder.decode(readResult.value, { stream: true });
1484
1777
  logStream?.write(chunk);
1485
1778
  buffer += chunk;
1779
+ bytesRead += chunk.length;
1486
1780
  processBufferedEvents();
1487
1781
  }
1488
1782
  } finally {
1783
+ clearInterval(stallTimer);
1489
1784
  streamAbortController.abort();
1490
1785
  await reader.cancel().catch(() => void 0);
1491
1786
  }
1787
+ appendDebugLog("opencode:stream:end", {
1788
+ sessionId,
1789
+ elapsedMs: Date.now() - streamStartedAt,
1790
+ bytesRead,
1791
+ sawSessionIdle,
1792
+ telemetry: buildTelemetry()
1793
+ });
1492
1794
  const messageResult = await messageRequest;
1493
1795
  if (!messageResult.ok) {
1494
1796
  if (isAbortError$1(messageResult.error) || isAgentAbortError(messageResult.error)) throw createAbortError$1();
1495
1797
  throw messageResult.error;
1496
1798
  }
1497
1799
  const body = messageResult.body;
1498
- let response;
1499
- try {
1500
- response = JSON.parse(body);
1800
+ if (body) try {
1801
+ JSON.parse(body);
1501
1802
  } catch (error) {
1502
- throw new Error(`Failed to parse opencode response: ${error instanceof Error ? error.message : String(error)}`);
1503
- }
1504
- if (response.info?.role === "assistant") updateUsage(response.info.id, response.info.tokens);
1505
- for (const part of response.parts ?? []) {
1506
- if (part.type !== "text" || typeof part.text !== "string") continue;
1507
- if (!part.text.trim()) continue;
1508
- lastText = part.text;
1509
- if (part.metadata?.openai?.phase === "final_answer") lastFinalAnswerText = part.text;
1510
- }
1511
- if (response.info?.structured) return {
1512
- output: response.info.structured,
1513
- usage
1514
- };
1803
+ appendDebugLog("opencode:response:parse-error", {
1804
+ sessionId,
1805
+ bodyLength: body.length,
1806
+ bodySample: body.slice(0, 512),
1807
+ error: serializeError(error)
1808
+ });
1809
+ }
1810
+ if (structuredOutputFromSSE) {
1811
+ appendDebugLog("opencode:output:structured", {
1812
+ sessionId,
1813
+ source: "sse"
1814
+ });
1815
+ return {
1816
+ output: structuredOutputFromSSE,
1817
+ usage
1818
+ };
1819
+ }
1515
1820
  const outputText = lastFinalAnswerText ?? lastText;
1516
- if (!outputText) throw new Error("opencode returned no text output");
1821
+ if (!outputText) {
1822
+ appendDebugLog("opencode:output:missing", {
1823
+ sessionId,
1824
+ hasStructuredOutput: structuredOutputFromSSE !== null
1825
+ });
1826
+ throw new Error("opencode returned no text output");
1827
+ }
1517
1828
  try {
1829
+ const output = JSON.parse(outputText);
1830
+ appendDebugLog("opencode:output:structured", {
1831
+ sessionId,
1832
+ source: lastFinalAnswerText ? "final_answer" : "last_text",
1833
+ outputTextLength: outputText.length
1834
+ });
1518
1835
  return {
1519
- output: JSON.parse(outputText),
1836
+ output,
1520
1837
  usage
1521
1838
  };
1522
1839
  } catch (error) {
1840
+ appendDebugLog("opencode:output:parse-error", {
1841
+ sessionId,
1842
+ outputTextLength: outputText.length,
1843
+ outputTextSample: outputText.slice(0, 512),
1844
+ error: serializeError(error)
1845
+ });
1523
1846
  throw new Error(`Failed to parse opencode output: ${error instanceof Error ? error.message : String(error)}`);
1524
1847
  }
1525
1848
  }
@@ -1529,7 +1852,13 @@ var OpenCodeAgent = class {
1529
1852
  method: "DELETE",
1530
1853
  timeoutMs: 1e3
1531
1854
  });
1532
- } catch {}
1855
+ appendDebugLog("opencode:session:delete", { sessionId });
1856
+ } catch (error) {
1857
+ appendDebugLog("opencode:session:delete-failed", {
1858
+ sessionId,
1859
+ error: serializeError(error)
1860
+ });
1861
+ }
1533
1862
  }
1534
1863
  async abortSession(server, sessionId) {
1535
1864
  try {
@@ -1537,7 +1866,13 @@ var OpenCodeAgent = class {
1537
1866
  method: "POST",
1538
1867
  timeoutMs: 1e3
1539
1868
  });
1540
- } catch {}
1869
+ appendDebugLog("opencode:session:abort", { sessionId });
1870
+ } catch (error) {
1871
+ appendDebugLog("opencode:session:abort-failed", {
1872
+ sessionId,
1873
+ error: serializeError(error)
1874
+ });
1875
+ }
1541
1876
  }
1542
1877
  async shutdownServer() {
1543
1878
  if (!this.server || this.server.closed) {
@@ -1549,9 +1884,11 @@ var OpenCodeAgent = class {
1549
1884
  return;
1550
1885
  }
1551
1886
  const server = this.server;
1887
+ const shutdownStartedAt = Date.now();
1552
1888
  appendDebugLog("opencode:shutdown", {
1553
1889
  cwd: server.cwd,
1554
- port: server.port
1890
+ port: server.port,
1891
+ pid: server.child.pid
1555
1892
  });
1556
1893
  this.closingPromise = (this.platform === "win32" && server.child.pid ? killWindowsProcessTree(server.child.pid) : shutdownChildProcess(server.child, {
1557
1894
  detached: server.detached,
@@ -1560,6 +1897,10 @@ var OpenCodeAgent = class {
1560
1897
  })).finally(() => {
1561
1898
  if (this.server === server) this.server = null;
1562
1899
  this.closingPromise = null;
1900
+ appendDebugLog("opencode:shutdown:done", {
1901
+ port: server.port,
1902
+ elapsedMs: Date.now() - shutdownStartedAt
1903
+ });
1563
1904
  });
1564
1905
  await this.closingPromise;
1565
1906
  }
@@ -1574,14 +1915,35 @@ var OpenCodeAgent = class {
1574
1915
  const headers = new Headers(options.headers);
1575
1916
  if (options.body !== void 0) headers.set("content-type", "application/json");
1576
1917
  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
- });
1918
+ const startedAt = Date.now();
1919
+ let response;
1920
+ try {
1921
+ response = await this.fetchFn(`${server.baseUrl}${path}`, {
1922
+ method: options.method,
1923
+ headers,
1924
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
1925
+ signal
1926
+ });
1927
+ } catch (error) {
1928
+ appendDebugLog("opencode:request:error", {
1929
+ method: options.method,
1930
+ path,
1931
+ elapsedMs: Date.now() - startedAt,
1932
+ timeoutMs: options.timeoutMs,
1933
+ error: serializeError(error),
1934
+ serverClosed: server.closed
1935
+ });
1936
+ throw error;
1937
+ }
1583
1938
  if (!response.ok) {
1584
1939
  const body = await response.text();
1940
+ appendDebugLog("opencode:request:non-ok", {
1941
+ method: options.method,
1942
+ path,
1943
+ status: response.status,
1944
+ elapsedMs: Date.now() - startedAt,
1945
+ bodySample: body.slice(0, 1024)
1946
+ });
1585
1947
  throw new Error(`opencode ${options.method} ${path} failed with ${response.status}: ${body}`);
1586
1948
  }
1587
1949
  return response;
@@ -1706,11 +2068,18 @@ var RovoDevAgent = class {
1706
2068
  const logStream = logPath ? createWriteStream(logPath) : null;
1707
2069
  const runController = new AbortController();
1708
2070
  let sessionId = null;
2071
+ const runStartedAt = Date.now();
2072
+ appendDebugLog("rovodev:run:start", {
2073
+ cwd,
2074
+ promptLength: prompt.length,
2075
+ hasLogPath: logPath !== void 0
2076
+ });
1709
2077
  const onAbort = () => {
1710
2078
  runController.abort();
1711
2079
  };
1712
2080
  if (signal?.aborted) {
1713
2081
  logStream?.end();
2082
+ appendDebugLog("rovodev:run:aborted-early", {});
1714
2083
  throw createAbortError();
1715
2084
  }
1716
2085
  signal?.addEventListener("abort", onAbort, { once: true });
@@ -1719,9 +2088,30 @@ var RovoDevAgent = class {
1719
2088
  sessionId = await this.createSession(server, runController.signal);
1720
2089
  await this.setInlineSystemPrompt(server, sessionId, runController.signal);
1721
2090
  await this.setChatMessage(server, sessionId, prompt, runController.signal);
1722
- return await this.streamChat(server, sessionId, runController.signal, logStream, onUsage, onMessage);
2091
+ const result = await this.streamChat(server, sessionId, runController.signal, logStream, onUsage, onMessage);
2092
+ appendDebugLog("rovodev:run:end", {
2093
+ sessionId,
2094
+ elapsedMs: Date.now() - runStartedAt,
2095
+ inputTokens: result.usage.inputTokens,
2096
+ outputTokens: result.usage.outputTokens
2097
+ });
2098
+ return result;
1723
2099
  } catch (error) {
1724
- if (runController.signal.aborted || isAbortError(error)) throw createAbortError();
2100
+ if (runController.signal.aborted || isAbortError(error)) {
2101
+ appendDebugLog("rovodev:run:aborted", {
2102
+ sessionId,
2103
+ elapsedMs: Date.now() - runStartedAt
2104
+ });
2105
+ throw createAbortError();
2106
+ }
2107
+ appendDebugLog("rovodev:run:error", {
2108
+ sessionId,
2109
+ elapsedMs: Date.now() - runStartedAt,
2110
+ error: serializeError(error),
2111
+ serverStderr: this.server?.stderr.slice(-2048),
2112
+ serverStdout: this.server?.stdout.slice(-2048),
2113
+ serverClosed: this.server?.closed ?? true
2114
+ });
1725
2115
  throw error;
1726
2116
  } finally {
1727
2117
  signal?.removeEventListener("abort", onAbort);
@@ -1779,17 +2169,40 @@ var RovoDevAgent = class {
1779
2169
  server.stderr += data.toString();
1780
2170
  if (server.stderr.length > MAX_OUTPUT) server.stderr = server.stderr.slice(-MAX_OUTPUT);
1781
2171
  });
1782
- child.on("close", () => {
2172
+ child.on("close", (code, closeSignal) => {
1783
2173
  server.closed = true;
2174
+ appendDebugLog("rovodev:server:close", {
2175
+ cwd: server.cwd,
2176
+ port: server.port,
2177
+ code,
2178
+ signal: closeSignal,
2179
+ stderr: server.stderr.slice(-2048),
2180
+ stdout: server.stdout.slice(-2048)
2181
+ });
1784
2182
  if (this.server === server) this.server = null;
1785
2183
  });
1786
2184
  this.server = server;
2185
+ const spawnedAt = Date.now();
1787
2186
  appendDebugLog("rovodev:spawn", {
1788
2187
  cwd,
1789
2188
  port,
1790
- detached
2189
+ detached,
2190
+ pid: child.pid,
2191
+ bin: this.bin
1791
2192
  });
1792
- server.readyPromise = this.waitForHealthy(server, signal).catch(async (error) => {
2193
+ server.readyPromise = this.waitForHealthy(server, signal).then(() => {
2194
+ appendDebugLog("rovodev:server:ready", {
2195
+ port,
2196
+ elapsedMs: Date.now() - spawnedAt
2197
+ });
2198
+ }).catch(async (error) => {
2199
+ appendDebugLog("rovodev:server:ready-failed", {
2200
+ port,
2201
+ elapsedMs: Date.now() - spawnedAt,
2202
+ error: serializeError(error),
2203
+ stderr: server.stderr.slice(-2048),
2204
+ stdout: server.stdout.slice(-2048)
2205
+ });
1793
2206
  await this.shutdownServer();
1794
2207
  throw error;
1795
2208
  });
@@ -1822,11 +2235,13 @@ var RovoDevAgent = class {
1822
2235
  throw new Error(`Timed out waiting for rovodev serve to become ready on port ${server.port}`);
1823
2236
  }
1824
2237
  async createSession(server, signal) {
1825
- return (await this.requestJSON(server, "/v3/sessions/create", {
2238
+ const response = await this.requestJSON(server, "/v3/sessions/create", {
1826
2239
  method: "POST",
1827
2240
  body: { custom_title: "gnhf" },
1828
2241
  signal
1829
- })).session_id;
2242
+ });
2243
+ appendDebugLog("rovodev:session:create", { sessionId: response.session_id });
2244
+ return response.session_id;
1830
2245
  }
1831
2246
  async setInlineSystemPrompt(server, sessionId, signal) {
1832
2247
  const schema = readFileSync(this.schemaPath, "utf-8").trim();
@@ -1852,7 +2267,13 @@ var RovoDevAgent = class {
1852
2267
  sessionId,
1853
2268
  timeoutMs: 1e3
1854
2269
  });
1855
- } catch {}
2270
+ appendDebugLog("rovodev:session:cancel", { sessionId });
2271
+ } catch (error) {
2272
+ appendDebugLog("rovodev:session:cancel-failed", {
2273
+ sessionId,
2274
+ error: serializeError(error)
2275
+ });
2276
+ }
1856
2277
  }
1857
2278
  async deleteSession(server, sessionId) {
1858
2279
  try {
@@ -1861,16 +2282,27 @@ var RovoDevAgent = class {
1861
2282
  sessionId,
1862
2283
  timeoutMs: 1e3
1863
2284
  });
1864
- } catch {}
2285
+ appendDebugLog("rovodev:session:delete", { sessionId });
2286
+ } catch (error) {
2287
+ appendDebugLog("rovodev:session:delete-failed", {
2288
+ sessionId,
2289
+ error: serializeError(error)
2290
+ });
2291
+ }
1865
2292
  }
1866
2293
  async streamChat(server, sessionId, signal, logStream, onUsage, onMessage) {
2294
+ const streamStartedAt = Date.now();
2295
+ appendDebugLog("rovodev:stream:start", { sessionId });
1867
2296
  const response = await this.request(server, "/v3/stream_chat", {
1868
2297
  method: "GET",
1869
2298
  sessionId,
1870
2299
  headers: { accept: "text/event-stream" },
1871
2300
  signal
1872
2301
  });
1873
- if (!response.body) throw new Error("rovodev returned no response body");
2302
+ if (!response.body) {
2303
+ appendDebugLog("rovodev:stream:no-body", { sessionId });
2304
+ throw new Error("rovodev returned no response body");
2305
+ }
1874
2306
  const usage = {
1875
2307
  inputTokens: 0,
1876
2308
  outputTokens: 0,
@@ -1959,11 +2391,20 @@ var RovoDevAgent = class {
1959
2391
  }
1960
2392
  }
1961
2393
  };
2394
+ let bytesRead = 0;
1962
2395
  while (true) {
1963
2396
  let readResult;
1964
2397
  try {
1965
2398
  readResult = await reader.read();
1966
2399
  } catch (error) {
2400
+ appendDebugLog("rovodev:stream:error", {
2401
+ sessionId,
2402
+ elapsedMs: Date.now() - streamStartedAt,
2403
+ bytesRead,
2404
+ error: serializeError(error),
2405
+ serverClosed: server.closed,
2406
+ serverStderr: server.stderr.slice(-2048)
2407
+ });
1967
2408
  if (isAbortError(error)) throw createAbortError();
1968
2409
  throw error;
1969
2410
  }
@@ -1971,6 +2412,7 @@ var RovoDevAgent = class {
1971
2412
  const chunk = decoder.decode(readResult.value, { stream: true });
1972
2413
  logStream?.write(chunk);
1973
2414
  buffer += chunk;
2415
+ bytesRead += chunk.length;
1974
2416
  while (true) {
1975
2417
  const lfBoundary = buffer.indexOf("\n\n");
1976
2418
  const crlfBoundary = buffer.indexOf("\r\n\r\n");
@@ -1991,14 +2433,33 @@ var RovoDevAgent = class {
1991
2433
  }
1992
2434
  buffer += decoder.decode();
1993
2435
  if (buffer.trim()) handleEvent(buffer);
2436
+ appendDebugLog("rovodev:stream:end", {
2437
+ sessionId,
2438
+ elapsedMs: Date.now() - streamStartedAt,
2439
+ bytesRead
2440
+ });
1994
2441
  const finalText = latestTextSegment.trim();
1995
- if (!finalText) throw new Error("rovodev returned no text output");
2442
+ if (!finalText) {
2443
+ appendDebugLog("rovodev:output:missing", { sessionId });
2444
+ throw new Error("rovodev returned no text output");
2445
+ }
1996
2446
  try {
2447
+ const output = JSON.parse(finalText);
2448
+ appendDebugLog("rovodev:output:parsed", {
2449
+ sessionId,
2450
+ outputTextLength: finalText.length
2451
+ });
1997
2452
  return {
1998
- output: JSON.parse(finalText),
2453
+ output,
1999
2454
  usage
2000
2455
  };
2001
2456
  } catch (error) {
2457
+ appendDebugLog("rovodev:output:parse-error", {
2458
+ sessionId,
2459
+ outputTextLength: finalText.length,
2460
+ outputTextSample: finalText.slice(0, 512),
2461
+ error: serializeError(error)
2462
+ });
2002
2463
  throw new Error(`Failed to parse rovodev output: ${error instanceof Error ? error.message : String(error)}`);
2003
2464
  }
2004
2465
  }
@@ -2012,9 +2473,11 @@ var RovoDevAgent = class {
2012
2473
  return;
2013
2474
  }
2014
2475
  const server = this.server;
2476
+ const shutdownStartedAt = Date.now();
2015
2477
  appendDebugLog("rovodev:shutdown", {
2016
2478
  cwd: server.cwd,
2017
- port: server.port
2479
+ port: server.port,
2480
+ pid: server.child.pid
2018
2481
  });
2019
2482
  this.closingPromise = this.platform === "win32" ? new Promise((resolve) => {
2020
2483
  const handleClose = () => {
@@ -2041,6 +2504,10 @@ var RovoDevAgent = class {
2041
2504
  this.closingPromise = this.closingPromise.finally(() => {
2042
2505
  if (this.server === server) this.server = null;
2043
2506
  this.closingPromise = null;
2507
+ appendDebugLog("rovodev:shutdown:done", {
2508
+ port: server.port,
2509
+ elapsedMs: Date.now() - shutdownStartedAt
2510
+ });
2044
2511
  });
2045
2512
  await this.closingPromise;
2046
2513
  }
@@ -2052,14 +2519,35 @@ var RovoDevAgent = class {
2052
2519
  if (options.sessionId) headers.set("x-session-id", options.sessionId);
2053
2520
  if (options.body !== void 0 && !headers.has("content-type")) headers.set("content-type", "application/json");
2054
2521
  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
- });
2522
+ const startedAt = Date.now();
2523
+ let response;
2524
+ try {
2525
+ response = await this.fetchFn(`${server.baseUrl}${path}`, {
2526
+ method: options.method,
2527
+ headers,
2528
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
2529
+ signal
2530
+ });
2531
+ } catch (error) {
2532
+ appendDebugLog("rovodev:request:error", {
2533
+ method: options.method,
2534
+ path,
2535
+ elapsedMs: Date.now() - startedAt,
2536
+ timeoutMs: options.timeoutMs,
2537
+ error: serializeError(error),
2538
+ serverClosed: server.closed
2539
+ });
2540
+ throw error;
2541
+ }
2061
2542
  if (!response.ok) {
2062
2543
  const body = await response.text();
2544
+ appendDebugLog("rovodev:request:non-ok", {
2545
+ method: options.method,
2546
+ path,
2547
+ status: response.status,
2548
+ elapsedMs: Date.now() - startedAt,
2549
+ bodySample: body.slice(0, 1024)
2550
+ });
2063
2551
  throw new Error(`rovodev ${options.method} ${path} failed with ${response.status}: ${body}`);
2064
2552
  }
2065
2553
  return response;
@@ -2151,6 +2639,11 @@ var Orchestrator = class extends EventEmitter {
2151
2639
  }
2152
2640
  stop() {
2153
2641
  this.stopRequested = true;
2642
+ appendDebugLog("orchestrator:stop-requested", {
2643
+ iteration: this.state.currentIteration,
2644
+ hasActiveIteration: this.activeIterationPromise !== null,
2645
+ loopDone: this.loopDone
2646
+ });
2154
2647
  this.activeAbortController?.abort();
2155
2648
  if (this.loopDone) {
2156
2649
  this.emit("stopped");
@@ -2185,6 +2678,16 @@ var Orchestrator = class extends EventEmitter {
2185
2678
  this.state.startTime = /* @__PURE__ */ new Date();
2186
2679
  this.state.status = "running";
2187
2680
  this.emit("state", this.getState());
2681
+ appendDebugLog("orchestrator:start", {
2682
+ agent: this.agent.name,
2683
+ runId: this.runInfo.runId,
2684
+ startIteration: this.state.currentIteration,
2685
+ maxIterations: this.limits.maxIterations,
2686
+ maxTokens: this.limits.maxTokens,
2687
+ maxConsecutiveFailures: this.config.maxConsecutiveFailures,
2688
+ baseCommit: this.runInfo.baseCommit,
2689
+ initialCommitCount: this.state.commitCount
2690
+ });
2188
2691
  try {
2189
2692
  while (!this.stopRequested) {
2190
2693
  const preIterationAbortReason = this.getPreIterationAbortReason();
@@ -2201,11 +2704,32 @@ var Orchestrator = class extends EventEmitter {
2201
2704
  runId: this.runInfo.runId,
2202
2705
  prompt: this.prompt
2203
2706
  });
2707
+ appendDebugLog("iteration:start", {
2708
+ iteration: this.state.currentIteration,
2709
+ promptLength: iterationPrompt.length,
2710
+ consecutiveFailures: this.state.consecutiveFailures,
2711
+ totalInputTokens: this.state.totalInputTokens,
2712
+ totalOutputTokens: this.state.totalOutputTokens,
2713
+ git: this.snapshotGitState()
2714
+ });
2715
+ const iterationStartedAt = Date.now();
2204
2716
  this.activeIterationPromise = this.runIteration(iterationPrompt);
2205
2717
  const result = await this.activeIterationPromise;
2206
2718
  this.activeIterationPromise = null;
2207
- if (result.type === "stopped") break;
2719
+ const iterationElapsedMs = Date.now() - iterationStartedAt;
2720
+ if (result.type === "stopped") {
2721
+ appendDebugLog("iteration:stopped", {
2722
+ iteration: this.state.currentIteration,
2723
+ elapsedMs: iterationElapsedMs
2724
+ });
2725
+ break;
2726
+ }
2208
2727
  if (result.type === "aborted") {
2728
+ appendDebugLog("iteration:aborted", {
2729
+ iteration: this.state.currentIteration,
2730
+ elapsedMs: iterationElapsedMs,
2731
+ reason: result.reason
2732
+ });
2209
2733
  this.abort(result.reason);
2210
2734
  break;
2211
2735
  }
@@ -2213,6 +2737,18 @@ var Orchestrator = class extends EventEmitter {
2213
2737
  this.state.iterations.push(record);
2214
2738
  this.emit("iteration:end", record);
2215
2739
  this.emit("state", this.getState());
2740
+ appendDebugLog("iteration:end", {
2741
+ iteration: record.number,
2742
+ elapsedMs: iterationElapsedMs,
2743
+ success: record.success,
2744
+ summary: record.summary,
2745
+ keyChanges: record.keyChanges.length,
2746
+ keyLearnings: record.keyLearnings.length,
2747
+ consecutiveFailures: this.state.consecutiveFailures,
2748
+ totalInputTokens: this.state.totalInputTokens,
2749
+ totalOutputTokens: this.state.totalOutputTokens,
2750
+ commitCount: this.state.commitCount
2751
+ });
2216
2752
  const postIterationAbortReason = this.getPostIterationAbortReason();
2217
2753
  if (postIterationAbortReason) {
2218
2754
  this.abort(postIterationAbortReason);
@@ -2227,7 +2763,16 @@ var Orchestrator = class extends EventEmitter {
2227
2763
  this.state.status = "waiting";
2228
2764
  this.state.waitingUntil = new Date(Date.now() + backoffMs);
2229
2765
  this.emit("state", this.getState());
2766
+ appendDebugLog("backoff:start", {
2767
+ iteration: this.state.currentIteration,
2768
+ consecutiveFailures: this.state.consecutiveFailures,
2769
+ backoffMs
2770
+ });
2230
2771
  await this.interruptibleSleep(backoffMs);
2772
+ appendDebugLog("backoff:end", {
2773
+ iteration: this.state.currentIteration,
2774
+ stopRequested: this.stopRequested
2775
+ });
2231
2776
  this.state.waitingUntil = null;
2232
2777
  if (!this.stopRequested) {
2233
2778
  this.state.status = "running";
@@ -2240,6 +2785,15 @@ var Orchestrator = class extends EventEmitter {
2240
2785
  if (this.stopPromise) await this.stopPromise;
2241
2786
  else await this.closeAgent();
2242
2787
  this.loopDone = true;
2788
+ appendDebugLog("orchestrator:end", {
2789
+ status: this.state.status,
2790
+ iterations: this.state.currentIteration,
2791
+ successCount: this.state.successCount,
2792
+ failCount: this.state.failCount,
2793
+ totalInputTokens: this.state.totalInputTokens,
2794
+ totalOutputTokens: this.state.totalOutputTokens,
2795
+ commitCount: this.state.commitCount
2796
+ });
2243
2797
  }
2244
2798
  }
2245
2799
  async runIteration(prompt) {
@@ -2262,6 +2816,12 @@ var Orchestrator = class extends EventEmitter {
2262
2816
  this.emit("state", this.getState());
2263
2817
  };
2264
2818
  const logPath = join(this.runInfo.runDir, `iteration-${this.state.currentIteration}.jsonl`);
2819
+ const agentStartedAt = Date.now();
2820
+ appendDebugLog("agent:run:start", {
2821
+ iteration: this.state.currentIteration,
2822
+ agent: this.agent.name,
2823
+ logPath
2824
+ });
2265
2825
  try {
2266
2826
  const result = await this.agent.run(prompt, this.cwd, {
2267
2827
  onUsage,
@@ -2269,6 +2829,15 @@ var Orchestrator = class extends EventEmitter {
2269
2829
  signal: this.activeAbortController.signal,
2270
2830
  logPath
2271
2831
  });
2832
+ appendDebugLog("agent:run:end", {
2833
+ iteration: this.state.currentIteration,
2834
+ elapsedMs: Date.now() - agentStartedAt,
2835
+ success: result.output.success,
2836
+ inputTokens: result.usage.inputTokens,
2837
+ outputTokens: result.usage.outputTokens,
2838
+ cacheReadTokens: result.usage.cacheReadTokens,
2839
+ cacheCreationTokens: result.usage.cacheCreationTokens
2840
+ });
2272
2841
  if (this.stopRequested) return { type: "stopped" };
2273
2842
  if (result.output.success) return {
2274
2843
  type: "completed",
@@ -2279,14 +2848,31 @@ var Orchestrator = class extends EventEmitter {
2279
2848
  record: this.recordFailure(`[FAIL] ${result.output.summary}`, result.output.summary, result.output.key_learnings)
2280
2849
  };
2281
2850
  } catch (err) {
2851
+ const elapsedMs = Date.now() - agentStartedAt;
2282
2852
  if (this.pendingAbortReason && err instanceof Error && err.message === "Agent was aborted") {
2853
+ appendDebugLog("agent:run:aborted", {
2854
+ iteration: this.state.currentIteration,
2855
+ elapsedMs,
2856
+ reason: this.pendingAbortReason
2857
+ });
2283
2858
  resetHard(this.cwd);
2284
2859
  return {
2285
2860
  type: "aborted",
2286
2861
  reason: this.pendingAbortReason
2287
2862
  };
2288
2863
  }
2289
- if (this.stopRequested) return { type: "stopped" };
2864
+ if (this.stopRequested) {
2865
+ appendDebugLog("agent:run:stopped", {
2866
+ iteration: this.state.currentIteration,
2867
+ elapsedMs
2868
+ });
2869
+ return { type: "stopped" };
2870
+ }
2871
+ appendDebugLog("agent:run:error", {
2872
+ iteration: this.state.currentIteration,
2873
+ elapsedMs,
2874
+ error: serializeError(err)
2875
+ });
2290
2876
  const summary = err instanceof Error ? err.message : String(err);
2291
2877
  return {
2292
2878
  type: "completed",
@@ -2358,13 +2944,31 @@ var Orchestrator = class extends EventEmitter {
2358
2944
  this.state.status = "aborted";
2359
2945
  this.state.lastMessage = reason;
2360
2946
  this.state.waitingUntil = null;
2947
+ appendDebugLog("orchestrator:abort", {
2948
+ reason,
2949
+ iteration: this.state.currentIteration,
2950
+ consecutiveFailures: this.state.consecutiveFailures
2951
+ });
2361
2952
  this.emit("abort", reason);
2362
2953
  this.emit("state", this.getState());
2363
2954
  }
2364
2955
  async closeAgent() {
2365
2956
  try {
2366
2957
  await this.agent.close?.();
2367
- } catch {}
2958
+ } catch (err) {
2959
+ appendDebugLog("agent:close:error", { error: serializeError(err) });
2960
+ }
2961
+ }
2962
+ snapshotGitState() {
2963
+ try {
2964
+ return {
2965
+ head: getHeadCommit(this.cwd),
2966
+ branch: getCurrentBranch(this.cwd),
2967
+ commitCount: this.state.commitCount
2968
+ };
2969
+ } catch (err) {
2970
+ return { error: serializeError(err) };
2971
+ }
2368
2972
  }
2369
2973
  };
2370
2974
  //#endregion
@@ -3285,7 +3889,22 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
3285
3889
  if (!reexeced) persistedPrompt?.cleanup();
3286
3890
  }
3287
3891
  }
3288
- appendDebugLog("run:start", { args: process$1.argv.slice(2) });
3892
+ initDebugLog(runInfo.logPath);
3893
+ appendDebugLog("run:start", {
3894
+ args: process$1.argv.slice(2),
3895
+ runId: runInfo.runId,
3896
+ runDir: runInfo.runDir,
3897
+ agent: config.agent,
3898
+ promptLength: prompt.length,
3899
+ promptFromStdin,
3900
+ startIteration,
3901
+ maxIterations: options.maxIterations,
3902
+ maxTokens: options.maxTokens,
3903
+ preventSleep: config.preventSleep,
3904
+ platform: process$1.platform,
3905
+ nodeVersion: process$1.version,
3906
+ gnhfVersion: packageVersion
3907
+ });
3289
3908
  const orchestrator = new Orchestrator(config, createAgent(config.agent, runInfo, config.agentPathOverride[config.agent]), runInfo, prompt, cwd, startIteration, {
3290
3909
  maxIterations: options.maxIterations,
3291
3910
  maxTokens: options.maxTokens
@@ -3308,6 +3927,7 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
3308
3927
  const orchestratorPromise = orchestrator.start().finally(() => {
3309
3928
  if (!(orchestrator.getState().status === "aborted" && process$1.stdin.isTTY)) renderer.stop();
3310
3929
  }).catch((err) => {
3930
+ appendDebugLog("orchestrator:fatal", { error: serializeError(err) });
3311
3931
  exitAltScreen();
3312
3932
  die(err instanceof Error ? err.message : String(err));
3313
3933
  });
@@ -3329,7 +3949,19 @@ program.name("gnhf").description("Before I go to bed, I tell my agents: good nig
3329
3949
  process$1.off("SIGTERM", handleSigTerm);
3330
3950
  await sleepPreventionCleanup?.();
3331
3951
  }
3332
- appendDebugLog("run:complete", { signal: shutdownSignal });
3952
+ {
3953
+ const finalState = orchestrator.getState();
3954
+ appendDebugLog("run:complete", {
3955
+ signal: shutdownSignal,
3956
+ status: finalState.status,
3957
+ iterations: finalState.currentIteration,
3958
+ successCount: finalState.successCount,
3959
+ failCount: finalState.failCount,
3960
+ totalInputTokens: finalState.totalInputTokens,
3961
+ totalOutputTokens: finalState.totalOutputTokens,
3962
+ commitCount: finalState.commitCount
3963
+ });
3964
+ }
3333
3965
  if (shutdownSignal) process$1.exit(getSignalExitCode(shutdownSignal));
3334
3966
  });
3335
3967
  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.14",
4
4
  "description": "Before I go to bed, I tell my agents: good night, have fun",
5
5
  "type": "module",
6
6
  "bin": {