@evident-ai/cli 3.5.0 → 3.5.1-dev.107f9e4

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.0" : void 0) ?? process.env.npm_package_version ?? "unknown";
1161
+ var CLI_VERSION = (true ? "3.5.1-dev.107f9e4" : void 0) ?? process.env.npm_package_version ?? "unknown";
1162
1162
  function getCliVersion() {
1163
1163
  return CLI_VERSION;
1164
1164
  }
@@ -1620,6 +1620,9 @@ async function checkOpenCodeHealth(port) {
1620
1620
  signal: AbortSignal.timeout(2e3)
1621
1621
  // 2 second timeout
1622
1622
  });
1623
+ if (response.status === 401) {
1624
+ return { healthy: false, authFailed: true, error: "HTTP 401" };
1625
+ }
1623
1626
  if (!response.ok) {
1624
1627
  return { healthy: false, error: `HTTP ${response.status}` };
1625
1628
  }
@@ -1630,12 +1633,18 @@ async function checkOpenCodeHealth(port) {
1630
1633
  return { healthy: false, error: message };
1631
1634
  }
1632
1635
  }
1636
+ function bodyExcerpt(body) {
1637
+ const excerpt = body.replace(/\s+/g, " ").trim().slice(0, 160);
1638
+ return excerpt ? ` (body="${excerpt}")` : " (empty body)";
1639
+ }
1633
1640
  async function checkOpenCode2Health(port, password) {
1634
1641
  try {
1635
- const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
1636
- headers: {
1637
- Authorization: buildOpenCodeBasicAuthHeader(password)
1638
- },
1642
+ const headers = {};
1643
+ if (password !== void 0) {
1644
+ headers.Authorization = buildOpenCodeBasicAuthHeader(password);
1645
+ }
1646
+ const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
1647
+ headers,
1639
1648
  signal: AbortSignal.timeout(2e3)
1640
1649
  });
1641
1650
  if (response.status === 401) {
@@ -1644,8 +1653,31 @@ async function checkOpenCode2Health(port, password) {
1644
1653
  if (!response.ok) {
1645
1654
  return { healthy: false, error: `HTTP ${response.status}` };
1646
1655
  }
1647
- const data = await response.json().catch(() => ({}));
1648
- return { healthy: true, version: data.version };
1656
+ const body = await response.text();
1657
+ const excerpt = bodyExcerpt(body);
1658
+ let data;
1659
+ try {
1660
+ data = JSON.parse(body);
1661
+ } catch (error2) {
1662
+ return {
1663
+ healthy: false,
1664
+ error: `Invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
1665
+ };
1666
+ }
1667
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
1668
+ return {
1669
+ healthy: false,
1670
+ error: `Invalid health response${excerpt}: expected healthy=true`
1671
+ };
1672
+ }
1673
+ const health = data;
1674
+ if (health.healthy !== true) {
1675
+ return {
1676
+ healthy: false,
1677
+ error: `Invalid health response${excerpt}: expected healthy=true`
1678
+ };
1679
+ }
1680
+ return typeof health.version === "string" ? { healthy: true, version: health.version } : { healthy: true };
1649
1681
  } catch (error2) {
1650
1682
  const message = error2 instanceof Error ? error2.message : "Unknown error";
1651
1683
  return { healthy: false, error: message };
@@ -1655,7 +1687,7 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
1655
1687
  const startTime = Date.now();
1656
1688
  while (Date.now() - startTime < timeoutMs) {
1657
1689
  const health = await checkOpenCodeHealth(port);
1658
- if (health.healthy) {
1690
+ if (health.healthy || health.authFailed) {
1659
1691
  return health;
1660
1692
  }
1661
1693
  await new Promise((resolve4) => setTimeout(resolve4, 1e3));
@@ -4282,6 +4314,9 @@ function isSessionDbProvenanceRecord(value) {
4282
4314
  return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
4283
4315
  }
4284
4316
 
4317
+ // src/lib/opencode/opencode2-package.ts
4318
+ var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
4319
+
4285
4320
  // src/lib/opencode/opencode-version-gate.ts
4286
4321
  var QUEUE_VALIDATED_OPENCODE_VERSIONS = ["1.17.11", "1.18.3"];
4287
4322
  function isQueueValidatedVersion(version2) {
@@ -4572,7 +4607,8 @@ async function startOpenCode(port, options = {}) {
4572
4607
  const child = spawn3(command, args, {
4573
4608
  detached: true,
4574
4609
  stdio: options.inheritStdio ? "inherit" : "ignore",
4575
- cwd: process.cwd()
4610
+ cwd: process.cwd(),
4611
+ env: { ...process.env, OPENCODE_SERVER_PASSWORD: void 0 }
4576
4612
  });
4577
4613
  return child;
4578
4614
  }
@@ -4588,7 +4624,7 @@ async function startOpenCode2(port, options = {}) {
4588
4624
  args = [
4589
4625
  "-y",
4590
4626
  "-p",
4591
- "@opencode-ai/cli@beta",
4627
+ OPENCODE2_PACKAGE_SPEC,
4592
4628
  "--",
4593
4629
  "opencode2",
4594
4630
  "serve",
@@ -4665,7 +4701,7 @@ async function promptOpenCodeInstall(interactive) {
4665
4701
  npm: "npm install -g opencode-ai",
4666
4702
  curl: "curl -fsSL https://opencode.ai/install.sh | sh",
4667
4703
  v2: {
4668
- npm: "npm install -g @opencode-ai/cli@beta",
4704
+ npm: `npm install -g ${OPENCODE2_PACKAGE_SPEC}`,
4669
4705
  curl: "curl -fsSL https://opencode.ai/v2/install | bash"
4670
4706
  }
4671
4707
  }
@@ -6399,6 +6435,7 @@ var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
6399
6435
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
6400
6436
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
6401
6437
  var HEARTBEAT_MS = 6e4;
6438
+ var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
6402
6439
  var ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1e3;
6403
6440
  var B2_ABANDONMENT_MIN_PINNED_MS = 3 * 6e4;
6404
6441
  var AMBIGUOUS_FINISH_MAX_PINNED_MS = 3 * 6e4;
@@ -7603,7 +7640,7 @@ var ChannelDriver = class _ChannelDriver {
7603
7640
  return "dispatch";
7604
7641
  }
7605
7642
  }
7606
- return this.reattachRedrive(conv, sessionId, message, ocId);
7643
+ return this.reattachRedrive(conv, sessionId, message, ocId, state);
7607
7644
  }
7608
7645
  if (ongoing === false) {
7609
7646
  if (state === "running" && isAmbiguousFinishPinnedRunning(messages, ocId ?? "")) {
@@ -7620,11 +7657,12 @@ var ChannelDriver = class _ChannelDriver {
7620
7657
  return "dispatch";
7621
7658
  }
7622
7659
  /**
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.
7660
+ * The `reattached` outcome (Task 3.3): opencode's own status map says the session
7661
+ * is ongoing, so undo the false reclaim instead of starting a second turn. `state`
7662
+ * carries the correlated message's observed state so the outcome does not claim
7663
+ * that a queued message's own turn was already running.
7626
7664
  */
7627
- async reattachRedrive(conv, sessionId, message, ocId) {
7665
+ async reattachRedrive(conv, sessionId, message, ocId, state) {
7628
7666
  let anchorMs;
7629
7667
  const parsed = message.processing_started_at ? Date.parse(message.processing_started_at) : NaN;
7630
7668
  if (!Number.isNaN(parsed)) {
@@ -7633,7 +7671,7 @@ var ChannelDriver = class _ChannelDriver {
7633
7671
  anchorMs = this.now();
7634
7672
  this.log({
7635
7673
  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`,
7674
+ 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
7675
  conversation_id: conv.id,
7638
7676
  message_id: message.id
7639
7677
  });
@@ -7662,17 +7700,19 @@ var ChannelDriver = class _ChannelDriver {
7662
7700
  return bound === "abandoned" ? "abandoned" : "unresolved";
7663
7701
  }
7664
7702
  this.clearRedriveUnresolved(message.id);
7665
- this.registerReadopted(conv, sessionId, message, ocId ?? "", anchorMs);
7703
+ this.registerReadopted(conv, sessionId, message, ocId ?? "", anchorMs, this.now());
7666
7704
  this.dispatched.add(message.id);
7667
7705
  this.readopted.add(message.id);
7668
- this.ensureWatcherRunning(sessionId);
7706
+ const watcherState = this.ensureWatcherRunning(sessionId);
7669
7707
  const watchedForMs = this.now() - anchorMs;
7670
7708
  void this.postSignal(conv.id, message.id, "redrive_reattached", {
7671
- watched_for_ms: watchedForMs
7709
+ watched_for_ms: watchedForMs,
7710
+ run_state: state,
7711
+ watcher_state: watcherState
7672
7712
  });
7673
7713
  this.log({
7674
7714
  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`,
7715
+ 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
7716
  conversation_id: conv.id,
7677
7717
  message_id: message.id
7678
7718
  });
@@ -8279,6 +8319,7 @@ var ChannelDriver = class _ChannelDriver {
8279
8319
  stuckReported: false,
8280
8320
  lastAliveAt: 0,
8281
8321
  aliveInFlight: false,
8322
+ aliveWithheldSignalled: false,
8282
8323
  titleSynced: false,
8283
8324
  titleSyncInFlight: false,
8284
8325
  awaitingHumanLatched: false,
@@ -8338,9 +8379,12 @@ var ChannelDriver = class _ChannelDriver {
8338
8379
  /**
8339
8380
  * Register a RE-ADOPTED `processing` message with its session watcher
8340
8381
  * (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).
8382
+ * `deadline` to the attempt's SERVER-SIDE `processed_at` (Invariant 1), while
8383
+ * `processingAnchorMs` keeps the absolute-age ceiling tied to the original turn.
8384
+ * The deadline tracks THIS attempt; the ceiling tracks THE TURN. For a fresh
8385
+ * dispatch both anchors are `now`; a restart re-adopt passes the server's
8386
+ * `processed_at` for both, while a re-drive passes the newly stamped attempt time
8387
+ * only for the deadline.
8344
8388
  *
8345
8389
  * This re-attaches into the SAME watcher, so the ADR-0047 progressing-vs-paused
8346
8390
  * give-up (`serviceInFlightMessage`) applies unchanged: a re-adopted turn
@@ -8361,7 +8405,7 @@ var ChannelDriver = class _ChannelDriver {
8361
8405
  * server already flipped to `processing`; the running/done transitions still
8362
8406
  * fire from the watcher's normal branches.
8363
8407
  */
8364
- registerReadopted(conv, sessionId, message, opencodeMessageId, processedAtMs) {
8408
+ registerReadopted(conv, sessionId, message, opencodeMessageId, processedAtMs, watchDeadlineAnchorMs) {
8365
8409
  let watcher = this.watchers.get(sessionId);
8366
8410
  if (!watcher) {
8367
8411
  watcher = this.newSessionWatcher(conv);
@@ -8372,11 +8416,11 @@ var ChannelDriver = class _ChannelDriver {
8372
8416
  opencodeMessageId,
8373
8417
  message,
8374
8418
  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.
8419
+ // Anchor the absolute-age ceiling to the SERVER-SIDE `processed_at`, NOT
8420
+ // `dispatchedAt` — so a re-adopted zombie's age reflects the real turn duration
8421
+ // and the ceiling fires on the ORIGINAL turn.
8378
8422
  processingAnchorMs: processedAtMs,
8379
- deadline: processedAtMs + this.pausedMaxWaitMs,
8423
+ deadline: watchDeadlineAnchorMs + this.pausedMaxWaitMs,
8380
8424
  // The server row is ALREADY `processing`; do not re-fire markProcessing.
8381
8425
  started: true,
8382
8426
  done: false,
@@ -8393,6 +8437,7 @@ var ChannelDriver = class _ChannelDriver {
8393
8437
  // with no extra `re_adopted` signal needed (folds old WI-6).
8394
8438
  lastAliveAt: 0,
8395
8439
  aliveInFlight: false,
8440
+ aliveWithheldSignalled: false,
8396
8441
  titleSynced: false,
8397
8442
  titleSyncInFlight: false,
8398
8443
  awaitingHumanLatched: false,
@@ -8509,6 +8554,8 @@ var ChannelDriver = class _ChannelDriver {
8509
8554
  * tracked on the watcher and cleared when it settles; it never rejects (fully
8510
8555
  * guarded), so a failed poll/callback can never crash the run loop — the cron
8511
8556
  * stays as the safety net.
8557
+ * Returns the branch taken so re-attach telemetry can show whether it left a
8558
+ * ticking loop behind.
8512
8559
  *
8513
8560
  * The generation started here (#1618) is captured in the `.finally` closure
8514
8561
  * so a RETIRED loop settling late — after `reconcileWatchers` has already
@@ -8522,7 +8569,7 @@ var ChannelDriver = class _ChannelDriver {
8522
8569
  level: "debug",
8523
8570
  message: `No watcher entry exists for session ${sessionId} \u2014 nothing to do`
8524
8571
  });
8525
- return;
8572
+ return "no_watcher";
8526
8573
  }
8527
8574
  this.ensureSessionErrorStream();
8528
8575
  if (watcher.loop) {
@@ -8531,7 +8578,7 @@ var ChannelDriver = class _ChannelDriver {
8531
8578
  message: `Watcher loop already running for session ${sessionId}; no-op with ${watcher.inFlight.size} message(s) in flight`,
8532
8579
  conversation_id: watcher.conv.id
8533
8580
  });
8534
- return;
8581
+ return "already_running";
8535
8582
  }
8536
8583
  if (watcher.inFlight.size === 0) {
8537
8584
  this.log({
@@ -8540,7 +8587,7 @@ var ChannelDriver = class _ChannelDriver {
8540
8587
  conversation_id: watcher.conv.id
8541
8588
  });
8542
8589
  this.watchers.delete(sessionId);
8543
- return;
8590
+ return "no_in_flight";
8544
8591
  }
8545
8592
  const generation = watcher.generation;
8546
8593
  this.log({
@@ -8556,6 +8603,7 @@ var ChannelDriver = class _ChannelDriver {
8556
8603
  }
8557
8604
  });
8558
8605
  watcher.loop = loop;
8606
+ return "started";
8559
8607
  }
8560
8608
  ensureSessionErrorStream() {
8561
8609
  if (this.sessionErrorStream || this.stopped) return;
@@ -9081,7 +9129,8 @@ var ChannelDriver = class _ChannelDriver {
9081
9129
  this.removeInFlight(watcher, inFlight.evidentMessageId);
9082
9130
  return;
9083
9131
  }
9084
- if (activelyRunning && !inFlight.awaitingHumanLatched && !inFlight.aliveInFlight && this.now() - inFlight.lastAliveAt >= HEARTBEAT_MS) {
9132
+ const heartbeatDue = activelyRunning && !inFlight.awaitingHumanLatched && !inFlight.aliveInFlight && this.now() - inFlight.lastAliveAt >= HEARTBEAT_MS;
9133
+ if (heartbeatDue) {
9085
9134
  inFlight.aliveInFlight = true;
9086
9135
  this.log({
9087
9136
  level: "debug",
@@ -9091,7 +9140,10 @@ var ChannelDriver = class _ChannelDriver {
9091
9140
  });
9092
9141
  void this.postSignal(conv.id, inFlight.evidentMessageId, "alive").then((ok) => {
9093
9142
  inFlight.aliveInFlight = false;
9094
- if (ok) inFlight.lastAliveAt = this.now();
9143
+ if (ok) {
9144
+ inFlight.lastAliveAt = this.now();
9145
+ inFlight.aliveWithheldSignalled = false;
9146
+ }
9095
9147
  this.log({
9096
9148
  level: "debug",
9097
9149
  message: `Heartbeat POST for message ${inFlight.evidentMessageId} in session ${sessionId} completed with ok=${ok}`,
@@ -9143,6 +9195,16 @@ var ChannelDriver = class _ChannelDriver {
9143
9195
  inFlight.pausedOnPermission = false;
9144
9196
  inFlight.pausedClearConfirmed = false;
9145
9197
  }
9198
+ const watchedForMs = this.now() - Math.max(inFlight.lastAliveAt, inFlight.dispatchedAt);
9199
+ if (!heartbeatDue && !inFlight.aliveWithheldSignalled && watchedForMs >= ALIVE_WITHHELD_THRESHOLD_MS) {
9200
+ inFlight.aliveWithheldSignalled = true;
9201
+ const reason = inFlight.awaitingHumanLatched ? "awaiting_human" : inFlight.aliveInFlight ? "alive_in_flight" : "not_running";
9202
+ void this.postSignal(conv.id, inFlight.evidentMessageId, "alive_withheld", {
9203
+ reason,
9204
+ run_state: state,
9205
+ watched_for_ms: watchedForMs
9206
+ });
9207
+ }
9146
9208
  const siblingPaused = (sib) => openQuestions.has(sib.evidentMessageId) || openPermissions.has(sib.evidentMessageId) || sib.awaitingHumanLatched || sib.pausedOnQuestion || sib.pausedOnPermission;
9147
9209
  const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
9148
9210
  (sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
@@ -9153,15 +9215,15 @@ var ChannelDriver = class _ChannelDriver {
9153
9215
  const attempts = this.recordNeverStartedAttempt(id, sessionId);
9154
9216
  if (attempts >= MAX_NEVER_STARTED_ATTEMPTS) {
9155
9217
  inFlight.neverStartedResolved = true;
9156
- const watchedForMs = this.now() - inFlight.dispatchedAt;
9218
+ const watchedForMs2 = this.now() - inFlight.dispatchedAt;
9157
9219
  this.log({
9158
9220
  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`,
9221
+ 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
9222
  conversation_id: conv.id,
9161
9223
  message_id: id
9162
9224
  });
9163
9225
  void this.postSignal(conv.id, id, "never_started_failed", {
9164
- watched_for_ms: watchedForMs,
9226
+ watched_for_ms: watchedForMs2,
9165
9227
  attempts
9166
9228
  });
9167
9229
  await this.settleMessageNeverStarted(sessionId, watcher, inFlight, attempts);
@@ -9175,7 +9237,8 @@ var ChannelDriver = class _ChannelDriver {
9175
9237
  message_id: inFlight.evidentMessageId
9176
9238
  });
9177
9239
  void this.postSignal(conv.id, inFlight.evidentMessageId, "gave_up", {
9178
- watched_for_ms: this.now() - inFlight.dispatchedAt
9240
+ watched_for_ms: this.now() - inFlight.dispatchedAt,
9241
+ run_state: state
9179
9242
  });
9180
9243
  this.recordReleasedOpencodeId(
9181
9244
  inFlight.evidentMessageId,
@@ -9632,7 +9695,8 @@ var ChannelDriver = class _ChannelDriver {
9632
9695
  if ((state === "running" || state === "queued") && ocId) {
9633
9696
  const conv = this.convForRow(sessionId, row);
9634
9697
  const message = this.queuedMessageForRow(row);
9635
- this.registerReadopted(conv, sessionId, message, ocId, this.processedAtMs(row));
9698
+ const processedAtMs = this.processedAtMs(row);
9699
+ this.registerReadopted(conv, sessionId, message, ocId, processedAtMs, processedAtMs);
9636
9700
  this.dispatched.add(row.id);
9637
9701
  this.readopted.add(row.id);
9638
9702
  this.ensureWatcherRunning(sessionId);
@@ -9927,7 +9991,15 @@ var ChannelDriver = class _ChannelDriver {
9927
9991
  return;
9928
9992
  }
9929
9993
  this.unconfirmedDispatchFailures.delete(row.id);
9930
- this.registerReadopted(readoptConv, sessionId, readoptMessage, ocId, this.processedAtMs(row));
9994
+ const processedAtMs = this.processedAtMs(row);
9995
+ this.registerReadopted(
9996
+ readoptConv,
9997
+ sessionId,
9998
+ readoptMessage,
9999
+ ocId,
10000
+ processedAtMs,
10001
+ processedAtMs
10002
+ );
9931
10003
  this.dispatched.add(row.id);
9932
10004
  this.readopted.add(row.id);
9933
10005
  this.awaitingReadopt.delete(row.id);
@@ -11214,8 +11286,16 @@ function checkNonInteractivePortConflict(port, isPortInUseFn) {
11214
11286
  );
11215
11287
  }
11216
11288
  }
11289
+ function unknownPasswordError(port) {
11290
+ return new Error(
11291
+ `OpenCode is already running on port ${port}, but this runner cannot authenticate to it. It may have been started with OPENCODE_SERVER_PASSWORD set in its environment. Free the port or pass --port.`
11292
+ );
11293
+ }
11217
11294
  async function ensureOpenCodeRunning(ctx) {
11218
11295
  const healthCheck = await checkOpenCodeHealth(ctx.port);
11296
+ if (healthCheck.authFailed) {
11297
+ throw unknownPasswordError(ctx.port);
11298
+ }
11219
11299
  if (healthCheck.healthy) {
11220
11300
  return {
11221
11301
  port: ctx.port,
@@ -11266,6 +11346,14 @@ async function ensureOpenCodeRunning(ctx) {
11266
11346
  const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
11267
11347
  const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
11268
11348
  if (!health.healthy) {
11349
+ if (health.authFailed) {
11350
+ return {
11351
+ port: ctx.port,
11352
+ process: proc,
11353
+ version: null,
11354
+ notReadyReason: "it is answering with an authentication challenge, likely because OPENCODE_SERVER_PASSWORD is set in the environment of the process that started it; a longer start timeout will not resolve this"
11355
+ };
11356
+ }
11269
11357
  return {
11270
11358
  port: ctx.port,
11271
11359
  process: proc,
@@ -11335,6 +11423,9 @@ Port ${port} is already in use.`));
11335
11423
  const health = await waitForOpenCodeHealth(port, INTERACTIVE_START_TIMEOUT_MS);
11336
11424
  if (!health.healthy) {
11337
11425
  spinner.fail("Failed to start OpenCode");
11426
+ if (health.authFailed) {
11427
+ throw unknownPasswordError(port);
11428
+ }
11338
11429
  throw new Error("OpenCode failed to start");
11339
11430
  }
11340
11431
  spinner.stop();
@@ -11347,35 +11438,16 @@ Port ${port} is already in use.`));
11347
11438
  import chalk6 from "chalk";
11348
11439
  import ora3 from "ora";
11349
11440
  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
- function unknownPasswordError(port) {
11441
+ function unknownPasswordError2(port) {
11370
11442
  return new Error(
11371
11443
  `OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
11372
11444
  );
11373
11445
  }
11374
11446
  var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
11375
11447
  async function ensureOpenCode2Running(ctx) {
11376
- const initialHealth = await probeOpenCode2WithoutPassword(ctx.port);
11448
+ const initialHealth = await checkOpenCode2Health(ctx.port);
11377
11449
  if (initialHealth.authFailed) {
11378
- throw unknownPasswordError(ctx.port);
11450
+ throw unknownPasswordError2(ctx.port);
11379
11451
  }
11380
11452
  if (initialHealth.healthy) {
11381
11453
  return {
@@ -11388,7 +11460,7 @@ async function ensureOpenCode2Running(ctx) {
11388
11460
  }
11389
11461
  if (!isOpenCode2Installed()) {
11390
11462
  throw new Error(
11391
- "OpenCode V2 (opencode2) is not installed. Install it with: npm install -g @opencode-ai/cli@beta"
11463
+ `OpenCode V2 (opencode2) is not installed. Install it with: npm install -g ${OPENCODE2_PACKAGE_SPEC}`
11392
11464
  );
11393
11465
  }
11394
11466
  let port = ctx.port;
@@ -13322,6 +13394,12 @@ async function run(options) {
13322
13394
  "Skipping session-DB restore: OpenCode is already serving this database",
13323
13395
  "debug"
13324
13396
  );
13397
+ } else if (health.authFailed) {
13398
+ log2(
13399
+ state,
13400
+ "Skipping session-DB restore: OpenCode answered with an authentication challenge; assuming this database is in use",
13401
+ "debug"
13402
+ );
13325
13403
  } else {
13326
13404
  const result = await restoreAndVerifySessionDb({
13327
13405
  dbPath: sessionDbPath(),