@evident-ai/cli 3.0.1-dev.4be1aa5 → 3.0.1-dev.4e02c98
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 +3 -1
- package/dist/index.js +441 -89
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1173,17 +1173,113 @@ async function createOpenCodeSession(port, directory) {
|
|
|
1173
1173
|
const data = await response.json();
|
|
1174
1174
|
return data.id;
|
|
1175
1175
|
}
|
|
1176
|
+
async function getModelAttachmentCapability(port, model) {
|
|
1177
|
+
try {
|
|
1178
|
+
const res = await fetch(`${opencodeBase(port)}/config/providers`);
|
|
1179
|
+
if (!res.ok) {
|
|
1180
|
+
console.error(
|
|
1181
|
+
`[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
1182
|
+
);
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
const body = await res.json();
|
|
1186
|
+
const providers = Array.isArray(body?.providers) ? body.providers : null;
|
|
1187
|
+
if (!providers) {
|
|
1188
|
+
console.error(
|
|
1189
|
+
`[getModelAttachmentCapability] GET /config/providers body had no providers array (port ${port})`
|
|
1190
|
+
);
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
const slash = model ? model.indexOf("/") : -1;
|
|
1194
|
+
const providerId = slash > 0 ? model.slice(0, slash) : void 0;
|
|
1195
|
+
let modelId = slash > 0 ? model.slice(slash + 1) : void 0;
|
|
1196
|
+
const defaults2 = body?.default && typeof body.default === "object" ? body.default : void 0;
|
|
1197
|
+
let provider = providerId ? providers.find((p) => p?.id === providerId) : void 0;
|
|
1198
|
+
if (!provider && !providerId) {
|
|
1199
|
+
const defaultProviderIds = defaults2 ? Object.keys(defaults2) : [];
|
|
1200
|
+
if (defaultProviderIds.length === 1) {
|
|
1201
|
+
provider = providers.find((p) => p?.id === defaultProviderIds[0]);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (!provider || !provider.models) return null;
|
|
1205
|
+
if (!modelId && defaults2 && typeof provider.id === "string") {
|
|
1206
|
+
const def = defaults2[provider.id];
|
|
1207
|
+
if (typeof def === "string") modelId = def;
|
|
1208
|
+
}
|
|
1209
|
+
if (!modelId) {
|
|
1210
|
+
if (providerId) {
|
|
1211
|
+
const keys = Object.keys(provider.models);
|
|
1212
|
+
if (keys.length === 1) modelId = keys[0];
|
|
1213
|
+
}
|
|
1214
|
+
if (!modelId) return null;
|
|
1215
|
+
}
|
|
1216
|
+
const entry = provider.models[modelId];
|
|
1217
|
+
if (!entry || typeof entry !== "object") return null;
|
|
1218
|
+
return typeof entry.attachment === "boolean" ? entry.attachment : null;
|
|
1219
|
+
} catch (err) {
|
|
1220
|
+
console.error(
|
|
1221
|
+
`[getModelAttachmentCapability] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
1222
|
+
);
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
async function buildFileParts(attachments, capable) {
|
|
1227
|
+
const outcomes = [];
|
|
1228
|
+
const parts = [];
|
|
1229
|
+
const capabilityUnknown = capable === null;
|
|
1230
|
+
if (capable !== true) {
|
|
1231
|
+
for (const a of attachments.inputs) {
|
|
1232
|
+
outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: "skipped" });
|
|
1233
|
+
}
|
|
1234
|
+
return { parts, outcomes, capabilityUnknown };
|
|
1235
|
+
}
|
|
1236
|
+
for (const a of attachments.inputs) {
|
|
1237
|
+
let dataUrl = null;
|
|
1238
|
+
try {
|
|
1239
|
+
dataUrl = await attachments.fetchDataUrl(a.index);
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
console.error(
|
|
1242
|
+
`[buildFileParts] attachment ${a.index} (${a.mime}) fetch threw \u2014 omitting: ${err instanceof Error ? err.message : String(err)}`
|
|
1243
|
+
);
|
|
1244
|
+
dataUrl = null;
|
|
1245
|
+
}
|
|
1246
|
+
if (dataUrl == null) {
|
|
1247
|
+
outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: "failed" });
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
parts.push({
|
|
1251
|
+
type: "file",
|
|
1252
|
+
mime: a.mime,
|
|
1253
|
+
url: dataUrl,
|
|
1254
|
+
...a.filename ? { filename: a.filename } : {}
|
|
1255
|
+
});
|
|
1256
|
+
outcomes.push({ index: a.index, mime: a.mime, filename: a.filename, status: "sent" });
|
|
1257
|
+
}
|
|
1258
|
+
return { parts, outcomes, capabilityUnknown };
|
|
1259
|
+
}
|
|
1176
1260
|
function messageText(m) {
|
|
1177
1261
|
if (!m || !Array.isArray(m.parts)) return "";
|
|
1178
1262
|
return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
1179
1263
|
}
|
|
1180
|
-
async function sendPromptAsync(port, sessionId, content, options) {
|
|
1264
|
+
async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
1181
1265
|
const before = await getSessionMessages(port, sessionId);
|
|
1182
1266
|
const knownUserIds = new Set(
|
|
1183
1267
|
(before ?? []).filter((m) => roleOf(m) === "user").map((m) => idOf(m)).filter((id) => typeof id === "string")
|
|
1184
1268
|
);
|
|
1269
|
+
const parts = [{ type: "text", text: content }];
|
|
1270
|
+
let pendingOutcomes = null;
|
|
1271
|
+
if (attachments && attachments.inputs.length > 0) {
|
|
1272
|
+
const capable = await getModelAttachmentCapability(port, options?.model);
|
|
1273
|
+
const {
|
|
1274
|
+
parts: fileParts,
|
|
1275
|
+
outcomes,
|
|
1276
|
+
capabilityUnknown
|
|
1277
|
+
} = await buildFileParts(attachments, capable);
|
|
1278
|
+
parts.push(...fileParts);
|
|
1279
|
+
if (attachments.onOutcomes) pendingOutcomes = { outcomes, capabilityUnknown };
|
|
1280
|
+
}
|
|
1185
1281
|
const body = {
|
|
1186
|
-
parts
|
|
1282
|
+
parts
|
|
1187
1283
|
};
|
|
1188
1284
|
if (options?.agent) {
|
|
1189
1285
|
body.agent = options.agent;
|
|
@@ -1222,7 +1318,10 @@ async function sendPromptAsync(port, sessionId, content, options) {
|
|
|
1222
1318
|
best = { id, created };
|
|
1223
1319
|
}
|
|
1224
1320
|
}
|
|
1225
|
-
if (best)
|
|
1321
|
+
if (best) {
|
|
1322
|
+
if (pendingOutcomes && attachments?.onOutcomes) attachments.onOutcomes(pendingOutcomes);
|
|
1323
|
+
return best.id;
|
|
1324
|
+
}
|
|
1226
1325
|
}
|
|
1227
1326
|
if (attempt < READ_BACK_ATTEMPTS - 1) {
|
|
1228
1327
|
await new Promise((resolve2) => setTimeout(resolve2, READ_BACK_DELAY_MS));
|
|
@@ -1811,6 +1910,17 @@ function messageIdOf(m) {
|
|
|
1811
1910
|
const infoId = m.info?.id;
|
|
1812
1911
|
return typeof infoId === "string" ? infoId : void 0;
|
|
1813
1912
|
}
|
|
1913
|
+
function cleanImageMime(contentType) {
|
|
1914
|
+
if (!contentType) return null;
|
|
1915
|
+
const media = contentType.split(";")[0].trim().toLowerCase();
|
|
1916
|
+
return /^image\/[a-z0-9.+-]+$/.test(media) ? media : null;
|
|
1917
|
+
}
|
|
1918
|
+
var LOG_LEVELS = {
|
|
1919
|
+
debug: 0,
|
|
1920
|
+
info: 1,
|
|
1921
|
+
warn: 2,
|
|
1922
|
+
error: 3
|
|
1923
|
+
};
|
|
1814
1924
|
var DEFAULT_RETRY_POLICY = {
|
|
1815
1925
|
maxAttempts: 6,
|
|
1816
1926
|
baseDelayMs: 500,
|
|
@@ -1918,6 +2028,15 @@ var ChannelDriver = class {
|
|
|
1918
2028
|
* the row leaves the processing list, exactly like `dontRedispatch`.
|
|
1919
2029
|
*/
|
|
1920
2030
|
doneUndeliverable = /* @__PURE__ */ new Set();
|
|
2031
|
+
/**
|
|
2032
|
+
* "Already emitted `readopt_poll_unresolved` for this row" (#229). The b1 /
|
|
2033
|
+
* unreadable-status re-evaluate leaf leaves the row UN-tracked so it is re-read
|
|
2034
|
+
* every ~2s drain until the status map becomes readable — but the server-visible
|
|
2035
|
+
* signal is an OUTCOME, so it must fire at most ONCE per row, not once per drain
|
|
2036
|
+
* (Bugbot "Re-adopt signals flood every drain"). Cleared when the row leaves the
|
|
2037
|
+
* processing list, exactly like `dontRedispatch`/`doneUndeliverable`.
|
|
2038
|
+
*/
|
|
2039
|
+
readoptPollUnresolvedSignalled = /* @__PURE__ */ new Set();
|
|
1921
2040
|
/**
|
|
1922
2041
|
* "A null-id re-adopt re-dispatch is in flight, awaiting its read-back" (WI-5
|
|
1923
2042
|
* Task 5.4, High-2). Since we no longer send a caller-supplied id, a re-dispatch
|
|
@@ -1932,6 +2051,15 @@ var ChannelDriver = class {
|
|
|
1932
2051
|
* so the NEXT tick may retry exactly once more).
|
|
1933
2052
|
*/
|
|
1934
2053
|
awaitingReadopt = /* @__PURE__ */ new Set();
|
|
2054
|
+
/**
|
|
2055
|
+
* "Already signalled `attachments_skipped` for this Evident message id" (#376).
|
|
2056
|
+
* The in-thread skip note is an OUTCOME, so it must fire AT MOST ONCE per message
|
|
2057
|
+
* — never re-post on a re-dispatch of the same row (`forceReadoptRun` or the
|
|
2058
|
+
* next-tick null-id retry both re-run `sendPromptAsync`, which re-fires
|
|
2059
|
+
* `onOutcomes`). Mirrors `readoptPollUnresolvedSignalled`: a local dedup on the
|
|
2060
|
+
* outcome, not the dispatch. Not cleared (a message is signalled once for life).
|
|
2061
|
+
*/
|
|
2062
|
+
attachmentsSkippedSignalled = /* @__PURE__ */ new Set();
|
|
1935
2063
|
/**
|
|
1936
2064
|
* Cache of the opencode root directory (from `GET /path`). Resolved lazily on
|
|
1937
2065
|
* first session creation so drain-created sessions are rooted at the project
|
|
@@ -1949,6 +2077,16 @@ var ChannelDriver = class {
|
|
|
1949
2077
|
* entry = not yet resolved; `null` = resolved root (stop walking).
|
|
1950
2078
|
*/
|
|
1951
2079
|
sessionParents = /* @__PURE__ */ new Map();
|
|
2080
|
+
/**
|
|
2081
|
+
* Per-session OpenCode title cache (#310), keyed by sessionId. Only a resolved
|
|
2082
|
+
* NON-EMPTY name is stored (terminal — a real session name won't later un-name),
|
|
2083
|
+
* so we do NOT re-GET `/session/:id` every tick. A missing entry = not yet
|
|
2084
|
+
* resolved OR resolved-but-still-empty → re-fetch on next need, since OpenCode
|
|
2085
|
+
* names sessions asynchronously mid-turn. Driver-level (not per-watcher) so both
|
|
2086
|
+
* the watcher completion path AND the restart-recovery re-adopt path (which has
|
|
2087
|
+
* no watcher) can resolve the title.
|
|
2088
|
+
*/
|
|
2089
|
+
sessionTitles = /* @__PURE__ */ new Map();
|
|
1952
2090
|
/** Serialises drains so a reconnect during a drain doesn't double-process. */
|
|
1953
2091
|
draining = false;
|
|
1954
2092
|
/**
|
|
@@ -1986,9 +2124,6 @@ var ChannelDriver = class {
|
|
|
1986
2124
|
get opencodeBase() {
|
|
1987
2125
|
return `http://127.0.0.1:${this.port}`;
|
|
1988
2126
|
}
|
|
1989
|
-
// -------------------------------------------------------------------------
|
|
1990
|
-
// Public API
|
|
1991
|
-
// -------------------------------------------------------------------------
|
|
1992
2127
|
/**
|
|
1993
2128
|
* Drain all pending channel conversations once: poll → dispatch → register.
|
|
1994
2129
|
* Called on tunnel `connected` (WI-CHAN-4) and on each poll tick by `run.ts`.
|
|
@@ -2130,9 +2265,7 @@ var ChannelDriver = class {
|
|
|
2130
2265
|
if (!stillLive) return;
|
|
2131
2266
|
}
|
|
2132
2267
|
}
|
|
2133
|
-
// -------------------------------------------------------------------------
|
|
2134
2268
|
// Conversation processing (WI-3 — async dispatch)
|
|
2135
|
-
// -------------------------------------------------------------------------
|
|
2136
2269
|
/**
|
|
2137
2270
|
* Dispatch each pending message for a conversation to opencode's native queue
|
|
2138
2271
|
* via `prompt_async` (Task 3.2) and register it with the conversation's
|
|
@@ -2164,9 +2297,10 @@ var ChannelDriver = class {
|
|
|
2164
2297
|
conversation_id: conv.id,
|
|
2165
2298
|
message_id: message.id
|
|
2166
2299
|
});
|
|
2300
|
+
const sendAttachments = this.buildSendAttachments(conv, message);
|
|
2167
2301
|
opencodeMessageId = await this.dispatchLocked(
|
|
2168
2302
|
sessionId,
|
|
2169
|
-
() => sendPromptAsync(this.port, sessionId, message.content, options)
|
|
2303
|
+
() => sendPromptAsync(this.port, sessionId, message.content, options, sendAttachments)
|
|
2170
2304
|
);
|
|
2171
2305
|
} catch (err) {
|
|
2172
2306
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -2174,7 +2308,7 @@ var ChannelDriver = class {
|
|
|
2174
2308
|
if (await sessionExists(this.port, sessionId) === false) {
|
|
2175
2309
|
this.sessions.delete(conv.id);
|
|
2176
2310
|
this.log({
|
|
2177
|
-
level: "
|
|
2311
|
+
level: "warn",
|
|
2178
2312
|
message: `Message ${message.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was deleted mid-dispatch (cleanup race) \u2014 deferring this and later messages for conversation ${conv.id.slice(0, 8)} to the next tick (recreated then, in order). Already-dispatched turns keep their watcher.`,
|
|
2179
2313
|
conversation_id: conv.id,
|
|
2180
2314
|
message_id: message.id
|
|
@@ -2193,7 +2327,7 @@ var ChannelDriver = class {
|
|
|
2193
2327
|
}
|
|
2194
2328
|
if (opencodeMessageId === null) {
|
|
2195
2329
|
this.log({
|
|
2196
|
-
level: "
|
|
2330
|
+
level: "warn",
|
|
2197
2331
|
message: `Message ${message.id.slice(0, 8)} dispatched but its opencode id could not be read back \u2014 leaving un-tracked to retry next tick`,
|
|
2198
2332
|
conversation_id: conv.id,
|
|
2199
2333
|
message_id: message.id
|
|
@@ -2207,7 +2341,7 @@ var ChannelDriver = class {
|
|
|
2207
2341
|
}
|
|
2208
2342
|
if (messages.length > 0 && dispatched === 0 && skippedAlreadyDispatched === messages.length) {
|
|
2209
2343
|
this.log({
|
|
2210
|
-
level: "
|
|
2344
|
+
level: "warn",
|
|
2211
2345
|
message: `Conversation ${conv.id.slice(0, 8)} has ${messages.length} pending message(s) but ALL are already marked dispatched locally (in-flight set: ${this.dispatched.size}) \u2014 none sent to OpenCode this tick. If this repeats, a message may be stuck acknowledged-but-never-dispatched (its watcher never settled).`,
|
|
2212
2346
|
conversation_id: conv.id
|
|
2213
2347
|
});
|
|
@@ -2221,7 +2355,7 @@ var ChannelDriver = class {
|
|
|
2221
2355
|
const exists = await sessionExists(this.port, bound);
|
|
2222
2356
|
if (exists === false) {
|
|
2223
2357
|
this.log({
|
|
2224
|
-
level: "
|
|
2358
|
+
level: "debug",
|
|
2225
2359
|
message: `OpenCode session ${bound} for conversation ${conv.id.slice(0, 8)} no longer exists (deleted or DB reset) \u2014 creating a fresh session and rebinding.`,
|
|
2226
2360
|
conversation_id: conv.id
|
|
2227
2361
|
});
|
|
@@ -2256,15 +2390,13 @@ var ChannelDriver = class {
|
|
|
2256
2390
|
this.opencodeDirectory = await getOpenCodeDirectory(this.port);
|
|
2257
2391
|
if (!this.opencodeDirectory) {
|
|
2258
2392
|
this.log({
|
|
2259
|
-
level: "
|
|
2393
|
+
level: "warn",
|
|
2260
2394
|
message: "Could not determine opencode directory (GET /path) \u2014 new sessions may not appear in opencode web"
|
|
2261
2395
|
});
|
|
2262
2396
|
}
|
|
2263
2397
|
return this.opencodeDirectory;
|
|
2264
2398
|
}
|
|
2265
|
-
// -------------------------------------------------------------------------
|
|
2266
2399
|
// Per-session watcher (WI-3)
|
|
2267
|
-
// -------------------------------------------------------------------------
|
|
2268
2400
|
/**
|
|
2269
2401
|
* Run one dispatch (`sendPromptAsync` snapshot→POST→read-back) serialized per
|
|
2270
2402
|
* opencode session (Task 2.1a), so two dispatches into the SAME session can
|
|
@@ -2284,6 +2416,103 @@ var ChannelDriver = class {
|
|
|
2284
2416
|
);
|
|
2285
2417
|
return run2;
|
|
2286
2418
|
}
|
|
2419
|
+
// Inbound image attachments (#255, WI-8)
|
|
2420
|
+
/**
|
|
2421
|
+
* Build the `SendAttachmentsInput` for a message's inbound images, or
|
|
2422
|
+
* `undefined` when the message has none (so a text-only turn is unchanged).
|
|
2423
|
+
*
|
|
2424
|
+
* The driver OWNS the two channel-facing concerns the session module cannot:
|
|
2425
|
+
* - the AUTHENTICATED byte fetch through Evident's WI-6 endpoint
|
|
2426
|
+
* (`fetchAttachmentDataUrl`), using the SAME `getAuthHeader()` as every
|
|
2427
|
+
* other combinedAuth callback — the CLI NEVER talks to Slack directly;
|
|
2428
|
+
* - the in-thread SKIP NOTE (`signalAttachmentsSkipped`) posted over the
|
|
2429
|
+
* existing callback surface when any image was skipped/failed.
|
|
2430
|
+
* `sendPromptAsync` applies the capability gate + appends the `file` parts and
|
|
2431
|
+
* reports outcomes back via `onOutcomes`.
|
|
2432
|
+
*/
|
|
2433
|
+
buildSendAttachments(conv, message) {
|
|
2434
|
+
const refs = message.attachments;
|
|
2435
|
+
if (!refs || refs.length === 0) return void 0;
|
|
2436
|
+
return {
|
|
2437
|
+
inputs: refs.map((a, index) => ({
|
|
2438
|
+
index,
|
|
2439
|
+
mime: a.mime,
|
|
2440
|
+
...a.filename ? { filename: a.filename } : {}
|
|
2441
|
+
})),
|
|
2442
|
+
fetchDataUrl: (index) => this.fetchAttachmentDataUrl(message.id, index, refs[index].mime),
|
|
2443
|
+
onOutcomes: ({ outcomes, capabilityUnknown }) => this.signalAttachmentsSkipped(conv.id, message.id, outcomes, capabilityUnknown)
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Fetch ONE inbound image's bytes through Evident's WI-6 endpoint
|
|
2448
|
+
* (`GET {apiUrl}/agents/{agentId}/attachments/{messageId}/{index}`) using the
|
|
2449
|
+
* existing authenticated fetch, and base64-encode into a
|
|
2450
|
+
* `data:<mime>;base64,<…>` URL for the opencode `file` part's `url`.
|
|
2451
|
+
*
|
|
2452
|
+
* The endpoint streams the source bytes verbatim (200), or returns 404
|
|
2453
|
+
* (not-owned / out-of-range / deleted-at-source / workspace gone) / 413
|
|
2454
|
+
* (over-cap). On ANY non-2xx or thrown failure we return `null` so the caller
|
|
2455
|
+
* OMITS that one image and the text turn still sends — NEVER throws the turn.
|
|
2456
|
+
* Failures are logged with context (no silent swallow).
|
|
2457
|
+
*/
|
|
2458
|
+
async fetchAttachmentDataUrl(messageId, index, mime) {
|
|
2459
|
+
try {
|
|
2460
|
+
const res = await this.fetchImpl(
|
|
2461
|
+
`${this.apiUrl}/agents/${this.agentId}/attachments/${messageId}/${index}`,
|
|
2462
|
+
{ headers: { Authorization: this.getAuthHeader() } }
|
|
2463
|
+
);
|
|
2464
|
+
if (!res.ok) {
|
|
2465
|
+
this.log({
|
|
2466
|
+
level: "error",
|
|
2467
|
+
message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index} returned HTTP ${res.status} \u2014 omitting this image (text turn proceeds)`,
|
|
2468
|
+
message_id: messageId
|
|
2469
|
+
});
|
|
2470
|
+
return null;
|
|
2471
|
+
}
|
|
2472
|
+
const buf = await res.arrayBuffer();
|
|
2473
|
+
const base64 = Buffer.from(buf).toString("base64");
|
|
2474
|
+
const dataMime = cleanImageMime(res.headers.get("content-type")) || mime;
|
|
2475
|
+
return `data:${dataMime};base64,${base64}`;
|
|
2476
|
+
} catch (err) {
|
|
2477
|
+
this.log({
|
|
2478
|
+
level: "error",
|
|
2479
|
+
message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index} failed \u2014 omitting this image (text turn proceeds): ${err instanceof Error ? err.message : String(err)}`,
|
|
2480
|
+
message_id: messageId
|
|
2481
|
+
});
|
|
2482
|
+
return null;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* On any skipped/failed image, post an in-thread note to Evident over the
|
|
2487
|
+
* EXISTING combinedAuth callback surface — the CLI NEVER posts to Slack directly.
|
|
2488
|
+
* Evident routes the note to source via `conversation.deliver`.
|
|
2489
|
+
*
|
|
2490
|
+
* The `POST .../messages/:id/signal` route accepts `attachments_skipped` (in
|
|
2491
|
+
* `messageSignalSchema`) and turns it into an in-thread note delivered through
|
|
2492
|
+
* `conversation.deliver` (e.g. "N image(s) couldn't be forwarded"), so the note
|
|
2493
|
+
* reaches the channel.
|
|
2494
|
+
*
|
|
2495
|
+
* Fire-and-forget: never throws into the send/tick (logs its own failure).
|
|
2496
|
+
*/
|
|
2497
|
+
signalAttachmentsSkipped(conversationId, messageId, outcomes, capabilityUnknown) {
|
|
2498
|
+
const skipped = outcomes.filter((o) => o.status === "skipped").length;
|
|
2499
|
+
const failed = outcomes.filter((o) => o.status === "failed").length;
|
|
2500
|
+
if (skipped === 0 && failed === 0) return;
|
|
2501
|
+
if (this.attachmentsSkippedSignalled.has(messageId)) return;
|
|
2502
|
+
this.attachmentsSkippedSignalled.add(messageId);
|
|
2503
|
+
const skippedReason = capabilityUnknown ? "unknown" : "unsupported";
|
|
2504
|
+
this.log({
|
|
2505
|
+
level: "info",
|
|
2506
|
+
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`,
|
|
2507
|
+
conversation_id: conversationId,
|
|
2508
|
+
message_id: messageId
|
|
2509
|
+
});
|
|
2510
|
+
void this.postSignal(conversationId, messageId, "attachments_skipped", {
|
|
2511
|
+
skipped,
|
|
2512
|
+
failed,
|
|
2513
|
+
...skipped > 0 ? { skipped_reason: skippedReason } : {}
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2287
2516
|
/** Register a freshly-dispatched message with its session's watcher state. */
|
|
2288
2517
|
registerInFlight(conv, sessionId, message, opencodeMessageId) {
|
|
2289
2518
|
let watcher = this.watchers.get(sessionId);
|
|
@@ -2521,18 +2750,20 @@ var ChannelDriver = class {
|
|
|
2521
2750
|
const latchedPaused = inFlight.pausedOnQuestion || inFlight.pausedOnPermission;
|
|
2522
2751
|
const awaitingHuman = observedOpen || latchedPaused;
|
|
2523
2752
|
if ((state === "running" || state === "done" || state === "failed") && !inFlight.started) {
|
|
2753
|
+
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
2524
2754
|
let claimed;
|
|
2525
2755
|
try {
|
|
2526
2756
|
claimed = await this.markProcessing(
|
|
2527
2757
|
conv.id,
|
|
2528
2758
|
inFlight.evidentMessageId,
|
|
2529
2759
|
sessionId,
|
|
2530
|
-
inFlight.opencodeMessageId
|
|
2760
|
+
inFlight.opencodeMessageId,
|
|
2761
|
+
title
|
|
2531
2762
|
);
|
|
2532
2763
|
} catch (err) {
|
|
2533
2764
|
if (err instanceof ChannelAuthError) throw err;
|
|
2534
2765
|
this.log({
|
|
2535
|
-
level: "
|
|
2766
|
+
level: "warn",
|
|
2536
2767
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} processing (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
2537
2768
|
conversation_id: conv.id,
|
|
2538
2769
|
message_id: inFlight.evidentMessageId
|
|
@@ -2542,7 +2773,7 @@ var ChannelDriver = class {
|
|
|
2542
2773
|
inFlight.started = true;
|
|
2543
2774
|
if (!claimed) {
|
|
2544
2775
|
this.log({
|
|
2545
|
-
level: "
|
|
2776
|
+
level: "debug",
|
|
2546
2777
|
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} already marked processing \u2014 continuing`,
|
|
2547
2778
|
conversation_id: conv.id,
|
|
2548
2779
|
message_id: inFlight.evidentMessageId
|
|
@@ -2558,18 +2789,20 @@ var ChannelDriver = class {
|
|
|
2558
2789
|
conversation_id: conv.id,
|
|
2559
2790
|
message_id: inFlight.evidentMessageId
|
|
2560
2791
|
});
|
|
2792
|
+
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
2561
2793
|
try {
|
|
2562
2794
|
await this.markDone(
|
|
2563
2795
|
conv.id,
|
|
2564
2796
|
inFlight.evidentMessageId,
|
|
2565
2797
|
sessionId,
|
|
2566
|
-
inFlight.opencodeMessageId
|
|
2798
|
+
inFlight.opencodeMessageId,
|
|
2799
|
+
title
|
|
2567
2800
|
);
|
|
2568
2801
|
} catch (err) {
|
|
2569
2802
|
if (err instanceof ChannelAuthError) throw err;
|
|
2570
2803
|
if (err instanceof ChannelTerminalError) {
|
|
2571
2804
|
this.log({
|
|
2572
|
-
level: "
|
|
2805
|
+
level: "warn",
|
|
2573
2806
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (terminal HTTP ${err.status}) \u2014 leaving for the cron safety net: ${err.message}`,
|
|
2574
2807
|
conversation_id: conv.id,
|
|
2575
2808
|
message_id: inFlight.evidentMessageId
|
|
@@ -2579,7 +2812,7 @@ var ChannelDriver = class {
|
|
|
2579
2812
|
}
|
|
2580
2813
|
if (this.now() >= inFlight.deadline) {
|
|
2581
2814
|
this.log({
|
|
2582
|
-
level: "
|
|
2815
|
+
level: "warn",
|
|
2583
2816
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done within the watch window \u2014 leaving for the cron safety net: ${err instanceof Error ? err.message : String(err)}`,
|
|
2584
2817
|
conversation_id: conv.id,
|
|
2585
2818
|
message_id: inFlight.evidentMessageId
|
|
@@ -2588,7 +2821,7 @@ var ChannelDriver = class {
|
|
|
2588
2821
|
return;
|
|
2589
2822
|
}
|
|
2590
2823
|
this.log({
|
|
2591
|
-
level: "
|
|
2824
|
+
level: "warn",
|
|
2592
2825
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
2593
2826
|
conversation_id: conv.id,
|
|
2594
2827
|
message_id: inFlight.evidentMessageId
|
|
@@ -2616,7 +2849,7 @@ var ChannelDriver = class {
|
|
|
2616
2849
|
if (err instanceof ChannelAuthError) throw err;
|
|
2617
2850
|
if (err instanceof ChannelTerminalError) {
|
|
2618
2851
|
this.log({
|
|
2619
|
-
level: "
|
|
2852
|
+
level: "warn",
|
|
2620
2853
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed (terminal HTTP ${err.status}) \u2014 leaving for the cron safety net: ${err.message}`,
|
|
2621
2854
|
conversation_id: conv.id,
|
|
2622
2855
|
message_id: inFlight.evidentMessageId
|
|
@@ -2626,7 +2859,7 @@ var ChannelDriver = class {
|
|
|
2626
2859
|
}
|
|
2627
2860
|
if (this.now() >= inFlight.deadline) {
|
|
2628
2861
|
this.log({
|
|
2629
|
-
level: "
|
|
2862
|
+
level: "warn",
|
|
2630
2863
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed within the watch window \u2014 leaving for the cron safety net: ${err instanceof Error ? err.message : String(err)}`,
|
|
2631
2864
|
conversation_id: conv.id,
|
|
2632
2865
|
message_id: inFlight.evidentMessageId
|
|
@@ -2635,7 +2868,7 @@ var ChannelDriver = class {
|
|
|
2635
2868
|
return;
|
|
2636
2869
|
}
|
|
2637
2870
|
this.log({
|
|
2638
|
-
level: "
|
|
2871
|
+
level: "warn",
|
|
2639
2872
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
2640
2873
|
conversation_id: conv.id,
|
|
2641
2874
|
message_id: inFlight.evidentMessageId
|
|
@@ -2658,7 +2891,7 @@ var ChannelDriver = class {
|
|
|
2658
2891
|
const activelyRunning = state === "running" && !awaitingHuman;
|
|
2659
2892
|
if (activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
|
|
2660
2893
|
this.log({
|
|
2661
|
-
level: "
|
|
2894
|
+
level: "warn",
|
|
2662
2895
|
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} exceeded the absolute processing ceiling (${Math.round((this.now() - inFlight.processingAnchorMs) / 6e4)}min, session ${sessionId}) while still actively running \u2014 releasing so the cron can reclaim it`,
|
|
2663
2896
|
conversation_id: conv.id,
|
|
2664
2897
|
message_id: inFlight.evidentMessageId
|
|
@@ -2701,7 +2934,7 @@ var ChannelDriver = class {
|
|
|
2701
2934
|
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
2702
2935
|
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
2703
2936
|
this.log({
|
|
2704
|
-
level: "
|
|
2937
|
+
level: "debug",
|
|
2705
2938
|
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} did not complete within the watch window \u2014 leaving for the cron safety net`,
|
|
2706
2939
|
conversation_id: conv.id,
|
|
2707
2940
|
message_id: inFlight.evidentMessageId
|
|
@@ -2712,9 +2945,7 @@ var ChannelDriver = class {
|
|
|
2712
2945
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
2713
2946
|
}
|
|
2714
2947
|
}
|
|
2715
|
-
// -------------------------------------------------------------------------
|
|
2716
2948
|
// Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
|
|
2717
|
-
// -------------------------------------------------------------------------
|
|
2718
2949
|
/**
|
|
2719
2950
|
* Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).
|
|
2720
2951
|
*
|
|
@@ -2731,15 +2962,20 @@ var ChannelDriver = class {
|
|
|
2731
2962
|
*/
|
|
2732
2963
|
async readoptProcessing() {
|
|
2733
2964
|
const rows = await this.getProcessingMessages();
|
|
2734
|
-
if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0) {
|
|
2965
|
+
if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
|
|
2735
2966
|
const stillProcessing = new Set(rows.map((r) => r.id));
|
|
2736
|
-
for (const id of [
|
|
2967
|
+
for (const id of [
|
|
2968
|
+
...this.dontRedispatch,
|
|
2969
|
+
...this.doneUndeliverable,
|
|
2970
|
+
...this.readoptPollUnresolvedSignalled
|
|
2971
|
+
]) {
|
|
2737
2972
|
if (!stillProcessing.has(id)) {
|
|
2738
2973
|
const cleared = this.dontRedispatch.delete(id);
|
|
2739
2974
|
const clearedUndeliverable = this.doneUndeliverable.delete(id);
|
|
2975
|
+
this.readoptPollUnresolvedSignalled.delete(id);
|
|
2740
2976
|
if (cleared || clearedUndeliverable) {
|
|
2741
2977
|
this.log({
|
|
2742
|
-
level: "
|
|
2978
|
+
level: "debug",
|
|
2743
2979
|
message: `Re-adopt: message ${id.slice(0, 8)} left the processing list (cron reset) \u2014 cleared gave-up marker`,
|
|
2744
2980
|
message_id: id
|
|
2745
2981
|
});
|
|
@@ -2752,7 +2988,7 @@ var ChannelDriver = class {
|
|
|
2752
2988
|
for (const row of rows) {
|
|
2753
2989
|
if (!row.opencode_session_id) {
|
|
2754
2990
|
this.log({
|
|
2755
|
-
level: "
|
|
2991
|
+
level: "warn",
|
|
2756
2992
|
message: `Cannot re-adopt processing message ${row.id.slice(0, 8)} \u2014 no opencode session id; leaving for the cron safety net`,
|
|
2757
2993
|
conversation_id: row.conversation_id,
|
|
2758
2994
|
message_id: row.id
|
|
@@ -2769,7 +3005,7 @@ var ChannelDriver = class {
|
|
|
2769
3005
|
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
2770
3006
|
if (!res.ok) {
|
|
2771
3007
|
this.log({
|
|
2772
|
-
level: "
|
|
3008
|
+
level: "warn",
|
|
2773
3009
|
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 skipping this session this tick`
|
|
2774
3010
|
});
|
|
2775
3011
|
continue;
|
|
@@ -2777,7 +3013,7 @@ var ChannelDriver = class {
|
|
|
2777
3013
|
const body = await res.json();
|
|
2778
3014
|
if (!Array.isArray(body)) {
|
|
2779
3015
|
this.log({
|
|
2780
|
-
level: "
|
|
3016
|
+
level: "warn",
|
|
2781
3017
|
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned a non-array message body \u2014 skipping this session this tick`
|
|
2782
3018
|
});
|
|
2783
3019
|
continue;
|
|
@@ -2785,7 +3021,7 @@ var ChannelDriver = class {
|
|
|
2785
3021
|
messages = body;
|
|
2786
3022
|
} catch (err) {
|
|
2787
3023
|
this.log({
|
|
2788
|
-
level: "
|
|
3024
|
+
level: "warn",
|
|
2789
3025
|
message: `Re-adopt: failed to poll session ${sessionId.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`
|
|
2790
3026
|
});
|
|
2791
3027
|
continue;
|
|
@@ -2818,7 +3054,7 @@ var ChannelDriver = class {
|
|
|
2818
3054
|
async readoptOne(sessionId, row, messages, sessionOngoing) {
|
|
2819
3055
|
if (this.isTracked(sessionId, row.id)) {
|
|
2820
3056
|
this.log({
|
|
2821
|
-
level: "
|
|
3057
|
+
level: "debug",
|
|
2822
3058
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already tracked in-flight \u2014 skipping (owned by the watcher loop)`,
|
|
2823
3059
|
conversation_id: row.conversation_id,
|
|
2824
3060
|
message_id: row.id
|
|
@@ -2830,7 +3066,7 @@ var ChannelDriver = class {
|
|
|
2830
3066
|
if (state === "done") {
|
|
2831
3067
|
if (this.doneUndeliverable.has(row.id)) {
|
|
2832
3068
|
this.log({
|
|
2833
|
-
level: "
|
|
3069
|
+
level: "debug",
|
|
2834
3070
|
message: `Re-adopt: message ${row.id.slice(0, 8)} markDone is terminally undeliverable \u2014 left to the cron; skipping until it leaves processing`,
|
|
2835
3071
|
conversation_id: row.conversation_id,
|
|
2836
3072
|
message_id: row.id
|
|
@@ -2844,21 +3080,23 @@ var ChannelDriver = class {
|
|
|
2844
3080
|
message_id: row.id
|
|
2845
3081
|
});
|
|
2846
3082
|
try {
|
|
2847
|
-
await this.
|
|
3083
|
+
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
3084
|
+
await this.markDone(row.conversation_id, row.id, sessionId, ocId, title);
|
|
2848
3085
|
} catch (err) {
|
|
2849
3086
|
if (err instanceof ChannelAuthError) throw err;
|
|
2850
3087
|
if (err instanceof ChannelTerminalError) {
|
|
2851
3088
|
this.doneUndeliverable.add(row.id);
|
|
2852
3089
|
this.log({
|
|
2853
|
-
level: "
|
|
3090
|
+
level: "warn",
|
|
2854
3091
|
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (terminal HTTP ${err.status}) \u2014 parking until it leaves processing; leaving for the cron safety net: ${err.message}`,
|
|
2855
3092
|
conversation_id: row.conversation_id,
|
|
2856
3093
|
message_id: row.id
|
|
2857
3094
|
});
|
|
3095
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_undeliverable");
|
|
2858
3096
|
return;
|
|
2859
3097
|
}
|
|
2860
3098
|
this.log({
|
|
2861
|
-
level: "
|
|
3099
|
+
level: "warn",
|
|
2862
3100
|
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
2863
3101
|
conversation_id: row.conversation_id,
|
|
2864
3102
|
message_id: row.id
|
|
@@ -2866,6 +3104,7 @@ var ChannelDriver = class {
|
|
|
2866
3104
|
return;
|
|
2867
3105
|
}
|
|
2868
3106
|
this.dontRedispatch.delete(row.id);
|
|
3107
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
2869
3108
|
return;
|
|
2870
3109
|
}
|
|
2871
3110
|
if (state === "failed") {
|
|
@@ -2883,15 +3122,16 @@ var ChannelDriver = class {
|
|
|
2883
3122
|
if (err instanceof ChannelTerminalError) {
|
|
2884
3123
|
this.doneUndeliverable.add(row.id);
|
|
2885
3124
|
this.log({
|
|
2886
|
-
level: "
|
|
3125
|
+
level: "warn",
|
|
2887
3126
|
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} failed (terminal HTTP ${err.status}) \u2014 parking until it leaves processing; leaving for the cron safety net: ${err.message}`,
|
|
2888
3127
|
conversation_id: row.conversation_id,
|
|
2889
3128
|
message_id: row.id
|
|
2890
3129
|
});
|
|
3130
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_undeliverable");
|
|
2891
3131
|
return;
|
|
2892
3132
|
}
|
|
2893
3133
|
this.log({
|
|
2894
|
-
level: "
|
|
3134
|
+
level: "warn",
|
|
2895
3135
|
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} failed (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
2896
3136
|
conversation_id: row.conversation_id,
|
|
2897
3137
|
message_id: row.id
|
|
@@ -2899,11 +3139,12 @@ var ChannelDriver = class {
|
|
|
2899
3139
|
return;
|
|
2900
3140
|
}
|
|
2901
3141
|
this.dontRedispatch.delete(row.id);
|
|
3142
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
2902
3143
|
return;
|
|
2903
3144
|
}
|
|
2904
3145
|
if (this.dontRedispatch.has(row.id)) {
|
|
2905
3146
|
this.log({
|
|
2906
|
-
level: "
|
|
3147
|
+
level: "debug",
|
|
2907
3148
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
2908
3149
|
conversation_id: row.conversation_id,
|
|
2909
3150
|
message_id: row.id
|
|
@@ -2928,7 +3169,7 @@ var ChannelDriver = class {
|
|
|
2928
3169
|
}
|
|
2929
3170
|
if (ongoing === true) {
|
|
2930
3171
|
this.log({
|
|
2931
|
-
level: "
|
|
3172
|
+
level: "debug",
|
|
2932
3173
|
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per GET /session/status (busy/retry) \u2014 re-attaching watcher (no re-dispatch)`,
|
|
2933
3174
|
conversation_id: row.conversation_id,
|
|
2934
3175
|
message_id: row.id
|
|
@@ -2936,15 +3177,19 @@ var ChannelDriver = class {
|
|
|
2936
3177
|
} else {
|
|
2937
3178
|
if (shape === "b1") {
|
|
2938
3179
|
this.log({
|
|
2939
|
-
level: "
|
|
3180
|
+
level: "debug",
|
|
2940
3181
|
message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but GET /session/status was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 NOT latching a b1 row on a transient status blip; leaving it un-tracked to re-evaluate on the next drain`,
|
|
2941
3182
|
conversation_id: row.conversation_id,
|
|
2942
3183
|
message_id: row.id
|
|
2943
3184
|
});
|
|
3185
|
+
if (!this.readoptPollUnresolvedSignalled.has(row.id)) {
|
|
3186
|
+
this.readoptPollUnresolvedSignalled.add(row.id);
|
|
3187
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_poll_unresolved");
|
|
3188
|
+
}
|
|
2944
3189
|
return;
|
|
2945
3190
|
}
|
|
2946
3191
|
this.log({
|
|
2947
|
-
level: "
|
|
3192
|
+
level: "debug",
|
|
2948
3193
|
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but GET /session/status was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 falling back to the #253 preamble + descendant cross-check`,
|
|
2949
3194
|
conversation_id: row.conversation_id,
|
|
2950
3195
|
message_id: row.id
|
|
@@ -2955,7 +3200,7 @@ var ChannelDriver = class {
|
|
|
2955
3200
|
const descendantAlive = await this.isAnyDescendantSessionAlive(sessionId);
|
|
2956
3201
|
if (descendantAlive === true) {
|
|
2957
3202
|
this.log({
|
|
2958
|
-
level: "
|
|
3203
|
+
level: "debug",
|
|
2959
3204
|
message: `Re-adopt: message ${row.id.slice(0, 8)} preamble-pinned running (root ${sessionId.slice(0, 8)}) but a live descendant sub-agent session was found \u2014 treating as still running, re-attaching watcher (no re-dispatch)`,
|
|
2960
3205
|
conversation_id: row.conversation_id,
|
|
2961
3206
|
message_id: row.id
|
|
@@ -2979,11 +3224,12 @@ var ChannelDriver = class {
|
|
|
2979
3224
|
this.readopted.add(row.id);
|
|
2980
3225
|
this.ensureWatcherRunning(sessionId);
|
|
2981
3226
|
this.log({
|
|
2982
|
-
level: "
|
|
3227
|
+
level: "debug",
|
|
2983
3228
|
message: `Re-adopt: message ${row.id.slice(0, 8)} ${state} \u2014 re-attached watcher (stored id, no re-dispatch)`,
|
|
2984
3229
|
conversation_id: row.conversation_id,
|
|
2985
3230
|
message_id: row.id
|
|
2986
3231
|
});
|
|
3232
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_reattached");
|
|
2987
3233
|
return;
|
|
2988
3234
|
}
|
|
2989
3235
|
await this.forceReadoptRun(sessionId, row);
|
|
@@ -3012,7 +3258,7 @@ var ChannelDriver = class {
|
|
|
3012
3258
|
async forceReadoptRun(sessionId, row) {
|
|
3013
3259
|
if (this.stopped) {
|
|
3014
3260
|
this.log({
|
|
3015
|
-
level: "
|
|
3261
|
+
level: "debug",
|
|
3016
3262
|
message: `Re-adopt: message ${row.id.slice(0, 8)} orphaned but the runner is stopping \u2014 not starting a fresh turn; leaving for restart recovery`,
|
|
3017
3263
|
conversation_id: row.conversation_id,
|
|
3018
3264
|
message_id: row.id
|
|
@@ -3021,7 +3267,7 @@ var ChannelDriver = class {
|
|
|
3021
3267
|
}
|
|
3022
3268
|
if (this.awaitingReadopt.has(row.id)) {
|
|
3023
3269
|
this.log({
|
|
3024
|
-
level: "
|
|
3270
|
+
level: "debug",
|
|
3025
3271
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already has a re-dispatch awaiting read-back \u2014 skipping (at most once)`,
|
|
3026
3272
|
conversation_id: row.conversation_id,
|
|
3027
3273
|
message_id: row.id
|
|
@@ -3031,11 +3277,12 @@ var ChannelDriver = class {
|
|
|
3031
3277
|
if (this.processedAtMs(row) + this.pausedMaxWaitMs <= this.now()) {
|
|
3032
3278
|
this.dontRedispatch.add(row.id);
|
|
3033
3279
|
this.log({
|
|
3034
|
-
level: "
|
|
3280
|
+
level: "debug",
|
|
3035
3281
|
message: `Re-adopt: message ${row.id.slice(0, 8)} orphaned but its re-adopt window has already elapsed \u2014 not dispatching an unwatchable turn; parking until it leaves processing (cron will reset it)`,
|
|
3036
3282
|
conversation_id: row.conversation_id,
|
|
3037
3283
|
message_id: row.id
|
|
3038
3284
|
});
|
|
3285
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_window_elapsed");
|
|
3039
3286
|
return;
|
|
3040
3287
|
}
|
|
3041
3288
|
const options = {
|
|
@@ -3049,40 +3296,44 @@ var ChannelDriver = class {
|
|
|
3049
3296
|
message_id: row.id
|
|
3050
3297
|
});
|
|
3051
3298
|
this.awaitingReadopt.add(row.id);
|
|
3299
|
+
const readoptConv = this.convForRow(sessionId, row);
|
|
3300
|
+
const readoptMessage = this.queuedMessageForRow(row);
|
|
3301
|
+
const sendAttachments = this.buildSendAttachments(readoptConv, readoptMessage);
|
|
3052
3302
|
let ocId;
|
|
3053
3303
|
try {
|
|
3054
3304
|
ocId = await this.dispatchLocked(
|
|
3055
3305
|
sessionId,
|
|
3056
|
-
() => sendPromptAsync(this.port, sessionId, row.content, options)
|
|
3306
|
+
() => sendPromptAsync(this.port, sessionId, row.content, options, sendAttachments)
|
|
3057
3307
|
);
|
|
3058
3308
|
} catch (err) {
|
|
3059
3309
|
this.awaitingReadopt.delete(row.id);
|
|
3060
3310
|
if (err instanceof ChannelAuthError) throw err;
|
|
3061
3311
|
this.log({
|
|
3062
|
-
level: "
|
|
3312
|
+
level: "warn",
|
|
3063
3313
|
message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
3064
3314
|
conversation_id: row.conversation_id,
|
|
3065
3315
|
message_id: row.id
|
|
3066
3316
|
});
|
|
3317
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
3067
3318
|
return;
|
|
3068
3319
|
}
|
|
3069
3320
|
if (ocId === null) {
|
|
3070
3321
|
this.awaitingReadopt.delete(row.id);
|
|
3071
3322
|
this.log({
|
|
3072
|
-
level: "
|
|
3323
|
+
level: "warn",
|
|
3073
3324
|
message: `Re-adopt: message ${row.id.slice(0, 8)} re-dispatched but its opencode id could not be read back \u2014 leaving un-tracked to retry next drain`,
|
|
3074
3325
|
conversation_id: row.conversation_id,
|
|
3075
3326
|
message_id: row.id
|
|
3076
3327
|
});
|
|
3328
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
3077
3329
|
return;
|
|
3078
3330
|
}
|
|
3079
|
-
|
|
3080
|
-
const message = this.queuedMessageForRow(row);
|
|
3081
|
-
this.registerReadopted(conv, sessionId, message, ocId, this.processedAtMs(row));
|
|
3331
|
+
this.registerReadopted(readoptConv, sessionId, readoptMessage, ocId, this.processedAtMs(row));
|
|
3082
3332
|
this.dispatched.add(row.id);
|
|
3083
3333
|
this.readopted.add(row.id);
|
|
3084
3334
|
this.awaitingReadopt.delete(row.id);
|
|
3085
3335
|
this.ensureWatcherRunning(sessionId);
|
|
3336
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_redispatched");
|
|
3086
3337
|
}
|
|
3087
3338
|
/**
|
|
3088
3339
|
* True if `evidentMessageId` is already being driven — either in the
|
|
@@ -3131,7 +3382,8 @@ var ChannelDriver = class {
|
|
|
3131
3382
|
opencode_agent: row.opencode_agent,
|
|
3132
3383
|
opencode_model: row.opencode_model,
|
|
3133
3384
|
source_message_id: row.source_message_id,
|
|
3134
|
-
slack_user_id: row.slack_user_id
|
|
3385
|
+
slack_user_id: row.slack_user_id,
|
|
3386
|
+
attachments: row.attachments ?? null
|
|
3135
3387
|
};
|
|
3136
3388
|
}
|
|
3137
3389
|
/**
|
|
@@ -3152,7 +3404,7 @@ var ChannelDriver = class {
|
|
|
3152
3404
|
if (this.readopted.delete(evidentMessageId) && inFlight && !inFlight.done) {
|
|
3153
3405
|
this.dontRedispatch.add(evidentMessageId);
|
|
3154
3406
|
this.log({
|
|
3155
|
-
level: "
|
|
3407
|
+
level: "debug",
|
|
3156
3408
|
message: `Re-adopt: message ${evidentMessageId.slice(0, 8)} gave up \u2014 parking until it leaves the processing list (cron reset)`,
|
|
3157
3409
|
conversation_id: watcher.conv.id,
|
|
3158
3410
|
message_id: evidentMessageId
|
|
@@ -3292,6 +3544,51 @@ var ChannelDriver = class {
|
|
|
3292
3544
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
3293
3545
|
return parent;
|
|
3294
3546
|
}
|
|
3547
|
+
/**
|
|
3548
|
+
* Resolve (and cache in `sessionTitles`) the OpenCode session TITLE (#310) so the
|
|
3549
|
+
* status PATCH can carry it into the "Live sessions" list. Driver-level cache so
|
|
3550
|
+
* BOTH the watcher completion path and the restart-recovery re-adopt path (which
|
|
3551
|
+
* has no watcher) can use it. `conversationId` is passed only for log context.
|
|
3552
|
+
* Best-effort:
|
|
3553
|
+
* - a resolved NON-EMPTY title is cached and terminal (a real session name
|
|
3554
|
+
* won't later un-name), so we do NOT re-GET `/session/:id` every tick;
|
|
3555
|
+
* - while the title is still absent/empty we do NOT latch it — OpenCode names
|
|
3556
|
+
* sessions asynchronously mid-turn, so an early call (e.g. at `processing`)
|
|
3557
|
+
* must leave the cache unresolved and re-fetch on the next need so a later
|
|
3558
|
+
* call (e.g. at `done`) picks up the name assigned in the meantime. Such a
|
|
3559
|
+
* call returns `null` (omit the title on THIS PATCH) without caching;
|
|
3560
|
+
* - a failed request likewise leaves the cache unresolved (retry next need)
|
|
3561
|
+
* and returns `null` — it must NEVER throw or block completion.
|
|
3562
|
+
* A failure is logged with agent/session context (no silent catch).
|
|
3563
|
+
*/
|
|
3564
|
+
async resolveSessionTitle(sessionId, conversationId) {
|
|
3565
|
+
const cached = this.sessionTitles.get(sessionId);
|
|
3566
|
+
if (cached != null) return cached;
|
|
3567
|
+
try {
|
|
3568
|
+
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
|
|
3569
|
+
if (res.ok) {
|
|
3570
|
+
const body = await res.json();
|
|
3571
|
+
const title = body && typeof body.title === "string" ? body.title.trim() : "";
|
|
3572
|
+
if (title.length > 0) {
|
|
3573
|
+
this.sessionTitles.set(sessionId, title);
|
|
3574
|
+
return title;
|
|
3575
|
+
}
|
|
3576
|
+
return null;
|
|
3577
|
+
}
|
|
3578
|
+
this.log({
|
|
3579
|
+
level: "debug",
|
|
3580
|
+
message: `Session title fetch for session ${sessionId.slice(0, 8)} (agent ${this.agentId.slice(0, 8)}) returned HTTP ${res.status} \u2014 omitting title`,
|
|
3581
|
+
conversation_id: conversationId
|
|
3582
|
+
});
|
|
3583
|
+
} catch (err) {
|
|
3584
|
+
this.log({
|
|
3585
|
+
level: "debug",
|
|
3586
|
+
message: `Best-effort session title fetch failed for session ${sessionId.slice(0, 8)} (agent ${this.agentId.slice(0, 8)}) \u2014 omitting title: ${err instanceof Error ? err.message : String(err)}`,
|
|
3587
|
+
conversation_id: conversationId
|
|
3588
|
+
});
|
|
3589
|
+
}
|
|
3590
|
+
return null;
|
|
3591
|
+
}
|
|
3295
3592
|
/**
|
|
3296
3593
|
* DEFENSIVE cross-check for the restart-recovery path (WI-2): is any descendant
|
|
3297
3594
|
* (`task` sub-agent) session under `rootSessionId` still genuinely doing work?
|
|
@@ -3335,7 +3632,7 @@ var ChannelDriver = class {
|
|
|
3335
3632
|
const sessions = await listSessions(this.port);
|
|
3336
3633
|
if (!sessions) {
|
|
3337
3634
|
this.log({
|
|
3338
|
-
level: "
|
|
3635
|
+
level: "warn",
|
|
3339
3636
|
message: `Re-adopt: could not enumerate sessions to cross-check descendant liveness for root ${rootSessionId} (listSessions failed) \u2014 treating child liveness as indeterminate`
|
|
3340
3637
|
});
|
|
3341
3638
|
return null;
|
|
@@ -3419,9 +3716,7 @@ var ChannelDriver = class {
|
|
|
3419
3716
|
}
|
|
3420
3717
|
return inFlight.sort(byOldest)[0];
|
|
3421
3718
|
}
|
|
3422
|
-
// -------------------------------------------------------------------------
|
|
3423
3719
|
// Evident API calls (combinedAuth thread routes)
|
|
3424
|
-
// -------------------------------------------------------------------------
|
|
3425
3720
|
async getPendingConversations() {
|
|
3426
3721
|
const res = await this.fetchImpl(
|
|
3427
3722
|
`${this.apiUrl}/agents/${this.agentId}/conversations/pending`,
|
|
@@ -3501,7 +3796,7 @@ var ChannelDriver = class {
|
|
|
3501
3796
|
* A single attempt (no internal retry): the watcher's per-tick loop is the
|
|
3502
3797
|
* retry vehicle for the swap-to-running.
|
|
3503
3798
|
*/
|
|
3504
|
-
async markProcessing(conversationId, messageId, sessionId, opencodeMessageId) {
|
|
3799
|
+
async markProcessing(conversationId, messageId, sessionId, opencodeMessageId, title) {
|
|
3505
3800
|
const res = await this.fetchImpl(
|
|
3506
3801
|
`${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
3507
3802
|
{
|
|
@@ -3510,7 +3805,8 @@ var ChannelDriver = class {
|
|
|
3510
3805
|
body: JSON.stringify({
|
|
3511
3806
|
status: "processing",
|
|
3512
3807
|
opencode_session_id: sessionId,
|
|
3513
|
-
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {}
|
|
3808
|
+
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
3809
|
+
...title ? { title } : {}
|
|
3514
3810
|
})
|
|
3515
3811
|
}
|
|
3516
3812
|
);
|
|
@@ -3549,7 +3845,7 @@ var ChannelDriver = class {
|
|
|
3549
3845
|
* watcher retries next tick within the
|
|
3550
3846
|
* deadline, Finding 4).
|
|
3551
3847
|
*/
|
|
3552
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId) {
|
|
3848
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title) {
|
|
3553
3849
|
const res = await this.fetchImpl(
|
|
3554
3850
|
`${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
3555
3851
|
{
|
|
@@ -3558,7 +3854,8 @@ var ChannelDriver = class {
|
|
|
3558
3854
|
body: JSON.stringify({
|
|
3559
3855
|
status: "done",
|
|
3560
3856
|
opencode_session_id: sessionId,
|
|
3561
|
-
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {}
|
|
3857
|
+
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
3858
|
+
...title ? { title } : {}
|
|
3562
3859
|
})
|
|
3563
3860
|
}
|
|
3564
3861
|
);
|
|
@@ -3619,7 +3916,7 @@ var ChannelDriver = class {
|
|
|
3619
3916
|
);
|
|
3620
3917
|
if (!res.ok) {
|
|
3621
3918
|
this.log({
|
|
3622
|
-
level: "
|
|
3919
|
+
level: "warn",
|
|
3623
3920
|
message: `Signal '${signal}' for message ${messageId.slice(0, 8)} returned HTTP ${res.status} (telemetry-only, ignored)`,
|
|
3624
3921
|
conversation_id: conversationId,
|
|
3625
3922
|
message_id: messageId
|
|
@@ -3629,7 +3926,7 @@ var ChannelDriver = class {
|
|
|
3629
3926
|
return true;
|
|
3630
3927
|
} catch (err) {
|
|
3631
3928
|
this.log({
|
|
3632
|
-
level: "
|
|
3929
|
+
level: "warn",
|
|
3633
3930
|
message: `Signal '${signal}' for message ${messageId.slice(0, 8)} failed (telemetry-only, ignored): ${err instanceof Error ? err.message : String(err)}`,
|
|
3634
3931
|
conversation_id: conversationId,
|
|
3635
3932
|
message_id: messageId
|
|
@@ -3691,9 +3988,7 @@ var ChannelDriver = class {
|
|
|
3691
3988
|
return false;
|
|
3692
3989
|
}
|
|
3693
3990
|
}
|
|
3694
|
-
// -------------------------------------------------------------------------
|
|
3695
3991
|
// Retry wrapper
|
|
3696
|
-
// -------------------------------------------------------------------------
|
|
3697
3992
|
/**
|
|
3698
3993
|
* Invoke an Evident API call, retrying on transient failures (5xx / 429 /
|
|
3699
3994
|
* network errors) with exponential backoff + jitter (capped). Auth failures
|
|
@@ -3973,23 +4268,53 @@ var MAX_ACTIVITY_LOG_ENTRIES = 10;
|
|
|
3973
4268
|
var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
|
|
3974
4269
|
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
3975
4270
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
|
|
3976
|
-
function
|
|
4271
|
+
function resolveLogLevel(options) {
|
|
4272
|
+
const accepted = Object.keys(LOG_LEVELS);
|
|
4273
|
+
const validate = (value, source) => {
|
|
4274
|
+
const normalized = value.trim().toLowerCase();
|
|
4275
|
+
if (!accepted.includes(normalized)) {
|
|
4276
|
+
throw new Error(
|
|
4277
|
+
`Invalid log level "${value}"${source}; expected one of ${accepted.join(", ")}`
|
|
4278
|
+
);
|
|
4279
|
+
}
|
|
4280
|
+
return normalized;
|
|
4281
|
+
};
|
|
4282
|
+
if (options.logLevel !== void 0) {
|
|
4283
|
+
return validate(options.logLevel, " (--log-level)");
|
|
4284
|
+
}
|
|
4285
|
+
if (options.verbose) {
|
|
4286
|
+
return "debug";
|
|
4287
|
+
}
|
|
4288
|
+
const env = process.env.EVIDENT_LOG_LEVEL;
|
|
4289
|
+
if (env !== void 0 && env !== "") {
|
|
4290
|
+
return validate(env, " (EVIDENT_LOG_LEVEL)");
|
|
4291
|
+
}
|
|
4292
|
+
return "info";
|
|
4293
|
+
}
|
|
4294
|
+
function meetsThreshold(state, level) {
|
|
4295
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[state.logLevel];
|
|
4296
|
+
}
|
|
4297
|
+
function log2(state, message, level = "info") {
|
|
4298
|
+
if (!meetsThreshold(state, level)) return;
|
|
3977
4299
|
if (state.json) {
|
|
3978
4300
|
console.log(
|
|
3979
4301
|
JSON.stringify({
|
|
3980
4302
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3981
|
-
level
|
|
4303
|
+
level,
|
|
3982
4304
|
message
|
|
3983
4305
|
})
|
|
3984
4306
|
);
|
|
3985
4307
|
} else if (!state.interactive) {
|
|
3986
|
-
const prefix =
|
|
4308
|
+
const prefix = level === "error" ? chalk6.red("\u2717") : level === "warn" ? chalk6.yellow("!") : level === "debug" ? chalk6.dim("\xB7") : chalk6.green("\u2022");
|
|
3987
4309
|
console.log(`${prefix} ${message}`);
|
|
3988
4310
|
}
|
|
3989
4311
|
}
|
|
3990
4312
|
function logActivity(state, entry) {
|
|
4313
|
+
const level = entry.level ?? (entry.type === "error" ? "error" : "info");
|
|
4314
|
+
if (!meetsThreshold(state, level)) return;
|
|
3991
4315
|
const fullEntry = {
|
|
3992
4316
|
...entry,
|
|
4317
|
+
level,
|
|
3993
4318
|
timestamp: /* @__PURE__ */ new Date()
|
|
3994
4319
|
};
|
|
3995
4320
|
state.activityLog.push(fullEntry);
|
|
@@ -3998,9 +4323,9 @@ function logActivity(state, entry) {
|
|
|
3998
4323
|
}
|
|
3999
4324
|
if (!state.interactive) {
|
|
4000
4325
|
if (entry.type === "error") {
|
|
4001
|
-
log2(state, entry.error ?? "Unknown error",
|
|
4002
|
-
} else if (entry.
|
|
4003
|
-
log2(state, entry.message);
|
|
4326
|
+
log2(state, entry.error ?? "Unknown error", level);
|
|
4327
|
+
} else if (entry.message) {
|
|
4328
|
+
log2(state, entry.message, level);
|
|
4004
4329
|
}
|
|
4005
4330
|
}
|
|
4006
4331
|
}
|
|
@@ -4199,7 +4524,7 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
4199
4524
|
process.env
|
|
4200
4525
|
);
|
|
4201
4526
|
for (const warning2 of config2.warnings) {
|
|
4202
|
-
logActivity(state, { type: "info", message: `Session cleanup: ${warning2}` });
|
|
4527
|
+
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
4203
4528
|
}
|
|
4204
4529
|
if (!config2.enabled) return;
|
|
4205
4530
|
logActivity(state, {
|
|
@@ -4271,6 +4596,20 @@ async function cleanup(state, opts = {}) {
|
|
|
4271
4596
|
}
|
|
4272
4597
|
async function run(options) {
|
|
4273
4598
|
const interactive = isInteractive(options.json);
|
|
4599
|
+
let logLevel;
|
|
4600
|
+
try {
|
|
4601
|
+
logLevel = resolveLogLevel(options);
|
|
4602
|
+
} catch (error2) {
|
|
4603
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
4604
|
+
if (options.json) {
|
|
4605
|
+
console.log(JSON.stringify({ status: "error", error: message }));
|
|
4606
|
+
} else {
|
|
4607
|
+
printError(message);
|
|
4608
|
+
}
|
|
4609
|
+
await shutdownTelemetry();
|
|
4610
|
+
process.exit(1);
|
|
4611
|
+
return;
|
|
4612
|
+
}
|
|
4274
4613
|
const state = {
|
|
4275
4614
|
agentId: options.agent || "",
|
|
4276
4615
|
agentName: null,
|
|
@@ -4279,6 +4618,7 @@ async function run(options) {
|
|
|
4279
4618
|
idleTimeout: options.idleTimeout ?? null,
|
|
4280
4619
|
json: options.json ?? false,
|
|
4281
4620
|
interactive,
|
|
4621
|
+
logLevel,
|
|
4282
4622
|
connected: false,
|
|
4283
4623
|
opencodeConnected: false,
|
|
4284
4624
|
opencodeVersion: null,
|
|
@@ -4296,8 +4636,8 @@ async function run(options) {
|
|
|
4296
4636
|
if (state.idleTimeout === null && (process.env.GITHUB_ACTIONS || process.env.CI)) {
|
|
4297
4637
|
log2(
|
|
4298
4638
|
state,
|
|
4299
|
-
"
|
|
4300
|
-
|
|
4639
|
+
"No --idle-timeout set in CI environment. The runner will poll indefinitely until the job times out. Consider adding --idle-timeout 30 to avoid wasting runner minutes.",
|
|
4640
|
+
"warn"
|
|
4301
4641
|
);
|
|
4302
4642
|
}
|
|
4303
4643
|
const handleSignal = async () => {
|
|
@@ -4413,9 +4753,9 @@ async function run(options) {
|
|
|
4413
4753
|
ocSpinner?.succeed(`OpenCode running on port ${state.port}${version2}`);
|
|
4414
4754
|
const versionWarning = buildOpenCodeVersionWarning(state.opencodeVersion);
|
|
4415
4755
|
if (versionWarning) {
|
|
4416
|
-
log2(state, versionWarning,
|
|
4756
|
+
log2(state, versionWarning, "warn");
|
|
4417
4757
|
if (state.interactive && !state.json) {
|
|
4418
|
-
logActivity(state, { type: "info", message: versionWarning });
|
|
4758
|
+
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
4419
4759
|
}
|
|
4420
4760
|
}
|
|
4421
4761
|
} catch (error2) {
|
|
@@ -4430,11 +4770,17 @@ async function run(options) {
|
|
|
4430
4770
|
getAuthHeader: () => state.authHeader,
|
|
4431
4771
|
conversationFilter: state.conversationFilter,
|
|
4432
4772
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
4433
|
-
log: (entry) =>
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4773
|
+
log: (entry) => (
|
|
4774
|
+
// Thread the driver's real level straight through so `debug`/`warn`
|
|
4775
|
+
// survive the sink filter (they no longer collapse to info). `type`
|
|
4776
|
+
// stays the coarse error/non-error split the activity log renders with.
|
|
4777
|
+
logActivity(state, {
|
|
4778
|
+
type: entry.level === "error" ? "error" : "info",
|
|
4779
|
+
level: entry.level,
|
|
4780
|
+
message: entry.message,
|
|
4781
|
+
error: entry.level === "error" ? entry.message : void 0
|
|
4782
|
+
})
|
|
4783
|
+
)
|
|
4438
4784
|
});
|
|
4439
4785
|
state.channelDriver = channelDriver;
|
|
4440
4786
|
const connection = new RunnerConnection({
|
|
@@ -4588,7 +4934,10 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
|
|
|
4588
4934
|
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);
|
|
4589
4935
|
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 }));
|
|
4590
4936
|
program.command("whoami").description("Show the currently logged in user").action(whoami);
|
|
4591
|
-
program.command("run").description("Connect to Evident and process messages").option("-a, --agent [id]", "Agent ID to connect to (optional when EVIDENT_AGENT_KEY is set)").option("-p, --port <port>", "OpenCode port (default: 4096)", "4096").option(
|
|
4937
|
+
program.command("run").description("Connect to Evident and process messages").option("-a, --agent [id]", "Agent ID to connect to (optional when EVIDENT_AGENT_KEY is set)").option("-p, --port <port>", "OpenCode port (default: 4096)", "4096").option(
|
|
4938
|
+
"--log-level <level>",
|
|
4939
|
+
"Log verbosity: debug | info | warn | error (default: info). Env: EVIDENT_LOG_LEVEL"
|
|
4940
|
+
).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(
|
|
4592
4941
|
"--session-cleanup-max-age <duration>",
|
|
4593
4942
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
4594
4943
|
).option(
|
|
@@ -4602,6 +4951,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
4602
4951
|
run({
|
|
4603
4952
|
agent: options.agent,
|
|
4604
4953
|
port: parseInt(options.port, 10),
|
|
4954
|
+
// Raw string — validation/precedence is single-sourced in run.ts's
|
|
4955
|
+
// resolveLogLevel (flag > -v > EVIDENT_LOG_LEVEL > info).
|
|
4956
|
+
logLevel: options.logLevel,
|
|
4605
4957
|
verbose: options.verbose,
|
|
4606
4958
|
conversation: options.conversation,
|
|
4607
4959
|
idleTimeout: options.idleTimeout ? parseInt(options.idleTimeout, 10) : void 0,
|