@testchimp/cli 0.1.57 → 0.1.59
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/dist/chimphands/run.js +215 -28
- package/package.json +1 -1
package/dist/chimphands/run.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { execSync, spawn } from "node:child_process";
|
|
7
7
|
import { mkdirSync, openSync, writeFileSync } from "node:fs";
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
8
10
|
import http from "node:http";
|
|
9
11
|
import https from "node:https";
|
|
10
12
|
import { URL } from "node:url";
|
|
@@ -280,6 +282,30 @@ function extractOpencodeFatalError(raw) {
|
|
|
280
282
|
}
|
|
281
283
|
return null;
|
|
282
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* OpenCode `message.part.delta` always sends `field: "text"` for both reasoning and
|
|
287
|
+
* answer parts. Resolve kind from partID → type learned via `message.part.updated`.
|
|
288
|
+
*/
|
|
289
|
+
function resolveDeltaPartKind(partId, field, partType, partTypeById) {
|
|
290
|
+
if (partType) {
|
|
291
|
+
partTypeById.set(partId, partType);
|
|
292
|
+
}
|
|
293
|
+
const known = partTypeById.get(partId);
|
|
294
|
+
if (known === "reasoning" || partType === "reasoning") {
|
|
295
|
+
partTypeById.set(partId, "reasoning");
|
|
296
|
+
return "reasoning";
|
|
297
|
+
}
|
|
298
|
+
if (known === "text" || partType === "text") {
|
|
299
|
+
partTypeById.set(partId, "text");
|
|
300
|
+
return "text";
|
|
301
|
+
}
|
|
302
|
+
// Unknown part: field is unreliable (reasoning deltas also use field "text").
|
|
303
|
+
if (field === "reasoning") {
|
|
304
|
+
partTypeById.set(partId, "reasoning");
|
|
305
|
+
return "reasoning";
|
|
306
|
+
}
|
|
307
|
+
return "text";
|
|
308
|
+
}
|
|
283
309
|
function parseOpencodeEvent(line) {
|
|
284
310
|
try {
|
|
285
311
|
return JSON.parse(line);
|
|
@@ -292,8 +318,11 @@ function parseOpencodeEvent(line) {
|
|
|
292
318
|
* Newer OpenCode `--format json` lines often use the SSE bus shape
|
|
293
319
|
* (`message.part.updated` + `properties.part`) instead of legacy `type: "text"`.
|
|
294
320
|
* Normalize both into the same OpencodeEvent used by the stdout switch.
|
|
321
|
+
*
|
|
322
|
+
* `partTypeById` must be shared across lines: deltas use `field: "text"` for
|
|
323
|
+
* reasoning parts too — type comes from earlier `message.part.updated`.
|
|
295
324
|
*/
|
|
296
|
-
function normalizeStdoutOpencodeEvent(raw) {
|
|
325
|
+
function normalizeStdoutOpencodeEvent(raw, partTypeById) {
|
|
297
326
|
if (!raw || typeof raw !== "object")
|
|
298
327
|
return null;
|
|
299
328
|
const o = raw;
|
|
@@ -312,18 +341,25 @@ function normalizeStdoutOpencodeEvent(raw) {
|
|
|
312
341
|
const deltaProps = props;
|
|
313
342
|
const part = deltaProps.part;
|
|
314
343
|
if (part) {
|
|
315
|
-
const
|
|
344
|
+
const partId = part.id || part.messageID || deltaProps.partID || "";
|
|
345
|
+
const kind = partId
|
|
346
|
+
? resolveDeltaPartKind(partId, deltaProps.field, part.type, partTypeById)
|
|
347
|
+
: part.type === "reasoning"
|
|
348
|
+
? "reasoning"
|
|
349
|
+
: "text";
|
|
316
350
|
let mapped;
|
|
317
|
-
if (
|
|
351
|
+
if (kind === "text" || part.type === "text")
|
|
318
352
|
mapped = "text";
|
|
319
|
-
else if (
|
|
353
|
+
else if (kind === "reasoning" || part.type === "reasoning")
|
|
320
354
|
mapped = "reasoning";
|
|
321
|
-
else if (
|
|
355
|
+
else if (part.type === "tool")
|
|
322
356
|
mapped = "tool_use";
|
|
323
357
|
else
|
|
324
358
|
return null;
|
|
325
359
|
if (deltaProps.delta && !part.text)
|
|
326
360
|
part.text = deltaProps.delta;
|
|
361
|
+
if (mapped === "reasoning")
|
|
362
|
+
part.type = "reasoning";
|
|
327
363
|
return {
|
|
328
364
|
type: mapped,
|
|
329
365
|
sessionID: part.sessionID || deltaProps.sessionID,
|
|
@@ -331,22 +367,29 @@ function normalizeStdoutOpencodeEvent(raw) {
|
|
|
331
367
|
};
|
|
332
368
|
}
|
|
333
369
|
const partID = deltaProps.partID;
|
|
334
|
-
const field = deltaProps.field || "text";
|
|
335
370
|
const delta = deltaProps.delta;
|
|
336
371
|
if (!partID || delta == null || delta === "")
|
|
337
372
|
return null;
|
|
338
|
-
if (field !== "text" && field !== "reasoning")
|
|
373
|
+
if (deltaProps.field && deltaProps.field !== "text" && deltaProps.field !== "reasoning") {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
const kind = resolveDeltaPartKind(partID, deltaProps.field, undefined, partTypeById);
|
|
377
|
+
if (kind !== "text" && kind !== "reasoning")
|
|
339
378
|
return null;
|
|
340
379
|
return {
|
|
341
|
-
type:
|
|
380
|
+
type: kind,
|
|
342
381
|
sessionID: deltaProps.sessionID,
|
|
343
|
-
part: { id: partID, type:
|
|
382
|
+
part: { id: partID, type: kind, text: delta },
|
|
344
383
|
};
|
|
345
384
|
}
|
|
346
385
|
if (type === "message.part.updated") {
|
|
347
386
|
const part = props.part;
|
|
348
387
|
if (!part)
|
|
349
388
|
return null;
|
|
389
|
+
const partId = part.id || part.messageID;
|
|
390
|
+
if (partId && part.type) {
|
|
391
|
+
partTypeById.set(partId, part.type);
|
|
392
|
+
}
|
|
350
393
|
const partType = part.type || "";
|
|
351
394
|
let mapped;
|
|
352
395
|
if (partType === "text")
|
|
@@ -487,6 +530,8 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
487
530
|
const ac = new AbortController();
|
|
488
531
|
let stopped = false;
|
|
489
532
|
const textByPartId = new Map();
|
|
533
|
+
/** partID → "text" | "reasoning" | … from message.part.updated (deltas lie about field). */
|
|
534
|
+
const partTypeById = new Map();
|
|
490
535
|
const directory = process.cwd();
|
|
491
536
|
const sessionMatches = (sessionId) => {
|
|
492
537
|
const active = callbacks.getActiveSessionId();
|
|
@@ -519,18 +564,27 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
519
564
|
const ev = unwrapped;
|
|
520
565
|
const type = ev.type || "";
|
|
521
566
|
const props = ev.properties || {};
|
|
522
|
-
// Token stream: { partID, field, delta } —
|
|
567
|
+
// Token stream: { partID, field, delta } — field is usually "text" even for reasoning.
|
|
523
568
|
if (type === "message.part.delta") {
|
|
524
569
|
const part = props.part;
|
|
525
570
|
const partId = props.partID || part?.id || part?.messageID;
|
|
526
|
-
const field = props.field || part?.type || "text";
|
|
527
571
|
const sessionId = part?.sessionID || props.sessionID;
|
|
528
572
|
if (!sessionMatches(sessionId))
|
|
529
573
|
return;
|
|
530
574
|
callbacks.noteSessionId(sessionId);
|
|
531
575
|
if (!partId || props.delta == null || props.delta === "")
|
|
532
576
|
return;
|
|
533
|
-
|
|
577
|
+
const kind = resolveDeltaPartKind(partId, props.field, part?.type, partTypeById);
|
|
578
|
+
if (kind === "reasoning") {
|
|
579
|
+
const key = `reasoning:${partId}`;
|
|
580
|
+
const next = (textByPartId.get(key) || "") + props.delta;
|
|
581
|
+
textByPartId.set(key, next);
|
|
582
|
+
callbacks.postEvent(ROLE_REASONING, next, liveOpts({
|
|
583
|
+
messageId: `oc_reasoning_${partId}`,
|
|
584
|
+
}));
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (kind === "text") {
|
|
534
588
|
const next = (textByPartId.get(partId) || "") + props.delta;
|
|
535
589
|
textByPartId.set(partId, next);
|
|
536
590
|
if (isTextDuplicateOfReasoning(next, reasoningBodiesFromPartMap(textByPartId))) {
|
|
@@ -539,15 +593,6 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
539
593
|
callbacks.postEvent(ROLE_ASSISTANT, next, liveOpts({
|
|
540
594
|
messageId: `oc_text_${partId}`,
|
|
541
595
|
}));
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
if (field === "reasoning") {
|
|
545
|
-
const key = `reasoning:${partId}`;
|
|
546
|
-
const next = (textByPartId.get(key) || "") + props.delta;
|
|
547
|
-
textByPartId.set(key, next);
|
|
548
|
-
callbacks.postEvent(ROLE_REASONING, next, liveOpts({
|
|
549
|
-
messageId: `oc_reasoning_${partId}`,
|
|
550
|
-
}));
|
|
551
596
|
}
|
|
552
597
|
return;
|
|
553
598
|
}
|
|
@@ -559,8 +604,11 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
559
604
|
if (!sessionMatches(sessionId))
|
|
560
605
|
return;
|
|
561
606
|
callbacks.noteSessionId(sessionId);
|
|
607
|
+
const partId = part.id || part.messageID;
|
|
608
|
+
if (partId && part.type) {
|
|
609
|
+
partTypeById.set(partId, part.type);
|
|
610
|
+
}
|
|
562
611
|
if (part.type === "text") {
|
|
563
|
-
const partId = part.id || part.messageID;
|
|
564
612
|
if (!partId)
|
|
565
613
|
return;
|
|
566
614
|
let next = part.text || "";
|
|
@@ -584,12 +632,19 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
584
632
|
return;
|
|
585
633
|
}
|
|
586
634
|
if (part.type === "reasoning") {
|
|
587
|
-
const partId = part.id || part.messageID;
|
|
588
635
|
if (!partId)
|
|
589
636
|
return;
|
|
637
|
+
// Deltas before type was known may have been buffered under the text key.
|
|
638
|
+
const orphanText = textByPartId.get(partId);
|
|
639
|
+
if (orphanText) {
|
|
640
|
+
textByPartId.delete(partId);
|
|
641
|
+
}
|
|
590
642
|
let next = part.text || "";
|
|
591
643
|
if (props.delta && !part.text) {
|
|
592
|
-
next = (textByPartId.get(`reasoning:${partId}`) || "") + props.delta;
|
|
644
|
+
next = (textByPartId.get(`reasoning:${partId}`) || orphanText || "") + props.delta;
|
|
645
|
+
}
|
|
646
|
+
else if (!next && orphanText) {
|
|
647
|
+
next = orphanText;
|
|
593
648
|
}
|
|
594
649
|
if (part.text)
|
|
595
650
|
next = part.text;
|
|
@@ -776,9 +831,12 @@ function isAssistantEchoOfSentPrompt(assistant, ...sentPrompts) {
|
|
|
776
831
|
continue;
|
|
777
832
|
if (a === p)
|
|
778
833
|
return true;
|
|
779
|
-
// Streaming echo
|
|
834
|
+
// Streaming echo from the start of the wrapped prompt.
|
|
780
835
|
if (p.length >= 64 && a.length >= 24 && p.startsWith(a))
|
|
781
836
|
return true;
|
|
837
|
+
// Mid-wrap regurgitation (e.g. only the "Conversation so far:" section).
|
|
838
|
+
if (p.length >= 64 && a.length >= 40 && p.includes(a))
|
|
839
|
+
return true;
|
|
782
840
|
}
|
|
783
841
|
return false;
|
|
784
842
|
}
|
|
@@ -1003,6 +1061,7 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1003
1061
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1004
1062
|
env: childEnv,
|
|
1005
1063
|
});
|
|
1064
|
+
callbacks.onActiveChild?.(child);
|
|
1006
1065
|
try {
|
|
1007
1066
|
child.stdin?.end();
|
|
1008
1067
|
}
|
|
@@ -1020,6 +1079,7 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1020
1079
|
let buf = "";
|
|
1021
1080
|
let fatalError = null;
|
|
1022
1081
|
const textByPartId = new Map();
|
|
1082
|
+
const partTypeById = new Map();
|
|
1023
1083
|
let sawStdout = false;
|
|
1024
1084
|
let progressTicker = setInterval(() => {
|
|
1025
1085
|
if (sawStdout) {
|
|
@@ -1064,7 +1124,7 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1064
1124
|
return;
|
|
1065
1125
|
}
|
|
1066
1126
|
// Newer OpenCode --format json uses bus shape (message.part.updated); normalize first.
|
|
1067
|
-
const ev = normalizeStdoutOpencodeEvent(parsed) || parseOpencodeEvent(line);
|
|
1127
|
+
const ev = normalizeStdoutOpencodeEvent(parsed, partTypeById) || parseOpencodeEvent(line);
|
|
1068
1128
|
if (!ev?.type)
|
|
1069
1129
|
return;
|
|
1070
1130
|
noteSessionId(ev.sessionID);
|
|
@@ -1175,6 +1235,7 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1175
1235
|
}
|
|
1176
1236
|
});
|
|
1177
1237
|
child.on("close", (code) => {
|
|
1238
|
+
callbacks.onActiveChild?.(null);
|
|
1178
1239
|
if (progressTicker) {
|
|
1179
1240
|
clearInterval(progressTicker);
|
|
1180
1241
|
progressTicker = null;
|
|
@@ -1185,6 +1246,10 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1185
1246
|
const stderrFatal = extractOpencodeFatalError(err);
|
|
1186
1247
|
if (stderrFatal)
|
|
1187
1248
|
fatalError = stderrFatal;
|
|
1249
|
+
if (callbacks.getCancelRequested?.()) {
|
|
1250
|
+
resolve({ code: 0, err: "", opencodeSessionId: activeSessionId, cancelled: true });
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1188
1253
|
if (fatalError) {
|
|
1189
1254
|
resolve({ code: 1, err: fatalError, opencodeSessionId: activeSessionId });
|
|
1190
1255
|
return;
|
|
@@ -1201,6 +1266,63 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1201
1266
|
});
|
|
1202
1267
|
});
|
|
1203
1268
|
}
|
|
1269
|
+
function normalizeWorktreeRelativePath(filePath) {
|
|
1270
|
+
let p = String(filePath || "").trim().replace(/\\/g, "/");
|
|
1271
|
+
while (p.startsWith("/"))
|
|
1272
|
+
p = p.slice(1);
|
|
1273
|
+
if (!p || p.includes("\0")) {
|
|
1274
|
+
throw new Error("invalid path");
|
|
1275
|
+
}
|
|
1276
|
+
const segments = [];
|
|
1277
|
+
for (const part of p.split("/")) {
|
|
1278
|
+
if (!part || part === ".")
|
|
1279
|
+
continue;
|
|
1280
|
+
if (part === "..") {
|
|
1281
|
+
if (!segments.length)
|
|
1282
|
+
throw new Error("invalid path");
|
|
1283
|
+
segments.pop();
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
segments.push(part);
|
|
1287
|
+
}
|
|
1288
|
+
if (!segments.length)
|
|
1289
|
+
throw new Error("invalid path");
|
|
1290
|
+
return segments.join("/");
|
|
1291
|
+
}
|
|
1292
|
+
async function ackWorktreeFileWrite(backend, apiKey, sessionId, requestId, ok, errorMessage) {
|
|
1293
|
+
const body = {
|
|
1294
|
+
sessionId,
|
|
1295
|
+
requestId,
|
|
1296
|
+
ok,
|
|
1297
|
+
};
|
|
1298
|
+
if (errorMessage)
|
|
1299
|
+
body.errorMessage = errorMessage.slice(0, 2000);
|
|
1300
|
+
await postJson(backend, apiKey, "/api/chimphands/ack_worktree_file_write", body).catch((err) => {
|
|
1301
|
+
console.error(`ChimpHands ack_worktree_file_write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
async function applyUserFileEdit(backend, apiKey, sessionId, edit, turnActive) {
|
|
1305
|
+
if (turnActive()) {
|
|
1306
|
+
await ackWorktreeFileWrite(backend, apiKey, sessionId, edit.requestId, false, "agent turn in progress");
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
try {
|
|
1310
|
+
const relative = normalizeWorktreeRelativePath(edit.path);
|
|
1311
|
+
const root = process.cwd();
|
|
1312
|
+
const full = path.resolve(root, relative);
|
|
1313
|
+
const rootResolved = path.resolve(root);
|
|
1314
|
+
if (full !== rootResolved && !full.startsWith(rootResolved + path.sep)) {
|
|
1315
|
+
throw new Error("path outside worktree");
|
|
1316
|
+
}
|
|
1317
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
1318
|
+
await fs.writeFile(full, edit.content, "utf8");
|
|
1319
|
+
await ackWorktreeFileWrite(backend, apiKey, sessionId, edit.requestId, true);
|
|
1320
|
+
}
|
|
1321
|
+
catch (err) {
|
|
1322
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1323
|
+
await ackWorktreeFileWrite(backend, apiKey, sessionId, edit.requestId, false, msg);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1204
1326
|
function connectInboundStream(backend, apiKey, sessionId, handlers) {
|
|
1205
1327
|
const url = new URL(`${backend}/api/chimphands/sessions/${encodeURIComponent(sessionId)}/inbound`);
|
|
1206
1328
|
const lib = url.protocol === "https:" ? https : http;
|
|
@@ -1248,6 +1370,37 @@ function connectInboundStream(backend, apiKey, sessionId, handlers) {
|
|
|
1248
1370
|
if (eventName === "idle") {
|
|
1249
1371
|
handlers.onIdle();
|
|
1250
1372
|
}
|
|
1373
|
+
else if (eventName === "cancel_turn") {
|
|
1374
|
+
try {
|
|
1375
|
+
const payload = JSON.parse(data);
|
|
1376
|
+
if (payload.sessionId && payload.sessionId !== sessionId) {
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
catch {
|
|
1381
|
+
/* ignore malformed payload */
|
|
1382
|
+
}
|
|
1383
|
+
handlers.onCancelTurn?.();
|
|
1384
|
+
}
|
|
1385
|
+
else if (eventName === "user_file_edit") {
|
|
1386
|
+
try {
|
|
1387
|
+
const edit = JSON.parse(data);
|
|
1388
|
+
if (edit.sessionId && edit.sessionId !== sessionId) {
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
const requestId = edit.requestId || edit.request_id;
|
|
1392
|
+
if (requestId && edit.path) {
|
|
1393
|
+
handlers.onUserFileEdit?.({
|
|
1394
|
+
requestId,
|
|
1395
|
+
path: edit.path,
|
|
1396
|
+
content: edit.content ?? "",
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
catch {
|
|
1401
|
+
/* ignore */
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1251
1404
|
else if (eventName === "user_message" || eventName === "message") {
|
|
1252
1405
|
try {
|
|
1253
1406
|
const msg = JSON.parse(data);
|
|
@@ -1441,6 +1594,15 @@ export async function runChimphands(opts) {
|
|
|
1441
1594
|
let sessionActive = true;
|
|
1442
1595
|
let lastUserActivity = Date.now();
|
|
1443
1596
|
let exitCode;
|
|
1597
|
+
let cancelTurnRequested = false;
|
|
1598
|
+
let activeOpencodeChild = null;
|
|
1599
|
+
let agentTurnInProgress = false;
|
|
1600
|
+
const turnControl = {
|
|
1601
|
+
getCancelRequested: () => cancelTurnRequested,
|
|
1602
|
+
onActiveChild: (child) => {
|
|
1603
|
+
activeOpencodeChild = child;
|
|
1604
|
+
},
|
|
1605
|
+
};
|
|
1444
1606
|
const enqueueUserMessage = (msg) => {
|
|
1445
1607
|
const id = msg.id?.trim();
|
|
1446
1608
|
if (id) {
|
|
@@ -1512,6 +1674,21 @@ export async function runChimphands(opts) {
|
|
|
1512
1674
|
onIdle: () => {
|
|
1513
1675
|
idle = true;
|
|
1514
1676
|
},
|
|
1677
|
+
onCancelTurn: () => {
|
|
1678
|
+
cancelTurnRequested = true;
|
|
1679
|
+
const ch = activeOpencodeChild;
|
|
1680
|
+
if (ch && !ch.killed) {
|
|
1681
|
+
try {
|
|
1682
|
+
ch.kill("SIGTERM");
|
|
1683
|
+
}
|
|
1684
|
+
catch {
|
|
1685
|
+
/* ignore */
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
},
|
|
1689
|
+
onUserFileEdit: (edit) => {
|
|
1690
|
+
void applyUserFileEdit(backend, apiKey, sessionId, edit, () => agentTurnInProgress);
|
|
1691
|
+
},
|
|
1515
1692
|
shouldRun: () => sessionActive,
|
|
1516
1693
|
});
|
|
1517
1694
|
const shutdownRuntime = async () => {
|
|
@@ -1584,6 +1761,8 @@ export async function runChimphands(opts) {
|
|
|
1584
1761
|
tick();
|
|
1585
1762
|
});
|
|
1586
1763
|
while (prompt) {
|
|
1764
|
+
cancelTurnRequested = false;
|
|
1765
|
+
agentTurnInProgress = true;
|
|
1587
1766
|
let useOpencodeSessionId = opencodeSessionId;
|
|
1588
1767
|
let isNewOpencodeSession = !useOpencodeSessionId;
|
|
1589
1768
|
let effectivePrompt = wrapPromptWithContext(conversationSummary, prompt, isNewOpencodeSession, workingBranch, pullRequestUrl);
|
|
@@ -1601,6 +1780,7 @@ export async function runChimphands(opts) {
|
|
|
1601
1780
|
},
|
|
1602
1781
|
onWorkingBranch: noteWorkingBranch,
|
|
1603
1782
|
postEvent: turnPostEvent,
|
|
1783
|
+
...turnControl,
|
|
1604
1784
|
}, attachUrl);
|
|
1605
1785
|
if (result.code !== 0 &&
|
|
1606
1786
|
useOpencodeSessionId &&
|
|
@@ -1619,8 +1799,10 @@ export async function runChimphands(opts) {
|
|
|
1619
1799
|
},
|
|
1620
1800
|
onWorkingBranch: noteWorkingBranch,
|
|
1621
1801
|
postEvent: turnPostEvent,
|
|
1802
|
+
...turnControl,
|
|
1622
1803
|
}, attachUrl);
|
|
1623
1804
|
}
|
|
1805
|
+
agentTurnInProgress = false;
|
|
1624
1806
|
await poster.flush();
|
|
1625
1807
|
if (result.opencodeSessionId) {
|
|
1626
1808
|
opencodeSessionId = result.opencodeSessionId;
|
|
@@ -1643,7 +1825,7 @@ export async function runChimphands(opts) {
|
|
|
1643
1825
|
console.error(`ChimpHands turn-end reconcile failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1644
1826
|
}
|
|
1645
1827
|
}
|
|
1646
|
-
if (result.code !== 0) {
|
|
1828
|
+
if (result.code !== 0 && !result.cancelled) {
|
|
1647
1829
|
const errMsg = (result.err || "opencode failed").trim() || "opencode failed";
|
|
1648
1830
|
console.error(`ChimpHands OpenCode failed: ${errMsg}`);
|
|
1649
1831
|
try {
|
|
@@ -1667,7 +1849,12 @@ export async function runChimphands(opts) {
|
|
|
1667
1849
|
exitCode = result.code || 1;
|
|
1668
1850
|
break;
|
|
1669
1851
|
}
|
|
1670
|
-
|
|
1852
|
+
if (result.cancelled) {
|
|
1853
|
+
postEvent(ROLE_STATUS, "Turn stopped", { status: STATUS_WAITING_USER });
|
|
1854
|
+
}
|
|
1855
|
+
else {
|
|
1856
|
+
postEvent(ROLE_STATUS, "Waiting for user input", { status: STATUS_WAITING_USER });
|
|
1857
|
+
}
|
|
1671
1858
|
lastUserActivity = Date.now();
|
|
1672
1859
|
idle = false;
|
|
1673
1860
|
prompt = (await waitForNextPrompt()) || "";
|
package/package.json
CHANGED