@evident-ai/cli 3.0.1-dev.7272711 → 3.0.1-dev.7bc32fd

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
@@ -1126,6 +1126,36 @@ async function sessionExists(port, id) {
1126
1126
  return null;
1127
1127
  }
1128
1128
  }
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
+ }
1129
1159
  async function createOpenCodeSession(port, directory) {
1130
1160
  const url = new URL(`${opencodeBase(port)}/session`);
1131
1161
  if (directory && directory.trim()) {
@@ -1790,6 +1820,7 @@ var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
1790
1820
  var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
1791
1821
  var DEFAULT_STUCK_QUEUED_MS = 6e4;
1792
1822
  var HEARTBEAT_MS = 6e4;
1823
+ var ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1e3;
1793
1824
  var POLL_MISS_GRACE_MS = HEARTBEAT_MS;
1794
1825
  var ChannelAuthError = class extends Error {
1795
1826
  constructor(message) {
@@ -1887,6 +1918,15 @@ var ChannelDriver = class {
1887
1918
  * the row leaves the processing list, exactly like `dontRedispatch`.
1888
1919
  */
1889
1920
  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();
1890
1930
  /**
1891
1931
  * "A null-id re-adopt re-dispatch is in flight, awaiting its read-back" (WI-5
1892
1932
  * Task 5.4, High-2). Since we no longer send a caller-supplied id, a re-dispatch
@@ -2274,6 +2314,7 @@ var ChannelDriver = class {
2274
2314
  opencodeMessageId,
2275
2315
  message,
2276
2316
  dispatchedAt: now,
2317
+ processingAnchorMs: now,
2277
2318
  deadline: now + this.pausedMaxWaitMs,
2278
2319
  started: false,
2279
2320
  done: false,
@@ -2332,6 +2373,10 @@ var ChannelDriver = class {
2332
2373
  opencodeMessageId,
2333
2374
  message,
2334
2375
  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,
2335
2380
  deadline: processedAtMs + this.pausedMaxWaitMs,
2336
2381
  // The server row is ALREADY `processing`; do not re-fire markProcessing.
2337
2382
  started: true,
@@ -2620,6 +2665,19 @@ var ChannelDriver = class {
2620
2665
  });
2621
2666
  }
2622
2667
  const activelyRunning = state === "running" && !awaitingHuman;
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
+ }
2623
2681
  if (activelyRunning && !inFlight.awaitingHumanLatched && !inFlight.aliveInFlight && this.now() - inFlight.lastAliveAt >= HEARTBEAT_MS) {
2624
2682
  inFlight.aliveInFlight = true;
2625
2683
  void this.postSignal(conv.id, inFlight.evidentMessageId, "alive").then((ok) => {
@@ -2682,12 +2740,17 @@ var ChannelDriver = class {
2682
2740
  */
2683
2741
  async readoptProcessing() {
2684
2742
  const rows = await this.getProcessingMessages();
2685
- if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0) {
2743
+ if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
2686
2744
  const stillProcessing = new Set(rows.map((r) => r.id));
2687
- for (const id of [...this.dontRedispatch, ...this.doneUndeliverable]) {
2745
+ for (const id of [
2746
+ ...this.dontRedispatch,
2747
+ ...this.doneUndeliverable,
2748
+ ...this.readoptPollUnresolvedSignalled
2749
+ ]) {
2688
2750
  if (!stillProcessing.has(id)) {
2689
2751
  const cleared = this.dontRedispatch.delete(id);
2690
2752
  const clearedUndeliverable = this.doneUndeliverable.delete(id);
2753
+ this.readoptPollUnresolvedSignalled.delete(id);
2691
2754
  if (cleared || clearedUndeliverable) {
2692
2755
  this.log({
2693
2756
  level: "info",
@@ -2741,8 +2804,10 @@ var ChannelDriver = class {
2741
2804
  });
2742
2805
  continue;
2743
2806
  }
2807
+ const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
2808
+ const sessionOngoing = anyUntracked ? await isSessionOngoing(this.port, sessionId) : null;
2744
2809
  for (const row of sessionRows) {
2745
- await this.readoptOne(sessionId, row, messages);
2810
+ await this.readoptOne(sessionId, row, messages, sessionOngoing);
2746
2811
  }
2747
2812
  }
2748
2813
  }
@@ -2764,7 +2829,7 @@ var ChannelDriver = class {
2764
2829
  *
2765
2830
  * Only `ChannelAuthError` propagates.
2766
2831
  */
2767
- async readoptOne(sessionId, row, messages) {
2832
+ async readoptOne(sessionId, row, messages, sessionOngoing) {
2768
2833
  if (this.isTracked(sessionId, row.id)) {
2769
2834
  this.log({
2770
2835
  level: "info",
@@ -2804,6 +2869,7 @@ var ChannelDriver = class {
2804
2869
  conversation_id: row.conversation_id,
2805
2870
  message_id: row.id
2806
2871
  });
2872
+ void this.postSignal(row.conversation_id, row.id, "readopt_undeliverable");
2807
2873
  return;
2808
2874
  }
2809
2875
  this.log({
@@ -2815,6 +2881,7 @@ var ChannelDriver = class {
2815
2881
  return;
2816
2882
  }
2817
2883
  this.dontRedispatch.delete(row.id);
2884
+ void this.postSignal(row.conversation_id, row.id, "readopt_done");
2818
2885
  return;
2819
2886
  }
2820
2887
  if (state === "failed") {
@@ -2837,6 +2904,7 @@ var ChannelDriver = class {
2837
2904
  conversation_id: row.conversation_id,
2838
2905
  message_id: row.id
2839
2906
  });
2907
+ void this.postSignal(row.conversation_id, row.id, "readopt_undeliverable");
2840
2908
  return;
2841
2909
  }
2842
2910
  this.log({
@@ -2848,6 +2916,7 @@ var ChannelDriver = class {
2848
2916
  return;
2849
2917
  }
2850
2918
  this.dontRedispatch.delete(row.id);
2919
+ void this.postSignal(row.conversation_id, row.id, "readopt_failed");
2851
2920
  return;
2852
2921
  }
2853
2922
  if (this.dontRedispatch.has(row.id)) {
@@ -2859,7 +2928,52 @@ var ChannelDriver = class {
2859
2928
  });
2860
2929
  return;
2861
2930
  }
2862
- if (state === "running" && ocId && isPreamblePinnedRunning(messages, ocId)) {
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)) {
2863
2977
  const descendantAlive = await this.isAnyDescendantSessionAlive(sessionId);
2864
2978
  if (descendantAlive === true) {
2865
2979
  this.log({
@@ -2892,6 +3006,7 @@ var ChannelDriver = class {
2892
3006
  conversation_id: row.conversation_id,
2893
3007
  message_id: row.id
2894
3008
  });
3009
+ void this.postSignal(row.conversation_id, row.id, "readopt_reattached");
2895
3010
  return;
2896
3011
  }
2897
3012
  await this.forceReadoptRun(sessionId, row);
@@ -2944,6 +3059,7 @@ var ChannelDriver = class {
2944
3059
  conversation_id: row.conversation_id,
2945
3060
  message_id: row.id
2946
3061
  });
3062
+ void this.postSignal(row.conversation_id, row.id, "readopt_window_elapsed");
2947
3063
  return;
2948
3064
  }
2949
3065
  const options = {
@@ -2972,6 +3088,7 @@ var ChannelDriver = class {
2972
3088
  conversation_id: row.conversation_id,
2973
3089
  message_id: row.id
2974
3090
  });
3091
+ void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
2975
3092
  return;
2976
3093
  }
2977
3094
  if (ocId === null) {
@@ -2982,6 +3099,7 @@ var ChannelDriver = class {
2982
3099
  conversation_id: row.conversation_id,
2983
3100
  message_id: row.id
2984
3101
  });
3102
+ void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
2985
3103
  return;
2986
3104
  }
2987
3105
  const conv = this.convForRow(sessionId, row);
@@ -2991,6 +3109,7 @@ var ChannelDriver = class {
2991
3109
  this.readopted.add(row.id);
2992
3110
  this.awaitingReadopt.delete(row.id);
2993
3111
  this.ensureWatcherRunning(sessionId);
3112
+ void this.postSignal(row.conversation_id, row.id, "readopt_redispatched");
2994
3113
  }
2995
3114
  /**
2996
3115
  * True if `evidentMessageId` is already being driven — either in the
@@ -3258,6 +3377,25 @@ var ChannelDriver = class {
3258
3377
  }
3259
3378
  return false;
3260
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
+ }
3261
3399
  /**
3262
3400
  * Attribute a surfaced interaction to the in-flight message it paused on (M-1).
3263
3401
  *