@evident-ai/cli 3.1.1-dev.14c6359 → 3.1.1-dev.2f78b33

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/README.md CHANGED
@@ -85,6 +85,7 @@ Options:
85
85
 
86
86
  - `-a, --agent [id]` — Runner ID to connect to. Optional when `EVIDENT_AGENT_KEY`
87
87
  is set (the runner is then resolved automatically from the key).
88
+ - `--runner [id]` — Alias for `--agent` (preferred name; wins if both are given).
88
89
  - `-p, --port <port>` — OpenCode port (default: `4096`).
89
90
  - `--log-level <level>` — Log verbosity: `debug | info | warn | error` (default:
90
91
  `info`). Env: `EVIDENT_LOG_LEVEL`.
@@ -105,15 +106,18 @@ targets the **production** Evident platform by default.
105
106
 
106
107
  ## Environment variables
107
108
 
108
- - `EVIDENT_AGENT_KEY` — A runner key. When set, `evident run` authenticates as
109
- that runner and resolves the runner ID automatically, so `--agent` is not
110
- required. Ideal for CI/CD.
109
+ - `EVIDENT_RUNNER_KEY` — A runner key. When set, `evident run` authenticates as
110
+ that runner and resolves the runner ID automatically, so `--runner`/`--agent`
111
+ is not required. Ideal for CI/CD. Preferred name; wins over `EVIDENT_AGENT_KEY`
112
+ if both are set.
113
+ - `EVIDENT_AGENT_KEY` — Alias for `EVIDENT_RUNNER_KEY` (still fully supported).
111
114
  - `EVIDENT_TOKEN` — A user token used for authentication (alternative to a
112
115
  keychain login from `evident login`).
113
116
  - `EVIDENT_API_URL` — Override the API base URL (equivalent to `--endpoint`).
114
117
  - `EVIDENT_TUNNEL_URL` — Override the tunnel relay URL (equivalent to `--tunnel`).
115
118
 
116
- Authentication precedence for `run`: `EVIDENT_AGENT_KEY` → `EVIDENT_TOKEN` →
119
+ Authentication precedence for `run`: `EVIDENT_RUNNER_KEY`/`EVIDENT_AGENT_KEY`
120
+ (tied; `EVIDENT_RUNNER_KEY` wins if both are set) → `EVIDENT_TOKEN` →
117
121
  credentials stored by `evident login`. For the URL flags, an explicit
118
122
  `--endpoint` / `--tunnel` flag wins over the matching environment variable, which
119
123
  in turn overrides the production default.
@@ -150,6 +154,8 @@ For CI or unattended use, set `EVIDENT_AGENT_KEY` and omit `--agent`:
150
154
  EVIDENT_AGENT_KEY=<agent-key> evident run --idle-timeout 30
151
155
  ```
152
156
 
157
+ (`EVIDENT_RUNNER_KEY` is equivalent and the preferred name — use whichever you like.)
158
+
153
159
  ## How it works
154
160
 
155
161
  ```
package/dist/index.js CHANGED
@@ -499,6 +499,12 @@ function log(level, event, fields) {
499
499
  );
500
500
  }
501
501
  }
502
+ function errorFields(err) {
503
+ if (err instanceof Error) {
504
+ return { error: err.message, error_name: err.name };
505
+ }
506
+ return { error: String(err) };
507
+ }
502
508
  function stripQuery(url) {
503
509
  try {
504
510
  return new URL(url).pathname;
@@ -645,14 +651,27 @@ var EventTypes = {
645
651
  // CLI lifecycle
646
652
  CLI_STARTED: "cli.started",
647
653
  CLI_COMMAND: "cli.command",
648
- CLI_ERROR: "cli.error"
654
+ CLI_ERROR: "cli.error",
655
+ // Deprecation telemetry (#412) — usage of the old `--agent`/`EVIDENT_AGENT_KEY`
656
+ // names instead of the preferred `--runner`/`EVIDENT_RUNNER_KEY` (#409).
657
+ DEPRECATED_AGENT_FLAG_USED: "cli.deprecated_agent_flag_used",
658
+ DEPRECATED_AGENT_KEY_ENV_USED: "cli.deprecated_agent_key_env_used"
649
659
  };
650
660
 
651
661
  // src/lib/auth.ts
652
662
  async function getAuthCredentials() {
663
+ const runnerKey = process.env.EVIDENT_RUNNER_KEY;
653
664
  const agentKey = process.env.EVIDENT_AGENT_KEY;
665
+ if (runnerKey) {
666
+ return {
667
+ token: runnerKey,
668
+ authType: "agent_key",
669
+ keySource: "runner_key",
670
+ notice: agentKey ? "Both EVIDENT_RUNNER_KEY and EVIDENT_AGENT_KEY are set; using EVIDENT_RUNNER_KEY." : void 0
671
+ };
672
+ }
654
673
  if (agentKey) {
655
- return { token: agentKey, authType: "agent_key" };
674
+ return { token: agentKey, authType: "agent_key", keySource: "agent_key" };
656
675
  }
657
676
  const userToken = process.env.EVIDENT_TOKEN;
658
677
  if (userToken) {
@@ -721,7 +740,7 @@ function buildOpenCodeVersionWarning(version2) {
721
740
  if (isQueueValidatedVersion(version2)) return null;
722
741
  const detected = version2 ? `v${version2}` : "unknown";
723
742
  const validated = QUEUE_VALIDATED_OPENCODE_VERSIONS.map((v) => `v${v}`).join(", ");
724
- return `Warning: opencode ${detected} is not a queue-validated version (validated: ${validated}). Native message queuing \u2014 which channel (Slack/WhatsApp) message handling relies on \u2014 is unverified on this version; queued/follow-up messages may behave unexpectedly. Continuing anyway. Bumping the validated set requires re-running the queue validation.`;
743
+ return `Warning: opencode ${detected} is not a queue-validated version (validated: ${validated}). Native message queuing \u2014 which channel (Slack) message handling relies on \u2014 is unverified on this version; queued/follow-up messages may behave unexpectedly. Continuing anyway. Bumping the validated set requires re-running the queue validation.`;
725
744
  }
726
745
 
727
746
  // src/lib/opencode/process.ts
@@ -1013,6 +1032,12 @@ async function promptOpenCodeInstall(interactive) {
1013
1032
  return action;
1014
1033
  }
1015
1034
 
1035
+ // src/lib/opencode/provider-check.ts
1036
+ function buildNoProviderWarning(hasProvider) {
1037
+ if (hasProvider !== false) return null;
1038
+ return "Warning: opencode has no authenticated model provider configured, so it won't be able to answer prompts. Run `opencode auth login` to set one up (see https://opencode.ai for details).";
1039
+ }
1040
+
1016
1041
  // src/lib/opencode/session.ts
1017
1042
  function opencodeBase(port) {
1018
1043
  return `http://127.0.0.1:${port}`;
@@ -1215,6 +1240,11 @@ async function getModelAttachmentCapability(port, model) {
1215
1240
  }
1216
1241
  const entry = provider.models[modelId];
1217
1242
  if (!entry || typeof entry !== "object") return null;
1243
+ if (entry.capabilities && typeof entry.capabilities === "object") {
1244
+ if (typeof entry.capabilities.attachment === "boolean") {
1245
+ return entry.capabilities.attachment;
1246
+ }
1247
+ }
1218
1248
  return typeof entry.attachment === "boolean" ? entry.attachment : null;
1219
1249
  } catch (err) {
1220
1250
  console.error(
@@ -1243,6 +1273,16 @@ async function buildFileParts(attachments, capable) {
1243
1273
  );
1244
1274
  dataUrl = null;
1245
1275
  }
1276
+ if (dataUrl !== null && typeof dataUrl === "object") {
1277
+ outcomes.push({
1278
+ index: a.index,
1279
+ mime: a.mime,
1280
+ filename: a.filename,
1281
+ status: "failed",
1282
+ reason: "needs_reauth"
1283
+ });
1284
+ continue;
1285
+ }
1246
1286
  if (dataUrl == null) {
1247
1287
  outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: "failed" });
1248
1288
  continue;
@@ -1471,6 +1511,37 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
1471
1511
  (m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
1472
1512
  );
1473
1513
  }
1514
+ async function hasAnyConfiguredProvider(port) {
1515
+ try {
1516
+ const res = await fetch(`${opencodeBase(port)}/config/providers`);
1517
+ if (!res.ok) {
1518
+ console.error(
1519
+ `[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
1520
+ );
1521
+ return null;
1522
+ }
1523
+ const body = await res.json();
1524
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
1525
+ console.error(
1526
+ `[hasAnyConfiguredProvider] GET /config/providers body was not a plain object (port ${port})`
1527
+ );
1528
+ return null;
1529
+ }
1530
+ const defaults2 = body.default;
1531
+ if (!defaults2 || typeof defaults2 !== "object" || Array.isArray(defaults2)) {
1532
+ console.error(
1533
+ `[hasAnyConfiguredProvider] GET /config/providers body had no \`default\` object (port ${port})`
1534
+ );
1535
+ return null;
1536
+ }
1537
+ return Object.keys(defaults2).length > 0;
1538
+ } catch (err) {
1539
+ console.error(
1540
+ `[hasAnyConfiguredProvider] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
1541
+ );
1542
+ return null;
1543
+ }
1544
+ }
1474
1545
 
1475
1546
  // src/lib/opencode/session-cleanup.ts
1476
1547
  var DURATION_UNIT_MS = {
@@ -1629,10 +1700,11 @@ var StreamForwarder = class {
1629
1700
  * Abort every in-flight stream (e.g. on WebSocket close).
1630
1701
  */
1631
1702
  abortAll() {
1632
- for (const stream of this.inflight.values()) {
1703
+ for (const [sid, stream] of this.inflight.entries()) {
1633
1704
  try {
1634
1705
  stream.abort();
1635
- } catch {
1706
+ } catch (err) {
1707
+ log("error", "forwarder_abort_failed", { sid, ...errorFields(err) });
1636
1708
  }
1637
1709
  }
1638
1710
  this.inflight.clear();
@@ -1782,7 +1854,6 @@ function connectTunnel(options) {
1782
1854
  onConnected,
1783
1855
  onDisconnected,
1784
1856
  onError,
1785
- onRequest,
1786
1857
  onResponse,
1787
1858
  onInfo,
1788
1859
  onDrainPing
@@ -1795,18 +1866,8 @@ function connectTunnel(options) {
1795
1866
  Authorization: authHeader
1796
1867
  }
1797
1868
  });
1798
- const streamStartTimes = /* @__PURE__ */ new Map();
1799
1869
  const forwarder = new StreamForwarder(ws, port, {
1800
- onOpen: (sid, method, path) => {
1801
- if (path === TUNNEL_DRAIN_PING_PATH) return;
1802
- streamStartTimes.set(sid, Date.now());
1803
- onRequest?.(method, path, sid);
1804
- },
1805
- onHead: (sid, status) => {
1806
- const startedAt = streamStartTimes.get(sid);
1807
- streamStartTimes.delete(sid);
1808
- onResponse?.(status, startedAt ? Date.now() - startedAt : 0, sid);
1809
- },
1870
+ onHead: () => onResponse?.(),
1810
1871
  onDrainPing: () => onDrainPing?.()
1811
1872
  });
1812
1873
  const connectionTimeout = setTimeout(() => {
@@ -1882,7 +1943,6 @@ function connectTunnel(options) {
1882
1943
  ws.on("close", (code, reason) => {
1883
1944
  const reasonStr = reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
1884
1945
  forwarder.abortAll();
1885
- streamStartTimes.clear();
1886
1946
  onDisconnected?.(code, reasonStr);
1887
1947
  });
1888
1948
  });
@@ -1917,7 +1977,11 @@ var RunnerConnection = class {
1917
1977
  if (this.connection) {
1918
1978
  try {
1919
1979
  this.connection.close();
1920
- } catch {
1980
+ } catch (err) {
1981
+ log("error", "runner_connection_close_failed", {
1982
+ agent_id: this.resolvedAgentId,
1983
+ ...errorFields(err)
1984
+ });
1921
1985
  }
1922
1986
  this.connection = null;
1923
1987
  }
@@ -2020,7 +2084,7 @@ function backoffDelay(attempt, policy) {
2020
2084
  function isRetryableStatus(status) {
2021
2085
  return status === 429 || status >= 500 && status <= 599;
2022
2086
  }
2023
- var ChannelDriver = class {
2087
+ var ChannelDriver = class _ChannelDriver {
2024
2088
  agentId;
2025
2089
  port;
2026
2090
  apiUrl;
@@ -2145,9 +2209,12 @@ var ChannelDriver = class {
2145
2209
  sessionParents = /* @__PURE__ */ new Map();
2146
2210
  /**
2147
2211
  * Per-session OpenCode title cache (#310), keyed by sessionId. Only a resolved
2148
- * NON-EMPTY name is stored (terminal — a real session name won't later un-name),
2149
- * so we do NOT re-GET `/session/:id` every tick. A missing entry = not yet
2150
- * resolved OR resolved-but-still-empty re-fetch on next need, since OpenCode
2212
+ * NON-EMPTY, non-placeholder name is stored (terminal — a real session name
2213
+ * won't later un-name), so we do NOT re-GET `/session/:id` every tick. "Non-empty"
2214
+ * excludes OpenCode's synchronous default title (see
2215
+ * `OPENCODE_DEFAULT_TITLE_PREFIX`, #549) — that placeholder is treated the same
2216
+ * as an empty title so it never latches. A missing entry = not yet resolved OR
2217
+ * resolved-but-still-empty/placeholder → re-fetch on next need, since OpenCode
2151
2218
  * names sessions asynchronously mid-turn. Driver-level (not per-watcher) so both
2152
2219
  * the watcher completion path AND the restart-recovery re-adopt path (which has
2153
2220
  * no watcher) can resolve the title.
@@ -2371,7 +2438,8 @@ var ChannelDriver = class {
2371
2438
  } catch (err) {
2372
2439
  if (err instanceof ChannelAuthError) throw err;
2373
2440
  this.dispatched.delete(message.id);
2374
- if (await sessionExists(this.port, sessionId) === false) {
2441
+ const exists = await sessionExists(this.port, sessionId);
2442
+ if (exists === false) {
2375
2443
  this.sessions.delete(conv.id);
2376
2444
  this.log({
2377
2445
  level: "warn",
@@ -2381,15 +2449,32 @@ var ChannelDriver = class {
2381
2449
  });
2382
2450
  break;
2383
2451
  }
2384
- await this.markFailed(conv.id, message.id).catch(() => {
2452
+ if (exists === null) {
2453
+ this.log({
2454
+ level: "warn",
2455
+ message: `Message ${message.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) existence could not be confirmed (opencode momentarily unreachable) \u2014 deferring this and later messages for conversation ${conv.id.slice(0, 8)} to the next tick rather than treating it as a genuine failure.`,
2456
+ conversation_id: conv.id,
2457
+ message_id: message.id
2458
+ });
2459
+ break;
2460
+ }
2461
+ const errorMessage = err instanceof Error ? err.message : String(err);
2462
+ this.sessions.delete(conv.id);
2463
+ await this.markFailed(conv.id, message.id, null, errorMessage).catch((markErr) => {
2464
+ this.log({
2465
+ level: "warn",
2466
+ message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
2467
+ conversation_id: conv.id,
2468
+ message_id: message.id
2469
+ });
2385
2470
  });
2386
2471
  this.log({
2387
2472
  level: "error",
2388
- message: `Message ${message.id.slice(0, 8)} dispatch failed: ${err instanceof Error ? err.message : String(err)}`,
2473
+ message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage}`,
2389
2474
  conversation_id: conv.id,
2390
2475
  message_id: message.id
2391
2476
  });
2392
- continue;
2477
+ break;
2393
2478
  }
2394
2479
  if (opencodeMessageId === null) {
2395
2480
  this.log({
@@ -2511,7 +2596,7 @@ var ChannelDriver = class {
2511
2596
  }
2512
2597
  /**
2513
2598
  * Fetch ONE inbound image's bytes through Evident's WI-6 endpoint
2514
- * (`GET {apiUrl}/agents/{agentId}/attachments/{messageId}/{index}`) using the
2599
+ * (`GET {apiUrl}/runners/{agentId}/attachments/{messageId}/{index}`) using the
2515
2600
  * existing authenticated fetch, and base64-encode into a
2516
2601
  * `data:<mime>;base64,<…>` URL for the opencode `file` part's `url`.
2517
2602
  *
@@ -2519,15 +2604,38 @@ var ChannelDriver = class {
2519
2604
  * (not-owned / out-of-range / deleted-at-source / workspace gone) / 413
2520
2605
  * (over-cap). On ANY non-2xx or thrown failure we return `null` so the caller
2521
2606
  * OMITS that one image and the text turn still sends — NEVER throws the turn.
2522
- * Failures are logged with context (no silent swallow).
2607
+ * A 404 body carrying `{ reason: 'needs_reauth' }` (#547 the server CONFIRMED
2608
+ * a Slack `files:read` scope problem via `files.info`) instead resolves the
2609
+ * `AttachmentFetchNeedsReauth` sentinel, so the in-thread note can steer the
2610
+ * user to reconnect Slack instead of a generic "unavailable". Failures are
2611
+ * logged with context (no silent swallow).
2523
2612
  */
2524
2613
  async fetchAttachmentDataUrl(messageId, index, mime) {
2525
2614
  try {
2526
2615
  const res = await this.fetchImpl(
2527
- `${this.apiUrl}/agents/${this.agentId}/attachments/${messageId}/${index}`,
2616
+ `${this.apiUrl}/runners/${this.agentId}/attachments/${messageId}/${index}`,
2528
2617
  { headers: { Authorization: this.getAuthHeader() } }
2529
2618
  );
2530
2619
  if (!res.ok) {
2620
+ let reason;
2621
+ try {
2622
+ const body = await res.json();
2623
+ if (body && typeof body.reason === "string") reason = body.reason;
2624
+ } catch (parseErr) {
2625
+ this.log({
2626
+ level: "debug",
2627
+ message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index}: error body was not JSON (${parseErr instanceof Error ? parseErr.message : String(parseErr)}) \u2014 treating as a plain failure`,
2628
+ message_id: messageId
2629
+ });
2630
+ }
2631
+ if (reason === "needs_reauth") {
2632
+ this.log({
2633
+ level: "error",
2634
+ message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index} returned HTTP ${res.status} \u2014 server confirmed a Slack reauth/scope problem \u2014 omitting this image (text turn proceeds)`,
2635
+ message_id: messageId
2636
+ });
2637
+ return { needsReauth: true };
2638
+ }
2531
2639
  this.log({
2532
2640
  level: "error",
2533
2641
  message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index} returned HTTP ${res.status} \u2014 omitting this image (text turn proceeds)`,
@@ -2567,6 +2675,9 @@ var ChannelDriver = class {
2567
2675
  if (this.attachmentsSkippedSignalled.has(messageId)) return;
2568
2676
  this.attachmentsSkippedSignalled.add(messageId);
2569
2677
  const skippedReason = capabilityUnknown ? "unknown" : "unsupported";
2678
+ const failedReason = outcomes.some(
2679
+ (o) => o.status === "failed" && o.reason === "needs_reauth"
2680
+ ) ? "needs_reauth" : void 0;
2570
2681
  this.log({
2571
2682
  level: "info",
2572
2683
  message: `Message ${messageId.slice(0, 8)}: ${skipped} image(s) skipped (${capabilityUnknown ? "capability was unreadable \u2014 failed open to text-only" : "model not attachment-capable"}), ${failed} image(s) unavailable (deleted-at-source or fetch failure) \u2014 noting to Evident`,
@@ -2576,7 +2687,8 @@ var ChannelDriver = class {
2576
2687
  void this.postSignal(conversationId, messageId, "attachments_skipped", {
2577
2688
  skipped,
2578
2689
  failed,
2579
- ...skipped > 0 ? { skipped_reason: skippedReason } : {}
2690
+ ...skipped > 0 ? { skipped_reason: skippedReason } : {},
2691
+ ...failedReason ? { failed_reason: failedReason } : {}
2580
2692
  });
2581
2693
  }
2582
2694
  /** Register a freshly-dispatched message with its session's watcher state. */
@@ -3615,19 +3727,36 @@ var ChannelDriver = class {
3615
3727
  if (parent !== void 0) this.sessionParents.set(sessionId, parent);
3616
3728
  return parent;
3617
3729
  }
3730
+ /**
3731
+ * OpenCode's synchronous default session title (e.g.
3732
+ * `"New session - 1737800000000"`), assigned immediately when a session is
3733
+ * created — before OpenCode's async LLM-based auto-titling later renames it
3734
+ * mid-turn (#549). Matched by this literal, case-sensitive prefix only; the
3735
+ * timestamp suffix's exact format is deliberately NOT matched, since the prefix
3736
+ * alone is the stable, cheap signal and over-anchoring on the timestamp
3737
+ * representation risks silently breaking if OpenCode ever changes it. Accepted
3738
+ * trade-off: a genuine LLM-assigned title that happens to literally start with
3739
+ * this prefix would also fail to latch (see `resolveSessionTitle`) —
3740
+ * vanishingly unlikely in practice, and deliberately not engineered around.
3741
+ */
3742
+ static OPENCODE_DEFAULT_TITLE_PREFIX = /^New session - /;
3618
3743
  /**
3619
3744
  * Resolve (and cache in `sessionTitles`) the OpenCode session TITLE (#310) so the
3620
3745
  * status PATCH can carry it into the "Live sessions" list. Driver-level cache so
3621
3746
  * BOTH the watcher completion path and the restart-recovery re-adopt path (which
3622
3747
  * has no watcher) can use it. `conversationId` is passed only for log context.
3623
3748
  * Best-effort:
3624
- * - a resolved NON-EMPTY title is cached and terminal (a real session name
3749
+ * - a resolved NON-EMPTY title that does NOT match
3750
+ * `OPENCODE_DEFAULT_TITLE_PREFIX` is cached and terminal (a real session name
3625
3751
  * won't later un-name), so we do NOT re-GET `/session/:id` every tick;
3626
- * - while the title is still absent/empty we do NOT latch it — OpenCode names
3627
- * sessions asynchronously mid-turn, so an early call (e.g. at `processing`)
3628
- * must leave the cache unresolved and re-fetch on the next need so a later
3629
- * call (e.g. at `done`) picks up the name assigned in the meantime. Such a
3630
- * call returns `null` (omit the title on THIS PATCH) without caching;
3752
+ * - while the title is still absent, empty, or matches the OpenCode
3753
+ * placeholder prefix (#549) we do NOT latch it OpenCode names sessions
3754
+ * asynchronously mid-turn, so an early call (e.g. at `processing`) must leave
3755
+ * the cache unresolved and re-fetch on the next need so a later call (e.g. at
3756
+ * `done`) picks up the name assigned in the meantime. Such a call returns
3757
+ * `null` (omit the title on THIS PATCH) without caching. If a session is
3758
+ * never renamed, the title is omitted forever rather than ever persisting
3759
+ * the placeholder as a last resort;
3631
3760
  * - a failed request likewise leaves the cache unresolved (retry next need)
3632
3761
  * and returns `null` — it must NEVER throw or block completion.
3633
3762
  * A failure is logged with agent/session context (no silent catch).
@@ -3640,7 +3769,7 @@ var ChannelDriver = class {
3640
3769
  if (res.ok) {
3641
3770
  const body = await res.json();
3642
3771
  const title = body && typeof body.title === "string" ? body.title.trim() : "";
3643
- if (title.length > 0) {
3772
+ if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
3644
3773
  this.sessionTitles.set(sessionId, title);
3645
3774
  return title;
3646
3775
  }
@@ -3790,7 +3919,7 @@ var ChannelDriver = class {
3790
3919
  // Evident API calls (combinedAuth thread routes)
3791
3920
  async getPendingConversations() {
3792
3921
  const res = await this.fetchImpl(
3793
- `${this.apiUrl}/agents/${this.agentId}/conversations/pending`,
3922
+ `${this.apiUrl}/runners/${this.agentId}/conversations/pending`,
3794
3923
  {
3795
3924
  headers: { Authorization: this.getAuthHeader() }
3796
3925
  }
@@ -3808,7 +3937,7 @@ var ChannelDriver = class {
3808
3937
  }
3809
3938
  async getPendingMessages(conversationId) {
3810
3939
  const res = await this.fetchImpl(
3811
- `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages?status=pending`,
3940
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages?status=pending`,
3812
3941
  { headers: { Authorization: this.getAuthHeader() } }
3813
3942
  );
3814
3943
  this.assertAuth(res, "fetching pending messages");
@@ -3832,7 +3961,7 @@ var ChannelDriver = class {
3832
3961
  */
3833
3962
  async getProcessingMessages() {
3834
3963
  const res = await this.fetchImpl(
3835
- `${this.apiUrl}/agents/${this.agentId}/conversations/processing`,
3964
+ `${this.apiUrl}/runners/${this.agentId}/conversations/processing`,
3836
3965
  { headers: { Authorization: this.getAuthHeader() } }
3837
3966
  );
3838
3967
  this.assertAuth(res, "fetching processing messages");
@@ -3869,7 +3998,7 @@ var ChannelDriver = class {
3869
3998
  */
3870
3999
  async markProcessing(conversationId, messageId, sessionId, opencodeMessageId, title) {
3871
4000
  const res = await this.fetchImpl(
3872
- `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
4001
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
3873
4002
  {
3874
4003
  method: "PATCH",
3875
4004
  headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
@@ -3918,7 +4047,7 @@ var ChannelDriver = class {
3918
4047
  */
3919
4048
  async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
3920
4049
  const res = await this.fetchImpl(
3921
- `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
4050
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
3922
4051
  {
3923
4052
  method: "PATCH",
3924
4053
  headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
@@ -3940,10 +4069,15 @@ var ChannelDriver = class {
3940
4069
  }
3941
4070
  /**
3942
4071
  * Mark a message `failed`. `sessionId` / `error` are threaded to the API ONLY
3943
- * when provided (issue #182): a bare `markFailed(conv, msg)` sends
3944
- * `{status:'failed'}` unchanged (the dispatch-failure path), while an errored
3945
- * OpenCode turn sends `{status:'failed', opencode_session_id, error}` so the
3946
- * failure reason reaches the channel.
4072
+ * when provided (issue #182). Three states for `sessionId`:
4073
+ * - omitted (`undefined`) → don't send the field, leave the persisted
4074
+ * session untouched (unused today; kept for API symmetry).
4075
+ * - a real id (`string`) → send it, update the persisted session (the
4076
+ * turn-failure call sites: an errored OpenCode turn).
4077
+ * - explicit `null` → send it, CLEAR the persisted session (issue
4078
+ * #485's dispatch-handoff-failure call site: the session id still
4079
+ * exists but is wedged, so the next attempt must get a fresh one
4080
+ * instead of reusing it — see WI-1's server-side null-clearing PATCH).
3947
4081
  */
3948
4082
  async markFailed(conversationId, messageId, sessionId, error2, usage) {
3949
4083
  const body = { status: "failed" };
@@ -3953,7 +4087,7 @@ var ChannelDriver = class {
3953
4087
  await this.callWithRetry(
3954
4088
  "marking message as failed",
3955
4089
  () => this.fetchImpl(
3956
- `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
4090
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
3957
4091
  {
3958
4092
  method: "PATCH",
3959
4093
  headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
@@ -3980,7 +4114,7 @@ var ChannelDriver = class {
3980
4114
  async postSignal(conversationId, messageId, signal, extra) {
3981
4115
  try {
3982
4116
  const res = await this.fetchImpl(
3983
- `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}/signal`,
4117
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}/signal`,
3984
4118
  {
3985
4119
  method: "POST",
3986
4120
  headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
@@ -4009,7 +4143,7 @@ var ChannelDriver = class {
4009
4143
  }
4010
4144
  async persistSession(conversationId, sessionId) {
4011
4145
  const res = await this.fetchImpl(
4012
- `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}`,
4146
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}`,
4013
4147
  {
4014
4148
  method: "PATCH",
4015
4149
  headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
@@ -4035,7 +4169,7 @@ var ChannelDriver = class {
4035
4169
  await this.callWithRetry(
4036
4170
  "reporting interactive event",
4037
4171
  () => this.fetchImpl(
4038
- `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/interactive-event`,
4172
+ `${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/interactive-event`,
4039
4173
  {
4040
4174
  method: "POST",
4041
4175
  headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
@@ -4278,7 +4412,7 @@ async function resolveAgentIdFromKey(authHeader) {
4278
4412
  async function notifyAgentDisconnected(agentId, authHeader) {
4279
4413
  const apiUrl = getApiUrlConfig();
4280
4414
  try {
4281
- const response = await fetch(`${apiUrl}/agents/${agentId}/disconnect`, {
4415
+ const response = await fetch(`${apiUrl}/runners/${agentId}/disconnect`, {
4282
4416
  method: "POST",
4283
4417
  headers: { Authorization: authHeader }
4284
4418
  });
@@ -4297,7 +4431,7 @@ async function notifyAgentDisconnected(agentId, authHeader) {
4297
4431
  async function getAgentInfo(agentId, authHeader) {
4298
4432
  const apiUrl = getApiUrlConfig();
4299
4433
  try {
4300
- const response = await fetch(`${apiUrl}/agents/${agentId}`, {
4434
+ const response = await fetch(`${apiUrl}/runners/${agentId}`, {
4301
4435
  headers: { Authorization: authHeader }
4302
4436
  });
4303
4437
  if (response.status === 401) {
@@ -4684,7 +4818,7 @@ async function run(options) {
4684
4818
  return;
4685
4819
  }
4686
4820
  const state = {
4687
- agentId: options.agent || "",
4821
+ agentId: options.runner || options.agent || "",
4688
4822
  agentName: null,
4689
4823
  port: options.port ?? 4096,
4690
4824
  conversationFilter: options.conversation ?? null,
@@ -4706,6 +4840,19 @@ async function run(options) {
4706
4840
  sessionCleanupTimers: [],
4707
4841
  authHeader: ""
4708
4842
  };
4843
+ if (!options.runner && options.agent) {
4844
+ telemetry.info(
4845
+ EventTypes.DEPRECATED_AGENT_FLAG_USED,
4846
+ "Deprecated --agent flag used instead of --runner",
4847
+ { command: "run" },
4848
+ state.agentId
4849
+ );
4850
+ const agentFlagNotice = "--agent is deprecated, use --runner instead; will be removed in a future release.";
4851
+ log2(state, agentFlagNotice, "warn");
4852
+ if (state.interactive && !state.json) {
4853
+ logActivity(state, { type: "info", level: "warn", message: agentFlagNotice });
4854
+ }
4855
+ }
4709
4856
  if (state.idleTimeout === null && (process.env.GITHUB_ACTIONS || process.env.CI)) {
4710
4857
  log2(
4711
4858
  state,
@@ -4734,7 +4881,9 @@ async function run(options) {
4734
4881
  if (!interactive) {
4735
4882
  printError("Authentication required");
4736
4883
  blank();
4737
- console.log(chalk6.dim("Set EVIDENT_AGENT_KEY environment variable for CI"));
4884
+ console.log(
4885
+ chalk6.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
4886
+ );
4738
4887
  console.log(chalk6.dim("Or run `evident login` for interactive authentication"));
4739
4888
  blank();
4740
4889
  process.exit(1);
@@ -4748,6 +4897,25 @@ async function run(options) {
4748
4897
  );
4749
4898
  }
4750
4899
  state.authHeader = getAuthHeader(credentials2);
4900
+ if (credentials2.notice) {
4901
+ log2(state, credentials2.notice, "warn");
4902
+ if (state.interactive && !state.json) {
4903
+ logActivity(state, { type: "info", level: "warn", message: credentials2.notice });
4904
+ }
4905
+ }
4906
+ if (credentials2.keySource === "agent_key") {
4907
+ telemetry.info(
4908
+ EventTypes.DEPRECATED_AGENT_KEY_ENV_USED,
4909
+ "Deprecated EVIDENT_AGENT_KEY env var used instead of EVIDENT_RUNNER_KEY",
4910
+ { command: "run" },
4911
+ state.agentId
4912
+ );
4913
+ const agentKeyNotice = "EVIDENT_AGENT_KEY is deprecated, use EVIDENT_RUNNER_KEY instead; will be removed in a future release.";
4914
+ log2(state, agentKeyNotice, "warn");
4915
+ if (state.interactive && !state.json) {
4916
+ logActivity(state, { type: "info", level: "warn", message: agentKeyNotice });
4917
+ }
4918
+ }
4751
4919
  if (!state.agentId) {
4752
4920
  if (credentials2.authType === "agent_key") {
4753
4921
  const resolved = await resolveAgentIdFromKey(state.authHeader);
@@ -4765,9 +4933,15 @@ async function run(options) {
4765
4933
  process.exit(1);
4766
4934
  }
4767
4935
  } else {
4768
- printError("--agent is required when not using EVIDENT_AGENT_KEY");
4936
+ printError(
4937
+ "--runner (or --agent) is required when not using EVIDENT_RUNNER_KEY or EVIDENT_AGENT_KEY"
4938
+ );
4769
4939
  blank();
4770
- console.log(chalk6.dim("Either provide --agent <id> or set EVIDENT_AGENT_KEY"));
4940
+ console.log(
4941
+ chalk6.dim(
4942
+ "Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
4943
+ )
4944
+ );
4771
4945
  blank();
4772
4946
  process.exit(1);
4773
4947
  }
@@ -4831,6 +5005,21 @@ async function run(options) {
4831
5005
  logActivity(state, { type: "info", level: "warn", message: versionWarning });
4832
5006
  }
4833
5007
  }
5008
+ const noProviderWarning = buildNoProviderWarning(await hasAnyConfiguredProvider(state.port));
5009
+ if (noProviderWarning) {
5010
+ log2(state, noProviderWarning, "warn");
5011
+ if (state.interactive && !state.json) {
5012
+ logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
5013
+ blank();
5014
+ console.log(chalk6.yellow("\u26A0 No OpenCode model provider is configured."));
5015
+ console.log(
5016
+ chalk6.dim(
5017
+ `Run ${chalk6.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
5018
+ )
5019
+ );
5020
+ blank();
5021
+ }
5022
+ }
4834
5023
  } catch (error2) {
4835
5024
  ocSpinner?.fail(error2.message);
4836
5025
  throw error2;
@@ -4982,7 +5171,7 @@ async function run(options) {
4982
5171
  }
4983
5172
  telemetry.error(EventTypes.CLI_ERROR, `Run command failed: ${message}`, {
4984
5173
  command: "run",
4985
- agentId: options.agent
5174
+ agentId: options.runner || options.agent
4986
5175
  });
4987
5176
  await shutdownTelemetry();
4988
5177
  process.exit(1);
@@ -5007,7 +5196,7 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
5007
5196
  program.command("login").description("Authenticate with Evident").option("--token", "Use token-based authentication (for CI/CD)").option("--no-browser", "Do not open the browser automatically").action(login);
5008
5197
  program.command("logout").description("Remove stored credentials for the current endpoint").option("--all", "Remove stored credentials for all endpoints").action((options) => logout({ all: options.all }));
5009
5198
  program.command("whoami").description("Show the currently logged in user").action(whoami);
5010
- program.command("run").description("Connect to Evident and process messages").option("-a, --agent [id]", "Runner ID to connect to (optional when EVIDENT_AGENT_KEY is set)").option("-p, --port <port>", "OpenCode port (default: 4096)", "4096").option(
5199
+ program.command("run").description("Connect to Evident and process messages").option("-a, --agent [id]", "Runner ID to connect to (optional when EVIDENT_AGENT_KEY is set)").option("--runner [id]", "Alias for --agent (preferred name; wins if both are given)").option("-p, --port <port>", "OpenCode port (default: 4096)", "4096").option(
5011
5200
  "--log-level <level>",
5012
5201
  "Log verbosity: debug | info | warn | error (default: info). Env: EVIDENT_LOG_LEVEL"
5013
5202
  ).option("-v, --verbose", "Alias for --log-level debug (ignored if --log-level is set)").option("-c, --conversation <id>", "Process only this specific conversation").option("--idle-timeout <seconds>", "Exit after N seconds idle").option("--json", "Output in JSON format").option(
@@ -5023,6 +5212,7 @@ program.command("run").description("Connect to Evident and process messages").op
5023
5212
  (options) => {
5024
5213
  run({
5025
5214
  agent: options.agent,
5215
+ runner: options.runner,
5026
5216
  port: parseInt(options.port, 10),
5027
5217
  // Raw string — validation/precedence is single-sourced in run.ts's
5028
5218
  // resolveLogLevel (flag > -v > EVIDENT_LOG_LEVEL > info).