@testchimp/cli 0.1.48 → 0.1.50
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 +251 -53
- package/package.json +1 -1
package/dist/chimphands/run.js
CHANGED
|
@@ -113,7 +113,7 @@ class AgentEventPoster {
|
|
|
113
113
|
sessionId;
|
|
114
114
|
chain = Promise.resolve();
|
|
115
115
|
lastStreamPostAt = 0;
|
|
116
|
-
/** When false,
|
|
116
|
+
/** When false, liveStream tokens are dropped; completed events still persist. */
|
|
117
117
|
uiAttached = false;
|
|
118
118
|
constructor(backend, apiKey, sessionId) {
|
|
119
119
|
this.backend = backend;
|
|
@@ -136,23 +136,21 @@ class AgentEventPoster {
|
|
|
136
136
|
body.workingBranch = opts.workingBranch;
|
|
137
137
|
if (opts?.pullRequestUrl)
|
|
138
138
|
body.pullRequestUrl = opts.pullRequestUrl;
|
|
139
|
-
const
|
|
140
|
-
|
|
139
|
+
const streamRole = isStreamFanoutRole(role);
|
|
140
|
+
// Live token fanout only while UI watching; completed json events always go durable.
|
|
141
|
+
if (streamRole && opts?.liveStream && !this.uiAttached) {
|
|
141
142
|
return this.chain;
|
|
142
143
|
}
|
|
143
144
|
this.chain = this.chain.then(async () => {
|
|
144
|
-
if (opts?.throttle ||
|
|
145
|
+
if (opts?.throttle || (streamRole && opts?.liveStream)) {
|
|
145
146
|
const now = Date.now();
|
|
146
147
|
const wait = STREAM_POST_MIN_INTERVAL_MS - (now - this.lastStreamPostAt);
|
|
147
148
|
if (wait > 0)
|
|
148
149
|
await sleep(wait);
|
|
149
150
|
this.lastStreamPostAt = Date.now();
|
|
150
151
|
}
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
: "/api/chimphands/post_agent_event";
|
|
154
|
-
// Ephemeral API only accepts session/role/content/messageId/opencodeSessionId.
|
|
155
|
-
if (ephemeral) {
|
|
152
|
+
const tryEphemeral = streamRole && this.uiAttached;
|
|
153
|
+
if (tryEphemeral) {
|
|
156
154
|
const eph = {
|
|
157
155
|
sessionId: this.sessionId,
|
|
158
156
|
role,
|
|
@@ -162,10 +160,27 @@ class AgentEventPoster {
|
|
|
162
160
|
eph.messageId = opts.messageId;
|
|
163
161
|
if (opts?.opencodeSessionId)
|
|
164
162
|
eph.opencodeSessionId = opts.opencodeSessionId;
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
try {
|
|
164
|
+
const text = await postJson(this.backend, this.apiKey, "/api/chimphands/post_ephemeral_agent_event", eph);
|
|
165
|
+
let delivered = false;
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(text);
|
|
168
|
+
delivered = !!parsed.delivered;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
/* ignore */
|
|
172
|
+
}
|
|
173
|
+
if (delivered)
|
|
174
|
+
return;
|
|
175
|
+
// Cross-replica: UI SSE not on this FS pod — fall through to durable.
|
|
176
|
+
console.error("ChimpHands ephemeral not delivered (replica miss?) — persisting via post_agent_event");
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
180
|
+
console.error(`ChimpHands ephemeral post failed — durable fallback: ${detail}`);
|
|
181
|
+
}
|
|
167
182
|
}
|
|
168
|
-
await postJson(this.backend, this.apiKey,
|
|
183
|
+
await postJson(this.backend, this.apiKey, "/api/chimphands/post_agent_event", body);
|
|
169
184
|
});
|
|
170
185
|
return this.chain;
|
|
171
186
|
}
|
|
@@ -242,6 +257,122 @@ function parseOpencodeEvent(line) {
|
|
|
242
257
|
return null;
|
|
243
258
|
}
|
|
244
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Newer OpenCode `--format json` lines often use the SSE bus shape
|
|
262
|
+
* (`message.part.updated` + `properties.part`) instead of legacy `type: "text"`.
|
|
263
|
+
* Normalize both into the same OpencodeEvent used by the stdout switch.
|
|
264
|
+
*/
|
|
265
|
+
function normalizeStdoutOpencodeEvent(raw) {
|
|
266
|
+
if (!raw || typeof raw !== "object")
|
|
267
|
+
return null;
|
|
268
|
+
const o = raw;
|
|
269
|
+
// Global envelope
|
|
270
|
+
const inner = o.payload && typeof o.payload === "object"
|
|
271
|
+
? o.payload
|
|
272
|
+
: o.event && typeof o.event === "object"
|
|
273
|
+
? o.event
|
|
274
|
+
: o;
|
|
275
|
+
const type = String(inner.type || "");
|
|
276
|
+
const props = (inner.properties && typeof inner.properties === "object"
|
|
277
|
+
? inner.properties
|
|
278
|
+
: {});
|
|
279
|
+
if (type === "message.part.updated" || type === "message.part.delta") {
|
|
280
|
+
const part = props.part;
|
|
281
|
+
if (!part)
|
|
282
|
+
return null;
|
|
283
|
+
const partType = part.type || "";
|
|
284
|
+
let mapped;
|
|
285
|
+
if (partType === "text")
|
|
286
|
+
mapped = "text";
|
|
287
|
+
else if (partType === "reasoning")
|
|
288
|
+
mapped = "reasoning";
|
|
289
|
+
else if (partType === "tool")
|
|
290
|
+
mapped = "tool_use";
|
|
291
|
+
else
|
|
292
|
+
return null;
|
|
293
|
+
// Prefer cumulative text; append delta when that's all we got.
|
|
294
|
+
if (props.delta && !part.text) {
|
|
295
|
+
part.text = props.delta;
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
type: mapped,
|
|
299
|
+
sessionID: part.sessionID || props.sessionID,
|
|
300
|
+
part,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (type === "session.error") {
|
|
304
|
+
return {
|
|
305
|
+
type: "error",
|
|
306
|
+
sessionID: props.sessionID,
|
|
307
|
+
error: props.error,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
// Legacy flat shape: { type: "text"|"tool_use"|..., part, sessionID }
|
|
311
|
+
if (type) {
|
|
312
|
+
return inner;
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
/** Pull assistant/tool parts from OpenCode HTTP after attach exits early. */
|
|
317
|
+
async function reconcileOpencodeSessionMessages(attachUrl, opencodeSessionId, postEvent, onWorkingBranch) {
|
|
318
|
+
const base = attachUrl.replace(/\/$/, "");
|
|
319
|
+
const url = `${base}/session/${encodeURIComponent(opencodeSessionId)}/message`;
|
|
320
|
+
let res;
|
|
321
|
+
try {
|
|
322
|
+
res = await fetch(url, {
|
|
323
|
+
headers: { Accept: "application/json", "x-opencode-directory": process.cwd() },
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
console.error(`ChimpHands reconcile messages fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
330
|
+
if (!res.ok) {
|
|
331
|
+
console.error(`ChimpHands reconcile messages HTTP ${res.status}`);
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
const data = (await res.json());
|
|
335
|
+
if (!Array.isArray(data))
|
|
336
|
+
return 0;
|
|
337
|
+
let posted = 0;
|
|
338
|
+
for (const msg of data) {
|
|
339
|
+
if ((msg.info?.role || "").toLowerCase() !== "assistant")
|
|
340
|
+
continue;
|
|
341
|
+
for (const part of msg.parts || []) {
|
|
342
|
+
if (part.type === "text" && part.text?.trim()) {
|
|
343
|
+
postEvent(ROLE_ASSISTANT, part.text.trim(), {
|
|
344
|
+
messageId: opencodeMessageId("oc_text_", part),
|
|
345
|
+
});
|
|
346
|
+
posted += 1;
|
|
347
|
+
}
|
|
348
|
+
else if (part.type === "reasoning" && part.text?.trim()) {
|
|
349
|
+
postEvent(ROLE_REASONING, part.text.trim(), {
|
|
350
|
+
messageId: opencodeMessageId("oc_reasoning_", part),
|
|
351
|
+
});
|
|
352
|
+
posted += 1;
|
|
353
|
+
}
|
|
354
|
+
else if (part.type === "tool") {
|
|
355
|
+
const status = part.state?.status;
|
|
356
|
+
if (!status || status === "pending" || status === "running")
|
|
357
|
+
continue;
|
|
358
|
+
const toolContent = formatToolUseContent(part);
|
|
359
|
+
postEvent(ROLE_TOOL, toolContent, {
|
|
360
|
+
messageId: opencodeMessageId("oc_tool_", part),
|
|
361
|
+
});
|
|
362
|
+
posted += 1;
|
|
363
|
+
if (status === "completed") {
|
|
364
|
+
const detected = detectWorkingBranchFromToolOutput(toolContent);
|
|
365
|
+
if (detected.branch)
|
|
366
|
+
onWorkingBranch?.(detected.branch, detected.pullRequestUrl);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (posted) {
|
|
372
|
+
console.error(`ChimpHands reconciled ${posted} part(s) from OpenCode session API`);
|
|
373
|
+
}
|
|
374
|
+
return posted;
|
|
375
|
+
}
|
|
245
376
|
function formatToolUseContent(part) {
|
|
246
377
|
const title = part.state?.title || part.tool || "tool";
|
|
247
378
|
const status = part.state?.status?.trim();
|
|
@@ -273,6 +404,7 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
273
404
|
const ac = new AbortController();
|
|
274
405
|
let stopped = false;
|
|
275
406
|
const textByPartId = new Map();
|
|
407
|
+
const directory = process.cwd();
|
|
276
408
|
const sessionMatches = (sessionId) => {
|
|
277
409
|
const active = callbacks.getActiveSessionId();
|
|
278
410
|
if (!sessionId)
|
|
@@ -281,10 +413,27 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
281
413
|
return true;
|
|
282
414
|
return sessionId === active;
|
|
283
415
|
};
|
|
284
|
-
const
|
|
416
|
+
const liveOpts = (extra) => ({
|
|
417
|
+
...extra,
|
|
418
|
+
throttle: true,
|
|
419
|
+
liveStream: true,
|
|
420
|
+
});
|
|
421
|
+
const unwrapBusPayload = (raw) => {
|
|
285
422
|
if (!raw || typeof raw !== "object")
|
|
423
|
+
return raw;
|
|
424
|
+
const o = raw;
|
|
425
|
+
// /global/event wraps as { directory, payload } or { directory, event }
|
|
426
|
+
if (o.payload && typeof o.payload === "object")
|
|
427
|
+
return o.payload;
|
|
428
|
+
if (o.event && typeof o.event === "object")
|
|
429
|
+
return o.event;
|
|
430
|
+
return raw;
|
|
431
|
+
};
|
|
432
|
+
const handleBusEvent = (raw) => {
|
|
433
|
+
const unwrapped = unwrapBusPayload(raw);
|
|
434
|
+
if (!unwrapped || typeof unwrapped !== "object")
|
|
286
435
|
return;
|
|
287
|
-
const ev =
|
|
436
|
+
const ev = unwrapped;
|
|
288
437
|
const type = ev.type || "";
|
|
289
438
|
const props = ev.properties || {};
|
|
290
439
|
if (type === "message.part.updated" || type === "message.part.delta") {
|
|
@@ -312,10 +461,9 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
312
461
|
textByPartId.set(partId, next);
|
|
313
462
|
if (!next)
|
|
314
463
|
return;
|
|
315
|
-
callbacks.postEvent(ROLE_ASSISTANT, next, {
|
|
316
|
-
throttle: true,
|
|
464
|
+
callbacks.postEvent(ROLE_ASSISTANT, next, liveOpts({
|
|
317
465
|
messageId: opencodeMessageId("oc_text_", part),
|
|
318
|
-
});
|
|
466
|
+
}));
|
|
319
467
|
return;
|
|
320
468
|
}
|
|
321
469
|
if (part.type === "reasoning") {
|
|
@@ -331,10 +479,9 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
331
479
|
textByPartId.set(`reasoning:${partId}`, next);
|
|
332
480
|
if (!next)
|
|
333
481
|
return;
|
|
334
|
-
callbacks.postEvent(ROLE_REASONING, next, {
|
|
335
|
-
throttle: true,
|
|
482
|
+
callbacks.postEvent(ROLE_REASONING, next, liveOpts({
|
|
336
483
|
messageId: opencodeMessageId("oc_reasoning_", part),
|
|
337
|
-
});
|
|
484
|
+
}));
|
|
338
485
|
return;
|
|
339
486
|
}
|
|
340
487
|
if (part.type === "tool") {
|
|
@@ -342,9 +489,10 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
342
489
|
if (!status || status === "pending" || status === "running")
|
|
343
490
|
return;
|
|
344
491
|
const toolContent = formatToolUseContent(part);
|
|
345
|
-
callbacks.postEvent(ROLE_TOOL, toolContent, {
|
|
492
|
+
callbacks.postEvent(ROLE_TOOL, toolContent, liveOpts({
|
|
346
493
|
messageId: opencodeMessageId("oc_tool_", part),
|
|
347
|
-
|
|
494
|
+
throttle: false,
|
|
495
|
+
}));
|
|
348
496
|
if (status === "completed") {
|
|
349
497
|
const detected = detectWorkingBranchFromToolOutput(toolContent);
|
|
350
498
|
if (detected.branch) {
|
|
@@ -392,32 +540,55 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
392
540
|
}
|
|
393
541
|
}
|
|
394
542
|
};
|
|
543
|
+
const candidates = () => {
|
|
544
|
+
const dirQ = `directory=${encodeURIComponent(directory)}`;
|
|
545
|
+
return [
|
|
546
|
+
`/event?${dirQ}`,
|
|
547
|
+
`/event`,
|
|
548
|
+
`/global/event?${dirQ}`,
|
|
549
|
+
`/global/event`,
|
|
550
|
+
];
|
|
551
|
+
};
|
|
395
552
|
void (async () => {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
553
|
+
let attempt = 0;
|
|
554
|
+
while (!stopped) {
|
|
555
|
+
let connected = false;
|
|
556
|
+
for (const path of candidates()) {
|
|
557
|
+
if (stopped)
|
|
558
|
+
return;
|
|
559
|
+
try {
|
|
560
|
+
const res = await fetch(`${base}${path}`, {
|
|
561
|
+
headers: {
|
|
562
|
+
Accept: "text/event-stream",
|
|
563
|
+
"x-opencode-directory": directory,
|
|
564
|
+
},
|
|
565
|
+
signal: ac.signal,
|
|
566
|
+
});
|
|
567
|
+
if (!res.ok || !res.body) {
|
|
568
|
+
console.error(`ChimpHands OpenCode SSE ${path} HTTP ${res.status}`);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
console.error(`ChimpHands OpenCode SSE streaming via ${path}`);
|
|
572
|
+
connected = true;
|
|
573
|
+
await consume(res.body);
|
|
574
|
+
// Stream ended — retry if still attached.
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
578
|
+
if (stopped || ac.signal.aborted)
|
|
579
|
+
return;
|
|
580
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
581
|
+
console.error(`ChimpHands OpenCode SSE ${path} failed: ${detail}`);
|
|
407
582
|
}
|
|
408
|
-
console.error(`ChimpHands OpenCode SSE streaming via ${path}`);
|
|
409
|
-
await consume(res.body);
|
|
410
|
-
return;
|
|
411
583
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
console.error(`ChimpHands OpenCode SSE ${path} failed: ${detail}`);
|
|
584
|
+
if (stopped || ac.signal.aborted)
|
|
585
|
+
return;
|
|
586
|
+
if (!connected && attempt === 0) {
|
|
587
|
+
console.error("ChimpHands OpenCode SSE not connected yet — using completed-only --format json until SSE connects (retrying)");
|
|
417
588
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
589
|
+
attempt += 1;
|
|
590
|
+
const backoff = Math.min(30_000, 2_000 * attempt);
|
|
591
|
+
await sleep(backoff);
|
|
421
592
|
}
|
|
422
593
|
})();
|
|
423
594
|
return () => {
|
|
@@ -456,21 +627,15 @@ function isMissingOpencodeSessionError(message) {
|
|
|
456
627
|
m.includes("unknown session") ||
|
|
457
628
|
m.includes("invalid session"));
|
|
458
629
|
}
|
|
459
|
-
function isNonInteractivePrompt(userPrompt) {
|
|
460
|
-
return /(?:^|\s)--mode\s*=?\s*non-interactive\b|mode\s*=\s*non-interactive\b/i.test(userPrompt);
|
|
461
|
-
}
|
|
462
|
-
function isTestchimpWorkflowPrompt(userPrompt) {
|
|
463
|
-
return /(?:^|\s)\/?testchimp\b/i.test(userPrompt.trim());
|
|
464
|
-
}
|
|
465
630
|
function wrapPromptWithContext(conversationSummary, userPrompt, isNewOpencodeSession, workingBranch, pullRequestUrl) {
|
|
466
631
|
const parts = [];
|
|
467
632
|
if (workingBranch?.trim()) {
|
|
468
633
|
parts.push("## Conversation working branch (reuse for this thread)", `Branch: \`${workingBranch.trim()}\``, pullRequestUrl?.trim() ? `PR: ${pullRequestUrl.trim()}` : "", "Checkout this branch, commit and push here. Do NOT open a new PR unless the one above was merged/closed.", "");
|
|
469
634
|
}
|
|
470
635
|
const task = normalizeUserMessage(userPrompt);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
636
|
+
// Do NOT inject "Interactive turn reminder" into the user prompt — it is already in
|
|
637
|
+
// CHIMPHANDS_AGENT_PROMPT (system). Putting it here makes OpenCode store it as the
|
|
638
|
+
// user message and the UI shows host control text in chat.
|
|
474
639
|
if (isNewOpencodeSession && conversationSummary.trim()) {
|
|
475
640
|
parts.push(`Conversation so far:\n${conversationSummary.trim()}`, "", `Current task:\n${task}`);
|
|
476
641
|
return parts.filter(Boolean).join("\n");
|
|
@@ -687,6 +852,18 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
687
852
|
let fatalError = null;
|
|
688
853
|
const textByPartId = new Map();
|
|
689
854
|
let sawStdout = false;
|
|
855
|
+
let progressTicker = setInterval(() => {
|
|
856
|
+
if (sawStdout) {
|
|
857
|
+
if (progressTicker) {
|
|
858
|
+
clearInterval(progressTicker);
|
|
859
|
+
progressTicker = null;
|
|
860
|
+
}
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
callbacks.postEvent(ROLE_STATUS, "Agent is still working… (waiting for OpenCode output)", {
|
|
864
|
+
status: STATUS_RUNNING,
|
|
865
|
+
});
|
|
866
|
+
}, 45_000);
|
|
690
867
|
const noteSessionId = (sessionId) => {
|
|
691
868
|
const id = sessionId?.trim();
|
|
692
869
|
if (!id || id === activeSessionId)
|
|
@@ -700,6 +877,10 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
700
877
|
if (!sawStdout) {
|
|
701
878
|
sawStdout = true;
|
|
702
879
|
console.error("ChimpHands OpenCode first stdout event received");
|
|
880
|
+
if (progressTicker) {
|
|
881
|
+
clearInterval(progressTicker);
|
|
882
|
+
progressTicker = null;
|
|
883
|
+
}
|
|
703
884
|
}
|
|
704
885
|
const fatal = extractOpencodeFatalError(line);
|
|
705
886
|
if (fatal) {
|
|
@@ -786,6 +967,10 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
786
967
|
}
|
|
787
968
|
});
|
|
788
969
|
child.on("close", (code) => {
|
|
970
|
+
if (progressTicker) {
|
|
971
|
+
clearInterval(progressTicker);
|
|
972
|
+
progressTicker = null;
|
|
973
|
+
}
|
|
789
974
|
if (buf.trim()) {
|
|
790
975
|
handleOpencodeLine(buf.trim());
|
|
791
976
|
}
|
|
@@ -905,10 +1090,20 @@ export async function runChimphands(opts) {
|
|
|
905
1090
|
const poster = new AgentEventPoster(backend, apiKey, sessionId);
|
|
906
1091
|
poster.uiAttached = !!(boot.uiAttached ?? boot.ui_attached);
|
|
907
1092
|
let stopLiveSse = null;
|
|
1093
|
+
/** Do not open localhost OpenCode /event until serve has been (re)started with config. */
|
|
1094
|
+
let opencodeHttpReady = !attachUrl;
|
|
1095
|
+
let pendingUiAttached = !!(boot.uiAttached ?? boot.ui_attached);
|
|
908
1096
|
const syncLiveSse = (attached) => {
|
|
909
1097
|
poster.uiAttached = attached;
|
|
1098
|
+
pendingUiAttached = attached;
|
|
910
1099
|
if (!attachUrl)
|
|
911
1100
|
return;
|
|
1101
|
+
if (!opencodeHttpReady) {
|
|
1102
|
+
if (attached) {
|
|
1103
|
+
console.error("ChimpHands UI attached — deferring OpenCode SSE until serve is ready");
|
|
1104
|
+
}
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
912
1107
|
if (attached && !stopLiveSse) {
|
|
913
1108
|
console.error("ChimpHands UI attached — starting OpenCode SSE fanout");
|
|
914
1109
|
stopLiveSse = startOpencodeSseRelay(attachUrl, {
|
|
@@ -982,7 +1177,10 @@ export async function runChimphands(opts) {
|
|
|
982
1177
|
console.error(`ChimpHands OpenCode attach: ${attachUrl}`);
|
|
983
1178
|
// Serve must load opencode.json (provider + default_agent). Workflow often starts
|
|
984
1179
|
// serve before this file exists; restart so attach mode can omit --agent safely.
|
|
1180
|
+
// Heartbeat may have already reported ui_attached — wait until after restart to open SSE.
|
|
985
1181
|
await restartLocalOpencodeServer(attachUrl);
|
|
1182
|
+
opencodeHttpReady = true;
|
|
1183
|
+
syncLiveSse(pendingUiAttached);
|
|
986
1184
|
}
|
|
987
1185
|
let opencodeSessionId = bootStr(boot, "opencode_session_id", "opencodeSessionId") || undefined;
|
|
988
1186
|
const conversationSummary = bootStr(boot, "conversation_summary", "conversationSummary");
|
package/package.json
CHANGED