@evident-ai/cli 3.0.1-dev.664caa8 → 3.0.1-dev.6671916
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/dist/index.js +21 -397
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1078,12 +1078,6 @@ async function getSessionMessages(port, sessionId) {
|
|
|
1078
1078
|
return null;
|
|
1079
1079
|
}
|
|
1080
1080
|
}
|
|
1081
|
-
function isSessionActivelyGenerating(messages) {
|
|
1082
|
-
if (!messages || messages.length === 0) return false;
|
|
1083
|
-
const last = messages[messages.length - 1];
|
|
1084
|
-
if (roleOf(last) !== "assistant") return false;
|
|
1085
|
-
return completedOf(last) == null;
|
|
1086
|
-
}
|
|
1087
1081
|
function sessionLastActivityMs(session) {
|
|
1088
1082
|
const candidates = [
|
|
1089
1083
|
session.time?.updated,
|
|
@@ -1126,36 +1120,6 @@ async function sessionExists(port, id) {
|
|
|
1126
1120
|
return null;
|
|
1127
1121
|
}
|
|
1128
1122
|
}
|
|
1129
|
-
async function getSessionStatuses(port) {
|
|
1130
|
-
try {
|
|
1131
|
-
const res = await fetch(`${opencodeBase(port)}/session/status`);
|
|
1132
|
-
if (!res.ok) {
|
|
1133
|
-
console.error(
|
|
1134
|
-
`[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
|
|
1135
|
-
);
|
|
1136
|
-
return null;
|
|
1137
|
-
}
|
|
1138
|
-
const body = await res.json();
|
|
1139
|
-
if (body == null || typeof body !== "object" || Array.isArray(body)) {
|
|
1140
|
-
console.error(
|
|
1141
|
-
`[getSessionStatuses] GET /session/status body was not a plain object (port ${port})`
|
|
1142
|
-
);
|
|
1143
|
-
return null;
|
|
1144
|
-
}
|
|
1145
|
-
return body;
|
|
1146
|
-
} catch (err) {
|
|
1147
|
-
console.error(
|
|
1148
|
-
`[getSessionStatuses] GET /session/status failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
1149
|
-
);
|
|
1150
|
-
return null;
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
async function isSessionOngoing(port, id) {
|
|
1154
|
-
const map = await getSessionStatuses(port);
|
|
1155
|
-
if (map == null) return null;
|
|
1156
|
-
const entry = map[id];
|
|
1157
|
-
return entry != null && entry.type !== "idle";
|
|
1158
|
-
}
|
|
1159
1123
|
async function createOpenCodeSession(port, directory) {
|
|
1160
1124
|
const url = new URL(`${opencodeBase(port)}/session`);
|
|
1161
1125
|
if (directory && directory.trim()) {
|
|
@@ -1282,11 +1246,6 @@ function messageRunState(messages, userMessageId) {
|
|
|
1282
1246
|
if (isAssistantInFlight(reply)) return "running";
|
|
1283
1247
|
return errorOf(reply) != null ? "failed" : "done";
|
|
1284
1248
|
}
|
|
1285
|
-
function isPreamblePinnedRunning(messages, userMessageId) {
|
|
1286
|
-
if (messageRunState(messages, userMessageId) !== "running") return false;
|
|
1287
|
-
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
1288
|
-
return completedOf(reply) != null && finishOf(reply) === "tool-calls";
|
|
1289
|
-
}
|
|
1290
1249
|
function messageError(messages, userMessageId) {
|
|
1291
1250
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
1292
1251
|
const error2 = errorOf(reply);
|
|
@@ -1819,9 +1778,6 @@ var DEFAULT_RETRY_POLICY = {
|
|
|
1819
1778
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
1820
1779
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
1821
1780
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
1822
|
-
var HEARTBEAT_MS = 6e4;
|
|
1823
|
-
var ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1e3;
|
|
1824
|
-
var POLL_MISS_GRACE_MS = HEARTBEAT_MS;
|
|
1825
1781
|
var ChannelAuthError = class extends Error {
|
|
1826
1782
|
constructor(message) {
|
|
1827
1783
|
super(message);
|
|
@@ -1918,15 +1874,6 @@ var ChannelDriver = class {
|
|
|
1918
1874
|
* the row leaves the processing list, exactly like `dontRedispatch`.
|
|
1919
1875
|
*/
|
|
1920
1876
|
doneUndeliverable = /* @__PURE__ */ new Set();
|
|
1921
|
-
/**
|
|
1922
|
-
* "Already emitted `readopt_poll_unresolved` for this row" (#229). The b1 /
|
|
1923
|
-
* unreadable-status re-evaluate leaf leaves the row UN-tracked so it is re-read
|
|
1924
|
-
* every ~2s drain until the status map becomes readable — but the server-visible
|
|
1925
|
-
* signal is an OUTCOME, so it must fire at most ONCE per row, not once per drain
|
|
1926
|
-
* (Bugbot "Re-adopt signals flood every drain"). Cleared when the row leaves the
|
|
1927
|
-
* processing list, exactly like `dontRedispatch`/`doneUndeliverable`.
|
|
1928
|
-
*/
|
|
1929
|
-
readoptPollUnresolvedSignalled = /* @__PURE__ */ new Set();
|
|
1930
1877
|
/**
|
|
1931
1878
|
* "A null-id re-adopt re-dispatch is in flight, awaiting its read-back" (WI-5
|
|
1932
1879
|
* Task 5.4, High-2). Since we no longer send a caller-supplied id, a re-dispatch
|
|
@@ -2302,9 +2249,7 @@ var ChannelDriver = class {
|
|
|
2302
2249
|
inFlight: /* @__PURE__ */ new Map(),
|
|
2303
2250
|
loop: null,
|
|
2304
2251
|
reportedQuestions: /* @__PURE__ */ new Set(),
|
|
2305
|
-
reportedPermissions: /* @__PURE__ */ new Set()
|
|
2306
|
-
lastGoodPollAt: this.now(),
|
|
2307
|
-
hadUsablePoll: false
|
|
2252
|
+
reportedPermissions: /* @__PURE__ */ new Set()
|
|
2308
2253
|
};
|
|
2309
2254
|
this.watchers.set(sessionId, watcher);
|
|
2310
2255
|
}
|
|
@@ -2314,37 +2259,20 @@ var ChannelDriver = class {
|
|
|
2314
2259
|
opencodeMessageId,
|
|
2315
2260
|
message,
|
|
2316
2261
|
dispatchedAt: now,
|
|
2317
|
-
processingAnchorMs: now,
|
|
2318
2262
|
deadline: now + this.pausedMaxWaitMs,
|
|
2319
2263
|
started: false,
|
|
2320
2264
|
done: false,
|
|
2321
|
-
stuckReported: false
|
|
2322
|
-
lastAliveAt: 0,
|
|
2323
|
-
aliveInFlight: false,
|
|
2324
|
-
awaitingHumanLatched: false,
|
|
2325
|
-
pausedOnQuestion: false,
|
|
2326
|
-
pausedOnPermission: false,
|
|
2327
|
-
pausedClearConfirmed: false,
|
|
2328
|
-
pausedInFlight: false,
|
|
2329
|
-
deliveryDeadlineAnchored: false
|
|
2265
|
+
stuckReported: false
|
|
2330
2266
|
});
|
|
2331
2267
|
}
|
|
2332
2268
|
/**
|
|
2333
2269
|
* Register a RE-ADOPTED `processing` message with its session watcher
|
|
2334
2270
|
* (ADR-0046, WI-4). Mirrors `registerInFlight` but anchors the give-up
|
|
2335
2271
|
* `deadline` to the row's SERVER-SIDE `processed_at` (Invariant 1), NEVER to
|
|
2336
|
-
* `now
|
|
2337
|
-
*
|
|
2338
|
-
*
|
|
2339
|
-
*
|
|
2340
|
-
* give-up (`serviceInFlightMessage`) applies unchanged: a re-adopted turn
|
|
2341
|
-
* opencode reports ACTIVELY `running` is watched to completion (its liveness
|
|
2342
|
-
* heartbeat keeps the cron off its row), while a re-adopted turn that is paused
|
|
2343
|
-
* awaiting a human — or queued/unreachable — is still bounded by `deadline` and
|
|
2344
|
-
* handed to the cron. The old "the `deadline` must settle before the ~15-min
|
|
2345
|
-
* cron or they double-drive" reasoning is superseded: liveness now settles the
|
|
2346
|
-
* actively-running case; `deadline` settles the rest. `dispatchedAt` stays `now`
|
|
2347
|
-
* (only the appear-guard uses it).
|
|
2272
|
+
* `now`: a row already `processing` for e.g. 5 min must give up ~5 min from now
|
|
2273
|
+
* (10 min after `processed_at`), not 10 min from now — otherwise its deadline
|
|
2274
|
+
* lands ~15 min after `processed_at`, coinciding with the cron reset →
|
|
2275
|
+
* double-drive race. `dispatchedAt` stays `now` (only the appear-guard uses it).
|
|
2348
2276
|
*
|
|
2349
2277
|
* `evidentMessageId` addresses the SERVER row (for markProcessing/markDone);
|
|
2350
2278
|
* `opencodeMessageId` is the id the watcher polls for a reply — for the orphan
|
|
@@ -2362,9 +2290,7 @@ var ChannelDriver = class {
|
|
|
2362
2290
|
inFlight: /* @__PURE__ */ new Map(),
|
|
2363
2291
|
loop: null,
|
|
2364
2292
|
reportedQuestions: /* @__PURE__ */ new Set(),
|
|
2365
|
-
reportedPermissions: /* @__PURE__ */ new Set()
|
|
2366
|
-
lastGoodPollAt: this.now(),
|
|
2367
|
-
hadUsablePoll: false
|
|
2293
|
+
reportedPermissions: /* @__PURE__ */ new Set()
|
|
2368
2294
|
};
|
|
2369
2295
|
this.watchers.set(sessionId, watcher);
|
|
2370
2296
|
}
|
|
@@ -2373,10 +2299,6 @@ var ChannelDriver = class {
|
|
|
2373
2299
|
opencodeMessageId,
|
|
2374
2300
|
message,
|
|
2375
2301
|
dispatchedAt: this.now(),
|
|
2376
|
-
// Anchor the absolute-age ceiling to the SERVER-SIDE `processed_at` (the same
|
|
2377
|
-
// value seeding `deadline`), NOT `dispatchedAt` — so a re-adopted zombie's age
|
|
2378
|
-
// reflects the real turn duration and the ceiling fires on the ORIGINAL turn.
|
|
2379
|
-
processingAnchorMs: processedAtMs,
|
|
2380
2302
|
deadline: processedAtMs + this.pausedMaxWaitMs,
|
|
2381
2303
|
// The server row is ALREADY `processing`; do not re-fire markProcessing.
|
|
2382
2304
|
started: true,
|
|
@@ -2386,20 +2308,7 @@ var ChannelDriver = class {
|
|
|
2386
2308
|
// on `state === 'queued'` (turn produced no reply), not on `started`, so a
|
|
2387
2309
|
// re-adopted row left wedged in `queued` still emits the signal once
|
|
2388
2310
|
// (#210/#220 observability).
|
|
2389
|
-
stuckReported: false
|
|
2390
|
-
// Task 5.2: a re-adopted actively-running row re-attaches into the SAME
|
|
2391
|
-
// watcher and so hits the SAME actively-running heartbeat branch in
|
|
2392
|
-
// `serviceInFlightMessage` as a fresh dispatch — monitoring observes "runner
|
|
2393
|
-
// re-adopted and is confirming this row alive" via that `alive` heartbeat,
|
|
2394
|
-
// with no extra `re_adopted` signal needed (folds old WI-6).
|
|
2395
|
-
lastAliveAt: 0,
|
|
2396
|
-
aliveInFlight: false,
|
|
2397
|
-
awaitingHumanLatched: false,
|
|
2398
|
-
pausedOnQuestion: false,
|
|
2399
|
-
pausedOnPermission: false,
|
|
2400
|
-
pausedClearConfirmed: false,
|
|
2401
|
-
pausedInFlight: false,
|
|
2402
|
-
deliveryDeadlineAnchored: false
|
|
2311
|
+
stuckReported: false
|
|
2403
2312
|
});
|
|
2404
2313
|
}
|
|
2405
2314
|
/**
|
|
@@ -2450,30 +2359,12 @@ var ChannelDriver = class {
|
|
|
2450
2359
|
messages = Array.isArray(body) ? body : null;
|
|
2451
2360
|
}
|
|
2452
2361
|
} catch {
|
|
2362
|
+
continue;
|
|
2453
2363
|
}
|
|
2454
|
-
if (messages != null && messages.length > 0) {
|
|
2455
|
-
watcher.lastGoodPollAt = this.now();
|
|
2456
|
-
watcher.hadUsablePoll = true;
|
|
2457
|
-
} else {
|
|
2458
|
-
const emptyButReachable = messages != null;
|
|
2459
|
-
const graceApplies = !emptyButReachable || watcher.hadUsablePoll;
|
|
2460
|
-
if (graceApplies && this.now() - watcher.lastGoodPollAt < POLL_MISS_GRACE_MS) {
|
|
2461
|
-
continue;
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2464
|
-
const { openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk } = await this.pollInteractions(sessionId, watcher, messages);
|
|
2465
2364
|
for (const inFlight of [...watcher.inFlight.values()]) {
|
|
2466
|
-
await this.serviceInFlightMessage(
|
|
2467
|
-
sessionId,
|
|
2468
|
-
watcher,
|
|
2469
|
-
inFlight,
|
|
2470
|
-
messages,
|
|
2471
|
-
openQuestions,
|
|
2472
|
-
openPermissions,
|
|
2473
|
-
questionsPolledOk,
|
|
2474
|
-
permissionsPolledOk
|
|
2475
|
-
);
|
|
2365
|
+
await this.serviceInFlightMessage(sessionId, watcher, inFlight, messages);
|
|
2476
2366
|
}
|
|
2367
|
+
await this.pollInteractions(sessionId, watcher, messages);
|
|
2477
2368
|
}
|
|
2478
2369
|
} catch (err) {
|
|
2479
2370
|
if (err instanceof ChannelAuthError) {
|
|
@@ -2495,40 +2386,15 @@ var ChannelDriver = class {
|
|
|
2495
2386
|
});
|
|
2496
2387
|
}
|
|
2497
2388
|
}
|
|
2498
|
-
/**
|
|
2499
|
-
* On FIRST observing a terminal (done/failed) state, ensure the delivery
|
|
2500
|
-
* (markDone/markFailed) transient-retry path has a real window. A long
|
|
2501
|
-
* ACTIVELY-running turn is kept past its original `deadline`, so by completion
|
|
2502
|
-
* `now >= deadline` already holds and the retry bound below would fire on the
|
|
2503
|
-
* first transient PATCH failure — dropping the message before its reply lands
|
|
2504
|
-
* (Bugbot "Stale deadline aborts long-turn delivery"). Re-anchor once (latched)
|
|
2505
|
-
* to a fresh `pausedMaxWaitMs` window; only extend if the current deadline is at
|
|
2506
|
-
* or past now, so a still-ample window is left untouched.
|
|
2507
|
-
*/
|
|
2508
|
-
anchorDeliveryDeadline(inFlight) {
|
|
2509
|
-
if (inFlight.deliveryDeadlineAnchored) return;
|
|
2510
|
-
inFlight.deliveryDeadlineAnchored = true;
|
|
2511
|
-
if (this.now() >= inFlight.deadline) {
|
|
2512
|
-
inFlight.deadline = this.now() + this.pausedMaxWaitMs;
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
2389
|
/**
|
|
2516
2390
|
* Drive ONE in-flight message's lifecycle from the tick's message snapshot.
|
|
2517
2391
|
* Fires markProcessing on queued→running and markDone on done (each once),
|
|
2518
2392
|
* applies the idle-path re-dispatch guard, and removes the message from the
|
|
2519
2393
|
* in-flight set on completion or timeout.
|
|
2520
2394
|
*/
|
|
2521
|
-
async serviceInFlightMessage(sessionId, watcher, inFlight, messages
|
|
2395
|
+
async serviceInFlightMessage(sessionId, watcher, inFlight, messages) {
|
|
2522
2396
|
const conv = watcher.conv;
|
|
2523
2397
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
2524
|
-
const id = inFlight.evidentMessageId;
|
|
2525
|
-
if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
|
|
2526
|
-
else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
|
|
2527
|
-
if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
|
|
2528
|
-
else if (permissionsPolledOk) inFlight.pausedOnPermission = false;
|
|
2529
|
-
const observedOpen = openQuestions.has(id) || openPermissions.has(id);
|
|
2530
|
-
const latchedPaused = inFlight.pausedOnQuestion || inFlight.pausedOnPermission;
|
|
2531
|
-
const awaitingHuman = observedOpen || latchedPaused;
|
|
2532
2398
|
if ((state === "running" || state === "done" || state === "failed") && !inFlight.started) {
|
|
2533
2399
|
let claimed;
|
|
2534
2400
|
try {
|
|
@@ -2559,7 +2425,6 @@ var ChannelDriver = class {
|
|
|
2559
2425
|
}
|
|
2560
2426
|
}
|
|
2561
2427
|
if (state === "done") {
|
|
2562
|
-
this.anchorDeliveryDeadline(inFlight);
|
|
2563
2428
|
if (!inFlight.done) {
|
|
2564
2429
|
this.log({
|
|
2565
2430
|
level: "info",
|
|
@@ -2610,7 +2475,6 @@ var ChannelDriver = class {
|
|
|
2610
2475
|
return;
|
|
2611
2476
|
}
|
|
2612
2477
|
if (state === "failed") {
|
|
2613
|
-
this.anchorDeliveryDeadline(inFlight);
|
|
2614
2478
|
if (!inFlight.done) {
|
|
2615
2479
|
const error2 = messageError(messages, inFlight.opencodeMessageId) ?? void 0;
|
|
2616
2480
|
this.log({
|
|
@@ -2664,51 +2528,7 @@ var ChannelDriver = class {
|
|
|
2664
2528
|
stuck_for_ms: this.now() - inFlight.dispatchedAt
|
|
2665
2529
|
});
|
|
2666
2530
|
}
|
|
2667
|
-
|
|
2668
|
-
if (activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
|
|
2669
|
-
this.log({
|
|
2670
|
-
level: "error",
|
|
2671
|
-
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
|
-
conversation_id: conv.id,
|
|
2673
|
-
message_id: inFlight.evidentMessageId
|
|
2674
|
-
});
|
|
2675
|
-
void this.postSignal(conv.id, inFlight.evidentMessageId, "gave_up", {
|
|
2676
|
-
watched_for_ms: this.now() - inFlight.processingAnchorMs
|
|
2677
|
-
});
|
|
2678
|
-
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
2679
|
-
return;
|
|
2680
|
-
}
|
|
2681
|
-
if (activelyRunning && !inFlight.awaitingHumanLatched && !inFlight.aliveInFlight && this.now() - inFlight.lastAliveAt >= HEARTBEAT_MS) {
|
|
2682
|
-
inFlight.aliveInFlight = true;
|
|
2683
|
-
void this.postSignal(conv.id, inFlight.evidentMessageId, "alive").then((ok) => {
|
|
2684
|
-
inFlight.aliveInFlight = false;
|
|
2685
|
-
if (ok) inFlight.lastAliveAt = this.now();
|
|
2686
|
-
});
|
|
2687
|
-
}
|
|
2688
|
-
if (awaitingHuman) {
|
|
2689
|
-
if (!inFlight.awaitingHumanLatched) {
|
|
2690
|
-
inFlight.deadline = this.now() + this.pausedMaxWaitMs;
|
|
2691
|
-
inFlight.awaitingHumanLatched = true;
|
|
2692
|
-
}
|
|
2693
|
-
if (!inFlight.pausedClearConfirmed && !inFlight.pausedInFlight) {
|
|
2694
|
-
inFlight.pausedInFlight = true;
|
|
2695
|
-
void this.postSignal(conv.id, inFlight.evidentMessageId, "paused").then((ok) => {
|
|
2696
|
-
inFlight.pausedInFlight = false;
|
|
2697
|
-
if (ok && inFlight.awaitingHumanLatched) inFlight.pausedClearConfirmed = true;
|
|
2698
|
-
});
|
|
2699
|
-
}
|
|
2700
|
-
} else if (inFlight.awaitingHumanLatched) {
|
|
2701
|
-
inFlight.awaitingHumanLatched = false;
|
|
2702
|
-
inFlight.pausedOnQuestion = false;
|
|
2703
|
-
inFlight.pausedOnPermission = false;
|
|
2704
|
-
inFlight.pausedClearConfirmed = false;
|
|
2705
|
-
}
|
|
2706
|
-
const siblingPaused = (sib) => openQuestions.has(sib.evidentMessageId) || openPermissions.has(sib.evidentMessageId) || sib.awaitingHumanLatched || sib.pausedOnQuestion || sib.pausedOnPermission;
|
|
2707
|
-
const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
|
|
2708
|
-
(sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
|
|
2709
|
-
);
|
|
2710
|
-
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
2711
|
-
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
2531
|
+
if (this.now() >= inFlight.deadline) {
|
|
2712
2532
|
this.log({
|
|
2713
2533
|
level: "info",
|
|
2714
2534
|
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} did not complete within the watch window \u2014 leaving for the cron safety net`,
|
|
@@ -2740,17 +2560,12 @@ var ChannelDriver = class {
|
|
|
2740
2560
|
*/
|
|
2741
2561
|
async readoptProcessing() {
|
|
2742
2562
|
const rows = await this.getProcessingMessages();
|
|
2743
|
-
if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0
|
|
2563
|
+
if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0) {
|
|
2744
2564
|
const stillProcessing = new Set(rows.map((r) => r.id));
|
|
2745
|
-
for (const id of [
|
|
2746
|
-
...this.dontRedispatch,
|
|
2747
|
-
...this.doneUndeliverable,
|
|
2748
|
-
...this.readoptPollUnresolvedSignalled
|
|
2749
|
-
]) {
|
|
2565
|
+
for (const id of [...this.dontRedispatch, ...this.doneUndeliverable]) {
|
|
2750
2566
|
if (!stillProcessing.has(id)) {
|
|
2751
2567
|
const cleared = this.dontRedispatch.delete(id);
|
|
2752
2568
|
const clearedUndeliverable = this.doneUndeliverable.delete(id);
|
|
2753
|
-
this.readoptPollUnresolvedSignalled.delete(id);
|
|
2754
2569
|
if (cleared || clearedUndeliverable) {
|
|
2755
2570
|
this.log({
|
|
2756
2571
|
level: "info",
|
|
@@ -2804,10 +2619,8 @@ var ChannelDriver = class {
|
|
|
2804
2619
|
});
|
|
2805
2620
|
continue;
|
|
2806
2621
|
}
|
|
2807
|
-
const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
|
|
2808
|
-
const sessionOngoing = anyUntracked ? await isSessionOngoing(this.port, sessionId) : null;
|
|
2809
2622
|
for (const row of sessionRows) {
|
|
2810
|
-
await this.readoptOne(sessionId, row, messages
|
|
2623
|
+
await this.readoptOne(sessionId, row, messages);
|
|
2811
2624
|
}
|
|
2812
2625
|
}
|
|
2813
2626
|
}
|
|
@@ -2829,7 +2642,7 @@ var ChannelDriver = class {
|
|
|
2829
2642
|
*
|
|
2830
2643
|
* Only `ChannelAuthError` propagates.
|
|
2831
2644
|
*/
|
|
2832
|
-
async readoptOne(sessionId, row, messages
|
|
2645
|
+
async readoptOne(sessionId, row, messages) {
|
|
2833
2646
|
if (this.isTracked(sessionId, row.id)) {
|
|
2834
2647
|
this.log({
|
|
2835
2648
|
level: "info",
|
|
@@ -2869,7 +2682,6 @@ var ChannelDriver = class {
|
|
|
2869
2682
|
conversation_id: row.conversation_id,
|
|
2870
2683
|
message_id: row.id
|
|
2871
2684
|
});
|
|
2872
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_undeliverable");
|
|
2873
2685
|
return;
|
|
2874
2686
|
}
|
|
2875
2687
|
this.log({
|
|
@@ -2881,7 +2693,6 @@ var ChannelDriver = class {
|
|
|
2881
2693
|
return;
|
|
2882
2694
|
}
|
|
2883
2695
|
this.dontRedispatch.delete(row.id);
|
|
2884
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
2885
2696
|
return;
|
|
2886
2697
|
}
|
|
2887
2698
|
if (state === "failed") {
|
|
@@ -2904,7 +2715,6 @@ var ChannelDriver = class {
|
|
|
2904
2715
|
conversation_id: row.conversation_id,
|
|
2905
2716
|
message_id: row.id
|
|
2906
2717
|
});
|
|
2907
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_undeliverable");
|
|
2908
2718
|
return;
|
|
2909
2719
|
}
|
|
2910
2720
|
this.log({
|
|
@@ -2916,7 +2726,6 @@ var ChannelDriver = class {
|
|
|
2916
2726
|
return;
|
|
2917
2727
|
}
|
|
2918
2728
|
this.dontRedispatch.delete(row.id);
|
|
2919
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
2920
2729
|
return;
|
|
2921
2730
|
}
|
|
2922
2731
|
if (this.dontRedispatch.has(row.id)) {
|
|
@@ -2928,71 +2737,6 @@ var ChannelDriver = class {
|
|
|
2928
2737
|
});
|
|
2929
2738
|
return;
|
|
2930
2739
|
}
|
|
2931
|
-
let statusReadableOngoing = null;
|
|
2932
|
-
if (state === "running" && ocId) {
|
|
2933
|
-
const reply = findLastAssistantReplyFor(messages, ocId);
|
|
2934
|
-
const shape = this.replyCompletionShape(reply);
|
|
2935
|
-
const ongoing = sessionOngoing;
|
|
2936
|
-
statusReadableOngoing = ongoing;
|
|
2937
|
-
if (ongoing === false) {
|
|
2938
|
-
this.log({
|
|
2939
|
-
level: "info",
|
|
2940
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per GET /session/status (absent/idle) \u2014 re-dispatching from scratch (status-gated recovery)`,
|
|
2941
|
-
conversation_id: row.conversation_id,
|
|
2942
|
-
message_id: row.id
|
|
2943
|
-
});
|
|
2944
|
-
await this.forceReadoptRun(sessionId, row);
|
|
2945
|
-
return;
|
|
2946
|
-
}
|
|
2947
|
-
if (ongoing === true) {
|
|
2948
|
-
this.log({
|
|
2949
|
-
level: "info",
|
|
2950
|
-
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
|
-
conversation_id: row.conversation_id,
|
|
2952
|
-
message_id: row.id
|
|
2953
|
-
});
|
|
2954
|
-
} else {
|
|
2955
|
-
if (shape === "b1") {
|
|
2956
|
-
this.log({
|
|
2957
|
-
level: "info",
|
|
2958
|
-
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
|
-
conversation_id: row.conversation_id,
|
|
2960
|
-
message_id: row.id
|
|
2961
|
-
});
|
|
2962
|
-
if (!this.readoptPollUnresolvedSignalled.has(row.id)) {
|
|
2963
|
-
this.readoptPollUnresolvedSignalled.add(row.id);
|
|
2964
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_poll_unresolved");
|
|
2965
|
-
}
|
|
2966
|
-
return;
|
|
2967
|
-
}
|
|
2968
|
-
this.log({
|
|
2969
|
-
level: "info",
|
|
2970
|
-
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
|
-
conversation_id: row.conversation_id,
|
|
2972
|
-
message_id: row.id
|
|
2973
|
-
});
|
|
2974
|
-
}
|
|
2975
|
-
}
|
|
2976
|
-
if (statusReadableOngoing === null && state === "running" && ocId && isPreamblePinnedRunning(messages, ocId)) {
|
|
2977
|
-
const descendantAlive = await this.isAnyDescendantSessionAlive(sessionId);
|
|
2978
|
-
if (descendantAlive === true) {
|
|
2979
|
-
this.log({
|
|
2980
|
-
level: "info",
|
|
2981
|
-
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
|
-
conversation_id: row.conversation_id,
|
|
2983
|
-
message_id: row.id
|
|
2984
|
-
});
|
|
2985
|
-
} else {
|
|
2986
|
-
this.log({
|
|
2987
|
-
level: "info",
|
|
2988
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} preamble-pinned on recovery (root ${sessionId.slice(0, 8)}), no live descendant runner \u2014 re-dispatching from scratch${descendantAlive === null ? " (descendant liveness indeterminate; a restart guarantees no live runner, so this does NOT block the re-dispatch)" : ""}`,
|
|
2989
|
-
conversation_id: row.conversation_id,
|
|
2990
|
-
message_id: row.id
|
|
2991
|
-
});
|
|
2992
|
-
await this.forceReadoptRun(sessionId, row);
|
|
2993
|
-
return;
|
|
2994
|
-
}
|
|
2995
|
-
}
|
|
2996
2740
|
if ((state === "running" || state === "queued") && ocId) {
|
|
2997
2741
|
const conv = this.convForRow(sessionId, row);
|
|
2998
2742
|
const message = this.queuedMessageForRow(row);
|
|
@@ -3006,7 +2750,6 @@ var ChannelDriver = class {
|
|
|
3006
2750
|
conversation_id: row.conversation_id,
|
|
3007
2751
|
message_id: row.id
|
|
3008
2752
|
});
|
|
3009
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_reattached");
|
|
3010
2753
|
return;
|
|
3011
2754
|
}
|
|
3012
2755
|
await this.forceReadoptRun(sessionId, row);
|
|
@@ -3059,7 +2802,6 @@ var ChannelDriver = class {
|
|
|
3059
2802
|
conversation_id: row.conversation_id,
|
|
3060
2803
|
message_id: row.id
|
|
3061
2804
|
});
|
|
3062
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_window_elapsed");
|
|
3063
2805
|
return;
|
|
3064
2806
|
}
|
|
3065
2807
|
const options = {
|
|
@@ -3088,7 +2830,6 @@ var ChannelDriver = class {
|
|
|
3088
2830
|
conversation_id: row.conversation_id,
|
|
3089
2831
|
message_id: row.id
|
|
3090
2832
|
});
|
|
3091
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
3092
2833
|
return;
|
|
3093
2834
|
}
|
|
3094
2835
|
if (ocId === null) {
|
|
@@ -3099,7 +2840,6 @@ var ChannelDriver = class {
|
|
|
3099
2840
|
conversation_id: row.conversation_id,
|
|
3100
2841
|
message_id: row.id
|
|
3101
2842
|
});
|
|
3102
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
3103
2843
|
return;
|
|
3104
2844
|
}
|
|
3105
2845
|
const conv = this.convForRow(sessionId, row);
|
|
@@ -3109,7 +2849,6 @@ var ChannelDriver = class {
|
|
|
3109
2849
|
this.readopted.add(row.id);
|
|
3110
2850
|
this.awaitingReadopt.delete(row.id);
|
|
3111
2851
|
this.ensureWatcherRunning(sessionId);
|
|
3112
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_redispatched");
|
|
3113
2852
|
}
|
|
3114
2853
|
/**
|
|
3115
2854
|
* True if `evidentMessageId` is already being driven — either in the
|
|
@@ -3201,41 +2940,21 @@ var ChannelDriver = class {
|
|
|
3201
2940
|
* RUNNING (not done) is the one that paused. With one running message that is
|
|
3202
2941
|
* unambiguous; with several we prefer an explicit messageID match, else the
|
|
3203
2942
|
* oldest running message.
|
|
3204
|
-
*
|
|
3205
|
-
* Returns the set of in-flight Evident message ids that are paused awaiting a
|
|
3206
|
-
* human — an outstanding (still-open) question/permission is attributed to them.
|
|
3207
|
-
* `serviceInFlightMessage` uses this to keep an actively-running turn watched
|
|
3208
|
-
* forever (ADR-0047) while still bounding a turn merely blocked on a person who
|
|
3209
|
-
* may never answer. Attribution here covers ALL open interactions, not just
|
|
3210
|
-
* NEW (un-deduped) ones — a question stays "awaiting a human" until answered,
|
|
3211
|
-
* even after it was already surfaced to the channel.
|
|
3212
2943
|
*/
|
|
3213
2944
|
async pollInteractions(sessionId, watcher, messages) {
|
|
3214
|
-
const openQuestions = /* @__PURE__ */ new Set();
|
|
3215
|
-
const openPermissions = /* @__PURE__ */ new Set();
|
|
3216
|
-
let questionsPolledOk = true;
|
|
3217
|
-
let permissionsPolledOk = true;
|
|
3218
2945
|
let questions = [];
|
|
3219
2946
|
try {
|
|
3220
2947
|
const res = await this.fetchImpl(`${this.opencodeBase}/question`);
|
|
3221
2948
|
if (res.ok) {
|
|
3222
2949
|
const body = await res.json();
|
|
3223
|
-
|
|
3224
|
-
questions = body;
|
|
3225
|
-
} else {
|
|
3226
|
-
questionsPolledOk = false;
|
|
3227
|
-
}
|
|
3228
|
-
} else {
|
|
3229
|
-
questionsPolledOk = false;
|
|
2950
|
+
questions = Array.isArray(body) ? body : [];
|
|
3230
2951
|
}
|
|
3231
2952
|
} catch {
|
|
3232
|
-
questionsPolledOk = false;
|
|
3233
2953
|
}
|
|
3234
2954
|
for (const q of questions) {
|
|
2955
|
+
if (watcher.reportedQuestions.has(q.id)) continue;
|
|
3235
2956
|
if (!await this.sessionBelongsTo(q.sessionID, sessionId)) continue;
|
|
3236
2957
|
const paused = this.attributeInteraction(watcher, q.tool?.messageID, messages);
|
|
3237
|
-
if (paused) openQuestions.add(paused.evidentMessageId);
|
|
3238
|
-
if (watcher.reportedQuestions.has(q.id)) continue;
|
|
3239
2958
|
const reported = await this.reportInteraction(
|
|
3240
2959
|
watcher.conv.id,
|
|
3241
2960
|
"question",
|
|
@@ -3249,22 +2968,14 @@ var ChannelDriver = class {
|
|
|
3249
2968
|
const res = await this.fetchImpl(`${this.opencodeBase}/permission`);
|
|
3250
2969
|
if (res.ok) {
|
|
3251
2970
|
const body = await res.json();
|
|
3252
|
-
|
|
3253
|
-
permissions = body;
|
|
3254
|
-
} else {
|
|
3255
|
-
permissionsPolledOk = false;
|
|
3256
|
-
}
|
|
3257
|
-
} else {
|
|
3258
|
-
permissionsPolledOk = false;
|
|
2971
|
+
permissions = Array.isArray(body) ? body : [];
|
|
3259
2972
|
}
|
|
3260
2973
|
} catch {
|
|
3261
|
-
permissionsPolledOk = false;
|
|
3262
2974
|
}
|
|
3263
2975
|
for (const p of permissions) {
|
|
2976
|
+
if (watcher.reportedPermissions.has(p.id)) continue;
|
|
3264
2977
|
if (!await this.sessionBelongsTo(p.sessionID, sessionId)) continue;
|
|
3265
2978
|
const paused = this.attributeInteraction(watcher, p.messageID, messages);
|
|
3266
|
-
if (paused) openPermissions.add(paused.evidentMessageId);
|
|
3267
|
-
if (watcher.reportedPermissions.has(p.id)) continue;
|
|
3268
2979
|
const reported = await this.reportInteraction(
|
|
3269
2980
|
watcher.conv.id,
|
|
3270
2981
|
"permission",
|
|
@@ -3273,7 +2984,6 @@ var ChannelDriver = class {
|
|
|
3273
2984
|
);
|
|
3274
2985
|
if (reported) watcher.reportedPermissions.add(p.id);
|
|
3275
2986
|
}
|
|
3276
|
-
return { openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk };
|
|
3277
2987
|
}
|
|
3278
2988
|
/**
|
|
3279
2989
|
* True when `sessionId` is the `rootSessionId` itself OR a descendant of it —
|
|
@@ -3319,83 +3029,6 @@ var ChannelDriver = class {
|
|
|
3319
3029
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
3320
3030
|
return parent;
|
|
3321
3031
|
}
|
|
3322
|
-
/**
|
|
3323
|
-
* DEFENSIVE cross-check for the restart-recovery path (WI-2): is any descendant
|
|
3324
|
-
* (`task` sub-agent) session under `rootSessionId` still genuinely doing work?
|
|
3325
|
-
*
|
|
3326
|
-
* The PRIMARY recovery trigger is "preamble-pinned on recovery ⇒ idle" — a
|
|
3327
|
-
* runner restart wipes OpenCode's in-memory `SessionStatus`/`Runner`, so a
|
|
3328
|
-
* completed `finish: "tool-calls"` root reply encountered during re-adoption is
|
|
3329
|
-
* idle by OpenCode's own definition and is re-dispatched. This method exists only
|
|
3330
|
-
* so the WI-3 caller can VETO that re-dispatch in the rare case a descendant is
|
|
3331
|
-
* provably in flight at the exact moment of recovery.
|
|
3332
|
-
*
|
|
3333
|
-
* "Alive" criterion (TIGHTENED): a descendant is alive only when it is PROVABLY,
|
|
3334
|
-
* ACTIVELY generating — its LAST message is an assistant still mid-generation
|
|
3335
|
-
* (`completed == null`, via `isSessionActivelyGenerating`). An
|
|
3336
|
-
* INCOMPLETE-BUT-NOT-GENERATING child — last message a user message, or a
|
|
3337
|
-
* completed `finish: "tool-calls"` step — is NOT alive after a restart (nothing
|
|
3338
|
-
* is generating once the runner is gone), so it does NOT veto. (This is
|
|
3339
|
-
* deliberately NOT `!isTurnComplete`, which also matches those dead-but-non-terminal
|
|
3340
|
-
* shapes and would falsely veto — re-hanging the very turn this path recovers.)
|
|
3341
|
-
*
|
|
3342
|
-
* Return contract (encoded so WI-3 need not re-derive it):
|
|
3343
|
-
* - `true` → a descendant is provably, actively generating (veto re-dispatch).
|
|
3344
|
-
* - `false` → descendants exist but none is actively generating (the restart
|
|
3345
|
-
* case), OR no descendant is found at all.
|
|
3346
|
-
* - `null` → liveness is INDETERMINATE (enumeration via `listSessions` failed).
|
|
3347
|
-
*
|
|
3348
|
-
* ⚠️ `null` (UNKNOWN) MUST NOT be treated as "alive": WI-3 treats `null` the same
|
|
3349
|
-
* as `false` and does NOT veto — a restart guarantees no live runner, so an
|
|
3350
|
-
* indeterminate cross-check almost always means "couldn't reach a child that no
|
|
3351
|
-
* longer exists". The inversion lives in the caller; this method just reports
|
|
3352
|
-
* true/false/null faithfully.
|
|
3353
|
-
*
|
|
3354
|
-
* VERIFY-BEFORE-DEPEND: we depend ONLY on (a) `parentID` from `GET /session/:id`
|
|
3355
|
-
* (already proven by the existing child-session interaction tests, via
|
|
3356
|
-
* `resolveSessionParent`/`sessionBelongsTo`) and (b) the child's own message-list
|
|
3357
|
-
* terminal state. We do NOT depend on any session-level `busy`/`idle` field —
|
|
3358
|
-
* there is none on `GET /session/:id`; OpenCode's busy state is in-memory
|
|
3359
|
-
* `SessionStatus` only.
|
|
3360
|
-
*/
|
|
3361
|
-
async isAnyDescendantSessionAlive(rootSessionId) {
|
|
3362
|
-
const sessions = await listSessions(this.port);
|
|
3363
|
-
if (!sessions) {
|
|
3364
|
-
this.log({
|
|
3365
|
-
level: "error",
|
|
3366
|
-
message: `Re-adopt: could not enumerate sessions to cross-check descendant liveness for root ${rootSessionId} (listSessions failed) \u2014 treating child liveness as indeterminate`
|
|
3367
|
-
});
|
|
3368
|
-
return null;
|
|
3369
|
-
}
|
|
3370
|
-
for (const candidate of sessions) {
|
|
3371
|
-
if (!candidate?.id || candidate.id === rootSessionId) continue;
|
|
3372
|
-
if (!await this.sessionBelongsTo(candidate.id, rootSessionId)) continue;
|
|
3373
|
-
const childMsgs = await getSessionMessages(this.port, candidate.id);
|
|
3374
|
-
if (isSessionActivelyGenerating(childMsgs)) {
|
|
3375
|
-
return true;
|
|
3376
|
-
}
|
|
3377
|
-
}
|
|
3378
|
-
return false;
|
|
3379
|
-
}
|
|
3380
|
-
/**
|
|
3381
|
-
* Cheap decision-telemetry label for a running row's LAST correlated reply
|
|
3382
|
-
* (WI-2 Task 2.2): which running SHAPE it is, for the status-gated recovery log.
|
|
3383
|
-
* - `b1` — the reply itself is still in flight (`time.completed == null`) —
|
|
3384
|
-
* the aborted-in-flight production bug after a restart.
|
|
3385
|
-
* - `b2` — a COMPLETED reply pinned running only by `finish === "tool-calls"`
|
|
3386
|
-
* (the sub-agent preamble — #253's shape).
|
|
3387
|
-
* - `other` — any other shape (defensive; a running row is normally b1 or b2).
|
|
3388
|
-
* Reads `info.time.completed` / `info.finish` (tolerating the legacy top-level
|
|
3389
|
-
* shape) directly rather than re-importing the module-private `completedOf`/
|
|
3390
|
-
* `finishOf` — this is a display label only, not a correctness predicate.
|
|
3391
|
-
*/
|
|
3392
|
-
replyCompletionShape(reply) {
|
|
3393
|
-
if (!reply) return "other";
|
|
3394
|
-
const completed = reply.info?.time?.completed ?? reply.time?.completed;
|
|
3395
|
-
if (completed == null) return "b1";
|
|
3396
|
-
const finish = reply.info?.finish ?? reply.finish;
|
|
3397
|
-
return finish === "tool-calls" ? "b2" : "other";
|
|
3398
|
-
}
|
|
3399
3032
|
/**
|
|
3400
3033
|
* Attribute a surfaced interaction to the in-flight message it paused on (M-1).
|
|
3401
3034
|
*
|
|
@@ -3627,12 +3260,6 @@ var ChannelDriver = class {
|
|
|
3627
3260
|
* MUST NOT use `callWithRetry` (a telemetry ping must not block the sequential
|
|
3628
3261
|
* watcher tick — one attempt is enough). A failure is SWALLOWED but LOGGED with
|
|
3629
3262
|
* context (no silent catch, per development-workflow).
|
|
3630
|
-
*
|
|
3631
|
-
* Returns whether the POST SUCCEEDED (2xx). Most callers ignore this (pure
|
|
3632
|
-
* telemetry), but the `paused` liveness-clear uses it to know whether to
|
|
3633
|
-
* RE-ASSERT on a later tick — a single dropped `paused` POST must not leave a
|
|
3634
|
-
* stale `last_seen_alive_at` on a still-paused row (Bugbot "Failed paused signal
|
|
3635
|
-
* leaves liveness").
|
|
3636
3263
|
*/
|
|
3637
3264
|
async postSignal(conversationId, messageId, signal, extra) {
|
|
3638
3265
|
try {
|
|
@@ -3651,9 +3278,7 @@ var ChannelDriver = class {
|
|
|
3651
3278
|
conversation_id: conversationId,
|
|
3652
3279
|
message_id: messageId
|
|
3653
3280
|
});
|
|
3654
|
-
return false;
|
|
3655
3281
|
}
|
|
3656
|
-
return true;
|
|
3657
3282
|
} catch (err) {
|
|
3658
3283
|
this.log({
|
|
3659
3284
|
level: "error",
|
|
@@ -3661,7 +3286,6 @@ var ChannelDriver = class {
|
|
|
3661
3286
|
conversation_id: conversationId,
|
|
3662
3287
|
message_id: messageId
|
|
3663
3288
|
});
|
|
3664
|
-
return false;
|
|
3665
3289
|
}
|
|
3666
3290
|
}
|
|
3667
3291
|
async persistSession(conversationId, sessionId) {
|