@evident-ai/cli 3.0.1-dev.fc2f651 → 3.0.1-dev.fe4c93e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/index.js +190 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1811,6 +1811,12 @@ function messageIdOf(m) {
|
|
|
1811
1811
|
const infoId = m.info?.id;
|
|
1812
1812
|
return typeof infoId === "string" ? infoId : void 0;
|
|
1813
1813
|
}
|
|
1814
|
+
var LOG_LEVELS = {
|
|
1815
|
+
debug: 0,
|
|
1816
|
+
info: 1,
|
|
1817
|
+
warn: 2,
|
|
1818
|
+
error: 3
|
|
1819
|
+
};
|
|
1814
1820
|
var DEFAULT_RETRY_POLICY = {
|
|
1815
1821
|
maxAttempts: 6,
|
|
1816
1822
|
baseDelayMs: 500,
|
|
@@ -1958,6 +1964,16 @@ var ChannelDriver = class {
|
|
|
1958
1964
|
* entry = not yet resolved; `null` = resolved root (stop walking).
|
|
1959
1965
|
*/
|
|
1960
1966
|
sessionParents = /* @__PURE__ */ new Map();
|
|
1967
|
+
/**
|
|
1968
|
+
* Per-session OpenCode title cache (#310), keyed by sessionId. Only a resolved
|
|
1969
|
+
* NON-EMPTY name is stored (terminal — a real session name won't later un-name),
|
|
1970
|
+
* so we do NOT re-GET `/session/:id` every tick. A missing entry = not yet
|
|
1971
|
+
* resolved OR resolved-but-still-empty → re-fetch on next need, since OpenCode
|
|
1972
|
+
* names sessions asynchronously mid-turn. Driver-level (not per-watcher) so both
|
|
1973
|
+
* the watcher completion path AND the restart-recovery re-adopt path (which has
|
|
1974
|
+
* no watcher) can resolve the title.
|
|
1975
|
+
*/
|
|
1976
|
+
sessionTitles = /* @__PURE__ */ new Map();
|
|
1961
1977
|
/** Serialises drains so a reconnect during a drain doesn't double-process. */
|
|
1962
1978
|
draining = false;
|
|
1963
1979
|
/**
|
|
@@ -2183,7 +2199,7 @@ var ChannelDriver = class {
|
|
|
2183
2199
|
if (await sessionExists(this.port, sessionId) === false) {
|
|
2184
2200
|
this.sessions.delete(conv.id);
|
|
2185
2201
|
this.log({
|
|
2186
|
-
level: "
|
|
2202
|
+
level: "warn",
|
|
2187
2203
|
message: `Message ${message.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was deleted mid-dispatch (cleanup race) \u2014 deferring this and later messages for conversation ${conv.id.slice(0, 8)} to the next tick (recreated then, in order). Already-dispatched turns keep their watcher.`,
|
|
2188
2204
|
conversation_id: conv.id,
|
|
2189
2205
|
message_id: message.id
|
|
@@ -2202,7 +2218,7 @@ var ChannelDriver = class {
|
|
|
2202
2218
|
}
|
|
2203
2219
|
if (opencodeMessageId === null) {
|
|
2204
2220
|
this.log({
|
|
2205
|
-
level: "
|
|
2221
|
+
level: "warn",
|
|
2206
2222
|
message: `Message ${message.id.slice(0, 8)} dispatched but its opencode id could not be read back \u2014 leaving un-tracked to retry next tick`,
|
|
2207
2223
|
conversation_id: conv.id,
|
|
2208
2224
|
message_id: message.id
|
|
@@ -2216,7 +2232,7 @@ var ChannelDriver = class {
|
|
|
2216
2232
|
}
|
|
2217
2233
|
if (messages.length > 0 && dispatched === 0 && skippedAlreadyDispatched === messages.length) {
|
|
2218
2234
|
this.log({
|
|
2219
|
-
level: "
|
|
2235
|
+
level: "warn",
|
|
2220
2236
|
message: `Conversation ${conv.id.slice(0, 8)} has ${messages.length} pending message(s) but ALL are already marked dispatched locally (in-flight set: ${this.dispatched.size}) \u2014 none sent to OpenCode this tick. If this repeats, a message may be stuck acknowledged-but-never-dispatched (its watcher never settled).`,
|
|
2221
2237
|
conversation_id: conv.id
|
|
2222
2238
|
});
|
|
@@ -2230,7 +2246,7 @@ var ChannelDriver = class {
|
|
|
2230
2246
|
const exists = await sessionExists(this.port, bound);
|
|
2231
2247
|
if (exists === false) {
|
|
2232
2248
|
this.log({
|
|
2233
|
-
level: "
|
|
2249
|
+
level: "debug",
|
|
2234
2250
|
message: `OpenCode session ${bound} for conversation ${conv.id.slice(0, 8)} no longer exists (deleted or DB reset) \u2014 creating a fresh session and rebinding.`,
|
|
2235
2251
|
conversation_id: conv.id
|
|
2236
2252
|
});
|
|
@@ -2265,7 +2281,7 @@ var ChannelDriver = class {
|
|
|
2265
2281
|
this.opencodeDirectory = await getOpenCodeDirectory(this.port);
|
|
2266
2282
|
if (!this.opencodeDirectory) {
|
|
2267
2283
|
this.log({
|
|
2268
|
-
level: "
|
|
2284
|
+
level: "warn",
|
|
2269
2285
|
message: "Could not determine opencode directory (GET /path) \u2014 new sessions may not appear in opencode web"
|
|
2270
2286
|
});
|
|
2271
2287
|
}
|
|
@@ -2530,18 +2546,20 @@ var ChannelDriver = class {
|
|
|
2530
2546
|
const latchedPaused = inFlight.pausedOnQuestion || inFlight.pausedOnPermission;
|
|
2531
2547
|
const awaitingHuman = observedOpen || latchedPaused;
|
|
2532
2548
|
if ((state === "running" || state === "done" || state === "failed") && !inFlight.started) {
|
|
2549
|
+
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
2533
2550
|
let claimed;
|
|
2534
2551
|
try {
|
|
2535
2552
|
claimed = await this.markProcessing(
|
|
2536
2553
|
conv.id,
|
|
2537
2554
|
inFlight.evidentMessageId,
|
|
2538
2555
|
sessionId,
|
|
2539
|
-
inFlight.opencodeMessageId
|
|
2556
|
+
inFlight.opencodeMessageId,
|
|
2557
|
+
title
|
|
2540
2558
|
);
|
|
2541
2559
|
} catch (err) {
|
|
2542
2560
|
if (err instanceof ChannelAuthError) throw err;
|
|
2543
2561
|
this.log({
|
|
2544
|
-
level: "
|
|
2562
|
+
level: "warn",
|
|
2545
2563
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} processing (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
2546
2564
|
conversation_id: conv.id,
|
|
2547
2565
|
message_id: inFlight.evidentMessageId
|
|
@@ -2551,7 +2569,7 @@ var ChannelDriver = class {
|
|
|
2551
2569
|
inFlight.started = true;
|
|
2552
2570
|
if (!claimed) {
|
|
2553
2571
|
this.log({
|
|
2554
|
-
level: "
|
|
2572
|
+
level: "debug",
|
|
2555
2573
|
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} already marked processing \u2014 continuing`,
|
|
2556
2574
|
conversation_id: conv.id,
|
|
2557
2575
|
message_id: inFlight.evidentMessageId
|
|
@@ -2567,18 +2585,20 @@ var ChannelDriver = class {
|
|
|
2567
2585
|
conversation_id: conv.id,
|
|
2568
2586
|
message_id: inFlight.evidentMessageId
|
|
2569
2587
|
});
|
|
2588
|
+
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
2570
2589
|
try {
|
|
2571
2590
|
await this.markDone(
|
|
2572
2591
|
conv.id,
|
|
2573
2592
|
inFlight.evidentMessageId,
|
|
2574
2593
|
sessionId,
|
|
2575
|
-
inFlight.opencodeMessageId
|
|
2594
|
+
inFlight.opencodeMessageId,
|
|
2595
|
+
title
|
|
2576
2596
|
);
|
|
2577
2597
|
} catch (err) {
|
|
2578
2598
|
if (err instanceof ChannelAuthError) throw err;
|
|
2579
2599
|
if (err instanceof ChannelTerminalError) {
|
|
2580
2600
|
this.log({
|
|
2581
|
-
level: "
|
|
2601
|
+
level: "warn",
|
|
2582
2602
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (terminal HTTP ${err.status}) \u2014 leaving for the cron safety net: ${err.message}`,
|
|
2583
2603
|
conversation_id: conv.id,
|
|
2584
2604
|
message_id: inFlight.evidentMessageId
|
|
@@ -2588,7 +2608,7 @@ var ChannelDriver = class {
|
|
|
2588
2608
|
}
|
|
2589
2609
|
if (this.now() >= inFlight.deadline) {
|
|
2590
2610
|
this.log({
|
|
2591
|
-
level: "
|
|
2611
|
+
level: "warn",
|
|
2592
2612
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done within the watch window \u2014 leaving for the cron safety net: ${err instanceof Error ? err.message : String(err)}`,
|
|
2593
2613
|
conversation_id: conv.id,
|
|
2594
2614
|
message_id: inFlight.evidentMessageId
|
|
@@ -2597,7 +2617,7 @@ var ChannelDriver = class {
|
|
|
2597
2617
|
return;
|
|
2598
2618
|
}
|
|
2599
2619
|
this.log({
|
|
2600
|
-
level: "
|
|
2620
|
+
level: "warn",
|
|
2601
2621
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
2602
2622
|
conversation_id: conv.id,
|
|
2603
2623
|
message_id: inFlight.evidentMessageId
|
|
@@ -2625,7 +2645,7 @@ var ChannelDriver = class {
|
|
|
2625
2645
|
if (err instanceof ChannelAuthError) throw err;
|
|
2626
2646
|
if (err instanceof ChannelTerminalError) {
|
|
2627
2647
|
this.log({
|
|
2628
|
-
level: "
|
|
2648
|
+
level: "warn",
|
|
2629
2649
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed (terminal HTTP ${err.status}) \u2014 leaving for the cron safety net: ${err.message}`,
|
|
2630
2650
|
conversation_id: conv.id,
|
|
2631
2651
|
message_id: inFlight.evidentMessageId
|
|
@@ -2635,7 +2655,7 @@ var ChannelDriver = class {
|
|
|
2635
2655
|
}
|
|
2636
2656
|
if (this.now() >= inFlight.deadline) {
|
|
2637
2657
|
this.log({
|
|
2638
|
-
level: "
|
|
2658
|
+
level: "warn",
|
|
2639
2659
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed within the watch window \u2014 leaving for the cron safety net: ${err instanceof Error ? err.message : String(err)}`,
|
|
2640
2660
|
conversation_id: conv.id,
|
|
2641
2661
|
message_id: inFlight.evidentMessageId
|
|
@@ -2644,7 +2664,7 @@ var ChannelDriver = class {
|
|
|
2644
2664
|
return;
|
|
2645
2665
|
}
|
|
2646
2666
|
this.log({
|
|
2647
|
-
level: "
|
|
2667
|
+
level: "warn",
|
|
2648
2668
|
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
2649
2669
|
conversation_id: conv.id,
|
|
2650
2670
|
message_id: inFlight.evidentMessageId
|
|
@@ -2667,7 +2687,7 @@ var ChannelDriver = class {
|
|
|
2667
2687
|
const activelyRunning = state === "running" && !awaitingHuman;
|
|
2668
2688
|
if (activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
|
|
2669
2689
|
this.log({
|
|
2670
|
-
level: "
|
|
2690
|
+
level: "warn",
|
|
2671
2691
|
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
2692
|
conversation_id: conv.id,
|
|
2673
2693
|
message_id: inFlight.evidentMessageId
|
|
@@ -2710,7 +2730,7 @@ var ChannelDriver = class {
|
|
|
2710
2730
|
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
2711
2731
|
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
2712
2732
|
this.log({
|
|
2713
|
-
level: "
|
|
2733
|
+
level: "debug",
|
|
2714
2734
|
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} did not complete within the watch window \u2014 leaving for the cron safety net`,
|
|
2715
2735
|
conversation_id: conv.id,
|
|
2716
2736
|
message_id: inFlight.evidentMessageId
|
|
@@ -2753,7 +2773,7 @@ var ChannelDriver = class {
|
|
|
2753
2773
|
this.readoptPollUnresolvedSignalled.delete(id);
|
|
2754
2774
|
if (cleared || clearedUndeliverable) {
|
|
2755
2775
|
this.log({
|
|
2756
|
-
level: "
|
|
2776
|
+
level: "debug",
|
|
2757
2777
|
message: `Re-adopt: message ${id.slice(0, 8)} left the processing list (cron reset) \u2014 cleared gave-up marker`,
|
|
2758
2778
|
message_id: id
|
|
2759
2779
|
});
|
|
@@ -2766,7 +2786,7 @@ var ChannelDriver = class {
|
|
|
2766
2786
|
for (const row of rows) {
|
|
2767
2787
|
if (!row.opencode_session_id) {
|
|
2768
2788
|
this.log({
|
|
2769
|
-
level: "
|
|
2789
|
+
level: "warn",
|
|
2770
2790
|
message: `Cannot re-adopt processing message ${row.id.slice(0, 8)} \u2014 no opencode session id; leaving for the cron safety net`,
|
|
2771
2791
|
conversation_id: row.conversation_id,
|
|
2772
2792
|
message_id: row.id
|
|
@@ -2783,7 +2803,7 @@ var ChannelDriver = class {
|
|
|
2783
2803
|
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
2784
2804
|
if (!res.ok) {
|
|
2785
2805
|
this.log({
|
|
2786
|
-
level: "
|
|
2806
|
+
level: "warn",
|
|
2787
2807
|
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 skipping this session this tick`
|
|
2788
2808
|
});
|
|
2789
2809
|
continue;
|
|
@@ -2791,7 +2811,7 @@ var ChannelDriver = class {
|
|
|
2791
2811
|
const body = await res.json();
|
|
2792
2812
|
if (!Array.isArray(body)) {
|
|
2793
2813
|
this.log({
|
|
2794
|
-
level: "
|
|
2814
|
+
level: "warn",
|
|
2795
2815
|
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned a non-array message body \u2014 skipping this session this tick`
|
|
2796
2816
|
});
|
|
2797
2817
|
continue;
|
|
@@ -2799,7 +2819,7 @@ var ChannelDriver = class {
|
|
|
2799
2819
|
messages = body;
|
|
2800
2820
|
} catch (err) {
|
|
2801
2821
|
this.log({
|
|
2802
|
-
level: "
|
|
2822
|
+
level: "warn",
|
|
2803
2823
|
message: `Re-adopt: failed to poll session ${sessionId.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`
|
|
2804
2824
|
});
|
|
2805
2825
|
continue;
|
|
@@ -2832,7 +2852,7 @@ var ChannelDriver = class {
|
|
|
2832
2852
|
async readoptOne(sessionId, row, messages, sessionOngoing) {
|
|
2833
2853
|
if (this.isTracked(sessionId, row.id)) {
|
|
2834
2854
|
this.log({
|
|
2835
|
-
level: "
|
|
2855
|
+
level: "debug",
|
|
2836
2856
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already tracked in-flight \u2014 skipping (owned by the watcher loop)`,
|
|
2837
2857
|
conversation_id: row.conversation_id,
|
|
2838
2858
|
message_id: row.id
|
|
@@ -2844,7 +2864,7 @@ var ChannelDriver = class {
|
|
|
2844
2864
|
if (state === "done") {
|
|
2845
2865
|
if (this.doneUndeliverable.has(row.id)) {
|
|
2846
2866
|
this.log({
|
|
2847
|
-
level: "
|
|
2867
|
+
level: "debug",
|
|
2848
2868
|
message: `Re-adopt: message ${row.id.slice(0, 8)} markDone is terminally undeliverable \u2014 left to the cron; skipping until it leaves processing`,
|
|
2849
2869
|
conversation_id: row.conversation_id,
|
|
2850
2870
|
message_id: row.id
|
|
@@ -2858,13 +2878,14 @@ var ChannelDriver = class {
|
|
|
2858
2878
|
message_id: row.id
|
|
2859
2879
|
});
|
|
2860
2880
|
try {
|
|
2861
|
-
await this.
|
|
2881
|
+
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
2882
|
+
await this.markDone(row.conversation_id, row.id, sessionId, ocId, title);
|
|
2862
2883
|
} catch (err) {
|
|
2863
2884
|
if (err instanceof ChannelAuthError) throw err;
|
|
2864
2885
|
if (err instanceof ChannelTerminalError) {
|
|
2865
2886
|
this.doneUndeliverable.add(row.id);
|
|
2866
2887
|
this.log({
|
|
2867
|
-
level: "
|
|
2888
|
+
level: "warn",
|
|
2868
2889
|
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (terminal HTTP ${err.status}) \u2014 parking until it leaves processing; leaving for the cron safety net: ${err.message}`,
|
|
2869
2890
|
conversation_id: row.conversation_id,
|
|
2870
2891
|
message_id: row.id
|
|
@@ -2873,7 +2894,7 @@ var ChannelDriver = class {
|
|
|
2873
2894
|
return;
|
|
2874
2895
|
}
|
|
2875
2896
|
this.log({
|
|
2876
|
-
level: "
|
|
2897
|
+
level: "warn",
|
|
2877
2898
|
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
2878
2899
|
conversation_id: row.conversation_id,
|
|
2879
2900
|
message_id: row.id
|
|
@@ -2899,7 +2920,7 @@ var ChannelDriver = class {
|
|
|
2899
2920
|
if (err instanceof ChannelTerminalError) {
|
|
2900
2921
|
this.doneUndeliverable.add(row.id);
|
|
2901
2922
|
this.log({
|
|
2902
|
-
level: "
|
|
2923
|
+
level: "warn",
|
|
2903
2924
|
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} failed (terminal HTTP ${err.status}) \u2014 parking until it leaves processing; leaving for the cron safety net: ${err.message}`,
|
|
2904
2925
|
conversation_id: row.conversation_id,
|
|
2905
2926
|
message_id: row.id
|
|
@@ -2908,7 +2929,7 @@ var ChannelDriver = class {
|
|
|
2908
2929
|
return;
|
|
2909
2930
|
}
|
|
2910
2931
|
this.log({
|
|
2911
|
-
level: "
|
|
2932
|
+
level: "warn",
|
|
2912
2933
|
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} failed (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
2913
2934
|
conversation_id: row.conversation_id,
|
|
2914
2935
|
message_id: row.id
|
|
@@ -2921,7 +2942,7 @@ var ChannelDriver = class {
|
|
|
2921
2942
|
}
|
|
2922
2943
|
if (this.dontRedispatch.has(row.id)) {
|
|
2923
2944
|
this.log({
|
|
2924
|
-
level: "
|
|
2945
|
+
level: "debug",
|
|
2925
2946
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
2926
2947
|
conversation_id: row.conversation_id,
|
|
2927
2948
|
message_id: row.id
|
|
@@ -2946,7 +2967,7 @@ var ChannelDriver = class {
|
|
|
2946
2967
|
}
|
|
2947
2968
|
if (ongoing === true) {
|
|
2948
2969
|
this.log({
|
|
2949
|
-
level: "
|
|
2970
|
+
level: "debug",
|
|
2950
2971
|
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
2972
|
conversation_id: row.conversation_id,
|
|
2952
2973
|
message_id: row.id
|
|
@@ -2954,7 +2975,7 @@ var ChannelDriver = class {
|
|
|
2954
2975
|
} else {
|
|
2955
2976
|
if (shape === "b1") {
|
|
2956
2977
|
this.log({
|
|
2957
|
-
level: "
|
|
2978
|
+
level: "debug",
|
|
2958
2979
|
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
2980
|
conversation_id: row.conversation_id,
|
|
2960
2981
|
message_id: row.id
|
|
@@ -2966,7 +2987,7 @@ var ChannelDriver = class {
|
|
|
2966
2987
|
return;
|
|
2967
2988
|
}
|
|
2968
2989
|
this.log({
|
|
2969
|
-
level: "
|
|
2990
|
+
level: "debug",
|
|
2970
2991
|
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
2992
|
conversation_id: row.conversation_id,
|
|
2972
2993
|
message_id: row.id
|
|
@@ -2977,7 +2998,7 @@ var ChannelDriver = class {
|
|
|
2977
2998
|
const descendantAlive = await this.isAnyDescendantSessionAlive(sessionId);
|
|
2978
2999
|
if (descendantAlive === true) {
|
|
2979
3000
|
this.log({
|
|
2980
|
-
level: "
|
|
3001
|
+
level: "debug",
|
|
2981
3002
|
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
3003
|
conversation_id: row.conversation_id,
|
|
2983
3004
|
message_id: row.id
|
|
@@ -3001,7 +3022,7 @@ var ChannelDriver = class {
|
|
|
3001
3022
|
this.readopted.add(row.id);
|
|
3002
3023
|
this.ensureWatcherRunning(sessionId);
|
|
3003
3024
|
this.log({
|
|
3004
|
-
level: "
|
|
3025
|
+
level: "debug",
|
|
3005
3026
|
message: `Re-adopt: message ${row.id.slice(0, 8)} ${state} \u2014 re-attached watcher (stored id, no re-dispatch)`,
|
|
3006
3027
|
conversation_id: row.conversation_id,
|
|
3007
3028
|
message_id: row.id
|
|
@@ -3035,7 +3056,7 @@ var ChannelDriver = class {
|
|
|
3035
3056
|
async forceReadoptRun(sessionId, row) {
|
|
3036
3057
|
if (this.stopped) {
|
|
3037
3058
|
this.log({
|
|
3038
|
-
level: "
|
|
3059
|
+
level: "debug",
|
|
3039
3060
|
message: `Re-adopt: message ${row.id.slice(0, 8)} orphaned but the runner is stopping \u2014 not starting a fresh turn; leaving for restart recovery`,
|
|
3040
3061
|
conversation_id: row.conversation_id,
|
|
3041
3062
|
message_id: row.id
|
|
@@ -3044,7 +3065,7 @@ var ChannelDriver = class {
|
|
|
3044
3065
|
}
|
|
3045
3066
|
if (this.awaitingReadopt.has(row.id)) {
|
|
3046
3067
|
this.log({
|
|
3047
|
-
level: "
|
|
3068
|
+
level: "debug",
|
|
3048
3069
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already has a re-dispatch awaiting read-back \u2014 skipping (at most once)`,
|
|
3049
3070
|
conversation_id: row.conversation_id,
|
|
3050
3071
|
message_id: row.id
|
|
@@ -3054,7 +3075,7 @@ var ChannelDriver = class {
|
|
|
3054
3075
|
if (this.processedAtMs(row) + this.pausedMaxWaitMs <= this.now()) {
|
|
3055
3076
|
this.dontRedispatch.add(row.id);
|
|
3056
3077
|
this.log({
|
|
3057
|
-
level: "
|
|
3078
|
+
level: "debug",
|
|
3058
3079
|
message: `Re-adopt: message ${row.id.slice(0, 8)} orphaned but its re-adopt window has already elapsed \u2014 not dispatching an unwatchable turn; parking until it leaves processing (cron will reset it)`,
|
|
3059
3080
|
conversation_id: row.conversation_id,
|
|
3060
3081
|
message_id: row.id
|
|
@@ -3083,7 +3104,7 @@ var ChannelDriver = class {
|
|
|
3083
3104
|
this.awaitingReadopt.delete(row.id);
|
|
3084
3105
|
if (err instanceof ChannelAuthError) throw err;
|
|
3085
3106
|
this.log({
|
|
3086
|
-
level: "
|
|
3107
|
+
level: "warn",
|
|
3087
3108
|
message: `Re-adopt: re-dispatch failed for message ${row.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
3088
3109
|
conversation_id: row.conversation_id,
|
|
3089
3110
|
message_id: row.id
|
|
@@ -3094,7 +3115,7 @@ var ChannelDriver = class {
|
|
|
3094
3115
|
if (ocId === null) {
|
|
3095
3116
|
this.awaitingReadopt.delete(row.id);
|
|
3096
3117
|
this.log({
|
|
3097
|
-
level: "
|
|
3118
|
+
level: "warn",
|
|
3098
3119
|
message: `Re-adopt: message ${row.id.slice(0, 8)} re-dispatched but its opencode id could not be read back \u2014 leaving un-tracked to retry next drain`,
|
|
3099
3120
|
conversation_id: row.conversation_id,
|
|
3100
3121
|
message_id: row.id
|
|
@@ -3179,7 +3200,7 @@ var ChannelDriver = class {
|
|
|
3179
3200
|
if (this.readopted.delete(evidentMessageId) && inFlight && !inFlight.done) {
|
|
3180
3201
|
this.dontRedispatch.add(evidentMessageId);
|
|
3181
3202
|
this.log({
|
|
3182
|
-
level: "
|
|
3203
|
+
level: "debug",
|
|
3183
3204
|
message: `Re-adopt: message ${evidentMessageId.slice(0, 8)} gave up \u2014 parking until it leaves the processing list (cron reset)`,
|
|
3184
3205
|
conversation_id: watcher.conv.id,
|
|
3185
3206
|
message_id: evidentMessageId
|
|
@@ -3319,6 +3340,51 @@ var ChannelDriver = class {
|
|
|
3319
3340
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
3320
3341
|
return parent;
|
|
3321
3342
|
}
|
|
3343
|
+
/**
|
|
3344
|
+
* Resolve (and cache in `sessionTitles`) the OpenCode session TITLE (#310) so the
|
|
3345
|
+
* status PATCH can carry it into the "Live sessions" list. Driver-level cache so
|
|
3346
|
+
* BOTH the watcher completion path and the restart-recovery re-adopt path (which
|
|
3347
|
+
* has no watcher) can use it. `conversationId` is passed only for log context.
|
|
3348
|
+
* Best-effort:
|
|
3349
|
+
* - a resolved NON-EMPTY title is cached and terminal (a real session name
|
|
3350
|
+
* won't later un-name), so we do NOT re-GET `/session/:id` every tick;
|
|
3351
|
+
* - while the title is still absent/empty we do NOT latch it — OpenCode names
|
|
3352
|
+
* sessions asynchronously mid-turn, so an early call (e.g. at `processing`)
|
|
3353
|
+
* must leave the cache unresolved and re-fetch on the next need so a later
|
|
3354
|
+
* call (e.g. at `done`) picks up the name assigned in the meantime. Such a
|
|
3355
|
+
* call returns `null` (omit the title on THIS PATCH) without caching;
|
|
3356
|
+
* - a failed request likewise leaves the cache unresolved (retry next need)
|
|
3357
|
+
* and returns `null` — it must NEVER throw or block completion.
|
|
3358
|
+
* A failure is logged with agent/session context (no silent catch).
|
|
3359
|
+
*/
|
|
3360
|
+
async resolveSessionTitle(sessionId, conversationId) {
|
|
3361
|
+
const cached = this.sessionTitles.get(sessionId);
|
|
3362
|
+
if (cached != null) return cached;
|
|
3363
|
+
try {
|
|
3364
|
+
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
|
|
3365
|
+
if (res.ok) {
|
|
3366
|
+
const body = await res.json();
|
|
3367
|
+
const title = body && typeof body.title === "string" ? body.title.trim() : "";
|
|
3368
|
+
if (title.length > 0) {
|
|
3369
|
+
this.sessionTitles.set(sessionId, title);
|
|
3370
|
+
return title;
|
|
3371
|
+
}
|
|
3372
|
+
return null;
|
|
3373
|
+
}
|
|
3374
|
+
this.log({
|
|
3375
|
+
level: "debug",
|
|
3376
|
+
message: `Session title fetch for session ${sessionId.slice(0, 8)} (agent ${this.agentId.slice(0, 8)}) returned HTTP ${res.status} \u2014 omitting title`,
|
|
3377
|
+
conversation_id: conversationId
|
|
3378
|
+
});
|
|
3379
|
+
} catch (err) {
|
|
3380
|
+
this.log({
|
|
3381
|
+
level: "debug",
|
|
3382
|
+
message: `Best-effort session title fetch failed for session ${sessionId.slice(0, 8)} (agent ${this.agentId.slice(0, 8)}) \u2014 omitting title: ${err instanceof Error ? err.message : String(err)}`,
|
|
3383
|
+
conversation_id: conversationId
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3386
|
+
return null;
|
|
3387
|
+
}
|
|
3322
3388
|
/**
|
|
3323
3389
|
* DEFENSIVE cross-check for the restart-recovery path (WI-2): is any descendant
|
|
3324
3390
|
* (`task` sub-agent) session under `rootSessionId` still genuinely doing work?
|
|
@@ -3362,7 +3428,7 @@ var ChannelDriver = class {
|
|
|
3362
3428
|
const sessions = await listSessions(this.port);
|
|
3363
3429
|
if (!sessions) {
|
|
3364
3430
|
this.log({
|
|
3365
|
-
level: "
|
|
3431
|
+
level: "warn",
|
|
3366
3432
|
message: `Re-adopt: could not enumerate sessions to cross-check descendant liveness for root ${rootSessionId} (listSessions failed) \u2014 treating child liveness as indeterminate`
|
|
3367
3433
|
});
|
|
3368
3434
|
return null;
|
|
@@ -3528,7 +3594,7 @@ var ChannelDriver = class {
|
|
|
3528
3594
|
* A single attempt (no internal retry): the watcher's per-tick loop is the
|
|
3529
3595
|
* retry vehicle for the swap-to-running.
|
|
3530
3596
|
*/
|
|
3531
|
-
async markProcessing(conversationId, messageId, sessionId, opencodeMessageId) {
|
|
3597
|
+
async markProcessing(conversationId, messageId, sessionId, opencodeMessageId, title) {
|
|
3532
3598
|
const res = await this.fetchImpl(
|
|
3533
3599
|
`${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
3534
3600
|
{
|
|
@@ -3537,7 +3603,8 @@ var ChannelDriver = class {
|
|
|
3537
3603
|
body: JSON.stringify({
|
|
3538
3604
|
status: "processing",
|
|
3539
3605
|
opencode_session_id: sessionId,
|
|
3540
|
-
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {}
|
|
3606
|
+
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
3607
|
+
...title ? { title } : {}
|
|
3541
3608
|
})
|
|
3542
3609
|
}
|
|
3543
3610
|
);
|
|
@@ -3576,7 +3643,7 @@ var ChannelDriver = class {
|
|
|
3576
3643
|
* watcher retries next tick within the
|
|
3577
3644
|
* deadline, Finding 4).
|
|
3578
3645
|
*/
|
|
3579
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId) {
|
|
3646
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title) {
|
|
3580
3647
|
const res = await this.fetchImpl(
|
|
3581
3648
|
`${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
3582
3649
|
{
|
|
@@ -3585,7 +3652,8 @@ var ChannelDriver = class {
|
|
|
3585
3652
|
body: JSON.stringify({
|
|
3586
3653
|
status: "done",
|
|
3587
3654
|
opencode_session_id: sessionId,
|
|
3588
|
-
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {}
|
|
3655
|
+
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
3656
|
+
...title ? { title } : {}
|
|
3589
3657
|
})
|
|
3590
3658
|
}
|
|
3591
3659
|
);
|
|
@@ -3646,7 +3714,7 @@ var ChannelDriver = class {
|
|
|
3646
3714
|
);
|
|
3647
3715
|
if (!res.ok) {
|
|
3648
3716
|
this.log({
|
|
3649
|
-
level: "
|
|
3717
|
+
level: "warn",
|
|
3650
3718
|
message: `Signal '${signal}' for message ${messageId.slice(0, 8)} returned HTTP ${res.status} (telemetry-only, ignored)`,
|
|
3651
3719
|
conversation_id: conversationId,
|
|
3652
3720
|
message_id: messageId
|
|
@@ -3656,7 +3724,7 @@ var ChannelDriver = class {
|
|
|
3656
3724
|
return true;
|
|
3657
3725
|
} catch (err) {
|
|
3658
3726
|
this.log({
|
|
3659
|
-
level: "
|
|
3727
|
+
level: "warn",
|
|
3660
3728
|
message: `Signal '${signal}' for message ${messageId.slice(0, 8)} failed (telemetry-only, ignored): ${err instanceof Error ? err.message : String(err)}`,
|
|
3661
3729
|
conversation_id: conversationId,
|
|
3662
3730
|
message_id: messageId
|
|
@@ -4000,23 +4068,53 @@ var MAX_ACTIVITY_LOG_ENTRIES = 10;
|
|
|
4000
4068
|
var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
|
|
4001
4069
|
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
4002
4070
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
|
|
4003
|
-
function
|
|
4071
|
+
function resolveLogLevel(options) {
|
|
4072
|
+
const accepted = Object.keys(LOG_LEVELS);
|
|
4073
|
+
const validate = (value, source) => {
|
|
4074
|
+
const normalized = value.trim().toLowerCase();
|
|
4075
|
+
if (!accepted.includes(normalized)) {
|
|
4076
|
+
throw new Error(
|
|
4077
|
+
`Invalid log level "${value}"${source}; expected one of ${accepted.join(", ")}`
|
|
4078
|
+
);
|
|
4079
|
+
}
|
|
4080
|
+
return normalized;
|
|
4081
|
+
};
|
|
4082
|
+
if (options.logLevel !== void 0) {
|
|
4083
|
+
return validate(options.logLevel, " (--log-level)");
|
|
4084
|
+
}
|
|
4085
|
+
if (options.verbose) {
|
|
4086
|
+
return "debug";
|
|
4087
|
+
}
|
|
4088
|
+
const env = process.env.EVIDENT_LOG_LEVEL;
|
|
4089
|
+
if (env !== void 0 && env !== "") {
|
|
4090
|
+
return validate(env, " (EVIDENT_LOG_LEVEL)");
|
|
4091
|
+
}
|
|
4092
|
+
return "info";
|
|
4093
|
+
}
|
|
4094
|
+
function meetsThreshold(state, level) {
|
|
4095
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[state.logLevel];
|
|
4096
|
+
}
|
|
4097
|
+
function log2(state, message, level = "info") {
|
|
4098
|
+
if (!meetsThreshold(state, level)) return;
|
|
4004
4099
|
if (state.json) {
|
|
4005
4100
|
console.log(
|
|
4006
4101
|
JSON.stringify({
|
|
4007
4102
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4008
|
-
level
|
|
4103
|
+
level,
|
|
4009
4104
|
message
|
|
4010
4105
|
})
|
|
4011
4106
|
);
|
|
4012
4107
|
} else if (!state.interactive) {
|
|
4013
|
-
const prefix =
|
|
4108
|
+
const prefix = level === "error" ? chalk6.red("\u2717") : level === "warn" ? chalk6.yellow("!") : level === "debug" ? chalk6.dim("\xB7") : chalk6.green("\u2022");
|
|
4014
4109
|
console.log(`${prefix} ${message}`);
|
|
4015
4110
|
}
|
|
4016
4111
|
}
|
|
4017
4112
|
function logActivity(state, entry) {
|
|
4113
|
+
const level = entry.level ?? (entry.type === "error" ? "error" : "info");
|
|
4114
|
+
if (!meetsThreshold(state, level)) return;
|
|
4018
4115
|
const fullEntry = {
|
|
4019
4116
|
...entry,
|
|
4117
|
+
level,
|
|
4020
4118
|
timestamp: /* @__PURE__ */ new Date()
|
|
4021
4119
|
};
|
|
4022
4120
|
state.activityLog.push(fullEntry);
|
|
@@ -4025,9 +4123,9 @@ function logActivity(state, entry) {
|
|
|
4025
4123
|
}
|
|
4026
4124
|
if (!state.interactive) {
|
|
4027
4125
|
if (entry.type === "error") {
|
|
4028
|
-
log2(state, entry.error ?? "Unknown error",
|
|
4029
|
-
} else if (entry.
|
|
4030
|
-
log2(state, entry.message);
|
|
4126
|
+
log2(state, entry.error ?? "Unknown error", level);
|
|
4127
|
+
} else if (entry.message) {
|
|
4128
|
+
log2(state, entry.message, level);
|
|
4031
4129
|
}
|
|
4032
4130
|
}
|
|
4033
4131
|
}
|
|
@@ -4226,7 +4324,7 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
4226
4324
|
process.env
|
|
4227
4325
|
);
|
|
4228
4326
|
for (const warning2 of config2.warnings) {
|
|
4229
|
-
logActivity(state, { type: "info", message: `Session cleanup: ${warning2}` });
|
|
4327
|
+
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
4230
4328
|
}
|
|
4231
4329
|
if (!config2.enabled) return;
|
|
4232
4330
|
logActivity(state, {
|
|
@@ -4298,6 +4396,20 @@ async function cleanup(state, opts = {}) {
|
|
|
4298
4396
|
}
|
|
4299
4397
|
async function run(options) {
|
|
4300
4398
|
const interactive = isInteractive(options.json);
|
|
4399
|
+
let logLevel;
|
|
4400
|
+
try {
|
|
4401
|
+
logLevel = resolveLogLevel(options);
|
|
4402
|
+
} catch (error2) {
|
|
4403
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
4404
|
+
if (options.json) {
|
|
4405
|
+
console.log(JSON.stringify({ status: "error", error: message }));
|
|
4406
|
+
} else {
|
|
4407
|
+
printError(message);
|
|
4408
|
+
}
|
|
4409
|
+
await shutdownTelemetry();
|
|
4410
|
+
process.exit(1);
|
|
4411
|
+
return;
|
|
4412
|
+
}
|
|
4301
4413
|
const state = {
|
|
4302
4414
|
agentId: options.agent || "",
|
|
4303
4415
|
agentName: null,
|
|
@@ -4306,6 +4418,7 @@ async function run(options) {
|
|
|
4306
4418
|
idleTimeout: options.idleTimeout ?? null,
|
|
4307
4419
|
json: options.json ?? false,
|
|
4308
4420
|
interactive,
|
|
4421
|
+
logLevel,
|
|
4309
4422
|
connected: false,
|
|
4310
4423
|
opencodeConnected: false,
|
|
4311
4424
|
opencodeVersion: null,
|
|
@@ -4323,8 +4436,8 @@ async function run(options) {
|
|
|
4323
4436
|
if (state.idleTimeout === null && (process.env.GITHUB_ACTIONS || process.env.CI)) {
|
|
4324
4437
|
log2(
|
|
4325
4438
|
state,
|
|
4326
|
-
"
|
|
4327
|
-
|
|
4439
|
+
"No --idle-timeout set in CI environment. The runner will poll indefinitely until the job times out. Consider adding --idle-timeout 30 to avoid wasting runner minutes.",
|
|
4440
|
+
"warn"
|
|
4328
4441
|
);
|
|
4329
4442
|
}
|
|
4330
4443
|
const handleSignal = async () => {
|
|
@@ -4440,9 +4553,9 @@ async function run(options) {
|
|
|
4440
4553
|
ocSpinner?.succeed(`OpenCode running on port ${state.port}${version2}`);
|
|
4441
4554
|
const versionWarning = buildOpenCodeVersionWarning(state.opencodeVersion);
|
|
4442
4555
|
if (versionWarning) {
|
|
4443
|
-
log2(state, versionWarning,
|
|
4556
|
+
log2(state, versionWarning, "warn");
|
|
4444
4557
|
if (state.interactive && !state.json) {
|
|
4445
|
-
logActivity(state, { type: "info", message: versionWarning });
|
|
4558
|
+
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
4446
4559
|
}
|
|
4447
4560
|
}
|
|
4448
4561
|
} catch (error2) {
|
|
@@ -4457,11 +4570,17 @@ async function run(options) {
|
|
|
4457
4570
|
getAuthHeader: () => state.authHeader,
|
|
4458
4571
|
conversationFilter: state.conversationFilter,
|
|
4459
4572
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
4460
|
-
log: (entry) =>
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4573
|
+
log: (entry) => (
|
|
4574
|
+
// Thread the driver's real level straight through so `debug`/`warn`
|
|
4575
|
+
// survive the sink filter (they no longer collapse to info). `type`
|
|
4576
|
+
// stays the coarse error/non-error split the activity log renders with.
|
|
4577
|
+
logActivity(state, {
|
|
4578
|
+
type: entry.level === "error" ? "error" : "info",
|
|
4579
|
+
level: entry.level,
|
|
4580
|
+
message: entry.message,
|
|
4581
|
+
error: entry.level === "error" ? entry.message : void 0
|
|
4582
|
+
})
|
|
4583
|
+
)
|
|
4465
4584
|
});
|
|
4466
4585
|
state.channelDriver = channelDriver;
|
|
4467
4586
|
const connection = new RunnerConnection({
|
|
@@ -4615,7 +4734,10 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
|
|
|
4615
4734
|
program.command("login").description("Authenticate with Evident").option("--token", "Use token-based authentication (for CI/CD)").option("--no-browser", "Do not open the browser automatically").action(login);
|
|
4616
4735
|
program.command("logout").description("Remove stored credentials for the current endpoint").option("--all", "Remove stored credentials for all endpoints").action((options) => logout({ all: options.all }));
|
|
4617
4736
|
program.command("whoami").description("Show the currently logged in user").action(whoami);
|
|
4618
|
-
program.command("run").description("Connect to Evident and process messages").option("-a, --agent [id]", "Agent ID to connect to (optional when EVIDENT_AGENT_KEY is set)").option("-p, --port <port>", "OpenCode port (default: 4096)", "4096").option(
|
|
4737
|
+
program.command("run").description("Connect to Evident and process messages").option("-a, --agent [id]", "Agent ID to connect to (optional when EVIDENT_AGENT_KEY is set)").option("-p, --port <port>", "OpenCode port (default: 4096)", "4096").option(
|
|
4738
|
+
"--log-level <level>",
|
|
4739
|
+
"Log verbosity: debug | info | warn | error (default: info). Env: EVIDENT_LOG_LEVEL"
|
|
4740
|
+
).option("-v, --verbose", "Alias for --log-level debug (ignored if --log-level is set)").option("-c, --conversation <id>", "Process only this specific conversation").option("--idle-timeout <seconds>", "Exit after N seconds idle").option("--json", "Output in JSON format").option(
|
|
4619
4741
|
"--session-cleanup-max-age <duration>",
|
|
4620
4742
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
4621
4743
|
).option(
|
|
@@ -4629,6 +4751,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
4629
4751
|
run({
|
|
4630
4752
|
agent: options.agent,
|
|
4631
4753
|
port: parseInt(options.port, 10),
|
|
4754
|
+
// Raw string — validation/precedence is single-sourced in run.ts's
|
|
4755
|
+
// resolveLogLevel (flag > -v > EVIDENT_LOG_LEVEL > info).
|
|
4756
|
+
logLevel: options.logLevel,
|
|
4632
4757
|
verbose: options.verbose,
|
|
4633
4758
|
conversation: options.conversation,
|
|
4634
4759
|
idleTimeout: options.idleTimeout ? parseInt(options.idleTimeout, 10) : void 0,
|