@evident-ai/cli 3.0.1-dev.39d9b5f → 3.0.1-dev.3e59bb5
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 +231 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1078,6 +1078,12 @@ 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
|
+
}
|
|
1081
1087
|
function sessionLastActivityMs(session) {
|
|
1082
1088
|
const candidates = [
|
|
1083
1089
|
session.time?.updated,
|
|
@@ -1120,6 +1126,36 @@ async function sessionExists(port, id) {
|
|
|
1120
1126
|
return null;
|
|
1121
1127
|
}
|
|
1122
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
|
+
}
|
|
1123
1159
|
async function createOpenCodeSession(port, directory) {
|
|
1124
1160
|
const url = new URL(`${opencodeBase(port)}/session`);
|
|
1125
1161
|
if (directory && directory.trim()) {
|
|
@@ -1246,6 +1282,11 @@ function messageRunState(messages, userMessageId) {
|
|
|
1246
1282
|
if (isAssistantInFlight(reply)) return "running";
|
|
1247
1283
|
return errorOf(reply) != null ? "failed" : "done";
|
|
1248
1284
|
}
|
|
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
|
+
}
|
|
1249
1290
|
function messageError(messages, userMessageId) {
|
|
1250
1291
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
1251
1292
|
const error2 = errorOf(reply);
|
|
@@ -1779,6 +1820,7 @@ var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
|
1779
1820
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
1780
1821
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
1781
1822
|
var HEARTBEAT_MS = 6e4;
|
|
1823
|
+
var ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1e3;
|
|
1782
1824
|
var POLL_MISS_GRACE_MS = HEARTBEAT_MS;
|
|
1783
1825
|
var ChannelAuthError = class extends Error {
|
|
1784
1826
|
constructor(message) {
|
|
@@ -1876,6 +1918,15 @@ var ChannelDriver = class {
|
|
|
1876
1918
|
* the row leaves the processing list, exactly like `dontRedispatch`.
|
|
1877
1919
|
*/
|
|
1878
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();
|
|
1879
1930
|
/**
|
|
1880
1931
|
* "A null-id re-adopt re-dispatch is in flight, awaiting its read-back" (WI-5
|
|
1881
1932
|
* Task 5.4, High-2). Since we no longer send a caller-supplied id, a re-dispatch
|
|
@@ -2263,6 +2314,7 @@ var ChannelDriver = class {
|
|
|
2263
2314
|
opencodeMessageId,
|
|
2264
2315
|
message,
|
|
2265
2316
|
dispatchedAt: now,
|
|
2317
|
+
processingAnchorMs: now,
|
|
2266
2318
|
deadline: now + this.pausedMaxWaitMs,
|
|
2267
2319
|
started: false,
|
|
2268
2320
|
done: false,
|
|
@@ -2321,6 +2373,10 @@ var ChannelDriver = class {
|
|
|
2321
2373
|
opencodeMessageId,
|
|
2322
2374
|
message,
|
|
2323
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,
|
|
2324
2380
|
deadline: processedAtMs + this.pausedMaxWaitMs,
|
|
2325
2381
|
// The server row is ALREADY `processing`; do not re-fire markProcessing.
|
|
2326
2382
|
started: true,
|
|
@@ -2609,6 +2665,19 @@ var ChannelDriver = class {
|
|
|
2609
2665
|
});
|
|
2610
2666
|
}
|
|
2611
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
|
+
}
|
|
2612
2681
|
if (activelyRunning && !inFlight.awaitingHumanLatched && !inFlight.aliveInFlight && this.now() - inFlight.lastAliveAt >= HEARTBEAT_MS) {
|
|
2613
2682
|
inFlight.aliveInFlight = true;
|
|
2614
2683
|
void this.postSignal(conv.id, inFlight.evidentMessageId, "alive").then((ok) => {
|
|
@@ -2671,12 +2740,17 @@ var ChannelDriver = class {
|
|
|
2671
2740
|
*/
|
|
2672
2741
|
async readoptProcessing() {
|
|
2673
2742
|
const rows = await this.getProcessingMessages();
|
|
2674
|
-
if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0) {
|
|
2743
|
+
if (this.dontRedispatch.size > 0 || this.doneUndeliverable.size > 0 || this.readoptPollUnresolvedSignalled.size > 0) {
|
|
2675
2744
|
const stillProcessing = new Set(rows.map((r) => r.id));
|
|
2676
|
-
for (const id of [
|
|
2745
|
+
for (const id of [
|
|
2746
|
+
...this.dontRedispatch,
|
|
2747
|
+
...this.doneUndeliverable,
|
|
2748
|
+
...this.readoptPollUnresolvedSignalled
|
|
2749
|
+
]) {
|
|
2677
2750
|
if (!stillProcessing.has(id)) {
|
|
2678
2751
|
const cleared = this.dontRedispatch.delete(id);
|
|
2679
2752
|
const clearedUndeliverable = this.doneUndeliverable.delete(id);
|
|
2753
|
+
this.readoptPollUnresolvedSignalled.delete(id);
|
|
2680
2754
|
if (cleared || clearedUndeliverable) {
|
|
2681
2755
|
this.log({
|
|
2682
2756
|
level: "info",
|
|
@@ -2730,8 +2804,10 @@ var ChannelDriver = class {
|
|
|
2730
2804
|
});
|
|
2731
2805
|
continue;
|
|
2732
2806
|
}
|
|
2807
|
+
const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
|
|
2808
|
+
const sessionOngoing = anyUntracked ? await isSessionOngoing(this.port, sessionId) : null;
|
|
2733
2809
|
for (const row of sessionRows) {
|
|
2734
|
-
await this.readoptOne(sessionId, row, messages);
|
|
2810
|
+
await this.readoptOne(sessionId, row, messages, sessionOngoing);
|
|
2735
2811
|
}
|
|
2736
2812
|
}
|
|
2737
2813
|
}
|
|
@@ -2753,7 +2829,7 @@ var ChannelDriver = class {
|
|
|
2753
2829
|
*
|
|
2754
2830
|
* Only `ChannelAuthError` propagates.
|
|
2755
2831
|
*/
|
|
2756
|
-
async readoptOne(sessionId, row, messages) {
|
|
2832
|
+
async readoptOne(sessionId, row, messages, sessionOngoing) {
|
|
2757
2833
|
if (this.isTracked(sessionId, row.id)) {
|
|
2758
2834
|
this.log({
|
|
2759
2835
|
level: "info",
|
|
@@ -2793,6 +2869,7 @@ var ChannelDriver = class {
|
|
|
2793
2869
|
conversation_id: row.conversation_id,
|
|
2794
2870
|
message_id: row.id
|
|
2795
2871
|
});
|
|
2872
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_undeliverable");
|
|
2796
2873
|
return;
|
|
2797
2874
|
}
|
|
2798
2875
|
this.log({
|
|
@@ -2804,6 +2881,7 @@ var ChannelDriver = class {
|
|
|
2804
2881
|
return;
|
|
2805
2882
|
}
|
|
2806
2883
|
this.dontRedispatch.delete(row.id);
|
|
2884
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
2807
2885
|
return;
|
|
2808
2886
|
}
|
|
2809
2887
|
if (state === "failed") {
|
|
@@ -2826,6 +2904,7 @@ var ChannelDriver = class {
|
|
|
2826
2904
|
conversation_id: row.conversation_id,
|
|
2827
2905
|
message_id: row.id
|
|
2828
2906
|
});
|
|
2907
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_undeliverable");
|
|
2829
2908
|
return;
|
|
2830
2909
|
}
|
|
2831
2910
|
this.log({
|
|
@@ -2837,6 +2916,7 @@ var ChannelDriver = class {
|
|
|
2837
2916
|
return;
|
|
2838
2917
|
}
|
|
2839
2918
|
this.dontRedispatch.delete(row.id);
|
|
2919
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
2840
2920
|
return;
|
|
2841
2921
|
}
|
|
2842
2922
|
if (this.dontRedispatch.has(row.id)) {
|
|
@@ -2848,6 +2928,71 @@ var ChannelDriver = class {
|
|
|
2848
2928
|
});
|
|
2849
2929
|
return;
|
|
2850
2930
|
}
|
|
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
|
+
}
|
|
2851
2996
|
if ((state === "running" || state === "queued") && ocId) {
|
|
2852
2997
|
const conv = this.convForRow(sessionId, row);
|
|
2853
2998
|
const message = this.queuedMessageForRow(row);
|
|
@@ -2861,6 +3006,7 @@ var ChannelDriver = class {
|
|
|
2861
3006
|
conversation_id: row.conversation_id,
|
|
2862
3007
|
message_id: row.id
|
|
2863
3008
|
});
|
|
3009
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_reattached");
|
|
2864
3010
|
return;
|
|
2865
3011
|
}
|
|
2866
3012
|
await this.forceReadoptRun(sessionId, row);
|
|
@@ -2913,6 +3059,7 @@ var ChannelDriver = class {
|
|
|
2913
3059
|
conversation_id: row.conversation_id,
|
|
2914
3060
|
message_id: row.id
|
|
2915
3061
|
});
|
|
3062
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_window_elapsed");
|
|
2916
3063
|
return;
|
|
2917
3064
|
}
|
|
2918
3065
|
const options = {
|
|
@@ -2941,6 +3088,7 @@ var ChannelDriver = class {
|
|
|
2941
3088
|
conversation_id: row.conversation_id,
|
|
2942
3089
|
message_id: row.id
|
|
2943
3090
|
});
|
|
3091
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
2944
3092
|
return;
|
|
2945
3093
|
}
|
|
2946
3094
|
if (ocId === null) {
|
|
@@ -2951,6 +3099,7 @@ var ChannelDriver = class {
|
|
|
2951
3099
|
conversation_id: row.conversation_id,
|
|
2952
3100
|
message_id: row.id
|
|
2953
3101
|
});
|
|
3102
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
2954
3103
|
return;
|
|
2955
3104
|
}
|
|
2956
3105
|
const conv = this.convForRow(sessionId, row);
|
|
@@ -2960,6 +3109,7 @@ var ChannelDriver = class {
|
|
|
2960
3109
|
this.readopted.add(row.id);
|
|
2961
3110
|
this.awaitingReadopt.delete(row.id);
|
|
2962
3111
|
this.ensureWatcherRunning(sessionId);
|
|
3112
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_redispatched");
|
|
2963
3113
|
}
|
|
2964
3114
|
/**
|
|
2965
3115
|
* True if `evidentMessageId` is already being driven — either in the
|
|
@@ -3169,6 +3319,83 @@ var ChannelDriver = class {
|
|
|
3169
3319
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
3170
3320
|
return parent;
|
|
3171
3321
|
}
|
|
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
|
+
}
|
|
3172
3399
|
/**
|
|
3173
3400
|
* Attribute a surfaced interaction to the in-flight message it paused on (M-1).
|
|
3174
3401
|
*
|