@evident-ai/cli 3.0.1-dev.10c527f → 3.0.1-dev.18aac68
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 +414 -74
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
@@ -1958,6 +2077,16 @@ var ChannelDriver = class {
|
|
|
1958
2077
|
* entry = not yet resolved; `null` = resolved root (stop walking).
|
|
1959
2078
|
*/
|
|
1960
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();
|
|
1961
2090
|
/** Serialises drains so a reconnect during a drain doesn't double-process. */
|
|
1962
2091
|
draining = false;
|
|
1963
2092
|
/**
|
|
@@ -2173,9 +2302,10 @@ var ChannelDriver = class {
|
|
|
2173
2302
|
conversation_id: conv.id,
|
|
2174
2303
|
message_id: message.id
|
|
2175
2304
|
});
|
|
2305
|
+
const sendAttachments = this.buildSendAttachments(conv, message);
|
|
2176
2306
|
opencodeMessageId = await this.dispatchLocked(
|
|
2177
2307
|
sessionId,
|
|
2178
|
-
() => sendPromptAsync(this.port, sessionId, message.content, options)
|
|
2308
|
+
() => sendPromptAsync(this.port, sessionId, message.content, options, sendAttachments)
|
|
2179
2309
|
);
|
|
2180
2310
|
} catch (err) {
|
|
2181
2311
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -2183,7 +2313,7 @@ var ChannelDriver = class {
|
|
|
2183
2313
|
if (await sessionExists(this.port, sessionId) === false) {
|
|
2184
2314
|
this.sessions.delete(conv.id);
|
|
2185
2315
|
this.log({
|
|
2186
|
-
level: "
|
|
2316
|
+
level: "warn",
|
|
2187
2317
|
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.`,
|
|
2188
2318
|
conversation_id: conv.id,
|
|
2189
2319
|
message_id: message.id
|
|
@@ -2202,7 +2332,7 @@ var ChannelDriver = class {
|
|
|
2202
2332
|
}
|
|
2203
2333
|
if (opencodeMessageId === null) {
|
|
2204
2334
|
this.log({
|
|
2205
|
-
level: "
|
|
2335
|
+
level: "warn",
|
|
2206
2336
|
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`,
|
|
2207
2337
|
conversation_id: conv.id,
|
|
2208
2338
|
message_id: message.id
|
|
@@ -2216,7 +2346,7 @@ var ChannelDriver = class {
|
|
|
2216
2346
|
}
|
|
2217
2347
|
if (messages.length > 0 && dispatched === 0 && skippedAlreadyDispatched === messages.length) {
|
|
2218
2348
|
this.log({
|
|
2219
|
-
level: "
|
|
2349
|
+
level: "warn",
|
|
2220
2350
|
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).`,
|
|
2221
2351
|
conversation_id: conv.id
|
|
2222
2352
|
});
|
|
@@ -2230,7 +2360,7 @@ var ChannelDriver = class {
|
|
|
2230
2360
|
const exists = await sessionExists(this.port, bound);
|
|
2231
2361
|
if (exists === false) {
|
|
2232
2362
|
this.log({
|
|
2233
|
-
level: "
|
|
2363
|
+
level: "debug",
|
|
2234
2364
|
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.`,
|
|
2235
2365
|
conversation_id: conv.id
|
|
2236
2366
|
});
|
|
@@ -2265,7 +2395,7 @@ var ChannelDriver = class {
|
|
|
2265
2395
|
this.opencodeDirectory = await getOpenCodeDirectory(this.port);
|
|
2266
2396
|
if (!this.opencodeDirectory) {
|
|
2267
2397
|
this.log({
|
|
2268
|
-
level: "
|
|
2398
|
+
level: "warn",
|
|
2269
2399
|
message: "Could not determine opencode directory (GET /path) \u2014 new sessions may not appear in opencode web"
|
|
2270
2400
|
});
|
|
2271
2401
|
}
|
|
@@ -2293,6 +2423,105 @@ var ChannelDriver = class {
|
|
|
2293
2423
|
);
|
|
2294
2424
|
return run2;
|
|
2295
2425
|
}
|
|
2426
|
+
// -------------------------------------------------------------------------
|
|
2427
|
+
// Inbound image attachments (#255, WI-8)
|
|
2428
|
+
// -------------------------------------------------------------------------
|
|
2429
|
+
/**
|
|
2430
|
+
* Build the `SendAttachmentsInput` for a message's inbound images, or
|
|
2431
|
+
* `undefined` when the message has none (so a text-only turn is unchanged).
|
|
2432
|
+
*
|
|
2433
|
+
* The driver OWNS the two channel-facing concerns the session module cannot:
|
|
2434
|
+
* - the AUTHENTICATED byte fetch through Evident's WI-6 endpoint
|
|
2435
|
+
* (`fetchAttachmentDataUrl`), using the SAME `getAuthHeader()` as every
|
|
2436
|
+
* other combinedAuth callback — the CLI NEVER talks to Slack directly;
|
|
2437
|
+
* - the in-thread SKIP NOTE (`signalAttachmentsSkipped`) posted over the
|
|
2438
|
+
* existing callback surface when any image was skipped/failed.
|
|
2439
|
+
* `sendPromptAsync` applies the capability gate + appends the `file` parts and
|
|
2440
|
+
* reports outcomes back via `onOutcomes`.
|
|
2441
|
+
*/
|
|
2442
|
+
buildSendAttachments(conv, message) {
|
|
2443
|
+
const refs = message.attachments;
|
|
2444
|
+
if (!refs || refs.length === 0) return void 0;
|
|
2445
|
+
return {
|
|
2446
|
+
inputs: refs.map((a, index) => ({
|
|
2447
|
+
index,
|
|
2448
|
+
mime: a.mime,
|
|
2449
|
+
...a.filename ? { filename: a.filename } : {}
|
|
2450
|
+
})),
|
|
2451
|
+
fetchDataUrl: (index) => this.fetchAttachmentDataUrl(message.id, index, refs[index].mime),
|
|
2452
|
+
onOutcomes: ({ outcomes, capabilityUnknown }) => this.signalAttachmentsSkipped(conv.id, message.id, outcomes, capabilityUnknown)
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Fetch ONE inbound image's bytes through Evident's WI-6 endpoint
|
|
2457
|
+
* (`GET {apiUrl}/agents/{agentId}/attachments/{messageId}/{index}`) using the
|
|
2458
|
+
* existing authenticated fetch, and base64-encode into a
|
|
2459
|
+
* `data:<mime>;base64,<…>` URL for the opencode `file` part's `url`.
|
|
2460
|
+
*
|
|
2461
|
+
* The endpoint streams the source bytes verbatim (200), or returns 404
|
|
2462
|
+
* (not-owned / out-of-range / deleted-at-source / workspace gone) / 413
|
|
2463
|
+
* (over-cap). On ANY non-2xx or thrown failure we return `null` so the caller
|
|
2464
|
+
* OMITS that one image and the text turn still sends — NEVER throws the turn.
|
|
2465
|
+
* Failures are logged with context (no silent swallow).
|
|
2466
|
+
*/
|
|
2467
|
+
async fetchAttachmentDataUrl(messageId, index, mime) {
|
|
2468
|
+
try {
|
|
2469
|
+
const res = await this.fetchImpl(
|
|
2470
|
+
`${this.apiUrl}/agents/${this.agentId}/attachments/${messageId}/${index}`,
|
|
2471
|
+
{ headers: { Authorization: this.getAuthHeader() } }
|
|
2472
|
+
);
|
|
2473
|
+
if (!res.ok) {
|
|
2474
|
+
this.log({
|
|
2475
|
+
level: "error",
|
|
2476
|
+
message: `Attachment fetch for message ${messageId.slice(0, 8)} index ${index} returned HTTP ${res.status} \u2014 omitting this image (text turn proceeds)`,
|
|
2477
|
+
message_id: messageId
|
|
2478
|
+
});
|
|
2479
|
+
return null;
|
|
2480
|
+
}
|
|
2481
|
+
const buf = await res.arrayBuffer();
|
|
2482
|
+
const base64 = Buffer.from(buf).toString("base64");
|
|
2483
|
+
const dataMime = cleanImageMime(res.headers.get("content-type")) || mime;
|
|
2484
|
+
return `data:${dataMime};base64,${base64}`;
|
|
2485
|
+
} catch (err) {
|
|
2486
|
+
this.log({
|
|
2487
|
+
level: "error",
|
|
2488
|
+
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)}`,
|
|
2489
|
+
message_id: messageId
|
|
2490
|
+
});
|
|
2491
|
+
return null;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* On any skipped/failed image, post an in-thread note to Evident over the
|
|
2496
|
+
* EXISTING combinedAuth callback surface — the CLI NEVER posts to Slack directly.
|
|
2497
|
+
* Evident routes the note to source via `conversation.deliver`.
|
|
2498
|
+
*
|
|
2499
|
+
* The `POST .../messages/:id/signal` route accepts `attachments_skipped` (in
|
|
2500
|
+
* `messageSignalSchema`) and turns it into an in-thread note delivered through
|
|
2501
|
+
* `conversation.deliver` (e.g. "N image(s) couldn't be forwarded"), so the note
|
|
2502
|
+
* reaches the channel.
|
|
2503
|
+
*
|
|
2504
|
+
* Fire-and-forget: never throws into the send/tick (logs its own failure).
|
|
2505
|
+
*/
|
|
2506
|
+
signalAttachmentsSkipped(conversationId, messageId, outcomes, capabilityUnknown) {
|
|
2507
|
+
const skipped = outcomes.filter((o) => o.status === "skipped").length;
|
|
2508
|
+
const failed = outcomes.filter((o) => o.status === "failed").length;
|
|
2509
|
+
if (skipped === 0 && failed === 0) return;
|
|
2510
|
+
if (this.attachmentsSkippedSignalled.has(messageId)) return;
|
|
2511
|
+
this.attachmentsSkippedSignalled.add(messageId);
|
|
2512
|
+
const skippedReason = capabilityUnknown ? "unknown" : "unsupported";
|
|
2513
|
+
this.log({
|
|
2514
|
+
level: "info",
|
|
2515
|
+
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`,
|
|
2516
|
+
conversation_id: conversationId,
|
|
2517
|
+
message_id: messageId
|
|
2518
|
+
});
|
|
2519
|
+
void this.postSignal(conversationId, messageId, "attachments_skipped", {
|
|
2520
|
+
skipped,
|
|
2521
|
+
failed,
|
|
2522
|
+
...skipped > 0 ? { skipped_reason: skippedReason } : {}
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2296
2525
|
/** Register a freshly-dispatched message with its session's watcher state. */
|
|
2297
2526
|
registerInFlight(conv, sessionId, message, opencodeMessageId) {
|
|
2298
2527
|
let watcher = this.watchers.get(sessionId);
|
|
@@ -2530,18 +2759,20 @@ var ChannelDriver = class {
|
|
|
2530
2759
|
const latchedPaused = inFlight.pausedOnQuestion || inFlight.pausedOnPermission;
|
|
2531
2760
|
const awaitingHuman = observedOpen || latchedPaused;
|
|
2532
2761
|
if ((state === "running" || state === "done" || state === "failed") && !inFlight.started) {
|
|
2762
|
+
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
2533
2763
|
let claimed;
|
|
2534
2764
|
try {
|
|
2535
2765
|
claimed = await this.markProcessing(
|
|
2536
2766
|
conv.id,
|
|
2537
2767
|
inFlight.evidentMessageId,
|
|
2538
2768
|
sessionId,
|
|
2539
|
-
inFlight.opencodeMessageId
|
|
2769
|
+
inFlight.opencodeMessageId,
|
|
2770
|
+
title
|
|
2540
2771
|
);
|
|
2541
2772
|
} catch (err) {
|
|
2542
2773
|
if (err instanceof ChannelAuthError) throw err;
|
|
2543
2774
|
this.log({
|
|
2544
|
-
level: "
|
|
2775
|
+
level: "warn",
|
|
2545
2776
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} processing (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
2546
2777
|
conversation_id: conv.id,
|
|
2547
2778
|
message_id: inFlight.evidentMessageId
|
|
@@ -2551,7 +2782,7 @@ var ChannelDriver = class {
|
|
|
2551
2782
|
inFlight.started = true;
|
|
2552
2783
|
if (!claimed) {
|
|
2553
2784
|
this.log({
|
|
2554
|
-
level: "
|
|
2785
|
+
level: "debug",
|
|
2555
2786
|
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} already marked processing \u2014 continuing`,
|
|
2556
2787
|
conversation_id: conv.id,
|
|
2557
2788
|
message_id: inFlight.evidentMessageId
|
|
@@ -2567,18 +2798,20 @@ var ChannelDriver = class {
|
|
|
2567
2798
|
conversation_id: conv.id,
|
|
2568
2799
|
message_id: inFlight.evidentMessageId
|
|
2569
2800
|
});
|
|
2801
|
+
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
2570
2802
|
try {
|
|
2571
2803
|
await this.markDone(
|
|
2572
2804
|
conv.id,
|
|
2573
2805
|
inFlight.evidentMessageId,
|
|
2574
2806
|
sessionId,
|
|
2575
|
-
inFlight.opencodeMessageId
|
|
2807
|
+
inFlight.opencodeMessageId,
|
|
2808
|
+
title
|
|
2576
2809
|
);
|
|
2577
2810
|
} catch (err) {
|
|
2578
2811
|
if (err instanceof ChannelAuthError) throw err;
|
|
2579
2812
|
if (err instanceof ChannelTerminalError) {
|
|
2580
2813
|
this.log({
|
|
2581
|
-
level: "
|
|
2814
|
+
level: "warn",
|
|
2582
2815
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (terminal HTTP ${err.status}) \u2014 leaving for the cron safety net: ${err.message}`,
|
|
2583
2816
|
conversation_id: conv.id,
|
|
2584
2817
|
message_id: inFlight.evidentMessageId
|
|
@@ -2588,7 +2821,7 @@ var ChannelDriver = class {
|
|
|
2588
2821
|
}
|
|
2589
2822
|
if (this.now() >= inFlight.deadline) {
|
|
2590
2823
|
this.log({
|
|
2591
|
-
level: "
|
|
2824
|
+
level: "warn",
|
|
2592
2825
|
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)}`,
|
|
2593
2826
|
conversation_id: conv.id,
|
|
2594
2827
|
message_id: inFlight.evidentMessageId
|
|
@@ -2597,7 +2830,7 @@ var ChannelDriver = class {
|
|
|
2597
2830
|
return;
|
|
2598
2831
|
}
|
|
2599
2832
|
this.log({
|
|
2600
|
-
level: "
|
|
2833
|
+
level: "warn",
|
|
2601
2834
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
2602
2835
|
conversation_id: conv.id,
|
|
2603
2836
|
message_id: inFlight.evidentMessageId
|
|
@@ -2625,7 +2858,7 @@ var ChannelDriver = class {
|
|
|
2625
2858
|
if (err instanceof ChannelAuthError) throw err;
|
|
2626
2859
|
if (err instanceof ChannelTerminalError) {
|
|
2627
2860
|
this.log({
|
|
2628
|
-
level: "
|
|
2861
|
+
level: "warn",
|
|
2629
2862
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed (terminal HTTP ${err.status}) \u2014 leaving for the cron safety net: ${err.message}`,
|
|
2630
2863
|
conversation_id: conv.id,
|
|
2631
2864
|
message_id: inFlight.evidentMessageId
|
|
@@ -2635,7 +2868,7 @@ var ChannelDriver = class {
|
|
|
2635
2868
|
}
|
|
2636
2869
|
if (this.now() >= inFlight.deadline) {
|
|
2637
2870
|
this.log({
|
|
2638
|
-
level: "
|
|
2871
|
+
level: "warn",
|
|
2639
2872
|
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)}`,
|
|
2640
2873
|
conversation_id: conv.id,
|
|
2641
2874
|
message_id: inFlight.evidentMessageId
|
|
@@ -2644,7 +2877,7 @@ var ChannelDriver = class {
|
|
|
2644
2877
|
return;
|
|
2645
2878
|
}
|
|
2646
2879
|
this.log({
|
|
2647
|
-
level: "
|
|
2880
|
+
level: "warn",
|
|
2648
2881
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
2649
2882
|
conversation_id: conv.id,
|
|
2650
2883
|
message_id: inFlight.evidentMessageId
|
|
@@ -2667,7 +2900,7 @@ var ChannelDriver = class {
|
|
|
2667
2900
|
const activelyRunning = state === "running" && !awaitingHuman;
|
|
2668
2901
|
if (activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
|
|
2669
2902
|
this.log({
|
|
2670
|
-
level: "
|
|
2903
|
+
level: "warn",
|
|
2671
2904
|
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`,
|
|
2672
2905
|
conversation_id: conv.id,
|
|
2673
2906
|
message_id: inFlight.evidentMessageId
|
|
@@ -2710,7 +2943,7 @@ var ChannelDriver = class {
|
|
|
2710
2943
|
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
2711
2944
|
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
2712
2945
|
this.log({
|
|
2713
|
-
level: "
|
|
2946
|
+
level: "debug",
|
|
2714
2947
|
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} did not complete within the watch window \u2014 leaving for the cron safety net`,
|
|
2715
2948
|
conversation_id: conv.id,
|
|
2716
2949
|
message_id: inFlight.evidentMessageId
|
|
@@ -2753,7 +2986,7 @@ var ChannelDriver = class {
|
|
|
2753
2986
|
this.readoptPollUnresolvedSignalled.delete(id);
|
|
2754
2987
|
if (cleared || clearedUndeliverable) {
|
|
2755
2988
|
this.log({
|
|
2756
|
-
level: "
|
|
2989
|
+
level: "debug",
|
|
2757
2990
|
message: `Re-adopt: message ${id.slice(0, 8)} left the processing list (cron reset) \u2014 cleared gave-up marker`,
|
|
2758
2991
|
message_id: id
|
|
2759
2992
|
});
|
|
@@ -2766,7 +2999,7 @@ var ChannelDriver = class {
|
|
|
2766
2999
|
for (const row of rows) {
|
|
2767
3000
|
if (!row.opencode_session_id) {
|
|
2768
3001
|
this.log({
|
|
2769
|
-
level: "
|
|
3002
|
+
level: "warn",
|
|
2770
3003
|
message: `Cannot re-adopt processing message ${row.id.slice(0, 8)} \u2014 no opencode session id; leaving for the cron safety net`,
|
|
2771
3004
|
conversation_id: row.conversation_id,
|
|
2772
3005
|
message_id: row.id
|
|
@@ -2783,7 +3016,7 @@ var ChannelDriver = class {
|
|
|
2783
3016
|
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
2784
3017
|
if (!res.ok) {
|
|
2785
3018
|
this.log({
|
|
2786
|
-
level: "
|
|
3019
|
+
level: "warn",
|
|
2787
3020
|
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 skipping this session this tick`
|
|
2788
3021
|
});
|
|
2789
3022
|
continue;
|
|
@@ -2791,7 +3024,7 @@ var ChannelDriver = class {
|
|
|
2791
3024
|
const body = await res.json();
|
|
2792
3025
|
if (!Array.isArray(body)) {
|
|
2793
3026
|
this.log({
|
|
2794
|
-
level: "
|
|
3027
|
+
level: "warn",
|
|
2795
3028
|
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned a non-array message body \u2014 skipping this session this tick`
|
|
2796
3029
|
});
|
|
2797
3030
|
continue;
|
|
@@ -2799,7 +3032,7 @@ var ChannelDriver = class {
|
|
|
2799
3032
|
messages = body;
|
|
2800
3033
|
} catch (err) {
|
|
2801
3034
|
this.log({
|
|
2802
|
-
level: "
|
|
3035
|
+
level: "warn",
|
|
2803
3036
|
message: `Re-adopt: failed to poll session ${sessionId.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`
|
|
2804
3037
|
});
|
|
2805
3038
|
continue;
|
|
@@ -2832,7 +3065,7 @@ var ChannelDriver = class {
|
|
|
2832
3065
|
async readoptOne(sessionId, row, messages, sessionOngoing) {
|
|
2833
3066
|
if (this.isTracked(sessionId, row.id)) {
|
|
2834
3067
|
this.log({
|
|
2835
|
-
level: "
|
|
3068
|
+
level: "debug",
|
|
2836
3069
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already tracked in-flight \u2014 skipping (owned by the watcher loop)`,
|
|
2837
3070
|
conversation_id: row.conversation_id,
|
|
2838
3071
|
message_id: row.id
|
|
@@ -2844,7 +3077,7 @@ var ChannelDriver = class {
|
|
|
2844
3077
|
if (state === "done") {
|
|
2845
3078
|
if (this.doneUndeliverable.has(row.id)) {
|
|
2846
3079
|
this.log({
|
|
2847
|
-
level: "
|
|
3080
|
+
level: "debug",
|
|
2848
3081
|
message: `Re-adopt: message ${row.id.slice(0, 8)} markDone is terminally undeliverable \u2014 left to the cron; skipping until it leaves processing`,
|
|
2849
3082
|
conversation_id: row.conversation_id,
|
|
2850
3083
|
message_id: row.id
|
|
@@ -2858,13 +3091,14 @@ var ChannelDriver = class {
|
|
|
2858
3091
|
message_id: row.id
|
|
2859
3092
|
});
|
|
2860
3093
|
try {
|
|
2861
|
-
await this.
|
|
3094
|
+
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
3095
|
+
await this.markDone(row.conversation_id, row.id, sessionId, ocId, title);
|
|
2862
3096
|
} catch (err) {
|
|
2863
3097
|
if (err instanceof ChannelAuthError) throw err;
|
|
2864
3098
|
if (err instanceof ChannelTerminalError) {
|
|
2865
3099
|
this.doneUndeliverable.add(row.id);
|
|
2866
3100
|
this.log({
|
|
2867
|
-
level: "
|
|
3101
|
+
level: "warn",
|
|
2868
3102
|
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}`,
|
|
2869
3103
|
conversation_id: row.conversation_id,
|
|
2870
3104
|
message_id: row.id
|
|
@@ -2873,7 +3107,7 @@ var ChannelDriver = class {
|
|
|
2873
3107
|
return;
|
|
2874
3108
|
}
|
|
2875
3109
|
this.log({
|
|
2876
|
-
level: "
|
|
3110
|
+
level: "warn",
|
|
2877
3111
|
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
2878
3112
|
conversation_id: row.conversation_id,
|
|
2879
3113
|
message_id: row.id
|
|
@@ -2899,7 +3133,7 @@ var ChannelDriver = class {
|
|
|
2899
3133
|
if (err instanceof ChannelTerminalError) {
|
|
2900
3134
|
this.doneUndeliverable.add(row.id);
|
|
2901
3135
|
this.log({
|
|
2902
|
-
level: "
|
|
3136
|
+
level: "warn",
|
|
2903
3137
|
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}`,
|
|
2904
3138
|
conversation_id: row.conversation_id,
|
|
2905
3139
|
message_id: row.id
|
|
@@ -2908,7 +3142,7 @@ var ChannelDriver = class {
|
|
|
2908
3142
|
return;
|
|
2909
3143
|
}
|
|
2910
3144
|
this.log({
|
|
2911
|
-
level: "
|
|
3145
|
+
level: "warn",
|
|
2912
3146
|
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} failed (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
2913
3147
|
conversation_id: row.conversation_id,
|
|
2914
3148
|
message_id: row.id
|
|
@@ -2921,7 +3155,7 @@ var ChannelDriver = class {
|
|
|
2921
3155
|
}
|
|
2922
3156
|
if (this.dontRedispatch.has(row.id)) {
|
|
2923
3157
|
this.log({
|
|
2924
|
-
level: "
|
|
3158
|
+
level: "debug",
|
|
2925
3159
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
2926
3160
|
conversation_id: row.conversation_id,
|
|
2927
3161
|
message_id: row.id
|
|
@@ -2946,7 +3180,7 @@ var ChannelDriver = class {
|
|
|
2946
3180
|
}
|
|
2947
3181
|
if (ongoing === true) {
|
|
2948
3182
|
this.log({
|
|
2949
|
-
level: "
|
|
3183
|
+
level: "debug",
|
|
2950
3184
|
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)`,
|
|
2951
3185
|
conversation_id: row.conversation_id,
|
|
2952
3186
|
message_id: row.id
|
|
@@ -2954,7 +3188,7 @@ var ChannelDriver = class {
|
|
|
2954
3188
|
} else {
|
|
2955
3189
|
if (shape === "b1") {
|
|
2956
3190
|
this.log({
|
|
2957
|
-
level: "
|
|
3191
|
+
level: "debug",
|
|
2958
3192
|
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`,
|
|
2959
3193
|
conversation_id: row.conversation_id,
|
|
2960
3194
|
message_id: row.id
|
|
@@ -2966,7 +3200,7 @@ var ChannelDriver = class {
|
|
|
2966
3200
|
return;
|
|
2967
3201
|
}
|
|
2968
3202
|
this.log({
|
|
2969
|
-
level: "
|
|
3203
|
+
level: "debug",
|
|
2970
3204
|
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`,
|
|
2971
3205
|
conversation_id: row.conversation_id,
|
|
2972
3206
|
message_id: row.id
|
|
@@ -2977,7 +3211,7 @@ var ChannelDriver = class {
|
|
|
2977
3211
|
const descendantAlive = await this.isAnyDescendantSessionAlive(sessionId);
|
|
2978
3212
|
if (descendantAlive === true) {
|
|
2979
3213
|
this.log({
|
|
2980
|
-
level: "
|
|
3214
|
+
level: "debug",
|
|
2981
3215
|
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)`,
|
|
2982
3216
|
conversation_id: row.conversation_id,
|
|
2983
3217
|
message_id: row.id
|
|
@@ -3001,7 +3235,7 @@ var ChannelDriver = class {
|
|
|
3001
3235
|
this.readopted.add(row.id);
|
|
3002
3236
|
this.ensureWatcherRunning(sessionId);
|
|
3003
3237
|
this.log({
|
|
3004
|
-
level: "
|
|
3238
|
+
level: "debug",
|
|
3005
3239
|
message: `Re-adopt: message ${row.id.slice(0, 8)} ${state} \u2014 re-attached watcher (stored id, no re-dispatch)`,
|
|
3006
3240
|
conversation_id: row.conversation_id,
|
|
3007
3241
|
message_id: row.id
|
|
@@ -3035,7 +3269,7 @@ var ChannelDriver = class {
|
|
|
3035
3269
|
async forceReadoptRun(sessionId, row) {
|
|
3036
3270
|
if (this.stopped) {
|
|
3037
3271
|
this.log({
|
|
3038
|
-
level: "
|
|
3272
|
+
level: "debug",
|
|
3039
3273
|
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`,
|
|
3040
3274
|
conversation_id: row.conversation_id,
|
|
3041
3275
|
message_id: row.id
|
|
@@ -3044,7 +3278,7 @@ var ChannelDriver = class {
|
|
|
3044
3278
|
}
|
|
3045
3279
|
if (this.awaitingReadopt.has(row.id)) {
|
|
3046
3280
|
this.log({
|
|
3047
|
-
level: "
|
|
3281
|
+
level: "debug",
|
|
3048
3282
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already has a re-dispatch awaiting read-back \u2014 skipping (at most once)`,
|
|
3049
3283
|
conversation_id: row.conversation_id,
|
|
3050
3284
|
message_id: row.id
|
|
@@ -3054,7 +3288,7 @@ var ChannelDriver = class {
|
|
|
3054
3288
|
if (this.processedAtMs(row) + this.pausedMaxWaitMs <= this.now()) {
|
|
3055
3289
|
this.dontRedispatch.add(row.id);
|
|
3056
3290
|
this.log({
|
|
3057
|
-
level: "
|
|
3291
|
+
level: "debug",
|
|
3058
3292
|
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)`,
|
|
3059
3293
|
conversation_id: row.conversation_id,
|
|
3060
3294
|
message_id: row.id
|
|
@@ -3073,17 +3307,20 @@ var ChannelDriver = class {
|
|
|
3073
3307
|
message_id: row.id
|
|
3074
3308
|
});
|
|
3075
3309
|
this.awaitingReadopt.add(row.id);
|
|
3310
|
+
const readoptConv = this.convForRow(sessionId, row);
|
|
3311
|
+
const readoptMessage = this.queuedMessageForRow(row);
|
|
3312
|
+
const sendAttachments = this.buildSendAttachments(readoptConv, readoptMessage);
|
|
3076
3313
|
let ocId;
|
|
3077
3314
|
try {
|
|
3078
3315
|
ocId = await this.dispatchLocked(
|
|
3079
3316
|
sessionId,
|
|
3080
|
-
() => sendPromptAsync(this.port, sessionId, row.content, options)
|
|
3317
|
+
() => sendPromptAsync(this.port, sessionId, row.content, options, sendAttachments)
|
|
3081
3318
|
);
|
|
3082
3319
|
} catch (err) {
|
|
3083
3320
|
this.awaitingReadopt.delete(row.id);
|
|
3084
3321
|
if (err instanceof ChannelAuthError) throw err;
|
|
3085
3322
|
this.log({
|
|
3086
|
-
level: "
|
|
3323
|
+
level: "warn",
|
|
3087
3324
|
message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
3088
3325
|
conversation_id: row.conversation_id,
|
|
3089
3326
|
message_id: row.id
|
|
@@ -3094,7 +3331,7 @@ var ChannelDriver = class {
|
|
|
3094
3331
|
if (ocId === null) {
|
|
3095
3332
|
this.awaitingReadopt.delete(row.id);
|
|
3096
3333
|
this.log({
|
|
3097
|
-
level: "
|
|
3334
|
+
level: "warn",
|
|
3098
3335
|
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`,
|
|
3099
3336
|
conversation_id: row.conversation_id,
|
|
3100
3337
|
message_id: row.id
|
|
@@ -3102,9 +3339,7 @@ var ChannelDriver = class {
|
|
|
3102
3339
|
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
3103
3340
|
return;
|
|
3104
3341
|
}
|
|
3105
|
-
|
|
3106
|
-
const message = this.queuedMessageForRow(row);
|
|
3107
|
-
this.registerReadopted(conv, sessionId, message, ocId, this.processedAtMs(row));
|
|
3342
|
+
this.registerReadopted(readoptConv, sessionId, readoptMessage, ocId, this.processedAtMs(row));
|
|
3108
3343
|
this.dispatched.add(row.id);
|
|
3109
3344
|
this.readopted.add(row.id);
|
|
3110
3345
|
this.awaitingReadopt.delete(row.id);
|
|
@@ -3158,7 +3393,8 @@ var ChannelDriver = class {
|
|
|
3158
3393
|
opencode_agent: row.opencode_agent,
|
|
3159
3394
|
opencode_model: row.opencode_model,
|
|
3160
3395
|
source_message_id: row.source_message_id,
|
|
3161
|
-
slack_user_id: row.slack_user_id
|
|
3396
|
+
slack_user_id: row.slack_user_id,
|
|
3397
|
+
attachments: row.attachments ?? null
|
|
3162
3398
|
};
|
|
3163
3399
|
}
|
|
3164
3400
|
/**
|
|
@@ -3179,7 +3415,7 @@ var ChannelDriver = class {
|
|
|
3179
3415
|
if (this.readopted.delete(evidentMessageId) && inFlight && !inFlight.done) {
|
|
3180
3416
|
this.dontRedispatch.add(evidentMessageId);
|
|
3181
3417
|
this.log({
|
|
3182
|
-
level: "
|
|
3418
|
+
level: "debug",
|
|
3183
3419
|
message: `Re-adopt: message ${evidentMessageId.slice(0, 8)} gave up \u2014 parking until it leaves the processing list (cron reset)`,
|
|
3184
3420
|
conversation_id: watcher.conv.id,
|
|
3185
3421
|
message_id: evidentMessageId
|
|
@@ -3319,6 +3555,51 @@ var ChannelDriver = class {
|
|
|
3319
3555
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
3320
3556
|
return parent;
|
|
3321
3557
|
}
|
|
3558
|
+
/**
|
|
3559
|
+
* Resolve (and cache in `sessionTitles`) the OpenCode session TITLE (#310) so the
|
|
3560
|
+
* status PATCH can carry it into the "Live sessions" list. Driver-level cache so
|
|
3561
|
+
* BOTH the watcher completion path and the restart-recovery re-adopt path (which
|
|
3562
|
+
* has no watcher) can use it. `conversationId` is passed only for log context.
|
|
3563
|
+
* Best-effort:
|
|
3564
|
+
* - a resolved NON-EMPTY title is cached and terminal (a real session name
|
|
3565
|
+
* won't later un-name), so we do NOT re-GET `/session/:id` every tick;
|
|
3566
|
+
* - while the title is still absent/empty we do NOT latch it — OpenCode names
|
|
3567
|
+
* sessions asynchronously mid-turn, so an early call (e.g. at `processing`)
|
|
3568
|
+
* must leave the cache unresolved and re-fetch on the next need so a later
|
|
3569
|
+
* call (e.g. at `done`) picks up the name assigned in the meantime. Such a
|
|
3570
|
+
* call returns `null` (omit the title on THIS PATCH) without caching;
|
|
3571
|
+
* - a failed request likewise leaves the cache unresolved (retry next need)
|
|
3572
|
+
* and returns `null` — it must NEVER throw or block completion.
|
|
3573
|
+
* A failure is logged with agent/session context (no silent catch).
|
|
3574
|
+
*/
|
|
3575
|
+
async resolveSessionTitle(sessionId, conversationId) {
|
|
3576
|
+
const cached = this.sessionTitles.get(sessionId);
|
|
3577
|
+
if (cached != null) return cached;
|
|
3578
|
+
try {
|
|
3579
|
+
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
|
|
3580
|
+
if (res.ok) {
|
|
3581
|
+
const body = await res.json();
|
|
3582
|
+
const title = body && typeof body.title === "string" ? body.title.trim() : "";
|
|
3583
|
+
if (title.length > 0) {
|
|
3584
|
+
this.sessionTitles.set(sessionId, title);
|
|
3585
|
+
return title;
|
|
3586
|
+
}
|
|
3587
|
+
return null;
|
|
3588
|
+
}
|
|
3589
|
+
this.log({
|
|
3590
|
+
level: "debug",
|
|
3591
|
+
message: `Session title fetch for session ${sessionId.slice(0, 8)} (agent ${this.agentId.slice(0, 8)}) returned HTTP ${res.status} \u2014 omitting title`,
|
|
3592
|
+
conversation_id: conversationId
|
|
3593
|
+
});
|
|
3594
|
+
} catch (err) {
|
|
3595
|
+
this.log({
|
|
3596
|
+
level: "debug",
|
|
3597
|
+
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)}`,
|
|
3598
|
+
conversation_id: conversationId
|
|
3599
|
+
});
|
|
3600
|
+
}
|
|
3601
|
+
return null;
|
|
3602
|
+
}
|
|
3322
3603
|
/**
|
|
3323
3604
|
* DEFENSIVE cross-check for the restart-recovery path (WI-2): is any descendant
|
|
3324
3605
|
* (`task` sub-agent) session under `rootSessionId` still genuinely doing work?
|
|
@@ -3362,7 +3643,7 @@ var ChannelDriver = class {
|
|
|
3362
3643
|
const sessions = await listSessions(this.port);
|
|
3363
3644
|
if (!sessions) {
|
|
3364
3645
|
this.log({
|
|
3365
|
-
level: "
|
|
3646
|
+
level: "warn",
|
|
3366
3647
|
message: `Re-adopt: could not enumerate sessions to cross-check descendant liveness for root ${rootSessionId} (listSessions failed) \u2014 treating child liveness as indeterminate`
|
|
3367
3648
|
});
|
|
3368
3649
|
return null;
|
|
@@ -3528,7 +3809,7 @@ var ChannelDriver = class {
|
|
|
3528
3809
|
* A single attempt (no internal retry): the watcher's per-tick loop is the
|
|
3529
3810
|
* retry vehicle for the swap-to-running.
|
|
3530
3811
|
*/
|
|
3531
|
-
async markProcessing(conversationId, messageId, sessionId, opencodeMessageId) {
|
|
3812
|
+
async markProcessing(conversationId, messageId, sessionId, opencodeMessageId, title) {
|
|
3532
3813
|
const res = await this.fetchImpl(
|
|
3533
3814
|
`${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
3534
3815
|
{
|
|
@@ -3537,7 +3818,8 @@ var ChannelDriver = class {
|
|
|
3537
3818
|
body: JSON.stringify({
|
|
3538
3819
|
status: "processing",
|
|
3539
3820
|
opencode_session_id: sessionId,
|
|
3540
|
-
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {}
|
|
3821
|
+
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
3822
|
+
...title ? { title } : {}
|
|
3541
3823
|
})
|
|
3542
3824
|
}
|
|
3543
3825
|
);
|
|
@@ -3576,7 +3858,7 @@ var ChannelDriver = class {
|
|
|
3576
3858
|
* watcher retries next tick within the
|
|
3577
3859
|
* deadline, Finding 4).
|
|
3578
3860
|
*/
|
|
3579
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId) {
|
|
3861
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title) {
|
|
3580
3862
|
const res = await this.fetchImpl(
|
|
3581
3863
|
`${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
3582
3864
|
{
|
|
@@ -3585,7 +3867,8 @@ var ChannelDriver = class {
|
|
|
3585
3867
|
body: JSON.stringify({
|
|
3586
3868
|
status: "done",
|
|
3587
3869
|
opencode_session_id: sessionId,
|
|
3588
|
-
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {}
|
|
3870
|
+
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
3871
|
+
...title ? { title } : {}
|
|
3589
3872
|
})
|
|
3590
3873
|
}
|
|
3591
3874
|
);
|
|
@@ -3646,7 +3929,7 @@ var ChannelDriver = class {
|
|
|
3646
3929
|
);
|
|
3647
3930
|
if (!res.ok) {
|
|
3648
3931
|
this.log({
|
|
3649
|
-
level: "
|
|
3932
|
+
level: "warn",
|
|
3650
3933
|
message: `Signal '${signal}' for message ${messageId.slice(0, 8)} returned HTTP ${res.status} (telemetry-only, ignored)`,
|
|
3651
3934
|
conversation_id: conversationId,
|
|
3652
3935
|
message_id: messageId
|
|
@@ -3656,7 +3939,7 @@ var ChannelDriver = class {
|
|
|
3656
3939
|
return true;
|
|
3657
3940
|
} catch (err) {
|
|
3658
3941
|
this.log({
|
|
3659
|
-
level: "
|
|
3942
|
+
level: "warn",
|
|
3660
3943
|
message: `Signal '${signal}' for message ${messageId.slice(0, 8)} failed (telemetry-only, ignored): ${err instanceof Error ? err.message : String(err)}`,
|
|
3661
3944
|
conversation_id: conversationId,
|
|
3662
3945
|
message_id: messageId
|
|
@@ -4000,23 +4283,53 @@ var MAX_ACTIVITY_LOG_ENTRIES = 10;
|
|
|
4000
4283
|
var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
|
|
4001
4284
|
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
4002
4285
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
|
|
4003
|
-
function
|
|
4286
|
+
function resolveLogLevel(options) {
|
|
4287
|
+
const accepted = Object.keys(LOG_LEVELS);
|
|
4288
|
+
const validate = (value, source) => {
|
|
4289
|
+
const normalized = value.trim().toLowerCase();
|
|
4290
|
+
if (!accepted.includes(normalized)) {
|
|
4291
|
+
throw new Error(
|
|
4292
|
+
`Invalid log level "${value}"${source}; expected one of ${accepted.join(", ")}`
|
|
4293
|
+
);
|
|
4294
|
+
}
|
|
4295
|
+
return normalized;
|
|
4296
|
+
};
|
|
4297
|
+
if (options.logLevel !== void 0) {
|
|
4298
|
+
return validate(options.logLevel, " (--log-level)");
|
|
4299
|
+
}
|
|
4300
|
+
if (options.verbose) {
|
|
4301
|
+
return "debug";
|
|
4302
|
+
}
|
|
4303
|
+
const env = process.env.EVIDENT_LOG_LEVEL;
|
|
4304
|
+
if (env !== void 0 && env !== "") {
|
|
4305
|
+
return validate(env, " (EVIDENT_LOG_LEVEL)");
|
|
4306
|
+
}
|
|
4307
|
+
return "info";
|
|
4308
|
+
}
|
|
4309
|
+
function meetsThreshold(state, level) {
|
|
4310
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[state.logLevel];
|
|
4311
|
+
}
|
|
4312
|
+
function log2(state, message, level = "info") {
|
|
4313
|
+
if (!meetsThreshold(state, level)) return;
|
|
4004
4314
|
if (state.json) {
|
|
4005
4315
|
console.log(
|
|
4006
4316
|
JSON.stringify({
|
|
4007
4317
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4008
|
-
level
|
|
4318
|
+
level,
|
|
4009
4319
|
message
|
|
4010
4320
|
})
|
|
4011
4321
|
);
|
|
4012
4322
|
} else if (!state.interactive) {
|
|
4013
|
-
const prefix =
|
|
4323
|
+
const prefix = level === "error" ? chalk6.red("\u2717") : level === "warn" ? chalk6.yellow("!") : level === "debug" ? chalk6.dim("\xB7") : chalk6.green("\u2022");
|
|
4014
4324
|
console.log(`${prefix} ${message}`);
|
|
4015
4325
|
}
|
|
4016
4326
|
}
|
|
4017
4327
|
function logActivity(state, entry) {
|
|
4328
|
+
const level = entry.level ?? (entry.type === "error" ? "error" : "info");
|
|
4329
|
+
if (!meetsThreshold(state, level)) return;
|
|
4018
4330
|
const fullEntry = {
|
|
4019
4331
|
...entry,
|
|
4332
|
+
level,
|
|
4020
4333
|
timestamp: /* @__PURE__ */ new Date()
|
|
4021
4334
|
};
|
|
4022
4335
|
state.activityLog.push(fullEntry);
|
|
@@ -4025,9 +4338,9 @@ function logActivity(state, entry) {
|
|
|
4025
4338
|
}
|
|
4026
4339
|
if (!state.interactive) {
|
|
4027
4340
|
if (entry.type === "error") {
|
|
4028
|
-
log2(state, entry.error ?? "Unknown error",
|
|
4029
|
-
} else if (entry.
|
|
4030
|
-
log2(state, entry.message);
|
|
4341
|
+
log2(state, entry.error ?? "Unknown error", level);
|
|
4342
|
+
} else if (entry.message) {
|
|
4343
|
+
log2(state, entry.message, level);
|
|
4031
4344
|
}
|
|
4032
4345
|
}
|
|
4033
4346
|
}
|
|
@@ -4226,7 +4539,7 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
4226
4539
|
process.env
|
|
4227
4540
|
);
|
|
4228
4541
|
for (const warning2 of config2.warnings) {
|
|
4229
|
-
logActivity(state, { type: "info", message: `Session cleanup: ${warning2}` });
|
|
4542
|
+
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
4230
4543
|
}
|
|
4231
4544
|
if (!config2.enabled) return;
|
|
4232
4545
|
logActivity(state, {
|
|
@@ -4298,6 +4611,20 @@ async function cleanup(state, opts = {}) {
|
|
|
4298
4611
|
}
|
|
4299
4612
|
async function run(options) {
|
|
4300
4613
|
const interactive = isInteractive(options.json);
|
|
4614
|
+
let logLevel;
|
|
4615
|
+
try {
|
|
4616
|
+
logLevel = resolveLogLevel(options);
|
|
4617
|
+
} catch (error2) {
|
|
4618
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
4619
|
+
if (options.json) {
|
|
4620
|
+
console.log(JSON.stringify({ status: "error", error: message }));
|
|
4621
|
+
} else {
|
|
4622
|
+
printError(message);
|
|
4623
|
+
}
|
|
4624
|
+
await shutdownTelemetry();
|
|
4625
|
+
process.exit(1);
|
|
4626
|
+
return;
|
|
4627
|
+
}
|
|
4301
4628
|
const state = {
|
|
4302
4629
|
agentId: options.agent || "",
|
|
4303
4630
|
agentName: null,
|
|
@@ -4306,6 +4633,7 @@ async function run(options) {
|
|
|
4306
4633
|
idleTimeout: options.idleTimeout ?? null,
|
|
4307
4634
|
json: options.json ?? false,
|
|
4308
4635
|
interactive,
|
|
4636
|
+
logLevel,
|
|
4309
4637
|
connected: false,
|
|
4310
4638
|
opencodeConnected: false,
|
|
4311
4639
|
opencodeVersion: null,
|
|
@@ -4323,8 +4651,8 @@ async function run(options) {
|
|
|
4323
4651
|
if (state.idleTimeout === null && (process.env.GITHUB_ACTIONS || process.env.CI)) {
|
|
4324
4652
|
log2(
|
|
4325
4653
|
state,
|
|
4326
|
-
"
|
|
4327
|
-
|
|
4654
|
+
"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.",
|
|
4655
|
+
"warn"
|
|
4328
4656
|
);
|
|
4329
4657
|
}
|
|
4330
4658
|
const handleSignal = async () => {
|
|
@@ -4440,9 +4768,9 @@ async function run(options) {
|
|
|
4440
4768
|
ocSpinner?.succeed(`OpenCode running on port ${state.port}${version2}`);
|
|
4441
4769
|
const versionWarning = buildOpenCodeVersionWarning(state.opencodeVersion);
|
|
4442
4770
|
if (versionWarning) {
|
|
4443
|
-
log2(state, versionWarning,
|
|
4771
|
+
log2(state, versionWarning, "warn");
|
|
4444
4772
|
if (state.interactive && !state.json) {
|
|
4445
|
-
logActivity(state, { type: "info", message: versionWarning });
|
|
4773
|
+
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
4446
4774
|
}
|
|
4447
4775
|
}
|
|
4448
4776
|
} catch (error2) {
|
|
@@ -4457,11 +4785,17 @@ async function run(options) {
|
|
|
4457
4785
|
getAuthHeader: () => state.authHeader,
|
|
4458
4786
|
conversationFilter: state.conversationFilter,
|
|
4459
4787
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
4460
|
-
log: (entry) =>
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4788
|
+
log: (entry) => (
|
|
4789
|
+
// Thread the driver's real level straight through so `debug`/`warn`
|
|
4790
|
+
// survive the sink filter (they no longer collapse to info). `type`
|
|
4791
|
+
// stays the coarse error/non-error split the activity log renders with.
|
|
4792
|
+
logActivity(state, {
|
|
4793
|
+
type: entry.level === "error" ? "error" : "info",
|
|
4794
|
+
level: entry.level,
|
|
4795
|
+
message: entry.message,
|
|
4796
|
+
error: entry.level === "error" ? entry.message : void 0
|
|
4797
|
+
})
|
|
4798
|
+
)
|
|
4465
4799
|
});
|
|
4466
4800
|
state.channelDriver = channelDriver;
|
|
4467
4801
|
const connection = new RunnerConnection({
|
|
@@ -4615,7 +4949,10 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
|
|
|
4615
4949
|
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);
|
|
4616
4950
|
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 }));
|
|
4617
4951
|
program.command("whoami").description("Show the currently logged in user").action(whoami);
|
|
4618
|
-
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(
|
|
4952
|
+
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(
|
|
4953
|
+
"--log-level <level>",
|
|
4954
|
+
"Log verbosity: debug | info | warn | error (default: info). Env: EVIDENT_LOG_LEVEL"
|
|
4955
|
+
).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(
|
|
4619
4956
|
"--session-cleanup-max-age <duration>",
|
|
4620
4957
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
4621
4958
|
).option(
|
|
@@ -4629,6 +4966,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
4629
4966
|
run({
|
|
4630
4967
|
agent: options.agent,
|
|
4631
4968
|
port: parseInt(options.port, 10),
|
|
4969
|
+
// Raw string — validation/precedence is single-sourced in run.ts's
|
|
4970
|
+
// resolveLogLevel (flag > -v > EVIDENT_LOG_LEVEL > info).
|
|
4971
|
+
logLevel: options.logLevel,
|
|
4632
4972
|
verbose: options.verbose,
|
|
4633
4973
|
conversation: options.conversation,
|
|
4634
4974
|
idleTimeout: options.idleTimeout ? parseInt(options.idleTimeout, 10) : void 0,
|