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.
- package/README.md +2 -4
- package/dist/cli.mjs +713 -81
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -192,11 +192,9 @@ When sleep prevention is enabled, `gnhf` uses the native mechanism for your OS:
|
|
|
192
192
|
|
|
193
193
|
## Debug Logs
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
Every run writes a JSONL debug log to `.gnhf/runs/<runId>/gnhf.log` alongside `notes.md`. Lifecycle events for the orchestrator, agent, and HTTP requests are captured with elapsed timings and (for failures) the full `error.cause` chain — which is what you need to tell a bare `TypeError: fetch failed` apart from an undici `UND_ERR_HEADERS_TIMEOUT`. The agent's own streaming output still goes to the per-iteration `iteration-<n>.jsonl` file next to it.
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
GNHF_DEBUG_LOG_PATH=/tmp/gnhf-debug.jsonl gnhf "ship it"
|
|
199
|
-
```
|
|
197
|
+
Including a snippet of `gnhf.log` is the single most useful thing you can attach when filing an issue.
|
|
200
198
|
|
|
201
199
|
## Agents
|
|
202
200
|
|
package/dist/cli.mjs
CHANGED
|
@@ -135,18 +135,111 @@ function loadConfig(overrides) {
|
|
|
135
135
|
}
|
|
136
136
|
//#endregion
|
|
137
137
|
//#region src/core/debug-log.ts
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
const PRE_INIT_BUFFER_CAPACITY = 1e3;
|
|
139
|
+
const STACK_LINE_LIMIT = 12;
|
|
140
|
+
const CAUSE_DEPTH_LIMIT = 6;
|
|
141
|
+
let logPath = null;
|
|
142
|
+
let preInitBuffer = [];
|
|
143
|
+
let preInitDroppedCount = 0;
|
|
144
|
+
function formatLine(event, details) {
|
|
145
|
+
const base = {
|
|
146
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
147
|
+
pid: process.pid,
|
|
148
|
+
event
|
|
149
|
+
};
|
|
141
150
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
pid: process.pid,
|
|
145
|
-
event,
|
|
151
|
+
return `${JSON.stringify({
|
|
152
|
+
...base,
|
|
146
153
|
...details
|
|
147
|
-
})}\n
|
|
154
|
+
})}\n`;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return `${JSON.stringify({
|
|
157
|
+
...base,
|
|
158
|
+
logError: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
|
|
159
|
+
detailsKeys: Object.keys(details)
|
|
160
|
+
})}\n`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function initDebugLog(path) {
|
|
164
|
+
logPath = path;
|
|
165
|
+
try {
|
|
166
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
167
|
+
} catch {}
|
|
168
|
+
if (preInitBuffer.length === 0 && preInitDroppedCount === 0) return;
|
|
169
|
+
const flushed = (preInitDroppedCount > 0 ? formatLine("debug-log:pre-init-overflow", {
|
|
170
|
+
droppedCount: preInitDroppedCount,
|
|
171
|
+
bufferCapacity: PRE_INIT_BUFFER_CAPACITY
|
|
172
|
+
}) : "") + preInitBuffer.join("");
|
|
173
|
+
preInitBuffer = [];
|
|
174
|
+
preInitDroppedCount = 0;
|
|
175
|
+
try {
|
|
176
|
+
appendFileSync(path, flushed, "utf-8");
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
179
|
+
function appendDebugLog(event, details = {}) {
|
|
180
|
+
const line = formatLine(event, details);
|
|
181
|
+
if (logPath === null) {
|
|
182
|
+
preInitBuffer.push(line);
|
|
183
|
+
if (preInitBuffer.length > PRE_INIT_BUFFER_CAPACITY) {
|
|
184
|
+
preInitBuffer.shift();
|
|
185
|
+
preInitDroppedCount += 1;
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
appendFileSync(logPath, line, "utf-8");
|
|
148
191
|
} catch {}
|
|
149
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Serialize an error (including its cause chain) to a plain object that's
|
|
195
|
+
* safe to embed in a JSONL log line. This is critical for diagnosing
|
|
196
|
+
* fetch failures, where the surface message is often just "fetch failed"
|
|
197
|
+
* but `err.cause` holds the real undici error (e.g. UND_ERR_HEADERS_TIMEOUT).
|
|
198
|
+
*
|
|
199
|
+
* Contract: must never throw. Callers invoke this inside catch blocks and
|
|
200
|
+
* timer/EventEmitter handlers, and a throw here would mask the original
|
|
201
|
+
* error or crash the process.
|
|
202
|
+
*/
|
|
203
|
+
function serializeError(error, depth = 0) {
|
|
204
|
+
try {
|
|
205
|
+
return serializeErrorUnsafe(error, depth);
|
|
206
|
+
} catch (serializationError) {
|
|
207
|
+
return {
|
|
208
|
+
value: "[serialization failed]",
|
|
209
|
+
serializationError: serializationError instanceof Error ? `${serializationError.name}: ${serializationError.message}` : String(serializationError)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function tryRead(read) {
|
|
214
|
+
try {
|
|
215
|
+
return read();
|
|
216
|
+
} catch {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function serializeErrorUnsafe(error, depth) {
|
|
221
|
+
if (depth > CAUSE_DEPTH_LIMIT) return { value: "[cause chain truncated]" };
|
|
222
|
+
if (error instanceof Error) {
|
|
223
|
+
const result = {
|
|
224
|
+
name: tryRead(() => error.name) ?? "Error",
|
|
225
|
+
message: tryRead(() => error.message) ?? ""
|
|
226
|
+
};
|
|
227
|
+
const code = tryRead(() => error.code);
|
|
228
|
+
if (typeof code === "string" || typeof code === "number") result.code = code;
|
|
229
|
+
const stack = tryRead(() => error.stack);
|
|
230
|
+
if (typeof stack === "string") result.stack = stack.split("\n").slice(0, STACK_LINE_LIMIT).join("\n");
|
|
231
|
+
const cause = tryRead(() => "cause" in error ? error.cause : void 0);
|
|
232
|
+
if (cause !== void 0) result.cause = serializeError(cause, depth + 1);
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
if (error === null || error === void 0) return { value: String(error) };
|
|
236
|
+
if (typeof error === "object") try {
|
|
237
|
+
return { value: JSON.parse(JSON.stringify(error)) };
|
|
238
|
+
} catch {
|
|
239
|
+
return { value: String(error) };
|
|
240
|
+
}
|
|
241
|
+
return { value: String(error) };
|
|
242
|
+
}
|
|
150
243
|
//#endregion
|
|
151
244
|
//#region src/core/git.ts
|
|
152
245
|
const NOT_GIT_REPOSITORY_MESSAGE = "This command must be run inside a Git repository. Change into a repo or run \"git init\" first.";
|
|
@@ -255,6 +348,7 @@ const AGENT_OUTPUT_SCHEMA = {
|
|
|
255
348
|
};
|
|
256
349
|
//#endregion
|
|
257
350
|
//#region src/core/run.ts
|
|
351
|
+
const LOG_FILENAME = "gnhf.log";
|
|
258
352
|
function writeSchemaFile(schemaPath) {
|
|
259
353
|
writeFileSync(schemaPath, JSON.stringify(AGENT_OUTPUT_SCHEMA, null, 2), "utf-8");
|
|
260
354
|
}
|
|
@@ -286,6 +380,7 @@ function setupRun(runId, prompt, baseCommit, cwd) {
|
|
|
286
380
|
writeFileSync(notesPath, `# gnhf run: ${runId}\n\nObjective: ${prompt}\n\n## Iteration Log\n`, "utf-8");
|
|
287
381
|
const schemaPath = join(runDir, "output-schema.json");
|
|
288
382
|
writeSchemaFile(schemaPath);
|
|
383
|
+
const logPath = join(runDir, LOG_FILENAME);
|
|
289
384
|
const baseCommitPath = join(runDir, "base-commit");
|
|
290
385
|
const hasStoredBaseCommit = existsSync(baseCommitPath);
|
|
291
386
|
const resolvedBaseCommit = hasStoredBaseCommit ? readFileSync(baseCommitPath, "utf-8").trim() : baseCommit;
|
|
@@ -296,6 +391,7 @@ function setupRun(runId, prompt, baseCommit, cwd) {
|
|
|
296
391
|
promptPath,
|
|
297
392
|
notesPath,
|
|
298
393
|
schemaPath,
|
|
394
|
+
logPath,
|
|
299
395
|
baseCommit: resolvedBaseCommit,
|
|
300
396
|
baseCommitPath
|
|
301
397
|
};
|
|
@@ -307,6 +403,7 @@ function resumeRun(runId, cwd) {
|
|
|
307
403
|
const notesPath = join(runDir, "notes.md");
|
|
308
404
|
const schemaPath = join(runDir, "output-schema.json");
|
|
309
405
|
writeSchemaFile(schemaPath);
|
|
406
|
+
const logPath = join(runDir, LOG_FILENAME);
|
|
310
407
|
const baseCommitPath = join(runDir, "base-commit");
|
|
311
408
|
return {
|
|
312
409
|
runId,
|
|
@@ -314,6 +411,7 @@ function resumeRun(runId, cwd) {
|
|
|
314
411
|
promptPath,
|
|
315
412
|
notesPath,
|
|
316
413
|
schemaPath,
|
|
414
|
+
logPath,
|
|
317
415
|
baseCommit: existsSync(baseCommitPath) ? readFileSync(baseCommitPath, "utf-8").trim() : backfillLegacyBaseCommit(runId, baseCommitPath, cwd),
|
|
318
416
|
baseCommitPath
|
|
319
417
|
};
|
|
@@ -1179,20 +1277,48 @@ var OpenCodeAgent = class {
|
|
|
1179
1277
|
const logStream = logPath ? createWriteStream(logPath) : null;
|
|
1180
1278
|
const runController = new AbortController();
|
|
1181
1279
|
let sessionId = null;
|
|
1280
|
+
const runStartedAt = Date.now();
|
|
1281
|
+
appendDebugLog("opencode:run:start", {
|
|
1282
|
+
cwd,
|
|
1283
|
+
promptLength: prompt.length,
|
|
1284
|
+
hasLogPath: logPath !== void 0
|
|
1285
|
+
});
|
|
1182
1286
|
const onAbort = () => {
|
|
1183
1287
|
runController.abort();
|
|
1184
1288
|
};
|
|
1185
1289
|
if (signal?.aborted) {
|
|
1186
1290
|
logStream?.end();
|
|
1291
|
+
appendDebugLog("opencode:run:aborted-early", {});
|
|
1187
1292
|
throw createAbortError$1();
|
|
1188
1293
|
}
|
|
1189
1294
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1190
1295
|
try {
|
|
1191
1296
|
const server = await this.ensureServer(cwd, runController.signal);
|
|
1192
1297
|
sessionId = await this.createSession(server, cwd, runController.signal);
|
|
1193
|
-
|
|
1298
|
+
const result = await this.streamMessage(server, sessionId, buildPrompt(prompt), runController.signal, logStream, onUsage, onMessage);
|
|
1299
|
+
appendDebugLog("opencode:run:end", {
|
|
1300
|
+
sessionId,
|
|
1301
|
+
elapsedMs: Date.now() - runStartedAt,
|
|
1302
|
+
inputTokens: result.usage.inputTokens,
|
|
1303
|
+
outputTokens: result.usage.outputTokens
|
|
1304
|
+
});
|
|
1305
|
+
return result;
|
|
1194
1306
|
} catch (error) {
|
|
1195
|
-
if (runController.signal.aborted || isAbortError$1(error))
|
|
1307
|
+
if (runController.signal.aborted || isAbortError$1(error)) {
|
|
1308
|
+
appendDebugLog("opencode:run:aborted", {
|
|
1309
|
+
sessionId,
|
|
1310
|
+
elapsedMs: Date.now() - runStartedAt
|
|
1311
|
+
});
|
|
1312
|
+
throw createAbortError$1();
|
|
1313
|
+
}
|
|
1314
|
+
appendDebugLog("opencode:run:error", {
|
|
1315
|
+
sessionId,
|
|
1316
|
+
elapsedMs: Date.now() - runStartedAt,
|
|
1317
|
+
error: serializeError(error),
|
|
1318
|
+
serverStderr: this.server?.stderr.slice(-2048),
|
|
1319
|
+
serverStdout: this.server?.stdout.slice(-2048),
|
|
1320
|
+
serverClosed: this.server?.closed ?? true
|
|
1321
|
+
});
|
|
1196
1322
|
throw error;
|
|
1197
1323
|
} finally {
|
|
1198
1324
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -1249,25 +1375,82 @@ var OpenCodeAgent = class {
|
|
|
1249
1375
|
stdout: ""
|
|
1250
1376
|
};
|
|
1251
1377
|
const maxOutput = 64 * 1024;
|
|
1378
|
+
const maxMirroredLineLength = 2048;
|
|
1379
|
+
const maxMirroredLinesPerRun = 500;
|
|
1380
|
+
let mirroredLineCount = 0;
|
|
1381
|
+
let mirroredSuppressionLogged = false;
|
|
1382
|
+
const stderrLineBuffer = { tail: "" };
|
|
1383
|
+
const mirrorStderrChunk = (chunk) => {
|
|
1384
|
+
stderrLineBuffer.tail += chunk;
|
|
1385
|
+
let newlineIndex = stderrLineBuffer.tail.indexOf("\n");
|
|
1386
|
+
while (newlineIndex !== -1) {
|
|
1387
|
+
const rawLine = stderrLineBuffer.tail.slice(0, newlineIndex);
|
|
1388
|
+
stderrLineBuffer.tail = stderrLineBuffer.tail.slice(newlineIndex + 1);
|
|
1389
|
+
const line = rawLine.replace(/\r$/, "");
|
|
1390
|
+
if (line.length > 0) if (mirroredLineCount >= maxMirroredLinesPerRun) {
|
|
1391
|
+
if (!mirroredSuppressionLogged) {
|
|
1392
|
+
appendDebugLog("opencode:server:stderr:suppressed", {
|
|
1393
|
+
port: server.port,
|
|
1394
|
+
cap: maxMirroredLinesPerRun
|
|
1395
|
+
});
|
|
1396
|
+
mirroredSuppressionLogged = true;
|
|
1397
|
+
}
|
|
1398
|
+
} else {
|
|
1399
|
+
mirroredLineCount += 1;
|
|
1400
|
+
appendDebugLog("opencode:server:stderr", {
|
|
1401
|
+
port: server.port,
|
|
1402
|
+
line: line.length > maxMirroredLineLength ? `${line.slice(0, maxMirroredLineLength)}…` : line,
|
|
1403
|
+
truncated: line.length > maxMirroredLineLength
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
newlineIndex = stderrLineBuffer.tail.indexOf("\n");
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1252
1409
|
child.stdout.on("data", (data) => {
|
|
1253
1410
|
server.stdout += data.toString();
|
|
1254
1411
|
if (server.stdout.length > maxOutput) server.stdout = server.stdout.slice(-maxOutput);
|
|
1255
1412
|
});
|
|
1256
1413
|
child.stderr.on("data", (data) => {
|
|
1257
|
-
|
|
1414
|
+
const text = data.toString();
|
|
1415
|
+
server.stderr += text;
|
|
1258
1416
|
if (server.stderr.length > maxOutput) server.stderr = server.stderr.slice(-maxOutput);
|
|
1417
|
+
mirrorStderrChunk(text);
|
|
1259
1418
|
});
|
|
1260
|
-
child.on("close", () => {
|
|
1419
|
+
child.on("close", (code, closeSignal) => {
|
|
1261
1420
|
server.closed = true;
|
|
1421
|
+
if (stderrLineBuffer.tail.length > 0) mirrorStderrChunk("\n");
|
|
1422
|
+
appendDebugLog("opencode:server:close", {
|
|
1423
|
+
cwd: server.cwd,
|
|
1424
|
+
port: server.port,
|
|
1425
|
+
code,
|
|
1426
|
+
signal: closeSignal,
|
|
1427
|
+
stderr: server.stderr.slice(-2048),
|
|
1428
|
+
stdout: server.stdout.slice(-2048)
|
|
1429
|
+
});
|
|
1262
1430
|
if (this.server === server) this.server = null;
|
|
1263
1431
|
});
|
|
1264
1432
|
this.server = server;
|
|
1433
|
+
const spawnedAt = Date.now();
|
|
1265
1434
|
appendDebugLog("opencode:spawn", {
|
|
1266
1435
|
cwd,
|
|
1267
1436
|
port,
|
|
1268
|
-
detached
|
|
1437
|
+
detached,
|
|
1438
|
+
pid: child.pid,
|
|
1439
|
+
bin: this.bin
|
|
1269
1440
|
});
|
|
1270
|
-
server.readyPromise = this.waitForHealthy(server, signal).
|
|
1441
|
+
server.readyPromise = this.waitForHealthy(server, signal).then(() => {
|
|
1442
|
+
appendDebugLog("opencode:server:ready", {
|
|
1443
|
+
port,
|
|
1444
|
+
elapsedMs: Date.now() - spawnedAt
|
|
1445
|
+
});
|
|
1446
|
+
}).catch(async (error) => {
|
|
1447
|
+
appendDebugLog("opencode:server:ready-failed", {
|
|
1448
|
+
port,
|
|
1449
|
+
elapsedMs: Date.now() - spawnedAt,
|
|
1450
|
+
error: serializeError(error),
|
|
1451
|
+
stderr: server.stderr.slice(-2048),
|
|
1452
|
+
stdout: server.stdout.slice(-2048)
|
|
1453
|
+
});
|
|
1271
1454
|
await this.shutdownServer();
|
|
1272
1455
|
throw error;
|
|
1273
1456
|
});
|
|
@@ -1300,44 +1483,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
|
-
|
|
1486
|
+
const response = await this.requestJSON(server, "/session", {
|
|
1304
1487
|
method: "POST",
|
|
1305
1488
|
body: {
|
|
1306
1489
|
directory: cwd,
|
|
1307
1490
|
permission: BLANKET_PERMISSION_RULESET
|
|
1308
1491
|
},
|
|
1309
1492
|
signal
|
|
1310
|
-
})
|
|
1493
|
+
});
|
|
1494
|
+
appendDebugLog("opencode:session:create", { sessionId: response.id });
|
|
1495
|
+
return response.id;
|
|
1311
1496
|
}
|
|
1312
1497
|
async streamMessage(server, sessionId, prompt, signal, logStream, onUsage, onMessage) {
|
|
1313
1498
|
const streamAbortController = new AbortController();
|
|
1314
1499
|
const streamSignal = AbortSignal.any([signal, streamAbortController.signal]);
|
|
1500
|
+
const streamStartedAt = Date.now();
|
|
1501
|
+
appendDebugLog("opencode:stream:start", { sessionId });
|
|
1315
1502
|
const eventResponse = await this.request(server, "/global/event", {
|
|
1316
1503
|
method: "GET",
|
|
1317
1504
|
headers: { accept: "text/event-stream" },
|
|
1318
1505
|
signal: streamSignal
|
|
1319
1506
|
});
|
|
1320
|
-
if (!eventResponse.body)
|
|
1507
|
+
if (!eventResponse.body) {
|
|
1508
|
+
appendDebugLog("opencode:stream:no-body", { sessionId });
|
|
1509
|
+
throw new Error("opencode returned no event stream body");
|
|
1510
|
+
}
|
|
1511
|
+
const messagePostStartedAt = Date.now();
|
|
1512
|
+
appendDebugLog("opencode:message-post:start", {
|
|
1513
|
+
sessionId,
|
|
1514
|
+
promptLength: prompt.length
|
|
1515
|
+
});
|
|
1321
1516
|
let messageRequestError = null;
|
|
1322
1517
|
const messageRequest = (async () => {
|
|
1323
1518
|
try {
|
|
1519
|
+
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:
|
|
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
|
-
|
|
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
|
-
|
|
1499
|
-
|
|
1500
|
-
response = JSON.parse(body);
|
|
1800
|
+
if (body) try {
|
|
1801
|
+
JSON.parse(body);
|
|
1501
1802
|
} catch (error) {
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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
|
-
|
|
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))
|
|
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).
|
|
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
|
-
|
|
2238
|
+
const response = await this.requestJSON(server, "/v3/sessions/create", {
|
|
1826
2239
|
method: "POST",
|
|
1827
2240
|
body: { custom_title: "gnhf" },
|
|
1828
2241
|
signal
|
|
1829
|
-
})
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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
|
|
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
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|