@testchimp/cli 0.1.56 → 0.1.58
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 +119 -39
- package/package.json +1 -1
package/dist/chimphands/run.js
CHANGED
|
@@ -280,6 +280,30 @@ function extractOpencodeFatalError(raw) {
|
|
|
280
280
|
}
|
|
281
281
|
return null;
|
|
282
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* OpenCode `message.part.delta` always sends `field: "text"` for both reasoning and
|
|
285
|
+
* answer parts. Resolve kind from partID → type learned via `message.part.updated`.
|
|
286
|
+
*/
|
|
287
|
+
function resolveDeltaPartKind(partId, field, partType, partTypeById) {
|
|
288
|
+
if (partType) {
|
|
289
|
+
partTypeById.set(partId, partType);
|
|
290
|
+
}
|
|
291
|
+
const known = partTypeById.get(partId);
|
|
292
|
+
if (known === "reasoning" || partType === "reasoning") {
|
|
293
|
+
partTypeById.set(partId, "reasoning");
|
|
294
|
+
return "reasoning";
|
|
295
|
+
}
|
|
296
|
+
if (known === "text" || partType === "text") {
|
|
297
|
+
partTypeById.set(partId, "text");
|
|
298
|
+
return "text";
|
|
299
|
+
}
|
|
300
|
+
// Unknown part: field is unreliable (reasoning deltas also use field "text").
|
|
301
|
+
if (field === "reasoning") {
|
|
302
|
+
partTypeById.set(partId, "reasoning");
|
|
303
|
+
return "reasoning";
|
|
304
|
+
}
|
|
305
|
+
return "text";
|
|
306
|
+
}
|
|
283
307
|
function parseOpencodeEvent(line) {
|
|
284
308
|
try {
|
|
285
309
|
return JSON.parse(line);
|
|
@@ -292,8 +316,11 @@ function parseOpencodeEvent(line) {
|
|
|
292
316
|
* Newer OpenCode `--format json` lines often use the SSE bus shape
|
|
293
317
|
* (`message.part.updated` + `properties.part`) instead of legacy `type: "text"`.
|
|
294
318
|
* Normalize both into the same OpencodeEvent used by the stdout switch.
|
|
319
|
+
*
|
|
320
|
+
* `partTypeById` must be shared across lines: deltas use `field: "text"` for
|
|
321
|
+
* reasoning parts too — type comes from earlier `message.part.updated`.
|
|
295
322
|
*/
|
|
296
|
-
function normalizeStdoutOpencodeEvent(raw) {
|
|
323
|
+
function normalizeStdoutOpencodeEvent(raw, partTypeById) {
|
|
297
324
|
if (!raw || typeof raw !== "object")
|
|
298
325
|
return null;
|
|
299
326
|
const o = raw;
|
|
@@ -312,18 +339,25 @@ function normalizeStdoutOpencodeEvent(raw) {
|
|
|
312
339
|
const deltaProps = props;
|
|
313
340
|
const part = deltaProps.part;
|
|
314
341
|
if (part) {
|
|
315
|
-
const
|
|
342
|
+
const partId = part.id || part.messageID || deltaProps.partID || "";
|
|
343
|
+
const kind = partId
|
|
344
|
+
? resolveDeltaPartKind(partId, deltaProps.field, part.type, partTypeById)
|
|
345
|
+
: part.type === "reasoning"
|
|
346
|
+
? "reasoning"
|
|
347
|
+
: "text";
|
|
316
348
|
let mapped;
|
|
317
|
-
if (
|
|
349
|
+
if (kind === "text" || part.type === "text")
|
|
318
350
|
mapped = "text";
|
|
319
|
-
else if (
|
|
351
|
+
else if (kind === "reasoning" || part.type === "reasoning")
|
|
320
352
|
mapped = "reasoning";
|
|
321
|
-
else if (
|
|
353
|
+
else if (part.type === "tool")
|
|
322
354
|
mapped = "tool_use";
|
|
323
355
|
else
|
|
324
356
|
return null;
|
|
325
357
|
if (deltaProps.delta && !part.text)
|
|
326
358
|
part.text = deltaProps.delta;
|
|
359
|
+
if (mapped === "reasoning")
|
|
360
|
+
part.type = "reasoning";
|
|
327
361
|
return {
|
|
328
362
|
type: mapped,
|
|
329
363
|
sessionID: part.sessionID || deltaProps.sessionID,
|
|
@@ -331,22 +365,29 @@ function normalizeStdoutOpencodeEvent(raw) {
|
|
|
331
365
|
};
|
|
332
366
|
}
|
|
333
367
|
const partID = deltaProps.partID;
|
|
334
|
-
const field = deltaProps.field || "text";
|
|
335
368
|
const delta = deltaProps.delta;
|
|
336
369
|
if (!partID || delta == null || delta === "")
|
|
337
370
|
return null;
|
|
338
|
-
if (field !== "text" && field !== "reasoning")
|
|
371
|
+
if (deltaProps.field && deltaProps.field !== "text" && deltaProps.field !== "reasoning") {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
const kind = resolveDeltaPartKind(partID, deltaProps.field, undefined, partTypeById);
|
|
375
|
+
if (kind !== "text" && kind !== "reasoning")
|
|
339
376
|
return null;
|
|
340
377
|
return {
|
|
341
|
-
type:
|
|
378
|
+
type: kind,
|
|
342
379
|
sessionID: deltaProps.sessionID,
|
|
343
|
-
part: { id: partID, type:
|
|
380
|
+
part: { id: partID, type: kind, text: delta },
|
|
344
381
|
};
|
|
345
382
|
}
|
|
346
383
|
if (type === "message.part.updated") {
|
|
347
384
|
const part = props.part;
|
|
348
385
|
if (!part)
|
|
349
386
|
return null;
|
|
387
|
+
const partId = part.id || part.messageID;
|
|
388
|
+
if (partId && part.type) {
|
|
389
|
+
partTypeById.set(partId, part.type);
|
|
390
|
+
}
|
|
350
391
|
const partType = part.type || "";
|
|
351
392
|
let mapped;
|
|
352
393
|
if (partType === "text")
|
|
@@ -487,6 +528,8 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
487
528
|
const ac = new AbortController();
|
|
488
529
|
let stopped = false;
|
|
489
530
|
const textByPartId = new Map();
|
|
531
|
+
/** partID → "text" | "reasoning" | … from message.part.updated (deltas lie about field). */
|
|
532
|
+
const partTypeById = new Map();
|
|
490
533
|
const directory = process.cwd();
|
|
491
534
|
const sessionMatches = (sessionId) => {
|
|
492
535
|
const active = callbacks.getActiveSessionId();
|
|
@@ -519,18 +562,27 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
519
562
|
const ev = unwrapped;
|
|
520
563
|
const type = ev.type || "";
|
|
521
564
|
const props = ev.properties || {};
|
|
522
|
-
// Token stream: { partID, field, delta } —
|
|
565
|
+
// Token stream: { partID, field, delta } — field is usually "text" even for reasoning.
|
|
523
566
|
if (type === "message.part.delta") {
|
|
524
567
|
const part = props.part;
|
|
525
568
|
const partId = props.partID || part?.id || part?.messageID;
|
|
526
|
-
const field = props.field || part?.type || "text";
|
|
527
569
|
const sessionId = part?.sessionID || props.sessionID;
|
|
528
570
|
if (!sessionMatches(sessionId))
|
|
529
571
|
return;
|
|
530
572
|
callbacks.noteSessionId(sessionId);
|
|
531
573
|
if (!partId || props.delta == null || props.delta === "")
|
|
532
574
|
return;
|
|
533
|
-
|
|
575
|
+
const kind = resolveDeltaPartKind(partId, props.field, part?.type, partTypeById);
|
|
576
|
+
if (kind === "reasoning") {
|
|
577
|
+
const key = `reasoning:${partId}`;
|
|
578
|
+
const next = (textByPartId.get(key) || "") + props.delta;
|
|
579
|
+
textByPartId.set(key, next);
|
|
580
|
+
callbacks.postEvent(ROLE_REASONING, next, liveOpts({
|
|
581
|
+
messageId: `oc_reasoning_${partId}`,
|
|
582
|
+
}));
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (kind === "text") {
|
|
534
586
|
const next = (textByPartId.get(partId) || "") + props.delta;
|
|
535
587
|
textByPartId.set(partId, next);
|
|
536
588
|
if (isTextDuplicateOfReasoning(next, reasoningBodiesFromPartMap(textByPartId))) {
|
|
@@ -539,15 +591,6 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
539
591
|
callbacks.postEvent(ROLE_ASSISTANT, next, liveOpts({
|
|
540
592
|
messageId: `oc_text_${partId}`,
|
|
541
593
|
}));
|
|
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
594
|
}
|
|
552
595
|
return;
|
|
553
596
|
}
|
|
@@ -559,8 +602,11 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
559
602
|
if (!sessionMatches(sessionId))
|
|
560
603
|
return;
|
|
561
604
|
callbacks.noteSessionId(sessionId);
|
|
605
|
+
const partId = part.id || part.messageID;
|
|
606
|
+
if (partId && part.type) {
|
|
607
|
+
partTypeById.set(partId, part.type);
|
|
608
|
+
}
|
|
562
609
|
if (part.type === "text") {
|
|
563
|
-
const partId = part.id || part.messageID;
|
|
564
610
|
if (!partId)
|
|
565
611
|
return;
|
|
566
612
|
let next = part.text || "";
|
|
@@ -584,12 +630,19 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
584
630
|
return;
|
|
585
631
|
}
|
|
586
632
|
if (part.type === "reasoning") {
|
|
587
|
-
const partId = part.id || part.messageID;
|
|
588
633
|
if (!partId)
|
|
589
634
|
return;
|
|
635
|
+
// Deltas before type was known may have been buffered under the text key.
|
|
636
|
+
const orphanText = textByPartId.get(partId);
|
|
637
|
+
if (orphanText) {
|
|
638
|
+
textByPartId.delete(partId);
|
|
639
|
+
}
|
|
590
640
|
let next = part.text || "";
|
|
591
641
|
if (props.delta && !part.text) {
|
|
592
|
-
next = (textByPartId.get(`reasoning:${partId}`) || "") + props.delta;
|
|
642
|
+
next = (textByPartId.get(`reasoning:${partId}`) || orphanText || "") + props.delta;
|
|
643
|
+
}
|
|
644
|
+
else if (!next && orphanText) {
|
|
645
|
+
next = orphanText;
|
|
593
646
|
}
|
|
594
647
|
if (part.text)
|
|
595
648
|
next = part.text;
|
|
@@ -753,6 +806,38 @@ function isMissingOpencodeSessionError(message) {
|
|
|
753
806
|
m.includes("unknown session") ||
|
|
754
807
|
m.includes("invalid session"));
|
|
755
808
|
}
|
|
809
|
+
/** Strip one layer of wrapping quotes models sometimes add when echoing. */
|
|
810
|
+
function stripOuterQuotes(text) {
|
|
811
|
+
const a = String(text || "").trim();
|
|
812
|
+
if ((a.startsWith('"') && a.endsWith('"')) ||
|
|
813
|
+
(a.startsWith("'") && a.endsWith("'"))) {
|
|
814
|
+
return a.slice(1, -1).trim();
|
|
815
|
+
}
|
|
816
|
+
return a;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* True when assistant text is an echo of a prompt we just sent to OpenCode
|
|
820
|
+
* (raw user text and/or host-wrapped effectivePrompt) — exact or streaming prefix.
|
|
821
|
+
*/
|
|
822
|
+
function isAssistantEchoOfSentPrompt(assistant, ...sentPrompts) {
|
|
823
|
+
const a = stripOuterQuotes(assistant);
|
|
824
|
+
if (!a)
|
|
825
|
+
return false;
|
|
826
|
+
for (const raw of sentPrompts) {
|
|
827
|
+
const p = String(raw || "").trim();
|
|
828
|
+
if (!p)
|
|
829
|
+
continue;
|
|
830
|
+
if (a === p)
|
|
831
|
+
return true;
|
|
832
|
+
// Streaming echo from the start of the wrapped prompt.
|
|
833
|
+
if (p.length >= 64 && a.length >= 24 && p.startsWith(a))
|
|
834
|
+
return true;
|
|
835
|
+
// Mid-wrap regurgitation (e.g. only the "Conversation so far:" section).
|
|
836
|
+
if (p.length >= 64 && a.length >= 40 && p.includes(a))
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
756
841
|
function wrapPromptWithContext(conversationSummary, userPrompt, isNewOpencodeSession, workingBranch, pullRequestUrl) {
|
|
757
842
|
const parts = [];
|
|
758
843
|
if (workingBranch?.trim()) {
|
|
@@ -991,6 +1076,7 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
991
1076
|
let buf = "";
|
|
992
1077
|
let fatalError = null;
|
|
993
1078
|
const textByPartId = new Map();
|
|
1079
|
+
const partTypeById = new Map();
|
|
994
1080
|
let sawStdout = false;
|
|
995
1081
|
let progressTicker = setInterval(() => {
|
|
996
1082
|
if (sawStdout) {
|
|
@@ -1035,7 +1121,7 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
1035
1121
|
return;
|
|
1036
1122
|
}
|
|
1037
1123
|
// Newer OpenCode --format json uses bus shape (message.part.updated); normalize first.
|
|
1038
|
-
const ev = normalizeStdoutOpencodeEvent(parsed) || parseOpencodeEvent(line);
|
|
1124
|
+
const ev = normalizeStdoutOpencodeEvent(parsed, partTypeById) || parseOpencodeEvent(line);
|
|
1039
1125
|
if (!ev?.type)
|
|
1040
1126
|
return;
|
|
1041
1127
|
noteSessionId(ev.sessionID);
|
|
@@ -1450,19 +1536,13 @@ export async function runChimphands(opts) {
|
|
|
1450
1536
|
}
|
|
1451
1537
|
poster.fireAndForget(role, content, bodyOpts);
|
|
1452
1538
|
};
|
|
1453
|
-
/**
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
? a.slice(1, -1).trim()
|
|
1461
|
-
: a;
|
|
1462
|
-
if (a === u || unquoted === u) {
|
|
1463
|
-
return;
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Drop ASSISTANT bubbles that echo the prompt we just sent — either the raw
|
|
1541
|
+
* user text or the host-wrapped effectivePrompt (exact string OpenCode got).
|
|
1542
|
+
*/
|
|
1543
|
+
const postEventForTurn = (userPrompt, wrappedPrompt, role, content, opts) => {
|
|
1544
|
+
if (role === ROLE_ASSISTANT && isAssistantEchoOfSentPrompt(content, userPrompt, wrappedPrompt)) {
|
|
1545
|
+
return;
|
|
1466
1546
|
}
|
|
1467
1547
|
postEvent(role, content, opts);
|
|
1468
1548
|
};
|
|
@@ -1564,7 +1644,7 @@ export async function runChimphands(opts) {
|
|
|
1564
1644
|
let useOpencodeSessionId = opencodeSessionId;
|
|
1565
1645
|
let isNewOpencodeSession = !useOpencodeSessionId;
|
|
1566
1646
|
let effectivePrompt = wrapPromptWithContext(conversationSummary, prompt, isNewOpencodeSession, workingBranch, pullRequestUrl);
|
|
1567
|
-
const turnPostEvent = (role, content, opts) => postEventForTurn(prompt, role, content, opts);
|
|
1647
|
+
const turnPostEvent = (role, content, opts) => postEventForTurn(prompt, effectivePrompt, role, content, opts);
|
|
1568
1648
|
// Visible in chat (not filtered as routine). OpenCode may not emit text until a
|
|
1569
1649
|
// part completes — without this the UI looks empty while the turn is running.
|
|
1570
1650
|
postEvent(ROLE_STATUS, "Agent is working…", { status: STATUS_RUNNING });
|
package/package.json
CHANGED