@evident-ai/cli 3.1.1-dev.dcb2f6d → 3.1.1-dev.f24491c
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 +284 -55
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -645,14 +645,27 @@ var EventTypes = {
|
|
|
645
645
|
// CLI lifecycle
|
|
646
646
|
CLI_STARTED: "cli.started",
|
|
647
647
|
CLI_COMMAND: "cli.command",
|
|
648
|
-
CLI_ERROR: "cli.error"
|
|
648
|
+
CLI_ERROR: "cli.error",
|
|
649
|
+
// Deprecation telemetry (#412) — usage of the old `--agent`/`EVIDENT_AGENT_KEY`
|
|
650
|
+
// names instead of the preferred `--runner`/`EVIDENT_RUNNER_KEY` (#409).
|
|
651
|
+
DEPRECATED_AGENT_FLAG_USED: "cli.deprecated_agent_flag_used",
|
|
652
|
+
DEPRECATED_AGENT_KEY_ENV_USED: "cli.deprecated_agent_key_env_used"
|
|
649
653
|
};
|
|
650
654
|
|
|
651
655
|
// src/lib/auth.ts
|
|
652
656
|
async function getAuthCredentials() {
|
|
657
|
+
const runnerKey = process.env.EVIDENT_RUNNER_KEY;
|
|
653
658
|
const agentKey = process.env.EVIDENT_AGENT_KEY;
|
|
659
|
+
if (runnerKey) {
|
|
660
|
+
return {
|
|
661
|
+
token: runnerKey,
|
|
662
|
+
authType: "agent_key",
|
|
663
|
+
keySource: "runner_key",
|
|
664
|
+
notice: agentKey ? "Both EVIDENT_RUNNER_KEY and EVIDENT_AGENT_KEY are set; using EVIDENT_RUNNER_KEY." : void 0
|
|
665
|
+
};
|
|
666
|
+
}
|
|
654
667
|
if (agentKey) {
|
|
655
|
-
return { token: agentKey, authType: "agent_key" };
|
|
668
|
+
return { token: agentKey, authType: "agent_key", keySource: "agent_key" };
|
|
656
669
|
}
|
|
657
670
|
const userToken = process.env.EVIDENT_TOKEN;
|
|
658
671
|
if (userToken) {
|
|
@@ -721,7 +734,7 @@ function buildOpenCodeVersionWarning(version2) {
|
|
|
721
734
|
if (isQueueValidatedVersion(version2)) return null;
|
|
722
735
|
const detected = version2 ? `v${version2}` : "unknown";
|
|
723
736
|
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
|
|
737
|
+
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
738
|
}
|
|
726
739
|
|
|
727
740
|
// src/lib/opencode/process.ts
|
|
@@ -1013,6 +1026,12 @@ async function promptOpenCodeInstall(interactive) {
|
|
|
1013
1026
|
return action;
|
|
1014
1027
|
}
|
|
1015
1028
|
|
|
1029
|
+
// src/lib/opencode/provider-check.ts
|
|
1030
|
+
function buildNoProviderWarning(hasProvider) {
|
|
1031
|
+
if (hasProvider !== false) return null;
|
|
1032
|
+
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).";
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1016
1035
|
// src/lib/opencode/session.ts
|
|
1017
1036
|
function opencodeBase(port) {
|
|
1018
1037
|
return `http://127.0.0.1:${port}`;
|
|
@@ -1215,6 +1234,11 @@ async function getModelAttachmentCapability(port, model) {
|
|
|
1215
1234
|
}
|
|
1216
1235
|
const entry = provider.models[modelId];
|
|
1217
1236
|
if (!entry || typeof entry !== "object") return null;
|
|
1237
|
+
if (entry.capabilities && typeof entry.capabilities === "object") {
|
|
1238
|
+
if (typeof entry.capabilities.attachment === "boolean") {
|
|
1239
|
+
return entry.capabilities.attachment;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1218
1242
|
return typeof entry.attachment === "boolean" ? entry.attachment : null;
|
|
1219
1243
|
} catch (err) {
|
|
1220
1244
|
console.error(
|
|
@@ -1243,6 +1267,16 @@ async function buildFileParts(attachments, capable) {
|
|
|
1243
1267
|
);
|
|
1244
1268
|
dataUrl = null;
|
|
1245
1269
|
}
|
|
1270
|
+
if (dataUrl !== null && typeof dataUrl === "object") {
|
|
1271
|
+
outcomes.push({
|
|
1272
|
+
index: a.index,
|
|
1273
|
+
mime: a.mime,
|
|
1274
|
+
filename: a.filename,
|
|
1275
|
+
status: "failed",
|
|
1276
|
+
reason: "needs_reauth"
|
|
1277
|
+
});
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1246
1280
|
if (dataUrl == null) {
|
|
1247
1281
|
outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: "failed" });
|
|
1248
1282
|
continue;
|
|
@@ -1370,6 +1404,72 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
1370
1404
|
}
|
|
1371
1405
|
return lastOk ?? last;
|
|
1372
1406
|
}
|
|
1407
|
+
function messageUsage(messages, userMessageId) {
|
|
1408
|
+
if (!messages || messages.length === 0) return null;
|
|
1409
|
+
const byParentAll = messages.filter(
|
|
1410
|
+
(m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
|
|
1411
|
+
);
|
|
1412
|
+
const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
|
|
1413
|
+
const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
|
|
1414
|
+
let correlated;
|
|
1415
|
+
if (byParent.length > 0) {
|
|
1416
|
+
correlated = byParent;
|
|
1417
|
+
} else {
|
|
1418
|
+
const reply = findAssistantReplyAfter(messages, userMessageId);
|
|
1419
|
+
correlated = reply ? [reply] : [];
|
|
1420
|
+
}
|
|
1421
|
+
if (correlated.length === 0) return null;
|
|
1422
|
+
let sawAnyUsage = false;
|
|
1423
|
+
let inputSum = 0;
|
|
1424
|
+
let outputSum = 0;
|
|
1425
|
+
let reasoningSum = 0;
|
|
1426
|
+
let cacheReadSum = 0;
|
|
1427
|
+
let cacheWriteSum = 0;
|
|
1428
|
+
let costSum = 0;
|
|
1429
|
+
let sawCost = false;
|
|
1430
|
+
let modelId = null;
|
|
1431
|
+
let providerId = null;
|
|
1432
|
+
for (const m of correlated) {
|
|
1433
|
+
const info = m.info;
|
|
1434
|
+
if (!info) continue;
|
|
1435
|
+
const tokens = info.tokens;
|
|
1436
|
+
if (tokens) {
|
|
1437
|
+
sawAnyUsage = true;
|
|
1438
|
+
inputSum += tokens.input ?? 0;
|
|
1439
|
+
outputSum += tokens.output ?? 0;
|
|
1440
|
+
reasoningSum += tokens.reasoning ?? 0;
|
|
1441
|
+
cacheReadSum += tokens.cache?.read ?? 0;
|
|
1442
|
+
cacheWriteSum += tokens.cache?.write ?? 0;
|
|
1443
|
+
}
|
|
1444
|
+
if (typeof info.cost === "number") {
|
|
1445
|
+
sawAnyUsage = true;
|
|
1446
|
+
sawCost = true;
|
|
1447
|
+
costSum += info.cost;
|
|
1448
|
+
}
|
|
1449
|
+
if (typeof info.modelID === "string") {
|
|
1450
|
+
sawAnyUsage = true;
|
|
1451
|
+
modelId = info.modelID;
|
|
1452
|
+
}
|
|
1453
|
+
if (typeof info.providerID === "string") {
|
|
1454
|
+
sawAnyUsage = true;
|
|
1455
|
+
providerId = info.providerID;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
if (!sawAnyUsage) return null;
|
|
1459
|
+
return {
|
|
1460
|
+
usage_provider_id: providerId,
|
|
1461
|
+
usage_model_id: modelId,
|
|
1462
|
+
usage_tokens_input: inputSum,
|
|
1463
|
+
usage_tokens_output: outputSum,
|
|
1464
|
+
usage_tokens_reasoning: reasoningSum,
|
|
1465
|
+
usage_tokens_cache_read: cacheReadSum,
|
|
1466
|
+
usage_tokens_cache_write: cacheWriteSum,
|
|
1467
|
+
// NULL means "OpenCode never reported a cost" (never inferred from
|
|
1468
|
+
// tokens) — distinct from a genuine 0-cost turn, which would set
|
|
1469
|
+
// `sawCost` true with `costSum === 0`.
|
|
1470
|
+
usage_cost_usd: sawCost ? costSum : null
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1373
1473
|
function messageRunState(messages, userMessageId) {
|
|
1374
1474
|
if (!messages || messages.length === 0) return "unknown";
|
|
1375
1475
|
const hasUser = messages.some((m) => idOf(m) === userMessageId);
|
|
@@ -1405,6 +1505,37 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
|
1405
1505
|
(m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
1406
1506
|
);
|
|
1407
1507
|
}
|
|
1508
|
+
async function hasAnyConfiguredProvider(port) {
|
|
1509
|
+
try {
|
|
1510
|
+
const res = await fetch(`${opencodeBase(port)}/config/providers`);
|
|
1511
|
+
if (!res.ok) {
|
|
1512
|
+
console.error(
|
|
1513
|
+
`[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
1514
|
+
);
|
|
1515
|
+
return null;
|
|
1516
|
+
}
|
|
1517
|
+
const body = await res.json();
|
|
1518
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
1519
|
+
console.error(
|
|
1520
|
+
`[hasAnyConfiguredProvider] GET /config/providers body was not a plain object (port ${port})`
|
|
1521
|
+
);
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
const defaults2 = body.default;
|
|
1525
|
+
if (!defaults2 || typeof defaults2 !== "object" || Array.isArray(defaults2)) {
|
|
1526
|
+
console.error(
|
|
1527
|
+
`[hasAnyConfiguredProvider] GET /config/providers body had no \`default\` object (port ${port})`
|
|
1528
|
+
);
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
return Object.keys(defaults2).length > 0;
|
|
1532
|
+
} catch (err) {
|
|
1533
|
+
console.error(
|
|
1534
|
+
`[hasAnyConfiguredProvider] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
1535
|
+
);
|
|
1536
|
+
return null;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1408
1539
|
|
|
1409
1540
|
// src/lib/opencode/session-cleanup.ts
|
|
1410
1541
|
var DURATION_UNIT_MS = {
|
|
@@ -1716,7 +1847,6 @@ function connectTunnel(options) {
|
|
|
1716
1847
|
onConnected,
|
|
1717
1848
|
onDisconnected,
|
|
1718
1849
|
onError,
|
|
1719
|
-
onRequest,
|
|
1720
1850
|
onResponse,
|
|
1721
1851
|
onInfo,
|
|
1722
1852
|
onDrainPing
|
|
@@ -1729,18 +1859,8 @@ function connectTunnel(options) {
|
|
|
1729
1859
|
Authorization: authHeader
|
|
1730
1860
|
}
|
|
1731
1861
|
});
|
|
1732
|
-
const streamStartTimes = /* @__PURE__ */ new Map();
|
|
1733
1862
|
const forwarder = new StreamForwarder(ws, port, {
|
|
1734
|
-
|
|
1735
|
-
if (path === TUNNEL_DRAIN_PING_PATH) return;
|
|
1736
|
-
streamStartTimes.set(sid, Date.now());
|
|
1737
|
-
onRequest?.(method, path, sid);
|
|
1738
|
-
},
|
|
1739
|
-
onHead: (sid, status) => {
|
|
1740
|
-
const startedAt = streamStartTimes.get(sid);
|
|
1741
|
-
streamStartTimes.delete(sid);
|
|
1742
|
-
onResponse?.(status, startedAt ? Date.now() - startedAt : 0, sid);
|
|
1743
|
-
},
|
|
1863
|
+
onHead: () => onResponse?.(),
|
|
1744
1864
|
onDrainPing: () => onDrainPing?.()
|
|
1745
1865
|
});
|
|
1746
1866
|
const connectionTimeout = setTimeout(() => {
|
|
@@ -1816,7 +1936,6 @@ function connectTunnel(options) {
|
|
|
1816
1936
|
ws.on("close", (code, reason) => {
|
|
1817
1937
|
const reasonStr = reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
|
|
1818
1938
|
forwarder.abortAll();
|
|
1819
|
-
streamStartTimes.clear();
|
|
1820
1939
|
onDisconnected?.(code, reasonStr);
|
|
1821
1940
|
});
|
|
1822
1941
|
});
|
|
@@ -1954,7 +2073,7 @@ function backoffDelay(attempt, policy) {
|
|
|
1954
2073
|
function isRetryableStatus(status) {
|
|
1955
2074
|
return status === 429 || status >= 500 && status <= 599;
|
|
1956
2075
|
}
|
|
1957
|
-
var ChannelDriver = class {
|
|
2076
|
+
var ChannelDriver = class _ChannelDriver {
|
|
1958
2077
|
agentId;
|
|
1959
2078
|
port;
|
|
1960
2079
|
apiUrl;
|
|
@@ -2079,9 +2198,12 @@ var ChannelDriver = class {
|
|
|
2079
2198
|
sessionParents = /* @__PURE__ */ new Map();
|
|
2080
2199
|
/**
|
|
2081
2200
|
* Per-session OpenCode title cache (#310), keyed by sessionId. Only a resolved
|
|
2082
|
-
* NON-EMPTY name is stored (terminal — a real session name
|
|
2083
|
-
* so we do NOT re-GET `/session/:id` every tick.
|
|
2084
|
-
*
|
|
2201
|
+
* NON-EMPTY, non-placeholder name is stored (terminal — a real session name
|
|
2202
|
+
* won't later un-name), so we do NOT re-GET `/session/:id` every tick. "Non-empty"
|
|
2203
|
+
* excludes OpenCode's synchronous default title (see
|
|
2204
|
+
* `OPENCODE_DEFAULT_TITLE_PREFIX`, #549) — that placeholder is treated the same
|
|
2205
|
+
* as an empty title so it never latches. A missing entry = not yet resolved OR
|
|
2206
|
+
* resolved-but-still-empty/placeholder → re-fetch on next need, since OpenCode
|
|
2085
2207
|
* names sessions asynchronously mid-turn. Driver-level (not per-watcher) so both
|
|
2086
2208
|
* the watcher completion path AND the restart-recovery re-adopt path (which has
|
|
2087
2209
|
* no watcher) can resolve the title.
|
|
@@ -2445,7 +2567,7 @@ var ChannelDriver = class {
|
|
|
2445
2567
|
}
|
|
2446
2568
|
/**
|
|
2447
2569
|
* Fetch ONE inbound image's bytes through Evident's WI-6 endpoint
|
|
2448
|
-
* (`GET {apiUrl}/
|
|
2570
|
+
* (`GET {apiUrl}/runners/{agentId}/attachments/{messageId}/{index}`) using the
|
|
2449
2571
|
* existing authenticated fetch, and base64-encode into a
|
|
2450
2572
|
* `data:<mime>;base64,<…>` URL for the opencode `file` part's `url`.
|
|
2451
2573
|
*
|
|
@@ -2453,15 +2575,38 @@ var ChannelDriver = class {
|
|
|
2453
2575
|
* (not-owned / out-of-range / deleted-at-source / workspace gone) / 413
|
|
2454
2576
|
* (over-cap). On ANY non-2xx or thrown failure we return `null` so the caller
|
|
2455
2577
|
* OMITS that one image and the text turn still sends — NEVER throws the turn.
|
|
2456
|
-
*
|
|
2578
|
+
* A 404 body carrying `{ reason: 'needs_reauth' }` (#547 — the server CONFIRMED
|
|
2579
|
+
* a Slack `files:read` scope problem via `files.info`) instead resolves the
|
|
2580
|
+
* `AttachmentFetchNeedsReauth` sentinel, so the in-thread note can steer the
|
|
2581
|
+
* user to reconnect Slack instead of a generic "unavailable". Failures are
|
|
2582
|
+
* logged with context (no silent swallow).
|
|
2457
2583
|
*/
|
|
2458
2584
|
async fetchAttachmentDataUrl(messageId, index, mime) {
|
|
2459
2585
|
try {
|
|
2460
2586
|
const res = await this.fetchImpl(
|
|
2461
|
-
`${this.apiUrl}/
|
|
2587
|
+
`${this.apiUrl}/runners/${this.agentId}/attachments/${messageId}/${index}`,
|
|
2462
2588
|
{ headers: { Authorization: this.getAuthHeader() } }
|
|
2463
2589
|
);
|
|
2464
2590
|
if (!res.ok) {
|
|
2591
|
+
let reason;
|
|
2592
|
+
try {
|
|
2593
|
+
const body = await res.json();
|
|
2594
|
+
if (body && typeof body.reason === "string") reason = body.reason;
|
|
2595
|
+
} catch (parseErr) {
|
|
2596
|
+
this.log({
|
|
2597
|
+
level: "debug",
|
|
2598
|
+
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`,
|
|
2599
|
+
message_id: messageId
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
if (reason === "needs_reauth") {
|
|
2603
|
+
this.log({
|
|
2604
|
+
level: "error",
|
|
2605
|
+
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)`,
|
|
2606
|
+
message_id: messageId
|
|
2607
|
+
});
|
|
2608
|
+
return { needsReauth: true };
|
|
2609
|
+
}
|
|
2465
2610
|
this.log({
|
|
2466
2611
|
level: "error",
|
|
2467
2612
|
message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index} returned HTTP ${res.status} \u2014 omitting this image (text turn proceeds)`,
|
|
@@ -2501,6 +2646,9 @@ var ChannelDriver = class {
|
|
|
2501
2646
|
if (this.attachmentsSkippedSignalled.has(messageId)) return;
|
|
2502
2647
|
this.attachmentsSkippedSignalled.add(messageId);
|
|
2503
2648
|
const skippedReason = capabilityUnknown ? "unknown" : "unsupported";
|
|
2649
|
+
const failedReason = outcomes.some(
|
|
2650
|
+
(o) => o.status === "failed" && o.reason === "needs_reauth"
|
|
2651
|
+
) ? "needs_reauth" : void 0;
|
|
2504
2652
|
this.log({
|
|
2505
2653
|
level: "info",
|
|
2506
2654
|
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`,
|
|
@@ -2510,7 +2658,8 @@ var ChannelDriver = class {
|
|
|
2510
2658
|
void this.postSignal(conversationId, messageId, "attachments_skipped", {
|
|
2511
2659
|
skipped,
|
|
2512
2660
|
failed,
|
|
2513
|
-
...skipped > 0 ? { skipped_reason: skippedReason } : {}
|
|
2661
|
+
...skipped > 0 ? { skipped_reason: skippedReason } : {},
|
|
2662
|
+
...failedReason ? { failed_reason: failedReason } : {}
|
|
2514
2663
|
});
|
|
2515
2664
|
}
|
|
2516
2665
|
/** Register a freshly-dispatched message with its session's watcher state. */
|
|
@@ -2790,13 +2939,15 @@ var ChannelDriver = class {
|
|
|
2790
2939
|
message_id: inFlight.evidentMessageId
|
|
2791
2940
|
});
|
|
2792
2941
|
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
2942
|
+
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
2793
2943
|
try {
|
|
2794
2944
|
await this.markDone(
|
|
2795
2945
|
conv.id,
|
|
2796
2946
|
inFlight.evidentMessageId,
|
|
2797
2947
|
sessionId,
|
|
2798
2948
|
inFlight.opencodeMessageId,
|
|
2799
|
-
title
|
|
2949
|
+
title,
|
|
2950
|
+
usage
|
|
2800
2951
|
);
|
|
2801
2952
|
} catch (err) {
|
|
2802
2953
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -2843,8 +2994,9 @@ var ChannelDriver = class {
|
|
|
2843
2994
|
conversation_id: conv.id,
|
|
2844
2995
|
message_id: inFlight.evidentMessageId
|
|
2845
2996
|
});
|
|
2997
|
+
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
2846
2998
|
try {
|
|
2847
|
-
await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2);
|
|
2999
|
+
await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2, usage);
|
|
2848
3000
|
} catch (err) {
|
|
2849
3001
|
if (err instanceof ChannelAuthError) throw err;
|
|
2850
3002
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -3081,7 +3233,8 @@ var ChannelDriver = class {
|
|
|
3081
3233
|
});
|
|
3082
3234
|
try {
|
|
3083
3235
|
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
3084
|
-
|
|
3236
|
+
const usage = messageUsage(messages, ocId ?? "");
|
|
3237
|
+
await this.markDone(row.conversation_id, row.id, sessionId, ocId, title, usage);
|
|
3085
3238
|
} catch (err) {
|
|
3086
3239
|
if (err instanceof ChannelAuthError) throw err;
|
|
3087
3240
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -3109,6 +3262,7 @@ var ChannelDriver = class {
|
|
|
3109
3262
|
}
|
|
3110
3263
|
if (state === "failed") {
|
|
3111
3264
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
3265
|
+
const usage = messageUsage(messages, ocId ?? "");
|
|
3112
3266
|
this.log({
|
|
3113
3267
|
level: "error",
|
|
3114
3268
|
message: `Re-adopt: message ${row.id.slice(0, 8)} errored while unwatched \u2014 marking failed: ${error2 ?? "(no error text)"}`,
|
|
@@ -3116,7 +3270,7 @@ var ChannelDriver = class {
|
|
|
3116
3270
|
message_id: row.id
|
|
3117
3271
|
});
|
|
3118
3272
|
try {
|
|
3119
|
-
await this.markFailed(row.conversation_id, row.id, sessionId, error2);
|
|
3273
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, error2, usage);
|
|
3120
3274
|
} catch (err) {
|
|
3121
3275
|
if (err instanceof ChannelAuthError) throw err;
|
|
3122
3276
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -3544,19 +3698,36 @@ var ChannelDriver = class {
|
|
|
3544
3698
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
3545
3699
|
return parent;
|
|
3546
3700
|
}
|
|
3701
|
+
/**
|
|
3702
|
+
* OpenCode's synchronous default session title (e.g.
|
|
3703
|
+
* `"New session - 1737800000000"`), assigned immediately when a session is
|
|
3704
|
+
* created — before OpenCode's async LLM-based auto-titling later renames it
|
|
3705
|
+
* mid-turn (#549). Matched by this literal, case-sensitive prefix only; the
|
|
3706
|
+
* timestamp suffix's exact format is deliberately NOT matched, since the prefix
|
|
3707
|
+
* alone is the stable, cheap signal and over-anchoring on the timestamp
|
|
3708
|
+
* representation risks silently breaking if OpenCode ever changes it. Accepted
|
|
3709
|
+
* trade-off: a genuine LLM-assigned title that happens to literally start with
|
|
3710
|
+
* this prefix would also fail to latch (see `resolveSessionTitle`) —
|
|
3711
|
+
* vanishingly unlikely in practice, and deliberately not engineered around.
|
|
3712
|
+
*/
|
|
3713
|
+
static OPENCODE_DEFAULT_TITLE_PREFIX = /^New session - /;
|
|
3547
3714
|
/**
|
|
3548
3715
|
* Resolve (and cache in `sessionTitles`) the OpenCode session TITLE (#310) so the
|
|
3549
3716
|
* status PATCH can carry it into the "Live sessions" list. Driver-level cache so
|
|
3550
3717
|
* BOTH the watcher completion path and the restart-recovery re-adopt path (which
|
|
3551
3718
|
* has no watcher) can use it. `conversationId` is passed only for log context.
|
|
3552
3719
|
* Best-effort:
|
|
3553
|
-
* - a resolved NON-EMPTY title
|
|
3720
|
+
* - a resolved NON-EMPTY title that does NOT match
|
|
3721
|
+
* `OPENCODE_DEFAULT_TITLE_PREFIX` is cached and terminal (a real session name
|
|
3554
3722
|
* won't later un-name), so we do NOT re-GET `/session/:id` every tick;
|
|
3555
|
-
* - while the title is still absent
|
|
3556
|
-
*
|
|
3557
|
-
*
|
|
3558
|
-
*
|
|
3559
|
-
*
|
|
3723
|
+
* - while the title is still absent, empty, or matches the OpenCode
|
|
3724
|
+
* placeholder prefix (#549) we do NOT latch it — OpenCode names sessions
|
|
3725
|
+
* asynchronously mid-turn, so an early call (e.g. at `processing`) must leave
|
|
3726
|
+
* the cache unresolved and re-fetch on the next need so a later call (e.g. at
|
|
3727
|
+
* `done`) picks up the name assigned in the meantime. Such a call returns
|
|
3728
|
+
* `null` (omit the title on THIS PATCH) without caching. If a session is
|
|
3729
|
+
* never renamed, the title is omitted forever rather than ever persisting
|
|
3730
|
+
* the placeholder as a last resort;
|
|
3560
3731
|
* - a failed request likewise leaves the cache unresolved (retry next need)
|
|
3561
3732
|
* and returns `null` — it must NEVER throw or block completion.
|
|
3562
3733
|
* A failure is logged with agent/session context (no silent catch).
|
|
@@ -3569,7 +3740,7 @@ var ChannelDriver = class {
|
|
|
3569
3740
|
if (res.ok) {
|
|
3570
3741
|
const body = await res.json();
|
|
3571
3742
|
const title = body && typeof body.title === "string" ? body.title.trim() : "";
|
|
3572
|
-
if (title.length > 0) {
|
|
3743
|
+
if (title.length > 0 && !_ChannelDriver.OPENCODE_DEFAULT_TITLE_PREFIX.test(title)) {
|
|
3573
3744
|
this.sessionTitles.set(sessionId, title);
|
|
3574
3745
|
return title;
|
|
3575
3746
|
}
|
|
@@ -3719,7 +3890,7 @@ var ChannelDriver = class {
|
|
|
3719
3890
|
// Evident API calls (combinedAuth thread routes)
|
|
3720
3891
|
async getPendingConversations() {
|
|
3721
3892
|
const res = await this.fetchImpl(
|
|
3722
|
-
`${this.apiUrl}/
|
|
3893
|
+
`${this.apiUrl}/runners/${this.agentId}/conversations/pending`,
|
|
3723
3894
|
{
|
|
3724
3895
|
headers: { Authorization: this.getAuthHeader() }
|
|
3725
3896
|
}
|
|
@@ -3737,7 +3908,7 @@ var ChannelDriver = class {
|
|
|
3737
3908
|
}
|
|
3738
3909
|
async getPendingMessages(conversationId) {
|
|
3739
3910
|
const res = await this.fetchImpl(
|
|
3740
|
-
`${this.apiUrl}/
|
|
3911
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages?status=pending`,
|
|
3741
3912
|
{ headers: { Authorization: this.getAuthHeader() } }
|
|
3742
3913
|
);
|
|
3743
3914
|
this.assertAuth(res, "fetching pending messages");
|
|
@@ -3761,7 +3932,7 @@ var ChannelDriver = class {
|
|
|
3761
3932
|
*/
|
|
3762
3933
|
async getProcessingMessages() {
|
|
3763
3934
|
const res = await this.fetchImpl(
|
|
3764
|
-
`${this.apiUrl}/
|
|
3935
|
+
`${this.apiUrl}/runners/${this.agentId}/conversations/processing`,
|
|
3765
3936
|
{ headers: { Authorization: this.getAuthHeader() } }
|
|
3766
3937
|
);
|
|
3767
3938
|
this.assertAuth(res, "fetching processing messages");
|
|
@@ -3798,7 +3969,7 @@ var ChannelDriver = class {
|
|
|
3798
3969
|
*/
|
|
3799
3970
|
async markProcessing(conversationId, messageId, sessionId, opencodeMessageId, title) {
|
|
3800
3971
|
const res = await this.fetchImpl(
|
|
3801
|
-
`${this.apiUrl}/
|
|
3972
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
3802
3973
|
{
|
|
3803
3974
|
method: "PATCH",
|
|
3804
3975
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
@@ -3845,9 +4016,9 @@ var ChannelDriver = class {
|
|
|
3845
4016
|
* watcher retries next tick within the
|
|
3846
4017
|
* deadline, Finding 4).
|
|
3847
4018
|
*/
|
|
3848
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title) {
|
|
4019
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
|
|
3849
4020
|
const res = await this.fetchImpl(
|
|
3850
|
-
`${this.apiUrl}/
|
|
4021
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
3851
4022
|
{
|
|
3852
4023
|
method: "PATCH",
|
|
3853
4024
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
@@ -3855,7 +4026,8 @@ var ChannelDriver = class {
|
|
|
3855
4026
|
status: "done",
|
|
3856
4027
|
opencode_session_id: sessionId,
|
|
3857
4028
|
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
3858
|
-
...title ? { title } : {}
|
|
4029
|
+
...title ? { title } : {},
|
|
4030
|
+
...usage ? usage : {}
|
|
3859
4031
|
})
|
|
3860
4032
|
}
|
|
3861
4033
|
);
|
|
@@ -3873,14 +4045,15 @@ var ChannelDriver = class {
|
|
|
3873
4045
|
* OpenCode turn sends `{status:'failed', opencode_session_id, error}` so the
|
|
3874
4046
|
* failure reason reaches the channel.
|
|
3875
4047
|
*/
|
|
3876
|
-
async markFailed(conversationId, messageId, sessionId, error2) {
|
|
4048
|
+
async markFailed(conversationId, messageId, sessionId, error2, usage) {
|
|
3877
4049
|
const body = { status: "failed" };
|
|
3878
4050
|
if (sessionId !== void 0) body.opencode_session_id = sessionId;
|
|
3879
4051
|
if (error2 !== void 0) body.error = error2;
|
|
4052
|
+
if (usage) Object.assign(body, usage);
|
|
3880
4053
|
await this.callWithRetry(
|
|
3881
4054
|
"marking message as failed",
|
|
3882
4055
|
() => this.fetchImpl(
|
|
3883
|
-
`${this.apiUrl}/
|
|
4056
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
3884
4057
|
{
|
|
3885
4058
|
method: "PATCH",
|
|
3886
4059
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
@@ -3907,7 +4080,7 @@ var ChannelDriver = class {
|
|
|
3907
4080
|
async postSignal(conversationId, messageId, signal, extra) {
|
|
3908
4081
|
try {
|
|
3909
4082
|
const res = await this.fetchImpl(
|
|
3910
|
-
`${this.apiUrl}/
|
|
4083
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}/signal`,
|
|
3911
4084
|
{
|
|
3912
4085
|
method: "POST",
|
|
3913
4086
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
@@ -3936,7 +4109,7 @@ var ChannelDriver = class {
|
|
|
3936
4109
|
}
|
|
3937
4110
|
async persistSession(conversationId, sessionId) {
|
|
3938
4111
|
const res = await this.fetchImpl(
|
|
3939
|
-
`${this.apiUrl}/
|
|
4112
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}`,
|
|
3940
4113
|
{
|
|
3941
4114
|
method: "PATCH",
|
|
3942
4115
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
@@ -3962,7 +4135,7 @@ var ChannelDriver = class {
|
|
|
3962
4135
|
await this.callWithRetry(
|
|
3963
4136
|
"reporting interactive event",
|
|
3964
4137
|
() => this.fetchImpl(
|
|
3965
|
-
`${this.apiUrl}/
|
|
4138
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/interactive-event`,
|
|
3966
4139
|
{
|
|
3967
4140
|
method: "POST",
|
|
3968
4141
|
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
@@ -4205,7 +4378,7 @@ async function resolveAgentIdFromKey(authHeader) {
|
|
|
4205
4378
|
async function notifyAgentDisconnected(agentId, authHeader) {
|
|
4206
4379
|
const apiUrl = getApiUrlConfig();
|
|
4207
4380
|
try {
|
|
4208
|
-
const response = await fetch(`${apiUrl}/
|
|
4381
|
+
const response = await fetch(`${apiUrl}/runners/${agentId}/disconnect`, {
|
|
4209
4382
|
method: "POST",
|
|
4210
4383
|
headers: { Authorization: authHeader }
|
|
4211
4384
|
});
|
|
@@ -4224,7 +4397,7 @@ async function notifyAgentDisconnected(agentId, authHeader) {
|
|
|
4224
4397
|
async function getAgentInfo(agentId, authHeader) {
|
|
4225
4398
|
const apiUrl = getApiUrlConfig();
|
|
4226
4399
|
try {
|
|
4227
|
-
const response = await fetch(`${apiUrl}/
|
|
4400
|
+
const response = await fetch(`${apiUrl}/runners/${agentId}`, {
|
|
4228
4401
|
headers: { Authorization: authHeader }
|
|
4229
4402
|
});
|
|
4230
4403
|
if (response.status === 401) {
|
|
@@ -4611,7 +4784,7 @@ async function run(options) {
|
|
|
4611
4784
|
return;
|
|
4612
4785
|
}
|
|
4613
4786
|
const state = {
|
|
4614
|
-
agentId: options.agent || "",
|
|
4787
|
+
agentId: options.runner || options.agent || "",
|
|
4615
4788
|
agentName: null,
|
|
4616
4789
|
port: options.port ?? 4096,
|
|
4617
4790
|
conversationFilter: options.conversation ?? null,
|
|
@@ -4633,6 +4806,19 @@ async function run(options) {
|
|
|
4633
4806
|
sessionCleanupTimers: [],
|
|
4634
4807
|
authHeader: ""
|
|
4635
4808
|
};
|
|
4809
|
+
if (!options.runner && options.agent) {
|
|
4810
|
+
telemetry.info(
|
|
4811
|
+
EventTypes.DEPRECATED_AGENT_FLAG_USED,
|
|
4812
|
+
"Deprecated --agent flag used instead of --runner",
|
|
4813
|
+
{ command: "run" },
|
|
4814
|
+
state.agentId
|
|
4815
|
+
);
|
|
4816
|
+
const agentFlagNotice = "--agent is deprecated, use --runner instead; will be removed in a future release.";
|
|
4817
|
+
log2(state, agentFlagNotice, "warn");
|
|
4818
|
+
if (state.interactive && !state.json) {
|
|
4819
|
+
logActivity(state, { type: "info", level: "warn", message: agentFlagNotice });
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4636
4822
|
if (state.idleTimeout === null && (process.env.GITHUB_ACTIONS || process.env.CI)) {
|
|
4637
4823
|
log2(
|
|
4638
4824
|
state,
|
|
@@ -4661,7 +4847,9 @@ async function run(options) {
|
|
|
4661
4847
|
if (!interactive) {
|
|
4662
4848
|
printError("Authentication required");
|
|
4663
4849
|
blank();
|
|
4664
|
-
console.log(
|
|
4850
|
+
console.log(
|
|
4851
|
+
chalk6.dim("Set EVIDENT_RUNNER_KEY (or EVIDENT_AGENT_KEY) environment variable for CI")
|
|
4852
|
+
);
|
|
4665
4853
|
console.log(chalk6.dim("Or run `evident login` for interactive authentication"));
|
|
4666
4854
|
blank();
|
|
4667
4855
|
process.exit(1);
|
|
@@ -4675,6 +4863,25 @@ async function run(options) {
|
|
|
4675
4863
|
);
|
|
4676
4864
|
}
|
|
4677
4865
|
state.authHeader = getAuthHeader(credentials2);
|
|
4866
|
+
if (credentials2.notice) {
|
|
4867
|
+
log2(state, credentials2.notice, "warn");
|
|
4868
|
+
if (state.interactive && !state.json) {
|
|
4869
|
+
logActivity(state, { type: "info", level: "warn", message: credentials2.notice });
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
if (credentials2.keySource === "agent_key") {
|
|
4873
|
+
telemetry.info(
|
|
4874
|
+
EventTypes.DEPRECATED_AGENT_KEY_ENV_USED,
|
|
4875
|
+
"Deprecated EVIDENT_AGENT_KEY env var used instead of EVIDENT_RUNNER_KEY",
|
|
4876
|
+
{ command: "run" },
|
|
4877
|
+
state.agentId
|
|
4878
|
+
);
|
|
4879
|
+
const agentKeyNotice = "EVIDENT_AGENT_KEY is deprecated, use EVIDENT_RUNNER_KEY instead; will be removed in a future release.";
|
|
4880
|
+
log2(state, agentKeyNotice, "warn");
|
|
4881
|
+
if (state.interactive && !state.json) {
|
|
4882
|
+
logActivity(state, { type: "info", level: "warn", message: agentKeyNotice });
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4678
4885
|
if (!state.agentId) {
|
|
4679
4886
|
if (credentials2.authType === "agent_key") {
|
|
4680
4887
|
const resolved = await resolveAgentIdFromKey(state.authHeader);
|
|
@@ -4692,9 +4899,15 @@ async function run(options) {
|
|
|
4692
4899
|
process.exit(1);
|
|
4693
4900
|
}
|
|
4694
4901
|
} else {
|
|
4695
|
-
printError(
|
|
4902
|
+
printError(
|
|
4903
|
+
"--runner (or --agent) is required when not using EVIDENT_RUNNER_KEY or EVIDENT_AGENT_KEY"
|
|
4904
|
+
);
|
|
4696
4905
|
blank();
|
|
4697
|
-
console.log(
|
|
4906
|
+
console.log(
|
|
4907
|
+
chalk6.dim(
|
|
4908
|
+
"Either provide --runner/--agent <id> or set EVIDENT_RUNNER_KEY/EVIDENT_AGENT_KEY"
|
|
4909
|
+
)
|
|
4910
|
+
);
|
|
4698
4911
|
blank();
|
|
4699
4912
|
process.exit(1);
|
|
4700
4913
|
}
|
|
@@ -4758,6 +4971,21 @@ async function run(options) {
|
|
|
4758
4971
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
4759
4972
|
}
|
|
4760
4973
|
}
|
|
4974
|
+
const noProviderWarning = buildNoProviderWarning(await hasAnyConfiguredProvider(state.port));
|
|
4975
|
+
if (noProviderWarning) {
|
|
4976
|
+
log2(state, noProviderWarning, "warn");
|
|
4977
|
+
if (state.interactive && !state.json) {
|
|
4978
|
+
logActivity(state, { type: "info", level: "warn", message: noProviderWarning });
|
|
4979
|
+
blank();
|
|
4980
|
+
console.log(chalk6.yellow("\u26A0 No OpenCode model provider is configured."));
|
|
4981
|
+
console.log(
|
|
4982
|
+
chalk6.dim(
|
|
4983
|
+
`Run ${chalk6.cyan("opencode auth login")} to set one up \u2014 messages will fail until then.`
|
|
4984
|
+
)
|
|
4985
|
+
);
|
|
4986
|
+
blank();
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4761
4989
|
} catch (error2) {
|
|
4762
4990
|
ocSpinner?.fail(error2.message);
|
|
4763
4991
|
throw error2;
|
|
@@ -4909,7 +5137,7 @@ async function run(options) {
|
|
|
4909
5137
|
}
|
|
4910
5138
|
telemetry.error(EventTypes.CLI_ERROR, `Run command failed: ${message}`, {
|
|
4911
5139
|
command: "run",
|
|
4912
|
-
agentId: options.agent
|
|
5140
|
+
agentId: options.runner || options.agent
|
|
4913
5141
|
});
|
|
4914
5142
|
await shutdownTelemetry();
|
|
4915
5143
|
process.exit(1);
|
|
@@ -4934,7 +5162,7 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
|
|
|
4934
5162
|
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);
|
|
4935
5163
|
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 }));
|
|
4936
5164
|
program.command("whoami").description("Show the currently logged in user").action(whoami);
|
|
4937
|
-
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(
|
|
5165
|
+
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(
|
|
4938
5166
|
"--log-level <level>",
|
|
4939
5167
|
"Log verbosity: debug | info | warn | error (default: info). Env: EVIDENT_LOG_LEVEL"
|
|
4940
5168
|
).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(
|
|
@@ -4950,6 +5178,7 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
4950
5178
|
(options) => {
|
|
4951
5179
|
run({
|
|
4952
5180
|
agent: options.agent,
|
|
5181
|
+
runner: options.runner,
|
|
4953
5182
|
port: parseInt(options.port, 10),
|
|
4954
5183
|
// Raw string — validation/precedence is single-sourced in run.ts's
|
|
4955
5184
|
// resolveLogLevel (flag > -v > EVIDENT_LOG_LEVEL > info).
|