@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 +10 -4
- package/dist/index.js +249 -59
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
- `
|
|
109
|
-
that runner and resolves the runner ID automatically, so `--agent`
|
|
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`
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
2149
|
-
* so we do NOT re-GET `/session/:id` every tick.
|
|
2150
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
|
|
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}/
|
|
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
|
-
*
|
|
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}/
|
|
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
|
|
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
|
|
3627
|
-
*
|
|
3628
|
-
*
|
|
3629
|
-
*
|
|
3630
|
-
*
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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)
|
|
3944
|
-
* `
|
|
3945
|
-
*
|
|
3946
|
-
*
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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(
|
|
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(
|
|
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(
|
|
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).
|