@evident-ai/cli 3.0.1-dev.e9b1720 → 3.0.1-dev.ec76080

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 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);
@@ -2293,9 +2249,7 @@ var ChannelDriver = class {
2293
2249
  inFlight: /* @__PURE__ */ new Map(),
2294
2250
  loop: null,
2295
2251
  reportedQuestions: /* @__PURE__ */ new Set(),
2296
- reportedPermissions: /* @__PURE__ */ new Set(),
2297
- lastGoodPollAt: this.now(),
2298
- hadUsablePoll: false
2252
+ reportedPermissions: /* @__PURE__ */ new Set()
2299
2253
  };
2300
2254
  this.watchers.set(sessionId, watcher);
2301
2255
  }
@@ -2305,37 +2259,20 @@ var ChannelDriver = class {
2305
2259
  opencodeMessageId,
2306
2260
  message,
2307
2261
  dispatchedAt: now,
2308
- processingAnchorMs: now,
2309
2262
  deadline: now + this.pausedMaxWaitMs,
2310
2263
  started: false,
2311
2264
  done: false,
2312
- stuckReported: false,
2313
- lastAliveAt: 0,
2314
- aliveInFlight: false,
2315
- awaitingHumanLatched: false,
2316
- pausedOnQuestion: false,
2317
- pausedOnPermission: false,
2318
- pausedClearConfirmed: false,
2319
- pausedInFlight: false,
2320
- deliveryDeadlineAnchored: false
2265
+ stuckReported: false
2321
2266
  });
2322
2267
  }
2323
2268
  /**
2324
2269
  * Register a RE-ADOPTED `processing` message with its session watcher
2325
2270
  * (ADR-0046, WI-4). Mirrors `registerInFlight` but anchors the give-up
2326
2271
  * `deadline` to the row's SERVER-SIDE `processed_at` (Invariant 1), NEVER to
2327
- * `now`, so the paused/queued/unreachable cases settle on the same wall-clock a
2328
- * fresh dispatch would (10 min after `processed_at`, not 10 min from now).
2329
- *
2330
- * This re-attaches into the SAME watcher, so the ADR-0047 progressing-vs-paused
2331
- * give-up (`serviceInFlightMessage`) applies unchanged: a re-adopted turn
2332
- * opencode reports ACTIVELY `running` is watched to completion (its liveness
2333
- * heartbeat keeps the cron off its row), while a re-adopted turn that is paused
2334
- * awaiting a human — or queued/unreachable — is still bounded by `deadline` and
2335
- * handed to the cron. The old "the `deadline` must settle before the ~15-min
2336
- * cron or they double-drive" reasoning is superseded: liveness now settles the
2337
- * actively-running case; `deadline` settles the rest. `dispatchedAt` stays `now`
2338
- * (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).
2339
2276
  *
2340
2277
  * `evidentMessageId` addresses the SERVER row (for markProcessing/markDone);
2341
2278
  * `opencodeMessageId` is the id the watcher polls for a reply — for the orphan
@@ -2353,9 +2290,7 @@ var ChannelDriver = class {
2353
2290
  inFlight: /* @__PURE__ */ new Map(),
2354
2291
  loop: null,
2355
2292
  reportedQuestions: /* @__PURE__ */ new Set(),
2356
- reportedPermissions: /* @__PURE__ */ new Set(),
2357
- lastGoodPollAt: this.now(),
2358
- hadUsablePoll: false
2293
+ reportedPermissions: /* @__PURE__ */ new Set()
2359
2294
  };
2360
2295
  this.watchers.set(sessionId, watcher);
2361
2296
  }
@@ -2364,10 +2299,6 @@ var ChannelDriver = class {
2364
2299
  opencodeMessageId,
2365
2300
  message,
2366
2301
  dispatchedAt: this.now(),
2367
- // Anchor the absolute-age ceiling to the SERVER-SIDE `processed_at` (the same
2368
- // value seeding `deadline`), NOT `dispatchedAt` — so a re-adopted zombie's age
2369
- // reflects the real turn duration and the ceiling fires on the ORIGINAL turn.
2370
- processingAnchorMs: processedAtMs,
2371
2302
  deadline: processedAtMs + this.pausedMaxWaitMs,
2372
2303
  // The server row is ALREADY `processing`; do not re-fire markProcessing.
2373
2304
  started: true,
@@ -2377,20 +2308,7 @@ var ChannelDriver = class {
2377
2308
  // on `state === 'queued'` (turn produced no reply), not on `started`, so a
2378
2309
  // re-adopted row left wedged in `queued` still emits the signal once
2379
2310
  // (#210/#220 observability).
2380
- stuckReported: false,
2381
- // Task 5.2: a re-adopted actively-running row re-attaches into the SAME
2382
- // watcher and so hits the SAME actively-running heartbeat branch in
2383
- // `serviceInFlightMessage` as a fresh dispatch — monitoring observes "runner
2384
- // re-adopted and is confirming this row alive" via that `alive` heartbeat,
2385
- // with no extra `re_adopted` signal needed (folds old WI-6).
2386
- lastAliveAt: 0,
2387
- aliveInFlight: false,
2388
- awaitingHumanLatched: false,
2389
- pausedOnQuestion: false,
2390
- pausedOnPermission: false,
2391
- pausedClearConfirmed: false,
2392
- pausedInFlight: false,
2393
- deliveryDeadlineAnchored: false
2311
+ stuckReported: false
2394
2312
  });
2395
2313
  }
2396
2314
  /**
@@ -2441,30 +2359,12 @@ var ChannelDriver = class {
2441
2359
  messages = Array.isArray(body) ? body : null;
2442
2360
  }
2443
2361
  } catch {
2362
+ continue;
2444
2363
  }
2445
- if (messages != null && messages.length > 0) {
2446
- watcher.lastGoodPollAt = this.now();
2447
- watcher.hadUsablePoll = true;
2448
- } else {
2449
- const emptyButReachable = messages != null;
2450
- const graceApplies = !emptyButReachable || watcher.hadUsablePoll;
2451
- if (graceApplies && this.now() - watcher.lastGoodPollAt < POLL_MISS_GRACE_MS) {
2452
- continue;
2453
- }
2454
- }
2455
- const { openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk } = await this.pollInteractions(sessionId, watcher, messages);
2456
2364
  for (const inFlight of [...watcher.inFlight.values()]) {
2457
- await this.serviceInFlightMessage(
2458
- sessionId,
2459
- watcher,
2460
- inFlight,
2461
- messages,
2462
- openQuestions,
2463
- openPermissions,
2464
- questionsPolledOk,
2465
- permissionsPolledOk
2466
- );
2365
+ await this.serviceInFlightMessage(sessionId, watcher, inFlight, messages);
2467
2366
  }
2367
+ await this.pollInteractions(sessionId, watcher, messages);
2468
2368
  }
2469
2369
  } catch (err) {
2470
2370
  if (err instanceof ChannelAuthError) {
@@ -2486,40 +2386,15 @@ var ChannelDriver = class {
2486
2386
  });
2487
2387
  }
2488
2388
  }
2489
- /**
2490
- * On FIRST observing a terminal (done/failed) state, ensure the delivery
2491
- * (markDone/markFailed) transient-retry path has a real window. A long
2492
- * ACTIVELY-running turn is kept past its original `deadline`, so by completion
2493
- * `now >= deadline` already holds and the retry bound below would fire on the
2494
- * first transient PATCH failure — dropping the message before its reply lands
2495
- * (Bugbot "Stale deadline aborts long-turn delivery"). Re-anchor once (latched)
2496
- * to a fresh `pausedMaxWaitMs` window; only extend if the current deadline is at
2497
- * or past now, so a still-ample window is left untouched.
2498
- */
2499
- anchorDeliveryDeadline(inFlight) {
2500
- if (inFlight.deliveryDeadlineAnchored) return;
2501
- inFlight.deliveryDeadlineAnchored = true;
2502
- if (this.now() >= inFlight.deadline) {
2503
- inFlight.deadline = this.now() + this.pausedMaxWaitMs;
2504
- }
2505
- }
2506
2389
  /**
2507
2390
  * Drive ONE in-flight message's lifecycle from the tick's message snapshot.
2508
2391
  * Fires markProcessing on queued→running and markDone on done (each once),
2509
2392
  * applies the idle-path re-dispatch guard, and removes the message from the
2510
2393
  * in-flight set on completion or timeout.
2511
2394
  */
2512
- async serviceInFlightMessage(sessionId, watcher, inFlight, messages, openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk) {
2395
+ async serviceInFlightMessage(sessionId, watcher, inFlight, messages) {
2513
2396
  const conv = watcher.conv;
2514
2397
  const state = messageRunState(messages, inFlight.opencodeMessageId);
2515
- const id = inFlight.evidentMessageId;
2516
- if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
2517
- else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
2518
- if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
2519
- else if (permissionsPolledOk) inFlight.pausedOnPermission = false;
2520
- const observedOpen = openQuestions.has(id) || openPermissions.has(id);
2521
- const latchedPaused = inFlight.pausedOnQuestion || inFlight.pausedOnPermission;
2522
- const awaitingHuman = observedOpen || latchedPaused;
2523
2398
  if ((state === "running" || state === "done" || state === "failed") && !inFlight.started) {
2524
2399
  let claimed;
2525
2400
  try {
@@ -2550,7 +2425,6 @@ var ChannelDriver = class {
2550
2425
  }
2551
2426
  }
2552
2427
  if (state === "done") {
2553
- this.anchorDeliveryDeadline(inFlight);
2554
2428
  if (!inFlight.done) {
2555
2429
  this.log({
2556
2430
  level: "info",
@@ -2601,7 +2475,6 @@ var ChannelDriver = class {
2601
2475
  return;
2602
2476
  }
2603
2477
  if (state === "failed") {
2604
- this.anchorDeliveryDeadline(inFlight);
2605
2478
  if (!inFlight.done) {
2606
2479
  const error2 = messageError(messages, inFlight.opencodeMessageId) ?? void 0;
2607
2480
  this.log({
@@ -2655,51 +2528,7 @@ var ChannelDriver = class {
2655
2528
  stuck_for_ms: this.now() - inFlight.dispatchedAt
2656
2529
  });
2657
2530
  }
2658
- const activelyRunning = state === "running" && !awaitingHuman;
2659
- if (activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
2660
- this.log({
2661
- level: "error",
2662
- message: `Message ${inFlight.evidentMessageId.slice(0, 8)} exceeded the absolute processing ceiling (${Math.round((this.now() - inFlight.processingAnchorMs) / 6e4)}min, session ${sessionId}) while still actively running \u2014 releasing so the cron can reclaim it`,
2663
- conversation_id: conv.id,
2664
- message_id: inFlight.evidentMessageId
2665
- });
2666
- void this.postSignal(conv.id, inFlight.evidentMessageId, "gave_up", {
2667
- watched_for_ms: this.now() - inFlight.processingAnchorMs
2668
- });
2669
- this.removeInFlight(watcher, inFlight.evidentMessageId);
2670
- return;
2671
- }
2672
- if (activelyRunning && !inFlight.awaitingHumanLatched && !inFlight.aliveInFlight && this.now() - inFlight.lastAliveAt >= HEARTBEAT_MS) {
2673
- inFlight.aliveInFlight = true;
2674
- void this.postSignal(conv.id, inFlight.evidentMessageId, "alive").then((ok) => {
2675
- inFlight.aliveInFlight = false;
2676
- if (ok) inFlight.lastAliveAt = this.now();
2677
- });
2678
- }
2679
- if (awaitingHuman) {
2680
- if (!inFlight.awaitingHumanLatched) {
2681
- inFlight.deadline = this.now() + this.pausedMaxWaitMs;
2682
- inFlight.awaitingHumanLatched = true;
2683
- }
2684
- if (!inFlight.pausedClearConfirmed && !inFlight.pausedInFlight) {
2685
- inFlight.pausedInFlight = true;
2686
- void this.postSignal(conv.id, inFlight.evidentMessageId, "paused").then((ok) => {
2687
- inFlight.pausedInFlight = false;
2688
- if (ok && inFlight.awaitingHumanLatched) inFlight.pausedClearConfirmed = true;
2689
- });
2690
- }
2691
- } else if (inFlight.awaitingHumanLatched) {
2692
- inFlight.awaitingHumanLatched = false;
2693
- inFlight.pausedOnQuestion = false;
2694
- inFlight.pausedOnPermission = false;
2695
- inFlight.pausedClearConfirmed = false;
2696
- }
2697
- const siblingPaused = (sib) => openQuestions.has(sib.evidentMessageId) || openPermissions.has(sib.evidentMessageId) || sib.awaitingHumanLatched || sib.pausedOnQuestion || sib.pausedOnPermission;
2698
- const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
2699
- (sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
2700
- );
2701
- const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
2702
- if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
2531
+ if (this.now() >= inFlight.deadline) {
2703
2532
  this.log({
2704
2533
  level: "info",
2705
2534
  message: `Message ${inFlight.evidentMessageId.slice(0, 8)} did not complete within the watch window \u2014 leaving for the cron safety net`,
@@ -2790,10 +2619,8 @@ var ChannelDriver = class {
2790
2619
  });
2791
2620
  continue;
2792
2621
  }
2793
- const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
2794
- const sessionOngoing = anyUntracked ? await isSessionOngoing(this.port, sessionId) : null;
2795
2622
  for (const row of sessionRows) {
2796
- await this.readoptOne(sessionId, row, messages, sessionOngoing);
2623
+ await this.readoptOne(sessionId, row, messages);
2797
2624
  }
2798
2625
  }
2799
2626
  }
@@ -2815,7 +2642,7 @@ var ChannelDriver = class {
2815
2642
  *
2816
2643
  * Only `ChannelAuthError` propagates.
2817
2644
  */
2818
- async readoptOne(sessionId, row, messages, sessionOngoing) {
2645
+ async readoptOne(sessionId, row, messages) {
2819
2646
  if (this.isTracked(sessionId, row.id)) {
2820
2647
  this.log({
2821
2648
  level: "info",
@@ -2910,67 +2737,6 @@ var ChannelDriver = class {
2910
2737
  });
2911
2738
  return;
2912
2739
  }
2913
- let statusReadableOngoing = null;
2914
- if (state === "running" && ocId) {
2915
- const reply = findLastAssistantReplyFor(messages, ocId);
2916
- const shape = this.replyCompletionShape(reply);
2917
- const ongoing = sessionOngoing;
2918
- statusReadableOngoing = ongoing;
2919
- if (ongoing === false) {
2920
- this.log({
2921
- level: "info",
2922
- 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)`,
2923
- conversation_id: row.conversation_id,
2924
- message_id: row.id
2925
- });
2926
- await this.forceReadoptRun(sessionId, row);
2927
- return;
2928
- }
2929
- if (ongoing === true) {
2930
- this.log({
2931
- level: "info",
2932
- message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} and session ${sessionId.slice(0, 8)} is ongoing per GET /session/status (busy/retry) \u2014 re-attaching watcher (no re-dispatch)`,
2933
- conversation_id: row.conversation_id,
2934
- message_id: row.id
2935
- });
2936
- } else {
2937
- if (shape === "b1") {
2938
- this.log({
2939
- level: "info",
2940
- message: `Re-adopt: message ${row.id.slice(0, 8)} running/b1 but GET /session/status was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 NOT latching a b1 row on a transient status blip; leaving it un-tracked to re-evaluate on the next drain`,
2941
- conversation_id: row.conversation_id,
2942
- message_id: row.id
2943
- });
2944
- return;
2945
- }
2946
- this.log({
2947
- level: "info",
2948
- message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but GET /session/status was unreadable (null) for session ${sessionId.slice(0, 8)} \u2014 falling back to the #253 preamble + descendant cross-check`,
2949
- conversation_id: row.conversation_id,
2950
- message_id: row.id
2951
- });
2952
- }
2953
- }
2954
- if (statusReadableOngoing === null && state === "running" && ocId && isPreamblePinnedRunning(messages, ocId)) {
2955
- const descendantAlive = await this.isAnyDescendantSessionAlive(sessionId);
2956
- if (descendantAlive === true) {
2957
- this.log({
2958
- level: "info",
2959
- message: `Re-adopt: message ${row.id.slice(0, 8)} preamble-pinned running (root ${sessionId.slice(0, 8)}) but a live descendant sub-agent session was found \u2014 treating as still running, re-attaching watcher (no re-dispatch)`,
2960
- conversation_id: row.conversation_id,
2961
- message_id: row.id
2962
- });
2963
- } else {
2964
- this.log({
2965
- level: "info",
2966
- 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)" : ""}`,
2967
- conversation_id: row.conversation_id,
2968
- message_id: row.id
2969
- });
2970
- await this.forceReadoptRun(sessionId, row);
2971
- return;
2972
- }
2973
- }
2974
2740
  if ((state === "running" || state === "queued") && ocId) {
2975
2741
  const conv = this.convForRow(sessionId, row);
2976
2742
  const message = this.queuedMessageForRow(row);
@@ -3174,41 +2940,21 @@ var ChannelDriver = class {
3174
2940
  * RUNNING (not done) is the one that paused. With one running message that is
3175
2941
  * unambiguous; with several we prefer an explicit messageID match, else the
3176
2942
  * oldest running message.
3177
- *
3178
- * Returns the set of in-flight Evident message ids that are paused awaiting a
3179
- * human — an outstanding (still-open) question/permission is attributed to them.
3180
- * `serviceInFlightMessage` uses this to keep an actively-running turn watched
3181
- * forever (ADR-0047) while still bounding a turn merely blocked on a person who
3182
- * may never answer. Attribution here covers ALL open interactions, not just
3183
- * NEW (un-deduped) ones — a question stays "awaiting a human" until answered,
3184
- * even after it was already surfaced to the channel.
3185
2943
  */
3186
2944
  async pollInteractions(sessionId, watcher, messages) {
3187
- const openQuestions = /* @__PURE__ */ new Set();
3188
- const openPermissions = /* @__PURE__ */ new Set();
3189
- let questionsPolledOk = true;
3190
- let permissionsPolledOk = true;
3191
2945
  let questions = [];
3192
2946
  try {
3193
2947
  const res = await this.fetchImpl(`${this.opencodeBase}/question`);
3194
2948
  if (res.ok) {
3195
2949
  const body = await res.json();
3196
- if (Array.isArray(body)) {
3197
- questions = body;
3198
- } else {
3199
- questionsPolledOk = false;
3200
- }
3201
- } else {
3202
- questionsPolledOk = false;
2950
+ questions = Array.isArray(body) ? body : [];
3203
2951
  }
3204
2952
  } catch {
3205
- questionsPolledOk = false;
3206
2953
  }
3207
2954
  for (const q of questions) {
2955
+ if (watcher.reportedQuestions.has(q.id)) continue;
3208
2956
  if (!await this.sessionBelongsTo(q.sessionID, sessionId)) continue;
3209
2957
  const paused = this.attributeInteraction(watcher, q.tool?.messageID, messages);
3210
- if (paused) openQuestions.add(paused.evidentMessageId);
3211
- if (watcher.reportedQuestions.has(q.id)) continue;
3212
2958
  const reported = await this.reportInteraction(
3213
2959
  watcher.conv.id,
3214
2960
  "question",
@@ -3222,22 +2968,14 @@ var ChannelDriver = class {
3222
2968
  const res = await this.fetchImpl(`${this.opencodeBase}/permission`);
3223
2969
  if (res.ok) {
3224
2970
  const body = await res.json();
3225
- if (Array.isArray(body)) {
3226
- permissions = body;
3227
- } else {
3228
- permissionsPolledOk = false;
3229
- }
3230
- } else {
3231
- permissionsPolledOk = false;
2971
+ permissions = Array.isArray(body) ? body : [];
3232
2972
  }
3233
2973
  } catch {
3234
- permissionsPolledOk = false;
3235
2974
  }
3236
2975
  for (const p of permissions) {
2976
+ if (watcher.reportedPermissions.has(p.id)) continue;
3237
2977
  if (!await this.sessionBelongsTo(p.sessionID, sessionId)) continue;
3238
2978
  const paused = this.attributeInteraction(watcher, p.messageID, messages);
3239
- if (paused) openPermissions.add(paused.evidentMessageId);
3240
- if (watcher.reportedPermissions.has(p.id)) continue;
3241
2979
  const reported = await this.reportInteraction(
3242
2980
  watcher.conv.id,
3243
2981
  "permission",
@@ -3246,7 +2984,6 @@ var ChannelDriver = class {
3246
2984
  );
3247
2985
  if (reported) watcher.reportedPermissions.add(p.id);
3248
2986
  }
3249
- return { openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk };
3250
2987
  }
3251
2988
  /**
3252
2989
  * True when `sessionId` is the `rootSessionId` itself OR a descendant of it —
@@ -3292,83 +3029,6 @@ var ChannelDriver = class {
3292
3029
  if (parent !== void 0) this.sessionParents.set(sessionId, parent);
3293
3030
  return parent;
3294
3031
  }
3295
- /**
3296
- * DEFENSIVE cross-check for the restart-recovery path (WI-2): is any descendant
3297
- * (`task` sub-agent) session under `rootSessionId` still genuinely doing work?
3298
- *
3299
- * The PRIMARY recovery trigger is "preamble-pinned on recovery ⇒ idle" — a
3300
- * runner restart wipes OpenCode's in-memory `SessionStatus`/`Runner`, so a
3301
- * completed `finish: "tool-calls"` root reply encountered during re-adoption is
3302
- * idle by OpenCode's own definition and is re-dispatched. This method exists only
3303
- * so the WI-3 caller can VETO that re-dispatch in the rare case a descendant is
3304
- * provably in flight at the exact moment of recovery.
3305
- *
3306
- * "Alive" criterion (TIGHTENED): a descendant is alive only when it is PROVABLY,
3307
- * ACTIVELY generating — its LAST message is an assistant still mid-generation
3308
- * (`completed == null`, via `isSessionActivelyGenerating`). An
3309
- * INCOMPLETE-BUT-NOT-GENERATING child — last message a user message, or a
3310
- * completed `finish: "tool-calls"` step — is NOT alive after a restart (nothing
3311
- * is generating once the runner is gone), so it does NOT veto. (This is
3312
- * deliberately NOT `!isTurnComplete`, which also matches those dead-but-non-terminal
3313
- * shapes and would falsely veto — re-hanging the very turn this path recovers.)
3314
- *
3315
- * Return contract (encoded so WI-3 need not re-derive it):
3316
- * - `true` → a descendant is provably, actively generating (veto re-dispatch).
3317
- * - `false` → descendants exist but none is actively generating (the restart
3318
- * case), OR no descendant is found at all.
3319
- * - `null` → liveness is INDETERMINATE (enumeration via `listSessions` failed).
3320
- *
3321
- * ⚠️ `null` (UNKNOWN) MUST NOT be treated as "alive": WI-3 treats `null` the same
3322
- * as `false` and does NOT veto — a restart guarantees no live runner, so an
3323
- * indeterminate cross-check almost always means "couldn't reach a child that no
3324
- * longer exists". The inversion lives in the caller; this method just reports
3325
- * true/false/null faithfully.
3326
- *
3327
- * VERIFY-BEFORE-DEPEND: we depend ONLY on (a) `parentID` from `GET /session/:id`
3328
- * (already proven by the existing child-session interaction tests, via
3329
- * `resolveSessionParent`/`sessionBelongsTo`) and (b) the child's own message-list
3330
- * terminal state. We do NOT depend on any session-level `busy`/`idle` field —
3331
- * there is none on `GET /session/:id`; OpenCode's busy state is in-memory
3332
- * `SessionStatus` only.
3333
- */
3334
- async isAnyDescendantSessionAlive(rootSessionId) {
3335
- const sessions = await listSessions(this.port);
3336
- if (!sessions) {
3337
- this.log({
3338
- level: "error",
3339
- message: `Re-adopt: could not enumerate sessions to cross-check descendant liveness for root ${rootSessionId} (listSessions failed) \u2014 treating child liveness as indeterminate`
3340
- });
3341
- return null;
3342
- }
3343
- for (const candidate of sessions) {
3344
- if (!candidate?.id || candidate.id === rootSessionId) continue;
3345
- if (!await this.sessionBelongsTo(candidate.id, rootSessionId)) continue;
3346
- const childMsgs = await getSessionMessages(this.port, candidate.id);
3347
- if (isSessionActivelyGenerating(childMsgs)) {
3348
- return true;
3349
- }
3350
- }
3351
- return false;
3352
- }
3353
- /**
3354
- * Cheap decision-telemetry label for a running row's LAST correlated reply
3355
- * (WI-2 Task 2.2): which running SHAPE it is, for the status-gated recovery log.
3356
- * - `b1` — the reply itself is still in flight (`time.completed == null`) —
3357
- * the aborted-in-flight production bug after a restart.
3358
- * - `b2` — a COMPLETED reply pinned running only by `finish === "tool-calls"`
3359
- * (the sub-agent preamble — #253's shape).
3360
- * - `other` — any other shape (defensive; a running row is normally b1 or b2).
3361
- * Reads `info.time.completed` / `info.finish` (tolerating the legacy top-level
3362
- * shape) directly rather than re-importing the module-private `completedOf`/
3363
- * `finishOf` — this is a display label only, not a correctness predicate.
3364
- */
3365
- replyCompletionShape(reply) {
3366
- if (!reply) return "other";
3367
- const completed = reply.info?.time?.completed ?? reply.time?.completed;
3368
- if (completed == null) return "b1";
3369
- const finish = reply.info?.finish ?? reply.finish;
3370
- return finish === "tool-calls" ? "b2" : "other";
3371
- }
3372
3032
  /**
3373
3033
  * Attribute a surfaced interaction to the in-flight message it paused on (M-1).
3374
3034
  *
@@ -3600,12 +3260,6 @@ var ChannelDriver = class {
3600
3260
  * MUST NOT use `callWithRetry` (a telemetry ping must not block the sequential
3601
3261
  * watcher tick — one attempt is enough). A failure is SWALLOWED but LOGGED with
3602
3262
  * context (no silent catch, per development-workflow).
3603
- *
3604
- * Returns whether the POST SUCCEEDED (2xx). Most callers ignore this (pure
3605
- * telemetry), but the `paused` liveness-clear uses it to know whether to
3606
- * RE-ASSERT on a later tick — a single dropped `paused` POST must not leave a
3607
- * stale `last_seen_alive_at` on a still-paused row (Bugbot "Failed paused signal
3608
- * leaves liveness").
3609
3263
  */
3610
3264
  async postSignal(conversationId, messageId, signal, extra) {
3611
3265
  try {
@@ -3624,9 +3278,7 @@ var ChannelDriver = class {
3624
3278
  conversation_id: conversationId,
3625
3279
  message_id: messageId
3626
3280
  });
3627
- return false;
3628
3281
  }
3629
- return true;
3630
3282
  } catch (err) {
3631
3283
  this.log({
3632
3284
  level: "error",
@@ -3634,7 +3286,6 @@ var ChannelDriver = class {
3634
3286
  conversation_id: conversationId,
3635
3287
  message_id: messageId
3636
3288
  });
3637
- return false;
3638
3289
  }
3639
3290
  }
3640
3291
  async persistSession(conversationId, sessionId) {