@evident-ai/cli 3.5.1-dev.bbaa769 → 3.5.1-dev.bf9e537

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/index.js CHANGED
@@ -1158,7 +1158,7 @@ import ora4 from "ora";
1158
1158
  import { select as select4 } from "@inquirer/prompts";
1159
1159
 
1160
1160
  // src/lib/telemetry.ts
1161
- var CLI_VERSION = (true ? "3.5.1-dev.bbaa769" : void 0) ?? process.env.npm_package_version ?? "unknown";
1161
+ var CLI_VERSION = (true ? "3.5.1-dev.bf9e537" : void 0) ?? process.env.npm_package_version ?? "unknown";
1162
1162
  function getCliVersion() {
1163
1163
  return CLI_VERSION;
1164
1164
  }
@@ -1630,12 +1630,18 @@ async function checkOpenCodeHealth(port) {
1630
1630
  return { healthy: false, error: message };
1631
1631
  }
1632
1632
  }
1633
+ function bodyExcerpt(body) {
1634
+ const excerpt = body.replace(/\s+/g, " ").trim().slice(0, 160);
1635
+ return excerpt ? ` (body="${excerpt}")` : " (empty body)";
1636
+ }
1633
1637
  async function checkOpenCode2Health(port, password) {
1634
1638
  try {
1635
- const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
1636
- headers: {
1637
- Authorization: buildOpenCodeBasicAuthHeader(password)
1638
- },
1639
+ const headers = {};
1640
+ if (password !== void 0) {
1641
+ headers.Authorization = buildOpenCodeBasicAuthHeader(password);
1642
+ }
1643
+ const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
1644
+ headers,
1639
1645
  signal: AbortSignal.timeout(2e3)
1640
1646
  });
1641
1647
  if (response.status === 401) {
@@ -1644,8 +1650,31 @@ async function checkOpenCode2Health(port, password) {
1644
1650
  if (!response.ok) {
1645
1651
  return { healthy: false, error: `HTTP ${response.status}` };
1646
1652
  }
1647
- const data = await response.json().catch(() => ({}));
1648
- return { healthy: true, version: data.version };
1653
+ const body = await response.text();
1654
+ const excerpt = bodyExcerpt(body);
1655
+ let data;
1656
+ try {
1657
+ data = JSON.parse(body);
1658
+ } catch (error2) {
1659
+ return {
1660
+ healthy: false,
1661
+ error: `Invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
1662
+ };
1663
+ }
1664
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
1665
+ return {
1666
+ healthy: false,
1667
+ error: `Invalid health response${excerpt}: expected healthy=true`
1668
+ };
1669
+ }
1670
+ const health = data;
1671
+ if (health.healthy !== true) {
1672
+ return {
1673
+ healthy: false,
1674
+ error: `Invalid health response${excerpt}: expected healthy=true`
1675
+ };
1676
+ }
1677
+ return typeof health.version === "string" ? { healthy: true, version: health.version } : { healthy: true };
1649
1678
  } catch (error2) {
1650
1679
  const message = error2 instanceof Error ? error2.message : "Unknown error";
1651
1680
  return { healthy: false, error: message };
@@ -6399,6 +6428,7 @@ var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
6399
6428
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
6400
6429
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
6401
6430
  var HEARTBEAT_MS = 6e4;
6431
+ var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
6402
6432
  var ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1e3;
6403
6433
  var B2_ABANDONMENT_MIN_PINNED_MS = 3 * 6e4;
6404
6434
  var AMBIGUOUS_FINISH_MAX_PINNED_MS = 3 * 6e4;
@@ -7603,7 +7633,7 @@ var ChannelDriver = class _ChannelDriver {
7603
7633
  return "dispatch";
7604
7634
  }
7605
7635
  }
7606
- return this.reattachRedrive(conv, sessionId, message, ocId);
7636
+ return this.reattachRedrive(conv, sessionId, message, ocId, state);
7607
7637
  }
7608
7638
  if (ongoing === false) {
7609
7639
  if (state === "running" && isAmbiguousFinishPinnedRunning(messages, ocId ?? "")) {
@@ -7620,11 +7650,12 @@ var ChannelDriver = class _ChannelDriver {
7620
7650
  return "dispatch";
7621
7651
  }
7622
7652
  /**
7623
- * The `reattached` outcome (Task 3.3): the prior turn is STILL ONGOING per
7624
- * opencode's own status map — undo the false reclaim instead of starting a
7625
- * second turn.
7653
+ * The `reattached` outcome (Task 3.3): opencode's own status map says the session
7654
+ * is ongoing, so undo the false reclaim instead of starting a second turn. `state`
7655
+ * carries the correlated message's observed state so the outcome does not claim
7656
+ * that a queued message's own turn was already running.
7626
7657
  */
7627
- async reattachRedrive(conv, sessionId, message, ocId) {
7658
+ async reattachRedrive(conv, sessionId, message, ocId, state) {
7628
7659
  let anchorMs;
7629
7660
  const parsed = message.processing_started_at ? Date.parse(message.processing_started_at) : NaN;
7630
7661
  if (!Number.isNaN(parsed)) {
@@ -7633,7 +7664,7 @@ var ChannelDriver = class _ChannelDriver {
7633
7664
  anchorMs = this.now();
7634
7665
  this.log({
7635
7666
  level: "warn",
7636
- message: `Re-drive: message ${message.id.slice(0, 8)} has null/unparseable processing_started_at (${String(message.processing_started_at)}) \u2014 this is expected when OpenCode accepted the turn but never started it; anchoring the watcher's absolute-age ceiling to now`,
7667
+ message: `Re-drive: message ${message.id.slice(0, 8)} has no usable processing_started_at (${String(message.processing_started_at)}) \u2014 anchoring the watcher's absolute-age ceiling to now`,
7637
7668
  conversation_id: conv.id,
7638
7669
  message_id: message.id
7639
7670
  });
@@ -7662,17 +7693,19 @@ var ChannelDriver = class _ChannelDriver {
7662
7693
  return bound === "abandoned" ? "abandoned" : "unresolved";
7663
7694
  }
7664
7695
  this.clearRedriveUnresolved(message.id);
7665
- this.registerReadopted(conv, sessionId, message, ocId ?? "", anchorMs);
7696
+ this.registerReadopted(conv, sessionId, message, ocId ?? "", anchorMs, this.now());
7666
7697
  this.dispatched.add(message.id);
7667
7698
  this.readopted.add(message.id);
7668
- this.ensureWatcherRunning(sessionId);
7699
+ const watcherState = this.ensureWatcherRunning(sessionId);
7669
7700
  const watchedForMs = this.now() - anchorMs;
7670
7701
  void this.postSignal(conv.id, message.id, "redrive_reattached", {
7671
- watched_for_ms: watchedForMs
7702
+ watched_for_ms: watchedForMs,
7703
+ run_state: state,
7704
+ watcher_state: watcherState
7672
7705
  });
7673
7706
  this.log({
7674
7707
  level: "warn",
7675
- message: `Re-drive: message ${message.id.slice(0, 8)} (session ${sessionId.slice(0, 8)}) was wrongly reclaimed to pending while its turn was still running (watched ${watchedForMs}ms) \u2014 restored to processing instead of re-dispatching`,
7708
+ message: state === "running" ? `Re-drive: message ${message.id.slice(0, 8)} (session ${sessionId.slice(0, 8)}) was wrongly reclaimed to pending while its turn was still running (watched ${watchedForMs}ms) \u2014 restored to processing instead of re-dispatching` : `Re-drive: message ${message.id.slice(0, 8)} (session ${sessionId.slice(0, 8)}) was reclaimed to pending while its own turn had not started and the session was busy (watched ${watchedForMs}ms) \u2014 restored to processing instead of re-dispatching to avoid a duplicate`,
7676
7709
  conversation_id: conv.id,
7677
7710
  message_id: message.id
7678
7711
  });
@@ -8279,6 +8312,7 @@ var ChannelDriver = class _ChannelDriver {
8279
8312
  stuckReported: false,
8280
8313
  lastAliveAt: 0,
8281
8314
  aliveInFlight: false,
8315
+ aliveWithheldSignalled: false,
8282
8316
  titleSynced: false,
8283
8317
  titleSyncInFlight: false,
8284
8318
  awaitingHumanLatched: false,
@@ -8338,9 +8372,12 @@ var ChannelDriver = class _ChannelDriver {
8338
8372
  /**
8339
8373
  * Register a RE-ADOPTED `processing` message with its session watcher
8340
8374
  * (ADR-0046, WI-4). Mirrors `registerInFlight` but anchors the give-up
8341
- * `deadline` to the row's SERVER-SIDE `processed_at` (Invariant 1), NEVER to
8342
- * `now`, so the paused/queued/unreachable cases settle on the same wall-clock a
8343
- * fresh dispatch would (10 min after `processed_at`, not 10 min from now).
8375
+ * `deadline` to the attempt's SERVER-SIDE `processed_at` (Invariant 1), while
8376
+ * `processingAnchorMs` keeps the absolute-age ceiling tied to the original turn.
8377
+ * The deadline tracks THIS attempt; the ceiling tracks THE TURN. For a fresh
8378
+ * dispatch both anchors are `now`; a restart re-adopt passes the server's
8379
+ * `processed_at` for both, while a re-drive passes the newly stamped attempt time
8380
+ * only for the deadline.
8344
8381
  *
8345
8382
  * This re-attaches into the SAME watcher, so the ADR-0047 progressing-vs-paused
8346
8383
  * give-up (`serviceInFlightMessage`) applies unchanged: a re-adopted turn
@@ -8361,7 +8398,7 @@ var ChannelDriver = class _ChannelDriver {
8361
8398
  * server already flipped to `processing`; the running/done transitions still
8362
8399
  * fire from the watcher's normal branches.
8363
8400
  */
8364
- registerReadopted(conv, sessionId, message, opencodeMessageId, processedAtMs) {
8401
+ registerReadopted(conv, sessionId, message, opencodeMessageId, processedAtMs, watchDeadlineAnchorMs) {
8365
8402
  let watcher = this.watchers.get(sessionId);
8366
8403
  if (!watcher) {
8367
8404
  watcher = this.newSessionWatcher(conv);
@@ -8372,11 +8409,11 @@ var ChannelDriver = class _ChannelDriver {
8372
8409
  opencodeMessageId,
8373
8410
  message,
8374
8411
  dispatchedAt: this.now(),
8375
- // Anchor the absolute-age ceiling to the SERVER-SIDE `processed_at` (the same
8376
- // value seeding `deadline`), NOT `dispatchedAt` — so a re-adopted zombie's age
8377
- // reflects the real turn duration and the ceiling fires on the ORIGINAL turn.
8412
+ // Anchor the absolute-age ceiling to the SERVER-SIDE `processed_at`, NOT
8413
+ // `dispatchedAt` — so a re-adopted zombie's age reflects the real turn duration
8414
+ // and the ceiling fires on the ORIGINAL turn.
8378
8415
  processingAnchorMs: processedAtMs,
8379
- deadline: processedAtMs + this.pausedMaxWaitMs,
8416
+ deadline: watchDeadlineAnchorMs + this.pausedMaxWaitMs,
8380
8417
  // The server row is ALREADY `processing`; do not re-fire markProcessing.
8381
8418
  started: true,
8382
8419
  done: false,
@@ -8393,6 +8430,7 @@ var ChannelDriver = class _ChannelDriver {
8393
8430
  // with no extra `re_adopted` signal needed (folds old WI-6).
8394
8431
  lastAliveAt: 0,
8395
8432
  aliveInFlight: false,
8433
+ aliveWithheldSignalled: false,
8396
8434
  titleSynced: false,
8397
8435
  titleSyncInFlight: false,
8398
8436
  awaitingHumanLatched: false,
@@ -8509,6 +8547,8 @@ var ChannelDriver = class _ChannelDriver {
8509
8547
  * tracked on the watcher and cleared when it settles; it never rejects (fully
8510
8548
  * guarded), so a failed poll/callback can never crash the run loop — the cron
8511
8549
  * stays as the safety net.
8550
+ * Returns the branch taken so re-attach telemetry can show whether it left a
8551
+ * ticking loop behind.
8512
8552
  *
8513
8553
  * The generation started here (#1618) is captured in the `.finally` closure
8514
8554
  * so a RETIRED loop settling late — after `reconcileWatchers` has already
@@ -8522,7 +8562,7 @@ var ChannelDriver = class _ChannelDriver {
8522
8562
  level: "debug",
8523
8563
  message: `No watcher entry exists for session ${sessionId} \u2014 nothing to do`
8524
8564
  });
8525
- return;
8565
+ return "no_watcher";
8526
8566
  }
8527
8567
  this.ensureSessionErrorStream();
8528
8568
  if (watcher.loop) {
@@ -8531,7 +8571,7 @@ var ChannelDriver = class _ChannelDriver {
8531
8571
  message: `Watcher loop already running for session ${sessionId}; no-op with ${watcher.inFlight.size} message(s) in flight`,
8532
8572
  conversation_id: watcher.conv.id
8533
8573
  });
8534
- return;
8574
+ return "already_running";
8535
8575
  }
8536
8576
  if (watcher.inFlight.size === 0) {
8537
8577
  this.log({
@@ -8540,7 +8580,7 @@ var ChannelDriver = class _ChannelDriver {
8540
8580
  conversation_id: watcher.conv.id
8541
8581
  });
8542
8582
  this.watchers.delete(sessionId);
8543
- return;
8583
+ return "no_in_flight";
8544
8584
  }
8545
8585
  const generation = watcher.generation;
8546
8586
  this.log({
@@ -8556,6 +8596,7 @@ var ChannelDriver = class _ChannelDriver {
8556
8596
  }
8557
8597
  });
8558
8598
  watcher.loop = loop;
8599
+ return "started";
8559
8600
  }
8560
8601
  ensureSessionErrorStream() {
8561
8602
  if (this.sessionErrorStream || this.stopped) return;
@@ -9081,7 +9122,8 @@ var ChannelDriver = class _ChannelDriver {
9081
9122
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9082
9123
  return;
9083
9124
  }
9084
- if (activelyRunning && !inFlight.awaitingHumanLatched && !inFlight.aliveInFlight && this.now() - inFlight.lastAliveAt >= HEARTBEAT_MS) {
9125
+ const heartbeatDue = activelyRunning && !inFlight.awaitingHumanLatched && !inFlight.aliveInFlight && this.now() - inFlight.lastAliveAt >= HEARTBEAT_MS;
9126
+ if (heartbeatDue) {
9085
9127
  inFlight.aliveInFlight = true;
9086
9128
  this.log({
9087
9129
  level: "debug",
@@ -9091,7 +9133,10 @@ var ChannelDriver = class _ChannelDriver {
9091
9133
  });
9092
9134
  void this.postSignal(conv.id, inFlight.evidentMessageId, "alive").then((ok) => {
9093
9135
  inFlight.aliveInFlight = false;
9094
- if (ok) inFlight.lastAliveAt = this.now();
9136
+ if (ok) {
9137
+ inFlight.lastAliveAt = this.now();
9138
+ inFlight.aliveWithheldSignalled = false;
9139
+ }
9095
9140
  this.log({
9096
9141
  level: "debug",
9097
9142
  message: `Heartbeat POST for message ${inFlight.evidentMessageId} in session ${sessionId} completed with ok=${ok}`,
@@ -9143,6 +9188,16 @@ var ChannelDriver = class _ChannelDriver {
9143
9188
  inFlight.pausedOnPermission = false;
9144
9189
  inFlight.pausedClearConfirmed = false;
9145
9190
  }
9191
+ const watchedForMs = this.now() - Math.max(inFlight.lastAliveAt, inFlight.dispatchedAt);
9192
+ if (!heartbeatDue && !inFlight.aliveWithheldSignalled && watchedForMs >= ALIVE_WITHHELD_THRESHOLD_MS) {
9193
+ inFlight.aliveWithheldSignalled = true;
9194
+ const reason = inFlight.awaitingHumanLatched ? "awaiting_human" : inFlight.aliveInFlight ? "alive_in_flight" : "not_running";
9195
+ void this.postSignal(conv.id, inFlight.evidentMessageId, "alive_withheld", {
9196
+ reason,
9197
+ run_state: state,
9198
+ watched_for_ms: watchedForMs
9199
+ });
9200
+ }
9146
9201
  const siblingPaused = (sib) => openQuestions.has(sib.evidentMessageId) || openPermissions.has(sib.evidentMessageId) || sib.awaitingHumanLatched || sib.pausedOnQuestion || sib.pausedOnPermission;
9147
9202
  const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
9148
9203
  (sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
@@ -9153,15 +9208,15 @@ var ChannelDriver = class _ChannelDriver {
9153
9208
  const attempts = this.recordNeverStartedAttempt(id, sessionId);
9154
9209
  if (attempts >= MAX_NEVER_STARTED_ATTEMPTS) {
9155
9210
  inFlight.neverStartedResolved = true;
9156
- const watchedForMs = this.now() - inFlight.dispatchedAt;
9211
+ const watchedForMs2 = this.now() - inFlight.dispatchedAt;
9157
9212
  this.log({
9158
9213
  level: "error",
9159
- message: `Message ${id.slice(0, 8)} never started after ${attempts} attempts (${watchedForMs}ms watched) \u2014 reporting the run failed; the accepted OpenCode copy was not cancelled`,
9214
+ message: `Message ${id.slice(0, 8)} never started after ${attempts} attempts (${watchedForMs2}ms watched) \u2014 reporting the run failed; the accepted OpenCode copy was not cancelled`,
9160
9215
  conversation_id: conv.id,
9161
9216
  message_id: id
9162
9217
  });
9163
9218
  void this.postSignal(conv.id, id, "never_started_failed", {
9164
- watched_for_ms: watchedForMs,
9219
+ watched_for_ms: watchedForMs2,
9165
9220
  attempts
9166
9221
  });
9167
9222
  await this.settleMessageNeverStarted(sessionId, watcher, inFlight, attempts);
@@ -9175,7 +9230,8 @@ var ChannelDriver = class _ChannelDriver {
9175
9230
  message_id: inFlight.evidentMessageId
9176
9231
  });
9177
9232
  void this.postSignal(conv.id, inFlight.evidentMessageId, "gave_up", {
9178
- watched_for_ms: this.now() - inFlight.dispatchedAt
9233
+ watched_for_ms: this.now() - inFlight.dispatchedAt,
9234
+ run_state: state
9179
9235
  });
9180
9236
  this.recordReleasedOpencodeId(
9181
9237
  inFlight.evidentMessageId,
@@ -9632,7 +9688,8 @@ var ChannelDriver = class _ChannelDriver {
9632
9688
  if ((state === "running" || state === "queued") && ocId) {
9633
9689
  const conv = this.convForRow(sessionId, row);
9634
9690
  const message = this.queuedMessageForRow(row);
9635
- this.registerReadopted(conv, sessionId, message, ocId, this.processedAtMs(row));
9691
+ const processedAtMs = this.processedAtMs(row);
9692
+ this.registerReadopted(conv, sessionId, message, ocId, processedAtMs, processedAtMs);
9636
9693
  this.dispatched.add(row.id);
9637
9694
  this.readopted.add(row.id);
9638
9695
  this.ensureWatcherRunning(sessionId);
@@ -9927,7 +9984,15 @@ var ChannelDriver = class _ChannelDriver {
9927
9984
  return;
9928
9985
  }
9929
9986
  this.unconfirmedDispatchFailures.delete(row.id);
9930
- this.registerReadopted(readoptConv, sessionId, readoptMessage, ocId, this.processedAtMs(row));
9987
+ const processedAtMs = this.processedAtMs(row);
9988
+ this.registerReadopted(
9989
+ readoptConv,
9990
+ sessionId,
9991
+ readoptMessage,
9992
+ ocId,
9993
+ processedAtMs,
9994
+ processedAtMs
9995
+ );
9931
9996
  this.dispatched.add(row.id);
9932
9997
  this.readopted.add(row.id);
9933
9998
  this.awaitingReadopt.delete(row.id);
@@ -11347,25 +11412,6 @@ Port ${port} is already in use.`));
11347
11412
  import chalk6 from "chalk";
11348
11413
  import ora3 from "ora";
11349
11414
  import { select as select3 } from "@inquirer/prompts";
11350
- async function probeOpenCode2WithoutPassword(port) {
11351
- try {
11352
- const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
11353
- signal: AbortSignal.timeout(2e3)
11354
- });
11355
- if (response.status === 401) {
11356
- return { healthy: false, authFailed: true, error: "HTTP 401" };
11357
- }
11358
- if (!response.ok) {
11359
- return { healthy: false, error: `HTTP ${response.status}` };
11360
- }
11361
- return { healthy: true };
11362
- } catch (error2) {
11363
- return {
11364
- healthy: false,
11365
- error: error2 instanceof Error ? error2.message : "Unknown error"
11366
- };
11367
- }
11368
- }
11369
11415
  function unknownPasswordError(port) {
11370
11416
  return new Error(
11371
11417
  `OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
@@ -11373,7 +11419,7 @@ function unknownPasswordError(port) {
11373
11419
  }
11374
11420
  var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
11375
11421
  async function ensureOpenCode2Running(ctx) {
11376
- const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
11422
+ const initialHealth = await checkOpenCode2Health(ctx.port);
11377
11423
  if (initialHealth.authFailed) {
11378
11424
  throw unknownPasswordError(ctx.port);
11379
11425
  }