@evident-ai/cli 3.0.1-dev.fbf2c4d → 3.0.1-dev.fbf37f6
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 +17 -15
- package/dist/index.js +361 -98
- 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,
|
|
@@ -1941,6 +2051,15 @@ var ChannelDriver = class {
|
|
|
1941
2051
|
* so the NEXT tick may retry exactly once more).
|
|
1942
2052
|
*/
|
|
1943
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();
|
|
1944
2063
|
/**
|
|
1945
2064
|
* Cache of the opencode root directory (from `GET /path`). Resolved lazily on
|
|
1946
2065
|
* first session creation so drain-created sessions are rooted at the project
|
|
@@ -2005,9 +2124,6 @@ var ChannelDriver = class {
|
|
|
2005
2124
|
get opencodeBase() {
|
|
2006
2125
|
return `http://127.0.0.1:${this.port}`;
|
|
2007
2126
|
}
|
|
2008
|
-
// -------------------------------------------------------------------------
|
|
2009
|
-
// Public API
|
|
2010
|
-
// -------------------------------------------------------------------------
|
|
2011
2127
|
/**
|
|
2012
2128
|
* Drain all pending channel conversations once: poll → dispatch → register.
|
|
2013
2129
|
* Called on tunnel `connected` (WI-CHAN-4) and on each poll tick by `run.ts`.
|
|
@@ -2149,9 +2265,7 @@ var ChannelDriver = class {
|
|
|
2149
2265
|
if (!stillLive) return;
|
|
2150
2266
|
}
|
|
2151
2267
|
}
|
|
2152
|
-
// -------------------------------------------------------------------------
|
|
2153
2268
|
// Conversation processing (WI-3 — async dispatch)
|
|
2154
|
-
// -------------------------------------------------------------------------
|
|
2155
2269
|
/**
|
|
2156
2270
|
* Dispatch each pending message for a conversation to opencode's native queue
|
|
2157
2271
|
* via `prompt_async` (Task 3.2) and register it with the conversation's
|
|
@@ -2183,9 +2297,10 @@ var ChannelDriver = class {
|
|
|
2183
2297
|
conversation_id: conv.id,
|
|
2184
2298
|
message_id: message.id
|
|
2185
2299
|
});
|
|
2300
|
+
const sendAttachments = this.buildSendAttachments(conv, message);
|
|
2186
2301
|
opencodeMessageId = await this.dispatchLocked(
|
|
2187
2302
|
sessionId,
|
|
2188
|
-
() => sendPromptAsync(this.port, sessionId, message.content, options)
|
|
2303
|
+
() => sendPromptAsync(this.port, sessionId, message.content, options, sendAttachments)
|
|
2189
2304
|
);
|
|
2190
2305
|
} catch (err) {
|
|
2191
2306
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -2193,7 +2308,7 @@ var ChannelDriver = class {
|
|
|
2193
2308
|
if (await sessionExists(this.port, sessionId) === false) {
|
|
2194
2309
|
this.sessions.delete(conv.id);
|
|
2195
2310
|
this.log({
|
|
2196
|
-
level: "
|
|
2311
|
+
level: "warn",
|
|
2197
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.`,
|
|
2198
2313
|
conversation_id: conv.id,
|
|
2199
2314
|
message_id: message.id
|
|
@@ -2212,7 +2327,7 @@ var ChannelDriver = class {
|
|
|
2212
2327
|
}
|
|
2213
2328
|
if (opencodeMessageId === null) {
|
|
2214
2329
|
this.log({
|
|
2215
|
-
level: "
|
|
2330
|
+
level: "warn",
|
|
2216
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`,
|
|
2217
2332
|
conversation_id: conv.id,
|
|
2218
2333
|
message_id: message.id
|
|
@@ -2226,7 +2341,7 @@ var ChannelDriver = class {
|
|
|
2226
2341
|
}
|
|
2227
2342
|
if (messages.length > 0 && dispatched === 0 && skippedAlreadyDispatched === messages.length) {
|
|
2228
2343
|
this.log({
|
|
2229
|
-
level: "
|
|
2344
|
+
level: "warn",
|
|
2230
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).`,
|
|
2231
2346
|
conversation_id: conv.id
|
|
2232
2347
|
});
|
|
@@ -2240,7 +2355,7 @@ var ChannelDriver = class {
|
|
|
2240
2355
|
const exists = await sessionExists(this.port, bound);
|
|
2241
2356
|
if (exists === false) {
|
|
2242
2357
|
this.log({
|
|
2243
|
-
level: "
|
|
2358
|
+
level: "debug",
|
|
2244
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.`,
|
|
2245
2360
|
conversation_id: conv.id
|
|
2246
2361
|
});
|
|
@@ -2275,15 +2390,13 @@ var ChannelDriver = class {
|
|
|
2275
2390
|
this.opencodeDirectory = await getOpenCodeDirectory(this.port);
|
|
2276
2391
|
if (!this.opencodeDirectory) {
|
|
2277
2392
|
this.log({
|
|
2278
|
-
level: "
|
|
2393
|
+
level: "warn",
|
|
2279
2394
|
message: "Could not determine opencode directory (GET /path) \u2014 new sessions may not appear in opencode web"
|
|
2280
2395
|
});
|
|
2281
2396
|
}
|
|
2282
2397
|
return this.opencodeDirectory;
|
|
2283
2398
|
}
|
|
2284
|
-
// -------------------------------------------------------------------------
|
|
2285
2399
|
// Per-session watcher (WI-3)
|
|
2286
|
-
// -------------------------------------------------------------------------
|
|
2287
2400
|
/**
|
|
2288
2401
|
* Run one dispatch (`sendPromptAsync` snapshot→POST→read-back) serialized per
|
|
2289
2402
|
* opencode session (Task 2.1a), so two dispatches into the SAME session can
|
|
@@ -2303,6 +2416,103 @@ var ChannelDriver = class {
|
|
|
2303
2416
|
);
|
|
2304
2417
|
return run2;
|
|
2305
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
|
+
}
|
|
2306
2516
|
/** Register a freshly-dispatched message with its session's watcher state. */
|
|
2307
2517
|
registerInFlight(conv, sessionId, message, opencodeMessageId) {
|
|
2308
2518
|
let watcher = this.watchers.get(sessionId);
|
|
@@ -2553,7 +2763,7 @@ var ChannelDriver = class {
|
|
|
2553
2763
|
} catch (err) {
|
|
2554
2764
|
if (err instanceof ChannelAuthError) throw err;
|
|
2555
2765
|
this.log({
|
|
2556
|
-
level: "
|
|
2766
|
+
level: "warn",
|
|
2557
2767
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} processing (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
2558
2768
|
conversation_id: conv.id,
|
|
2559
2769
|
message_id: inFlight.evidentMessageId
|
|
@@ -2563,7 +2773,7 @@ var ChannelDriver = class {
|
|
|
2563
2773
|
inFlight.started = true;
|
|
2564
2774
|
if (!claimed) {
|
|
2565
2775
|
this.log({
|
|
2566
|
-
level: "
|
|
2776
|
+
level: "debug",
|
|
2567
2777
|
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} already marked processing \u2014 continuing`,
|
|
2568
2778
|
conversation_id: conv.id,
|
|
2569
2779
|
message_id: inFlight.evidentMessageId
|
|
@@ -2592,7 +2802,7 @@ var ChannelDriver = class {
|
|
|
2592
2802
|
if (err instanceof ChannelAuthError) throw err;
|
|
2593
2803
|
if (err instanceof ChannelTerminalError) {
|
|
2594
2804
|
this.log({
|
|
2595
|
-
level: "
|
|
2805
|
+
level: "warn",
|
|
2596
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}`,
|
|
2597
2807
|
conversation_id: conv.id,
|
|
2598
2808
|
message_id: inFlight.evidentMessageId
|
|
@@ -2602,7 +2812,7 @@ var ChannelDriver = class {
|
|
|
2602
2812
|
}
|
|
2603
2813
|
if (this.now() >= inFlight.deadline) {
|
|
2604
2814
|
this.log({
|
|
2605
|
-
level: "
|
|
2815
|
+
level: "warn",
|
|
2606
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)}`,
|
|
2607
2817
|
conversation_id: conv.id,
|
|
2608
2818
|
message_id: inFlight.evidentMessageId
|
|
@@ -2611,7 +2821,7 @@ var ChannelDriver = class {
|
|
|
2611
2821
|
return;
|
|
2612
2822
|
}
|
|
2613
2823
|
this.log({
|
|
2614
|
-
level: "
|
|
2824
|
+
level: "warn",
|
|
2615
2825
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
2616
2826
|
conversation_id: conv.id,
|
|
2617
2827
|
message_id: inFlight.evidentMessageId
|
|
@@ -2639,7 +2849,7 @@ var ChannelDriver = class {
|
|
|
2639
2849
|
if (err instanceof ChannelAuthError) throw err;
|
|
2640
2850
|
if (err instanceof ChannelTerminalError) {
|
|
2641
2851
|
this.log({
|
|
2642
|
-
level: "
|
|
2852
|
+
level: "warn",
|
|
2643
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}`,
|
|
2644
2854
|
conversation_id: conv.id,
|
|
2645
2855
|
message_id: inFlight.evidentMessageId
|
|
@@ -2649,7 +2859,7 @@ var ChannelDriver = class {
|
|
|
2649
2859
|
}
|
|
2650
2860
|
if (this.now() >= inFlight.deadline) {
|
|
2651
2861
|
this.log({
|
|
2652
|
-
level: "
|
|
2862
|
+
level: "warn",
|
|
2653
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)}`,
|
|
2654
2864
|
conversation_id: conv.id,
|
|
2655
2865
|
message_id: inFlight.evidentMessageId
|
|
@@ -2658,7 +2868,7 @@ var ChannelDriver = class {
|
|
|
2658
2868
|
return;
|
|
2659
2869
|
}
|
|
2660
2870
|
this.log({
|
|
2661
|
-
level: "
|
|
2871
|
+
level: "warn",
|
|
2662
2872
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
2663
2873
|
conversation_id: conv.id,
|
|
2664
2874
|
message_id: inFlight.evidentMessageId
|
|
@@ -2681,7 +2891,7 @@ var ChannelDriver = class {
|
|
|
2681
2891
|
const activelyRunning = state === "running" && !awaitingHuman;
|
|
2682
2892
|
if (activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
|
|
2683
2893
|
this.log({
|
|
2684
|
-
level: "
|
|
2894
|
+
level: "warn",
|
|
2685
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`,
|
|
2686
2896
|
conversation_id: conv.id,
|
|
2687
2897
|
message_id: inFlight.evidentMessageId
|
|
@@ -2724,7 +2934,7 @@ var ChannelDriver = class {
|
|
|
2724
2934
|
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
2725
2935
|
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
2726
2936
|
this.log({
|
|
2727
|
-
level: "
|
|
2937
|
+
level: "debug",
|
|
2728
2938
|
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} did not complete within the watch window \u2014 leaving for the cron safety net`,
|
|
2729
2939
|
conversation_id: conv.id,
|
|
2730
2940
|
message_id: inFlight.evidentMessageId
|
|
@@ -2735,9 +2945,7 @@ var ChannelDriver = class {
|
|
|
2735
2945
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
2736
2946
|
}
|
|
2737
2947
|
}
|
|
2738
|
-
// -------------------------------------------------------------------------
|
|
2739
2948
|
// Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
|
|
2740
|
-
// -------------------------------------------------------------------------
|
|
2741
2949
|
/**
|
|
2742
2950
|
* Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).
|
|
2743
2951
|
*
|
|
@@ -2767,7 +2975,7 @@ var ChannelDriver = class {
|
|
|
2767
2975
|
this.readoptPollUnresolvedSignalled.delete(id);
|
|
2768
2976
|
if (cleared || clearedUndeliverable) {
|
|
2769
2977
|
this.log({
|
|
2770
|
-
level: "
|
|
2978
|
+
level: "debug",
|
|
2771
2979
|
message: `Re-adopt: message ${id.slice(0, 8)} left the processing list (cron reset) \u2014 cleared gave-up marker`,
|
|
2772
2980
|
message_id: id
|
|
2773
2981
|
});
|
|
@@ -2780,7 +2988,7 @@ var ChannelDriver = class {
|
|
|
2780
2988
|
for (const row of rows) {
|
|
2781
2989
|
if (!row.opencode_session_id) {
|
|
2782
2990
|
this.log({
|
|
2783
|
-
level: "
|
|
2991
|
+
level: "warn",
|
|
2784
2992
|
message: `Cannot re-adopt processing message ${row.id.slice(0, 8)} \u2014 no opencode session id; leaving for the cron safety net`,
|
|
2785
2993
|
conversation_id: row.conversation_id,
|
|
2786
2994
|
message_id: row.id
|
|
@@ -2797,7 +3005,7 @@ var ChannelDriver = class {
|
|
|
2797
3005
|
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
2798
3006
|
if (!res.ok) {
|
|
2799
3007
|
this.log({
|
|
2800
|
-
level: "
|
|
3008
|
+
level: "warn",
|
|
2801
3009
|
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 skipping this session this tick`
|
|
2802
3010
|
});
|
|
2803
3011
|
continue;
|
|
@@ -2805,7 +3013,7 @@ var ChannelDriver = class {
|
|
|
2805
3013
|
const body = await res.json();
|
|
2806
3014
|
if (!Array.isArray(body)) {
|
|
2807
3015
|
this.log({
|
|
2808
|
-
level: "
|
|
3016
|
+
level: "warn",
|
|
2809
3017
|
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned a non-array message body \u2014 skipping this session this tick`
|
|
2810
3018
|
});
|
|
2811
3019
|
continue;
|
|
@@ -2813,7 +3021,7 @@ var ChannelDriver = class {
|
|
|
2813
3021
|
messages = body;
|
|
2814
3022
|
} catch (err) {
|
|
2815
3023
|
this.log({
|
|
2816
|
-
level: "
|
|
3024
|
+
level: "warn",
|
|
2817
3025
|
message: `Re-adopt: failed to poll session ${sessionId.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`
|
|
2818
3026
|
});
|
|
2819
3027
|
continue;
|
|
@@ -2846,7 +3054,7 @@ var ChannelDriver = class {
|
|
|
2846
3054
|
async readoptOne(sessionId, row, messages, sessionOngoing) {
|
|
2847
3055
|
if (this.isTracked(sessionId, row.id)) {
|
|
2848
3056
|
this.log({
|
|
2849
|
-
level: "
|
|
3057
|
+
level: "debug",
|
|
2850
3058
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already tracked in-flight \u2014 skipping (owned by the watcher loop)`,
|
|
2851
3059
|
conversation_id: row.conversation_id,
|
|
2852
3060
|
message_id: row.id
|
|
@@ -2858,7 +3066,7 @@ var ChannelDriver = class {
|
|
|
2858
3066
|
if (state === "done") {
|
|
2859
3067
|
if (this.doneUndeliverable.has(row.id)) {
|
|
2860
3068
|
this.log({
|
|
2861
|
-
level: "
|
|
3069
|
+
level: "debug",
|
|
2862
3070
|
message: `Re-adopt: message ${row.id.slice(0, 8)} markDone is terminally undeliverable \u2014 left to the cron; skipping until it leaves processing`,
|
|
2863
3071
|
conversation_id: row.conversation_id,
|
|
2864
3072
|
message_id: row.id
|
|
@@ -2879,7 +3087,7 @@ var ChannelDriver = class {
|
|
|
2879
3087
|
if (err instanceof ChannelTerminalError) {
|
|
2880
3088
|
this.doneUndeliverable.add(row.id);
|
|
2881
3089
|
this.log({
|
|
2882
|
-
level: "
|
|
3090
|
+
level: "warn",
|
|
2883
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}`,
|
|
2884
3092
|
conversation_id: row.conversation_id,
|
|
2885
3093
|
message_id: row.id
|
|
@@ -2888,7 +3096,7 @@ var ChannelDriver = class {
|
|
|
2888
3096
|
return;
|
|
2889
3097
|
}
|
|
2890
3098
|
this.log({
|
|
2891
|
-
level: "
|
|
3099
|
+
level: "warn",
|
|
2892
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)}`,
|
|
2893
3101
|
conversation_id: row.conversation_id,
|
|
2894
3102
|
message_id: row.id
|
|
@@ -2914,7 +3122,7 @@ var ChannelDriver = class {
|
|
|
2914
3122
|
if (err instanceof ChannelTerminalError) {
|
|
2915
3123
|
this.doneUndeliverable.add(row.id);
|
|
2916
3124
|
this.log({
|
|
2917
|
-
level: "
|
|
3125
|
+
level: "warn",
|
|
2918
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}`,
|
|
2919
3127
|
conversation_id: row.conversation_id,
|
|
2920
3128
|
message_id: row.id
|
|
@@ -2923,7 +3131,7 @@ var ChannelDriver = class {
|
|
|
2923
3131
|
return;
|
|
2924
3132
|
}
|
|
2925
3133
|
this.log({
|
|
2926
|
-
level: "
|
|
3134
|
+
level: "warn",
|
|
2927
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)}`,
|
|
2928
3136
|
conversation_id: row.conversation_id,
|
|
2929
3137
|
message_id: row.id
|
|
@@ -2936,7 +3144,7 @@ var ChannelDriver = class {
|
|
|
2936
3144
|
}
|
|
2937
3145
|
if (this.dontRedispatch.has(row.id)) {
|
|
2938
3146
|
this.log({
|
|
2939
|
-
level: "
|
|
3147
|
+
level: "debug",
|
|
2940
3148
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
2941
3149
|
conversation_id: row.conversation_id,
|
|
2942
3150
|
message_id: row.id
|
|
@@ -2961,7 +3169,7 @@ var ChannelDriver = class {
|
|
|
2961
3169
|
}
|
|
2962
3170
|
if (ongoing === true) {
|
|
2963
3171
|
this.log({
|
|
2964
|
-
level: "
|
|
3172
|
+
level: "debug",
|
|
2965
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)`,
|
|
2966
3174
|
conversation_id: row.conversation_id,
|
|
2967
3175
|
message_id: row.id
|
|
@@ -2969,7 +3177,7 @@ var ChannelDriver = class {
|
|
|
2969
3177
|
} else {
|
|
2970
3178
|
if (shape === "b1") {
|
|
2971
3179
|
this.log({
|
|
2972
|
-
level: "
|
|
3180
|
+
level: "debug",
|
|
2973
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`,
|
|
2974
3182
|
conversation_id: row.conversation_id,
|
|
2975
3183
|
message_id: row.id
|
|
@@ -2981,7 +3189,7 @@ var ChannelDriver = class {
|
|
|
2981
3189
|
return;
|
|
2982
3190
|
}
|
|
2983
3191
|
this.log({
|
|
2984
|
-
level: "
|
|
3192
|
+
level: "debug",
|
|
2985
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`,
|
|
2986
3194
|
conversation_id: row.conversation_id,
|
|
2987
3195
|
message_id: row.id
|
|
@@ -2992,7 +3200,7 @@ var ChannelDriver = class {
|
|
|
2992
3200
|
const descendantAlive = await this.isAnyDescendantSessionAlive(sessionId);
|
|
2993
3201
|
if (descendantAlive === true) {
|
|
2994
3202
|
this.log({
|
|
2995
|
-
level: "
|
|
3203
|
+
level: "debug",
|
|
2996
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)`,
|
|
2997
3205
|
conversation_id: row.conversation_id,
|
|
2998
3206
|
message_id: row.id
|
|
@@ -3016,7 +3224,7 @@ var ChannelDriver = class {
|
|
|
3016
3224
|
this.readopted.add(row.id);
|
|
3017
3225
|
this.ensureWatcherRunning(sessionId);
|
|
3018
3226
|
this.log({
|
|
3019
|
-
level: "
|
|
3227
|
+
level: "debug",
|
|
3020
3228
|
message: `Re-adopt: message ${row.id.slice(0, 8)} ${state} \u2014 re-attached watcher (stored id, no re-dispatch)`,
|
|
3021
3229
|
conversation_id: row.conversation_id,
|
|
3022
3230
|
message_id: row.id
|
|
@@ -3050,7 +3258,7 @@ var ChannelDriver = class {
|
|
|
3050
3258
|
async forceReadoptRun(sessionId, row) {
|
|
3051
3259
|
if (this.stopped) {
|
|
3052
3260
|
this.log({
|
|
3053
|
-
level: "
|
|
3261
|
+
level: "debug",
|
|
3054
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`,
|
|
3055
3263
|
conversation_id: row.conversation_id,
|
|
3056
3264
|
message_id: row.id
|
|
@@ -3059,7 +3267,7 @@ var ChannelDriver = class {
|
|
|
3059
3267
|
}
|
|
3060
3268
|
if (this.awaitingReadopt.has(row.id)) {
|
|
3061
3269
|
this.log({
|
|
3062
|
-
level: "
|
|
3270
|
+
level: "debug",
|
|
3063
3271
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already has a re-dispatch awaiting read-back \u2014 skipping (at most once)`,
|
|
3064
3272
|
conversation_id: row.conversation_id,
|
|
3065
3273
|
message_id: row.id
|
|
@@ -3069,7 +3277,7 @@ var ChannelDriver = class {
|
|
|
3069
3277
|
if (this.processedAtMs(row) + this.pausedMaxWaitMs <= this.now()) {
|
|
3070
3278
|
this.dontRedispatch.add(row.id);
|
|
3071
3279
|
this.log({
|
|
3072
|
-
level: "
|
|
3280
|
+
level: "debug",
|
|
3073
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)`,
|
|
3074
3282
|
conversation_id: row.conversation_id,
|
|
3075
3283
|
message_id: row.id
|
|
@@ -3088,17 +3296,20 @@ var ChannelDriver = class {
|
|
|
3088
3296
|
message_id: row.id
|
|
3089
3297
|
});
|
|
3090
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);
|
|
3091
3302
|
let ocId;
|
|
3092
3303
|
try {
|
|
3093
3304
|
ocId = await this.dispatchLocked(
|
|
3094
3305
|
sessionId,
|
|
3095
|
-
() => sendPromptAsync(this.port, sessionId, row.content, options)
|
|
3306
|
+
() => sendPromptAsync(this.port, sessionId, row.content, options, sendAttachments)
|
|
3096
3307
|
);
|
|
3097
3308
|
} catch (err) {
|
|
3098
3309
|
this.awaitingReadopt.delete(row.id);
|
|
3099
3310
|
if (err instanceof ChannelAuthError) throw err;
|
|
3100
3311
|
this.log({
|
|
3101
|
-
level: "
|
|
3312
|
+
level: "warn",
|
|
3102
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)}`,
|
|
3103
3314
|
conversation_id: row.conversation_id,
|
|
3104
3315
|
message_id: row.id
|
|
@@ -3109,7 +3320,7 @@ var ChannelDriver = class {
|
|
|
3109
3320
|
if (ocId === null) {
|
|
3110
3321
|
this.awaitingReadopt.delete(row.id);
|
|
3111
3322
|
this.log({
|
|
3112
|
-
level: "
|
|
3323
|
+
level: "warn",
|
|
3113
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`,
|
|
3114
3325
|
conversation_id: row.conversation_id,
|
|
3115
3326
|
message_id: row.id
|
|
@@ -3117,9 +3328,7 @@ var ChannelDriver = class {
|
|
|
3117
3328
|
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
3118
3329
|
return;
|
|
3119
3330
|
}
|
|
3120
|
-
|
|
3121
|
-
const message = this.queuedMessageForRow(row);
|
|
3122
|
-
this.registerReadopted(conv, sessionId, message, ocId, this.processedAtMs(row));
|
|
3331
|
+
this.registerReadopted(readoptConv, sessionId, readoptMessage, ocId, this.processedAtMs(row));
|
|
3123
3332
|
this.dispatched.add(row.id);
|
|
3124
3333
|
this.readopted.add(row.id);
|
|
3125
3334
|
this.awaitingReadopt.delete(row.id);
|
|
@@ -3173,7 +3382,8 @@ var ChannelDriver = class {
|
|
|
3173
3382
|
opencode_agent: row.opencode_agent,
|
|
3174
3383
|
opencode_model: row.opencode_model,
|
|
3175
3384
|
source_message_id: row.source_message_id,
|
|
3176
|
-
slack_user_id: row.slack_user_id
|
|
3385
|
+
slack_user_id: row.slack_user_id,
|
|
3386
|
+
attachments: row.attachments ?? null
|
|
3177
3387
|
};
|
|
3178
3388
|
}
|
|
3179
3389
|
/**
|
|
@@ -3194,7 +3404,7 @@ var ChannelDriver = class {
|
|
|
3194
3404
|
if (this.readopted.delete(evidentMessageId) && inFlight && !inFlight.done) {
|
|
3195
3405
|
this.dontRedispatch.add(evidentMessageId);
|
|
3196
3406
|
this.log({
|
|
3197
|
-
level: "
|
|
3407
|
+
level: "debug",
|
|
3198
3408
|
message: `Re-adopt: message ${evidentMessageId.slice(0, 8)} gave up \u2014 parking until it leaves the processing list (cron reset)`,
|
|
3199
3409
|
conversation_id: watcher.conv.id,
|
|
3200
3410
|
message_id: evidentMessageId
|
|
@@ -3366,13 +3576,13 @@ var ChannelDriver = class {
|
|
|
3366
3576
|
return null;
|
|
3367
3577
|
}
|
|
3368
3578
|
this.log({
|
|
3369
|
-
level: "
|
|
3579
|
+
level: "debug",
|
|
3370
3580
|
message: `Session title fetch for session ${sessionId.slice(0, 8)} (agent ${this.agentId.slice(0, 8)}) returned HTTP ${res.status} \u2014 omitting title`,
|
|
3371
3581
|
conversation_id: conversationId
|
|
3372
3582
|
});
|
|
3373
3583
|
} catch (err) {
|
|
3374
3584
|
this.log({
|
|
3375
|
-
level: "
|
|
3585
|
+
level: "debug",
|
|
3376
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)}`,
|
|
3377
3587
|
conversation_id: conversationId
|
|
3378
3588
|
});
|
|
@@ -3422,7 +3632,7 @@ var ChannelDriver = class {
|
|
|
3422
3632
|
const sessions = await listSessions(this.port);
|
|
3423
3633
|
if (!sessions) {
|
|
3424
3634
|
this.log({
|
|
3425
|
-
level: "
|
|
3635
|
+
level: "warn",
|
|
3426
3636
|
message: `Re-adopt: could not enumerate sessions to cross-check descendant liveness for root ${rootSessionId} (listSessions failed) \u2014 treating child liveness as indeterminate`
|
|
3427
3637
|
});
|
|
3428
3638
|
return null;
|
|
@@ -3506,9 +3716,7 @@ var ChannelDriver = class {
|
|
|
3506
3716
|
}
|
|
3507
3717
|
return inFlight.sort(byOldest)[0];
|
|
3508
3718
|
}
|
|
3509
|
-
// -------------------------------------------------------------------------
|
|
3510
3719
|
// Evident API calls (combinedAuth thread routes)
|
|
3511
|
-
// -------------------------------------------------------------------------
|
|
3512
3720
|
async getPendingConversations() {
|
|
3513
3721
|
const res = await this.fetchImpl(
|
|
3514
3722
|
`${this.apiUrl}/agents/${this.agentId}/conversations/pending`,
|
|
@@ -3708,7 +3916,7 @@ var ChannelDriver = class {
|
|
|
3708
3916
|
);
|
|
3709
3917
|
if (!res.ok) {
|
|
3710
3918
|
this.log({
|
|
3711
|
-
level: "
|
|
3919
|
+
level: "warn",
|
|
3712
3920
|
message: `Signal '${signal}' for message ${messageId.slice(0, 8)} returned HTTP ${res.status} (telemetry-only, ignored)`,
|
|
3713
3921
|
conversation_id: conversationId,
|
|
3714
3922
|
message_id: messageId
|
|
@@ -3718,7 +3926,7 @@ var ChannelDriver = class {
|
|
|
3718
3926
|
return true;
|
|
3719
3927
|
} catch (err) {
|
|
3720
3928
|
this.log({
|
|
3721
|
-
level: "
|
|
3929
|
+
level: "warn",
|
|
3722
3930
|
message: `Signal '${signal}' for message ${messageId.slice(0, 8)} failed (telemetry-only, ignored): ${err instanceof Error ? err.message : String(err)}`,
|
|
3723
3931
|
conversation_id: conversationId,
|
|
3724
3932
|
message_id: messageId
|
|
@@ -3780,9 +3988,7 @@ var ChannelDriver = class {
|
|
|
3780
3988
|
return false;
|
|
3781
3989
|
}
|
|
3782
3990
|
}
|
|
3783
|
-
// -------------------------------------------------------------------------
|
|
3784
3991
|
// Retry wrapper
|
|
3785
|
-
// -------------------------------------------------------------------------
|
|
3786
3992
|
/**
|
|
3787
3993
|
* Invoke an Evident API call, retrying on transient failures (5xx / 429 /
|
|
3788
3994
|
* network errors) with exponential backoff + jitter (capped). Auth failures
|
|
@@ -3981,7 +4187,7 @@ async function resolveAgentIdFromKey(authHeader) {
|
|
|
3981
4187
|
if (!response.ok) {
|
|
3982
4188
|
const serverMessage = await readErrorMessage(response);
|
|
3983
4189
|
return {
|
|
3984
|
-
error: `Failed to resolve
|
|
4190
|
+
error: `Failed to resolve runner from key (HTTP ${response.status})${serverMessage ? `: ${serverMessage}` : ""}`
|
|
3985
4191
|
};
|
|
3986
4192
|
}
|
|
3987
4193
|
const data = await response.json();
|
|
@@ -3989,11 +4195,11 @@ async function resolveAgentIdFromKey(authHeader) {
|
|
|
3989
4195
|
return { agent_id: data.agent_id };
|
|
3990
4196
|
}
|
|
3991
4197
|
return {
|
|
3992
|
-
error: "Cannot resolve
|
|
4198
|
+
error: "Cannot resolve runner ID: auth type is not agent_key. Please provide --agent explicitly."
|
|
3993
4199
|
};
|
|
3994
4200
|
} catch (error2) {
|
|
3995
4201
|
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
3996
|
-
return { error: `Failed to resolve
|
|
4202
|
+
return { error: `Failed to resolve runner from key: ${message}` };
|
|
3997
4203
|
}
|
|
3998
4204
|
}
|
|
3999
4205
|
async function notifyAgentDisconnected(agentId, authHeader) {
|
|
@@ -4029,12 +4235,12 @@ async function getAgentInfo(agentId, authHeader) {
|
|
|
4029
4235
|
const serverMessage = await readErrorMessage(response);
|
|
4030
4236
|
return {
|
|
4031
4237
|
valid: false,
|
|
4032
|
-
error: serverMessage ?? "You do not have access to this
|
|
4238
|
+
error: serverMessage ?? "You do not have access to this runner (it may belong to a different team or organization)."
|
|
4033
4239
|
};
|
|
4034
4240
|
}
|
|
4035
4241
|
if (response.status === 404) {
|
|
4036
4242
|
const serverMessage = await readErrorMessage(response);
|
|
4037
|
-
return { valid: false, error: serverMessage ?? `
|
|
4243
|
+
return { valid: false, error: serverMessage ?? `Runner ${agentId} not found` };
|
|
4038
4244
|
}
|
|
4039
4245
|
if (!response.ok) {
|
|
4040
4246
|
const serverMessage = await readErrorMessage(response);
|
|
@@ -4047,13 +4253,13 @@ async function getAgentInfo(agentId, authHeader) {
|
|
|
4047
4253
|
if (agent.agent_type !== "local") {
|
|
4048
4254
|
return {
|
|
4049
4255
|
valid: false,
|
|
4050
|
-
error: `
|
|
4256
|
+
error: `Runner is type '${agent.agent_type}', must be 'local' for CLI connection`
|
|
4051
4257
|
};
|
|
4052
4258
|
}
|
|
4053
4259
|
return { valid: true, agent };
|
|
4054
4260
|
} catch (error2) {
|
|
4055
4261
|
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
4056
|
-
return { valid: false, error: `Failed to validate
|
|
4262
|
+
return { valid: false, error: `Failed to validate runner: ${message}` };
|
|
4057
4263
|
}
|
|
4058
4264
|
}
|
|
4059
4265
|
|
|
@@ -4062,23 +4268,53 @@ var MAX_ACTIVITY_LOG_ENTRIES = 10;
|
|
|
4062
4268
|
var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
|
|
4063
4269
|
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
4064
4270
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
|
|
4065
|
-
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;
|
|
4066
4299
|
if (state.json) {
|
|
4067
4300
|
console.log(
|
|
4068
4301
|
JSON.stringify({
|
|
4069
4302
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4070
|
-
level
|
|
4303
|
+
level,
|
|
4071
4304
|
message
|
|
4072
4305
|
})
|
|
4073
4306
|
);
|
|
4074
4307
|
} else if (!state.interactive) {
|
|
4075
|
-
const prefix =
|
|
4308
|
+
const prefix = level === "error" ? chalk6.red("\u2717") : level === "warn" ? chalk6.yellow("!") : level === "debug" ? chalk6.dim("\xB7") : chalk6.green("\u2022");
|
|
4076
4309
|
console.log(`${prefix} ${message}`);
|
|
4077
4310
|
}
|
|
4078
4311
|
}
|
|
4079
4312
|
function logActivity(state, entry) {
|
|
4313
|
+
const level = entry.level ?? (entry.type === "error" ? "error" : "info");
|
|
4314
|
+
if (!meetsThreshold(state, level)) return;
|
|
4080
4315
|
const fullEntry = {
|
|
4081
4316
|
...entry,
|
|
4317
|
+
level,
|
|
4082
4318
|
timestamp: /* @__PURE__ */ new Date()
|
|
4083
4319
|
};
|
|
4084
4320
|
state.activityLog.push(fullEntry);
|
|
@@ -4087,9 +4323,9 @@ function logActivity(state, entry) {
|
|
|
4087
4323
|
}
|
|
4088
4324
|
if (!state.interactive) {
|
|
4089
4325
|
if (entry.type === "error") {
|
|
4090
|
-
log2(state, entry.error ?? "Unknown error",
|
|
4091
|
-
} else if (entry.
|
|
4092
|
-
log2(state, entry.message);
|
|
4326
|
+
log2(state, entry.error ?? "Unknown error", level);
|
|
4327
|
+
} else if (entry.message) {
|
|
4328
|
+
log2(state, entry.message, level);
|
|
4093
4329
|
}
|
|
4094
4330
|
}
|
|
4095
4331
|
}
|
|
@@ -4288,7 +4524,7 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
4288
4524
|
process.env
|
|
4289
4525
|
);
|
|
4290
4526
|
for (const warning2 of config2.warnings) {
|
|
4291
|
-
logActivity(state, { type: "info", message: `Session cleanup: ${warning2}` });
|
|
4527
|
+
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
4292
4528
|
}
|
|
4293
4529
|
if (!config2.enabled) return;
|
|
4294
4530
|
logActivity(state, {
|
|
@@ -4310,7 +4546,7 @@ async function notifyOffline(state) {
|
|
|
4310
4546
|
}
|
|
4311
4547
|
const result = await notifyAgentDisconnected(state.agentId, state.authHeader);
|
|
4312
4548
|
if (result.ok) {
|
|
4313
|
-
log2(state, "Notified Evident the
|
|
4549
|
+
log2(state, "Notified Evident the runner is going offline");
|
|
4314
4550
|
} else {
|
|
4315
4551
|
logActivity(state, {
|
|
4316
4552
|
type: "error",
|
|
@@ -4360,6 +4596,20 @@ async function cleanup(state, opts = {}) {
|
|
|
4360
4596
|
}
|
|
4361
4597
|
async function run(options) {
|
|
4362
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
|
+
}
|
|
4363
4613
|
const state = {
|
|
4364
4614
|
agentId: options.agent || "",
|
|
4365
4615
|
agentName: null,
|
|
@@ -4368,6 +4618,7 @@ async function run(options) {
|
|
|
4368
4618
|
idleTimeout: options.idleTimeout ?? null,
|
|
4369
4619
|
json: options.json ?? false,
|
|
4370
4620
|
interactive,
|
|
4621
|
+
logLevel,
|
|
4371
4622
|
connected: false,
|
|
4372
4623
|
opencodeConnected: false,
|
|
4373
4624
|
opencodeVersion: null,
|
|
@@ -4385,8 +4636,8 @@ async function run(options) {
|
|
|
4385
4636
|
if (state.idleTimeout === null && (process.env.GITHUB_ACTIONS || process.env.CI)) {
|
|
4386
4637
|
log2(
|
|
4387
4638
|
state,
|
|
4388
|
-
"
|
|
4389
|
-
|
|
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"
|
|
4390
4641
|
);
|
|
4391
4642
|
}
|
|
4392
4643
|
const handleSignal = async () => {
|
|
@@ -4429,15 +4680,15 @@ async function run(options) {
|
|
|
4429
4680
|
const resolved = await resolveAgentIdFromKey(state.authHeader);
|
|
4430
4681
|
if (resolved.agent_id) {
|
|
4431
4682
|
state.agentId = resolved.agent_id;
|
|
4432
|
-
log2(state, `Resolved
|
|
4683
|
+
log2(state, `Resolved runner ID from key: ${state.agentId}`);
|
|
4433
4684
|
if (state.interactive && !state.json) {
|
|
4434
4685
|
logActivity(state, {
|
|
4435
4686
|
type: "info",
|
|
4436
|
-
message: `
|
|
4687
|
+
message: `Runner ID resolved from key: ${state.agentId}`
|
|
4437
4688
|
});
|
|
4438
4689
|
}
|
|
4439
4690
|
} else {
|
|
4440
|
-
printError(resolved.error || "Failed to resolve
|
|
4691
|
+
printError(resolved.error || "Failed to resolve runner ID from key");
|
|
4441
4692
|
process.exit(1);
|
|
4442
4693
|
}
|
|
4443
4694
|
} else {
|
|
@@ -4465,7 +4716,7 @@ async function run(options) {
|
|
|
4465
4716
|
console.log(chalk6.bold("Evident Run"));
|
|
4466
4717
|
console.log(chalk6.dim("-".repeat(40)));
|
|
4467
4718
|
}
|
|
4468
|
-
const spinner = interactive && !state.json ? ora3("Validating
|
|
4719
|
+
const spinner = interactive && !state.json ? ora3("Validating runner...").start() : null;
|
|
4469
4720
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
4470
4721
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
4471
4722
|
spinner?.fail("Authentication failed");
|
|
@@ -4477,14 +4728,14 @@ async function run(options) {
|
|
|
4477
4728
|
"Login successful! Retrying..."
|
|
4478
4729
|
);
|
|
4479
4730
|
state.authHeader = getAuthHeader(credentials2);
|
|
4480
|
-
spinner?.start("Validating
|
|
4731
|
+
spinner?.start("Validating runner...");
|
|
4481
4732
|
validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
4482
4733
|
}
|
|
4483
4734
|
if (!validation.valid) {
|
|
4484
|
-
spinner?.fail(`
|
|
4735
|
+
spinner?.fail(`Runner validation failed: ${validation.error}`);
|
|
4485
4736
|
throw new Error(validation.error);
|
|
4486
4737
|
}
|
|
4487
|
-
spinner?.succeed(`
|
|
4738
|
+
spinner?.succeed(`Runner: ${validation.agent.name || state.agentId}`);
|
|
4488
4739
|
state.agentName = validation.agent.name;
|
|
4489
4740
|
const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
|
|
4490
4741
|
try {
|
|
@@ -4502,9 +4753,9 @@ async function run(options) {
|
|
|
4502
4753
|
ocSpinner?.succeed(`OpenCode running on port ${state.port}${version2}`);
|
|
4503
4754
|
const versionWarning = buildOpenCodeVersionWarning(state.opencodeVersion);
|
|
4504
4755
|
if (versionWarning) {
|
|
4505
|
-
log2(state, versionWarning,
|
|
4756
|
+
log2(state, versionWarning, "warn");
|
|
4506
4757
|
if (state.interactive && !state.json) {
|
|
4507
|
-
logActivity(state, { type: "info", message: versionWarning });
|
|
4758
|
+
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
4508
4759
|
}
|
|
4509
4760
|
}
|
|
4510
4761
|
} catch (error2) {
|
|
@@ -4519,11 +4770,17 @@ async function run(options) {
|
|
|
4519
4770
|
getAuthHeader: () => state.authHeader,
|
|
4520
4771
|
conversationFilter: state.conversationFilter,
|
|
4521
4772
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
4522
|
-
log: (entry) =>
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
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
|
+
)
|
|
4527
4784
|
});
|
|
4528
4785
|
state.channelDriver = channelDriver;
|
|
4529
4786
|
const connection = new RunnerConnection({
|
|
@@ -4537,7 +4794,7 @@ async function run(options) {
|
|
|
4537
4794
|
state.agentId = agentId;
|
|
4538
4795
|
logActivity(state, {
|
|
4539
4796
|
type: "info",
|
|
4540
|
-
message: `Tunnel ${isReconnect ? "reconnected" : "connected"} (
|
|
4797
|
+
message: `Tunnel ${isReconnect ? "reconnected" : "connected"} (runner: ${agentId})`
|
|
4541
4798
|
});
|
|
4542
4799
|
emitAgentConnected(state.agentId, {
|
|
4543
4800
|
port: state.port,
|
|
@@ -4677,7 +4934,10 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
|
|
|
4677
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);
|
|
4678
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 }));
|
|
4679
4936
|
program.command("whoami").description("Show the currently logged in user").action(whoami);
|
|
4680
|
-
program.command("run").description("Connect to Evident and process messages").option("-a, --agent [id]", "
|
|
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(
|
|
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(
|
|
4681
4941
|
"--session-cleanup-max-age <duration>",
|
|
4682
4942
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
4683
4943
|
).option(
|
|
@@ -4691,6 +4951,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
4691
4951
|
run({
|
|
4692
4952
|
agent: options.agent,
|
|
4693
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,
|
|
4694
4957
|
verbose: options.verbose,
|
|
4695
4958
|
conversation: options.conversation,
|
|
4696
4959
|
idleTimeout: options.idleTimeout ? parseInt(options.idleTimeout, 10) : void 0,
|