@testchimp/cli 0.1.49 → 0.1.51

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.
@@ -137,10 +137,8 @@ class AgentEventPoster {
137
137
  if (opts?.pullRequestUrl)
138
138
  body.pullRequestUrl = opts.pullRequestUrl;
139
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) {
142
- return this.chain;
143
- }
140
+ // Never drop liveStream tokens: ephemeral when UI is on this replica path,
141
+ // otherwise durable post_agent_event (cross-replica / detached safe).
144
142
  this.chain = this.chain.then(async () => {
145
143
  if (opts?.throttle || (streamRole && opts?.liveStream)) {
146
144
  const now = Date.now();
@@ -584,7 +582,7 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
584
582
  if (stopped || ac.signal.aborted)
585
583
  return;
586
584
  if (!connected && attempt === 0) {
587
- console.error("ChimpHands OpenCode SSE unavailable falling back to completed-only --format json events (will keep retrying SSE)");
585
+ console.error("ChimpHands OpenCode SSE not connected yet using completed-only --format json until SSE connects (retrying)");
588
586
  }
589
587
  attempt += 1;
590
588
  const backoff = Math.min(30_000, 2_000 * attempt);
@@ -863,7 +861,7 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
863
861
  callbacks.postEvent(ROLE_STATUS, "Agent is still working… (waiting for OpenCode output)", {
864
862
  status: STATUS_RUNNING,
865
863
  });
866
- }, 45_000);
864
+ }, 15_000);
867
865
  const noteSessionId = (sessionId) => {
868
866
  const id = sessionId?.trim();
869
867
  if (!id || id === activeSessionId)
@@ -887,7 +885,15 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
887
885
  fatalError = fatal;
888
886
  return;
889
887
  }
890
- const ev = parseOpencodeEvent(line);
888
+ let parsed;
889
+ try {
890
+ parsed = JSON.parse(line);
891
+ }
892
+ catch {
893
+ return;
894
+ }
895
+ // Newer OpenCode --format json uses bus shape (message.part.updated); normalize first.
896
+ const ev = normalizeStdoutOpencodeEvent(parsed) || parseOpencodeEvent(line);
891
897
  if (!ev?.type)
892
898
  return;
893
899
  noteSessionId(ev.sessionID);
@@ -1090,29 +1096,58 @@ export async function runChimphands(opts) {
1090
1096
  const poster = new AgentEventPoster(backend, apiKey, sessionId);
1091
1097
  poster.uiAttached = !!(boot.uiAttached ?? boot.ui_attached);
1092
1098
  let stopLiveSse = null;
1099
+ /** Do not open localhost OpenCode /event until serve has been (re)started with config. */
1100
+ let opencodeHttpReady = !attachUrl;
1101
+ let pendingUiAttached = !!(boot.uiAttached ?? boot.ui_attached);
1102
+ /** Consecutive uiAttached=false heartbeats before stopping OpenCode SSE (multi-replica poison). */
1103
+ let consecutiveUiDetached = 0;
1104
+ const UI_DETACHED_STOP_AFTER = 3;
1105
+ const startLiveSseIfNeeded = (reason) => {
1106
+ if (!attachUrl || !opencodeHttpReady || stopLiveSse)
1107
+ return;
1108
+ console.error(`ChimpHands ${reason}`);
1109
+ stopLiveSse = startOpencodeSseRelay(attachUrl, {
1110
+ getActiveSessionId: () => opencodeSessionId,
1111
+ noteSessionId: (id) => {
1112
+ if (id?.trim())
1113
+ opencodeSessionId = id.trim();
1114
+ },
1115
+ postEvent: (role, content, opts) => {
1116
+ poster.fireAndForget(role, content, {
1117
+ ...opts,
1118
+ opencodeSessionId: opts?.opencodeSessionId || opencodeSessionId,
1119
+ });
1120
+ },
1121
+ onWorkingBranch: noteWorkingBranchPlaceholder,
1122
+ });
1123
+ };
1093
1124
  const syncLiveSse = (attached) => {
1094
1125
  poster.uiAttached = attached;
1126
+ pendingUiAttached = attached;
1095
1127
  if (!attachUrl)
1096
1128
  return;
1097
- if (attached && !stopLiveSse) {
1098
- console.error("ChimpHands UI attached — starting OpenCode SSE fanout");
1099
- stopLiveSse = startOpencodeSseRelay(attachUrl, {
1100
- getActiveSessionId: () => opencodeSessionId,
1101
- noteSessionId: (id) => {
1102
- if (id?.trim())
1103
- opencodeSessionId = id.trim();
1104
- },
1105
- postEvent: (role, content, opts) => {
1106
- poster.fireAndForget(role, content, {
1107
- ...opts,
1108
- opencodeSessionId: opts?.opencodeSessionId || opencodeSessionId,
1109
- });
1110
- },
1111
- onWorkingBranch: noteWorkingBranchPlaceholder,
1112
- });
1129
+ if (!opencodeHttpReady) {
1130
+ if (attached) {
1131
+ console.error("ChimpHands UI attached — deferring OpenCode SSE until serve is ready");
1132
+ }
1133
+ return;
1134
+ }
1135
+ if (attached) {
1136
+ consecutiveUiDetached = 0;
1137
+ startLiveSseIfNeeded("UI attached starting OpenCode SSE fanout");
1138
+ return;
1113
1139
  }
1114
- else if (!attached && stopLiveSse) {
1115
- console.error("ChimpHands UI detached stopping OpenCode SSE fanout");
1140
+ // Keep SSE running so live tokens can durable-post even when heartbeat
1141
+ // briefly reports false (UI EventSource on another FS replica).
1142
+ consecutiveUiDetached += 1;
1143
+ if (consecutiveUiDetached < UI_DETACHED_STOP_AFTER) {
1144
+ if (!stopLiveSse) {
1145
+ startLiveSseIfNeeded("starting OpenCode SSE fanout (durable until UI attaches)");
1146
+ }
1147
+ return;
1148
+ }
1149
+ if (stopLiveSse) {
1150
+ console.error(`ChimpHands UI detached for ${consecutiveUiDetached} heartbeats — stopping OpenCode SSE fanout`);
1116
1151
  stopLiveSse();
1117
1152
  stopLiveSse = null;
1118
1153
  }
@@ -1167,7 +1202,10 @@ export async function runChimphands(opts) {
1167
1202
  console.error(`ChimpHands OpenCode attach: ${attachUrl}`);
1168
1203
  // Serve must load opencode.json (provider + default_agent). Workflow often starts
1169
1204
  // serve before this file exists; restart so attach mode can omit --agent safely.
1205
+ // Heartbeat may have already reported ui_attached — wait until after restart to open SSE.
1170
1206
  await restartLocalOpencodeServer(attachUrl);
1207
+ opencodeHttpReady = true;
1208
+ syncLiveSse(pendingUiAttached);
1171
1209
  }
1172
1210
  let opencodeSessionId = bootStr(boot, "opencode_session_id", "opencodeSessionId") || undefined;
1173
1211
  const conversationSummary = bootStr(boot, "conversation_summary", "conversationSummary");
@@ -1307,10 +1345,22 @@ export async function runChimphands(opts) {
1307
1345
  try {
1308
1346
  poster.fireAndForget(ROLE_STATUS, "Agent ready", { status: STATUS_RUNNING });
1309
1347
  let prompt = normalizeUserMessage(promptInput || bootStr(boot, "initial_prompt", "initialPrompt"));
1348
+ // Bootstrap sets initialPrompt from the last pending user message AND returns
1349
+ // pendingUserMessages — enqueue only messages that are not the initial prompt
1350
+ // (otherwise the same turn runs twice: session=new then session=ses_…).
1310
1351
  const pending = boot.pending_user_messages || boot.pendingUserMessages || [];
1352
+ const initialNorm = normalizeUserMessage(prompt);
1311
1353
  for (const m of pending) {
1312
- if (m?.content)
1313
- enqueueUserMessage({ content: m.content });
1354
+ const content = normalizeUserMessage(m?.content || "");
1355
+ if (!content)
1356
+ continue;
1357
+ if (initialNorm && content === initialNorm)
1358
+ continue;
1359
+ const id = ("id" in m && m.id) ||
1360
+ ("message_id" in m && m.message_id) ||
1361
+ ("messageId" in m && m.messageId) ||
1362
+ undefined;
1363
+ enqueueUserMessage({ id: id || undefined, content });
1314
1364
  }
1315
1365
  const waitForNextPrompt = () => new Promise((resolve) => {
1316
1366
  let lastPollAt = 0;
@@ -1440,7 +1490,9 @@ function startRuntimeHeartbeat(backend, apiKey, runtimeId, onUiAttached) {
1440
1490
  try {
1441
1491
  const data = JSON.parse(text);
1442
1492
  const attached = !!(data.uiAttached ?? data.ui_attached);
1443
- if (attached !== lastAttached) {
1493
+ // Always notify on false so syncLiveSse can accumulate detach hysteresis;
1494
+ // still skip duplicate true→true noise.
1495
+ if (attached !== lastAttached || !attached) {
1444
1496
  lastAttached = attached;
1445
1497
  onUiAttached?.(attached);
1446
1498
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testchimp/cli",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
4
4
  "description": "TestChimp CLI and MCP server — coverage, plans, EaaS, TrueCoverage, API operations (calls /api/mcp/*)",
5
5
  "type": "module",
6
6
  "main": "dist/bin/testchimp.js",