@testchimp/cli 0.1.50 → 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();
@@ -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);
@@ -1093,6 +1099,28 @@ export async function runChimphands(opts) {
1093
1099
  /** Do not open localhost OpenCode /event until serve has been (re)started with config. */
1094
1100
  let opencodeHttpReady = !attachUrl;
1095
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
+ };
1096
1124
  const syncLiveSse = (attached) => {
1097
1125
  poster.uiAttached = attached;
1098
1126
  pendingUiAttached = attached;
@@ -1104,25 +1132,22 @@ export async function runChimphands(opts) {
1104
1132
  }
1105
1133
  return;
1106
1134
  }
1107
- if (attached && !stopLiveSse) {
1108
- console.error("ChimpHands UI attached — starting OpenCode SSE fanout");
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
- });
1135
+ if (attached) {
1136
+ consecutiveUiDetached = 0;
1137
+ startLiveSseIfNeeded("UI attached starting OpenCode SSE fanout");
1138
+ return;
1123
1139
  }
1124
- else if (!attached && stopLiveSse) {
1125
- 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`);
1126
1151
  stopLiveSse();
1127
1152
  stopLiveSse = null;
1128
1153
  }
@@ -1320,10 +1345,22 @@ export async function runChimphands(opts) {
1320
1345
  try {
1321
1346
  poster.fireAndForget(ROLE_STATUS, "Agent ready", { status: STATUS_RUNNING });
1322
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_…).
1323
1351
  const pending = boot.pending_user_messages || boot.pendingUserMessages || [];
1352
+ const initialNorm = normalizeUserMessage(prompt);
1324
1353
  for (const m of pending) {
1325
- if (m?.content)
1326
- 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 });
1327
1364
  }
1328
1365
  const waitForNextPrompt = () => new Promise((resolve) => {
1329
1366
  let lastPollAt = 0;
@@ -1453,7 +1490,9 @@ function startRuntimeHeartbeat(backend, apiKey, runtimeId, onUiAttached) {
1453
1490
  try {
1454
1491
  const data = JSON.parse(text);
1455
1492
  const attached = !!(data.uiAttached ?? data.ui_attached);
1456
- 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) {
1457
1496
  lastAttached = attached;
1458
1497
  onUiAttached?.(attached);
1459
1498
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testchimp/cli",
3
- "version": "0.1.50",
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",