@evident-ai/cli 3.0.1-dev.fbf2c4d → 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 CHANGED
@@ -86,7 +86,9 @@ Options:
86
86
  - `-a, --agent [id]` — Agent ID to connect to. Optional when `EVIDENT_AGENT_KEY`
87
87
  is set (the agent is then resolved automatically from the key).
88
88
  - `-p, --port <port>` — OpenCode port (default: `4096`).
89
- - `-v, --verbose`Show detailed request/response information.
89
+ - `--log-level <level>`Log verbosity: `debug | info | warn | error` (default:
90
+ `info`). Env: `EVIDENT_LOG_LEVEL`.
91
+ - `-v, --verbose` — Alias for `--log-level debug` (an explicit `--log-level` wins).
90
92
  - `-c, --conversation <id>` — Process only this specific conversation.
91
93
  - `--idle-timeout <seconds>` — Exit after N seconds with no pending work (useful
92
94
  in CI to avoid polling indefinitely).
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,
@@ -2193,7 +2199,7 @@ var ChannelDriver = class {
2193
2199
  if (await sessionExists(this.port, sessionId) === false) {
2194
2200
  this.sessions.delete(conv.id);
2195
2201
  this.log({
2196
- level: "info",
2202
+ level: "warn",
2197
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.`,
2198
2204
  conversation_id: conv.id,
2199
2205
  message_id: message.id
@@ -2212,7 +2218,7 @@ var ChannelDriver = class {
2212
2218
  }
2213
2219
  if (opencodeMessageId === null) {
2214
2220
  this.log({
2215
- level: "error",
2221
+ level: "warn",
2216
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`,
2217
2223
  conversation_id: conv.id,
2218
2224
  message_id: message.id
@@ -2226,7 +2232,7 @@ var ChannelDriver = class {
2226
2232
  }
2227
2233
  if (messages.length > 0 && dispatched === 0 && skippedAlreadyDispatched === messages.length) {
2228
2234
  this.log({
2229
- level: "error",
2235
+ level: "warn",
2230
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).`,
2231
2237
  conversation_id: conv.id
2232
2238
  });
@@ -2240,7 +2246,7 @@ var ChannelDriver = class {
2240
2246
  const exists = await sessionExists(this.port, bound);
2241
2247
  if (exists === false) {
2242
2248
  this.log({
2243
- level: "info",
2249
+ level: "debug",
2244
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.`,
2245
2251
  conversation_id: conv.id
2246
2252
  });
@@ -2275,7 +2281,7 @@ var ChannelDriver = class {
2275
2281
  this.opencodeDirectory = await getOpenCodeDirectory(this.port);
2276
2282
  if (!this.opencodeDirectory) {
2277
2283
  this.log({
2278
- level: "info",
2284
+ level: "warn",
2279
2285
  message: "Could not determine opencode directory (GET /path) \u2014 new sessions may not appear in opencode web"
2280
2286
  });
2281
2287
  }
@@ -2553,7 +2559,7 @@ var ChannelDriver = class {
2553
2559
  } catch (err) {
2554
2560
  if (err instanceof ChannelAuthError) throw err;
2555
2561
  this.log({
2556
- level: "error",
2562
+ level: "warn",
2557
2563
  message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} processing (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
2558
2564
  conversation_id: conv.id,
2559
2565
  message_id: inFlight.evidentMessageId
@@ -2563,7 +2569,7 @@ var ChannelDriver = class {
2563
2569
  inFlight.started = true;
2564
2570
  if (!claimed) {
2565
2571
  this.log({
2566
- level: "info",
2572
+ level: "debug",
2567
2573
  message: `Message ${inFlight.evidentMessageId.slice(0, 8)} already marked processing \u2014 continuing`,
2568
2574
  conversation_id: conv.id,
2569
2575
  message_id: inFlight.evidentMessageId
@@ -2592,7 +2598,7 @@ var ChannelDriver = class {
2592
2598
  if (err instanceof ChannelAuthError) throw err;
2593
2599
  if (err instanceof ChannelTerminalError) {
2594
2600
  this.log({
2595
- level: "error",
2601
+ level: "warn",
2596
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}`,
2597
2603
  conversation_id: conv.id,
2598
2604
  message_id: inFlight.evidentMessageId
@@ -2602,7 +2608,7 @@ var ChannelDriver = class {
2602
2608
  }
2603
2609
  if (this.now() >= inFlight.deadline) {
2604
2610
  this.log({
2605
- level: "error",
2611
+ level: "warn",
2606
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)}`,
2607
2613
  conversation_id: conv.id,
2608
2614
  message_id: inFlight.evidentMessageId
@@ -2611,7 +2617,7 @@ var ChannelDriver = class {
2611
2617
  return;
2612
2618
  }
2613
2619
  this.log({
2614
- level: "error",
2620
+ level: "warn",
2615
2621
  message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
2616
2622
  conversation_id: conv.id,
2617
2623
  message_id: inFlight.evidentMessageId
@@ -2639,7 +2645,7 @@ var ChannelDriver = class {
2639
2645
  if (err instanceof ChannelAuthError) throw err;
2640
2646
  if (err instanceof ChannelTerminalError) {
2641
2647
  this.log({
2642
- level: "error",
2648
+ level: "warn",
2643
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}`,
2644
2650
  conversation_id: conv.id,
2645
2651
  message_id: inFlight.evidentMessageId
@@ -2649,7 +2655,7 @@ var ChannelDriver = class {
2649
2655
  }
2650
2656
  if (this.now() >= inFlight.deadline) {
2651
2657
  this.log({
2652
- level: "error",
2658
+ level: "warn",
2653
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)}`,
2654
2660
  conversation_id: conv.id,
2655
2661
  message_id: inFlight.evidentMessageId
@@ -2658,7 +2664,7 @@ var ChannelDriver = class {
2658
2664
  return;
2659
2665
  }
2660
2666
  this.log({
2661
- level: "error",
2667
+ level: "warn",
2662
2668
  message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
2663
2669
  conversation_id: conv.id,
2664
2670
  message_id: inFlight.evidentMessageId
@@ -2681,7 +2687,7 @@ var ChannelDriver = class {
2681
2687
  const activelyRunning = state === "running" && !awaitingHuman;
2682
2688
  if (activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
2683
2689
  this.log({
2684
- level: "error",
2690
+ level: "warn",
2685
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`,
2686
2692
  conversation_id: conv.id,
2687
2693
  message_id: inFlight.evidentMessageId
@@ -2724,7 +2730,7 @@ var ChannelDriver = class {
2724
2730
  const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
2725
2731
  if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
2726
2732
  this.log({
2727
- level: "info",
2733
+ level: "debug",
2728
2734
  message: `Message ${inFlight.evidentMessageId.slice(0, 8)} did not complete within the watch window \u2014 leaving for the cron safety net`,
2729
2735
  conversation_id: conv.id,
2730
2736
  message_id: inFlight.evidentMessageId
@@ -2767,7 +2773,7 @@ var ChannelDriver = class {
2767
2773
  this.readoptPollUnresolvedSignalled.delete(id);
2768
2774
  if (cleared || clearedUndeliverable) {
2769
2775
  this.log({
2770
- level: "info",
2776
+ level: "debug",
2771
2777
  message: `Re-adopt: message ${id.slice(0, 8)} left the processing list (cron reset) \u2014 cleared gave-up marker`,
2772
2778
  message_id: id
2773
2779
  });
@@ -2780,7 +2786,7 @@ var ChannelDriver = class {
2780
2786
  for (const row of rows) {
2781
2787
  if (!row.opencode_session_id) {
2782
2788
  this.log({
2783
- level: "error",
2789
+ level: "warn",
2784
2790
  message: `Cannot re-adopt processing message ${row.id.slice(0, 8)} \u2014 no opencode session id; leaving for the cron safety net`,
2785
2791
  conversation_id: row.conversation_id,
2786
2792
  message_id: row.id
@@ -2797,7 +2803,7 @@ var ChannelDriver = class {
2797
2803
  const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
2798
2804
  if (!res.ok) {
2799
2805
  this.log({
2800
- level: "error",
2806
+ level: "warn",
2801
2807
  message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 skipping this session this tick`
2802
2808
  });
2803
2809
  continue;
@@ -2805,7 +2811,7 @@ var ChannelDriver = class {
2805
2811
  const body = await res.json();
2806
2812
  if (!Array.isArray(body)) {
2807
2813
  this.log({
2808
- level: "error",
2814
+ level: "warn",
2809
2815
  message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned a non-array message body \u2014 skipping this session this tick`
2810
2816
  });
2811
2817
  continue;
@@ -2813,7 +2819,7 @@ var ChannelDriver = class {
2813
2819
  messages = body;
2814
2820
  } catch (err) {
2815
2821
  this.log({
2816
- level: "error",
2822
+ level: "warn",
2817
2823
  message: `Re-adopt: failed to poll session ${sessionId.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`
2818
2824
  });
2819
2825
  continue;
@@ -2846,7 +2852,7 @@ var ChannelDriver = class {
2846
2852
  async readoptOne(sessionId, row, messages, sessionOngoing) {
2847
2853
  if (this.isTracked(sessionId, row.id)) {
2848
2854
  this.log({
2849
- level: "info",
2855
+ level: "debug",
2850
2856
  message: `Re-adopt: message ${row.id.slice(0, 8)} already tracked in-flight \u2014 skipping (owned by the watcher loop)`,
2851
2857
  conversation_id: row.conversation_id,
2852
2858
  message_id: row.id
@@ -2858,7 +2864,7 @@ var ChannelDriver = class {
2858
2864
  if (state === "done") {
2859
2865
  if (this.doneUndeliverable.has(row.id)) {
2860
2866
  this.log({
2861
- level: "info",
2867
+ level: "debug",
2862
2868
  message: `Re-adopt: message ${row.id.slice(0, 8)} markDone is terminally undeliverable \u2014 left to the cron; skipping until it leaves processing`,
2863
2869
  conversation_id: row.conversation_id,
2864
2870
  message_id: row.id
@@ -2879,7 +2885,7 @@ var ChannelDriver = class {
2879
2885
  if (err instanceof ChannelTerminalError) {
2880
2886
  this.doneUndeliverable.add(row.id);
2881
2887
  this.log({
2882
- level: "error",
2888
+ level: "warn",
2883
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}`,
2884
2890
  conversation_id: row.conversation_id,
2885
2891
  message_id: row.id
@@ -2888,7 +2894,7 @@ var ChannelDriver = class {
2888
2894
  return;
2889
2895
  }
2890
2896
  this.log({
2891
- level: "error",
2897
+ level: "warn",
2892
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)}`,
2893
2899
  conversation_id: row.conversation_id,
2894
2900
  message_id: row.id
@@ -2914,7 +2920,7 @@ var ChannelDriver = class {
2914
2920
  if (err instanceof ChannelTerminalError) {
2915
2921
  this.doneUndeliverable.add(row.id);
2916
2922
  this.log({
2917
- level: "error",
2923
+ level: "warn",
2918
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}`,
2919
2925
  conversation_id: row.conversation_id,
2920
2926
  message_id: row.id
@@ -2923,7 +2929,7 @@ var ChannelDriver = class {
2923
2929
  return;
2924
2930
  }
2925
2931
  this.log({
2926
- level: "error",
2932
+ level: "warn",
2927
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)}`,
2928
2934
  conversation_id: row.conversation_id,
2929
2935
  message_id: row.id
@@ -2936,7 +2942,7 @@ var ChannelDriver = class {
2936
2942
  }
2937
2943
  if (this.dontRedispatch.has(row.id)) {
2938
2944
  this.log({
2939
- level: "info",
2945
+ level: "debug",
2940
2946
  message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
2941
2947
  conversation_id: row.conversation_id,
2942
2948
  message_id: row.id
@@ -2961,7 +2967,7 @@ var ChannelDriver = class {
2961
2967
  }
2962
2968
  if (ongoing === true) {
2963
2969
  this.log({
2964
- level: "info",
2970
+ level: "debug",
2965
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)`,
2966
2972
  conversation_id: row.conversation_id,
2967
2973
  message_id: row.id
@@ -2969,7 +2975,7 @@ var ChannelDriver = class {
2969
2975
  } else {
2970
2976
  if (shape === "b1") {
2971
2977
  this.log({
2972
- level: "info",
2978
+ level: "debug",
2973
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`,
2974
2980
  conversation_id: row.conversation_id,
2975
2981
  message_id: row.id
@@ -2981,7 +2987,7 @@ var ChannelDriver = class {
2981
2987
  return;
2982
2988
  }
2983
2989
  this.log({
2984
- level: "info",
2990
+ level: "debug",
2985
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`,
2986
2992
  conversation_id: row.conversation_id,
2987
2993
  message_id: row.id
@@ -2992,7 +2998,7 @@ var ChannelDriver = class {
2992
2998
  const descendantAlive = await this.isAnyDescendantSessionAlive(sessionId);
2993
2999
  if (descendantAlive === true) {
2994
3000
  this.log({
2995
- level: "info",
3001
+ level: "debug",
2996
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)`,
2997
3003
  conversation_id: row.conversation_id,
2998
3004
  message_id: row.id
@@ -3016,7 +3022,7 @@ var ChannelDriver = class {
3016
3022
  this.readopted.add(row.id);
3017
3023
  this.ensureWatcherRunning(sessionId);
3018
3024
  this.log({
3019
- level: "info",
3025
+ level: "debug",
3020
3026
  message: `Re-adopt: message ${row.id.slice(0, 8)} ${state} \u2014 re-attached watcher (stored id, no re-dispatch)`,
3021
3027
  conversation_id: row.conversation_id,
3022
3028
  message_id: row.id
@@ -3050,7 +3056,7 @@ var ChannelDriver = class {
3050
3056
  async forceReadoptRun(sessionId, row) {
3051
3057
  if (this.stopped) {
3052
3058
  this.log({
3053
- level: "info",
3059
+ level: "debug",
3054
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`,
3055
3061
  conversation_id: row.conversation_id,
3056
3062
  message_id: row.id
@@ -3059,7 +3065,7 @@ var ChannelDriver = class {
3059
3065
  }
3060
3066
  if (this.awaitingReadopt.has(row.id)) {
3061
3067
  this.log({
3062
- level: "info",
3068
+ level: "debug",
3063
3069
  message: `Re-adopt: message ${row.id.slice(0, 8)} already has a re-dispatch awaiting read-back \u2014 skipping (at most once)`,
3064
3070
  conversation_id: row.conversation_id,
3065
3071
  message_id: row.id
@@ -3069,7 +3075,7 @@ var ChannelDriver = class {
3069
3075
  if (this.processedAtMs(row) + this.pausedMaxWaitMs <= this.now()) {
3070
3076
  this.dontRedispatch.add(row.id);
3071
3077
  this.log({
3072
- level: "info",
3078
+ level: "debug",
3073
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)`,
3074
3080
  conversation_id: row.conversation_id,
3075
3081
  message_id: row.id
@@ -3098,7 +3104,7 @@ var ChannelDriver = class {
3098
3104
  this.awaitingReadopt.delete(row.id);
3099
3105
  if (err instanceof ChannelAuthError) throw err;
3100
3106
  this.log({
3101
- level: "error",
3107
+ level: "warn",
3102
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)}`,
3103
3109
  conversation_id: row.conversation_id,
3104
3110
  message_id: row.id
@@ -3109,7 +3115,7 @@ var ChannelDriver = class {
3109
3115
  if (ocId === null) {
3110
3116
  this.awaitingReadopt.delete(row.id);
3111
3117
  this.log({
3112
- level: "error",
3118
+ level: "warn",
3113
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`,
3114
3120
  conversation_id: row.conversation_id,
3115
3121
  message_id: row.id
@@ -3194,7 +3200,7 @@ var ChannelDriver = class {
3194
3200
  if (this.readopted.delete(evidentMessageId) && inFlight && !inFlight.done) {
3195
3201
  this.dontRedispatch.add(evidentMessageId);
3196
3202
  this.log({
3197
- level: "info",
3203
+ level: "debug",
3198
3204
  message: `Re-adopt: message ${evidentMessageId.slice(0, 8)} gave up \u2014 parking until it leaves the processing list (cron reset)`,
3199
3205
  conversation_id: watcher.conv.id,
3200
3206
  message_id: evidentMessageId
@@ -3366,13 +3372,13 @@ var ChannelDriver = class {
3366
3372
  return null;
3367
3373
  }
3368
3374
  this.log({
3369
- level: "info",
3375
+ level: "debug",
3370
3376
  message: `Session title fetch for session ${sessionId.slice(0, 8)} (agent ${this.agentId.slice(0, 8)}) returned HTTP ${res.status} \u2014 omitting title`,
3371
3377
  conversation_id: conversationId
3372
3378
  });
3373
3379
  } catch (err) {
3374
3380
  this.log({
3375
- level: "info",
3381
+ level: "debug",
3376
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)}`,
3377
3383
  conversation_id: conversationId
3378
3384
  });
@@ -3422,7 +3428,7 @@ var ChannelDriver = class {
3422
3428
  const sessions = await listSessions(this.port);
3423
3429
  if (!sessions) {
3424
3430
  this.log({
3425
- level: "error",
3431
+ level: "warn",
3426
3432
  message: `Re-adopt: could not enumerate sessions to cross-check descendant liveness for root ${rootSessionId} (listSessions failed) \u2014 treating child liveness as indeterminate`
3427
3433
  });
3428
3434
  return null;
@@ -3708,7 +3714,7 @@ var ChannelDriver = class {
3708
3714
  );
3709
3715
  if (!res.ok) {
3710
3716
  this.log({
3711
- level: "error",
3717
+ level: "warn",
3712
3718
  message: `Signal '${signal}' for message ${messageId.slice(0, 8)} returned HTTP ${res.status} (telemetry-only, ignored)`,
3713
3719
  conversation_id: conversationId,
3714
3720
  message_id: messageId
@@ -3718,7 +3724,7 @@ var ChannelDriver = class {
3718
3724
  return true;
3719
3725
  } catch (err) {
3720
3726
  this.log({
3721
- level: "error",
3727
+ level: "warn",
3722
3728
  message: `Signal '${signal}' for message ${messageId.slice(0, 8)} failed (telemetry-only, ignored): ${err instanceof Error ? err.message : String(err)}`,
3723
3729
  conversation_id: conversationId,
3724
3730
  message_id: messageId
@@ -4062,23 +4068,53 @@ var MAX_ACTIVITY_LOG_ENTRIES = 10;
4062
4068
  var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
4063
4069
  var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
4064
4070
  var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
4065
- function log2(state, message, isError = false) {
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;
4066
4099
  if (state.json) {
4067
4100
  console.log(
4068
4101
  JSON.stringify({
4069
4102
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4070
- level: isError ? "error" : "info",
4103
+ level,
4071
4104
  message
4072
4105
  })
4073
4106
  );
4074
4107
  } else if (!state.interactive) {
4075
- const prefix = isError ? chalk6.red("\u2717") : chalk6.green("\u2022");
4108
+ const prefix = level === "error" ? chalk6.red("\u2717") : level === "warn" ? chalk6.yellow("!") : level === "debug" ? chalk6.dim("\xB7") : chalk6.green("\u2022");
4076
4109
  console.log(`${prefix} ${message}`);
4077
4110
  }
4078
4111
  }
4079
4112
  function logActivity(state, entry) {
4113
+ const level = entry.level ?? (entry.type === "error" ? "error" : "info");
4114
+ if (!meetsThreshold(state, level)) return;
4080
4115
  const fullEntry = {
4081
4116
  ...entry,
4117
+ level,
4082
4118
  timestamp: /* @__PURE__ */ new Date()
4083
4119
  };
4084
4120
  state.activityLog.push(fullEntry);
@@ -4087,9 +4123,9 @@ function logActivity(state, entry) {
4087
4123
  }
4088
4124
  if (!state.interactive) {
4089
4125
  if (entry.type === "error") {
4090
- log2(state, entry.error ?? "Unknown error", true);
4091
- } else if (entry.type === "info" && entry.message) {
4092
- log2(state, entry.message);
4126
+ log2(state, entry.error ?? "Unknown error", level);
4127
+ } else if (entry.message) {
4128
+ log2(state, entry.message, level);
4093
4129
  }
4094
4130
  }
4095
4131
  }
@@ -4288,7 +4324,7 @@ function scheduleSessionCleanup(state, driver, options) {
4288
4324
  process.env
4289
4325
  );
4290
4326
  for (const warning2 of config2.warnings) {
4291
- logActivity(state, { type: "info", message: `Session cleanup: ${warning2}` });
4327
+ logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
4292
4328
  }
4293
4329
  if (!config2.enabled) return;
4294
4330
  logActivity(state, {
@@ -4360,6 +4396,20 @@ async function cleanup(state, opts = {}) {
4360
4396
  }
4361
4397
  async function run(options) {
4362
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
+ }
4363
4413
  const state = {
4364
4414
  agentId: options.agent || "",
4365
4415
  agentName: null,
@@ -4368,6 +4418,7 @@ async function run(options) {
4368
4418
  idleTimeout: options.idleTimeout ?? null,
4369
4419
  json: options.json ?? false,
4370
4420
  interactive,
4421
+ logLevel,
4371
4422
  connected: false,
4372
4423
  opencodeConnected: false,
4373
4424
  opencodeVersion: null,
@@ -4385,8 +4436,8 @@ async function run(options) {
4385
4436
  if (state.idleTimeout === null && (process.env.GITHUB_ACTIONS || process.env.CI)) {
4386
4437
  log2(
4387
4438
  state,
4388
- "Warning: 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.",
4389
- false
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"
4390
4441
  );
4391
4442
  }
4392
4443
  const handleSignal = async () => {
@@ -4502,9 +4553,9 @@ async function run(options) {
4502
4553
  ocSpinner?.succeed(`OpenCode running on port ${state.port}${version2}`);
4503
4554
  const versionWarning = buildOpenCodeVersionWarning(state.opencodeVersion);
4504
4555
  if (versionWarning) {
4505
- log2(state, versionWarning, false);
4556
+ log2(state, versionWarning, "warn");
4506
4557
  if (state.interactive && !state.json) {
4507
- logActivity(state, { type: "info", message: versionWarning });
4558
+ logActivity(state, { type: "info", level: "warn", message: versionWarning });
4508
4559
  }
4509
4560
  }
4510
4561
  } catch (error2) {
@@ -4519,11 +4570,17 @@ async function run(options) {
4519
4570
  getAuthHeader: () => state.authHeader,
4520
4571
  conversationFilter: state.conversationFilter,
4521
4572
  stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
4522
- log: (entry) => logActivity(state, {
4523
- type: entry.level === "error" ? "error" : "info",
4524
- message: entry.message,
4525
- error: entry.level === "error" ? entry.message : void 0
4526
- })
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
+ )
4527
4584
  });
4528
4585
  state.channelDriver = channelDriver;
4529
4586
  const connection = new RunnerConnection({
@@ -4677,7 +4734,10 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
4677
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);
4678
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 }));
4679
4736
  program.command("whoami").description("Show the currently logged in user").action(whoami);
4680
- 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("-v, --verbose", "Show detailed request/response information").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(
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(
4681
4741
  "--session-cleanup-max-age <duration>",
4682
4742
  "Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
4683
4743
  ).option(
@@ -4691,6 +4751,9 @@ program.command("run").description("Connect to Evident and process messages").op
4691
4751
  run({
4692
4752
  agent: options.agent,
4693
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,
4694
4757
  verbose: options.verbose,
4695
4758
  conversation: options.conversation,
4696
4759
  idleTimeout: options.idleTimeout ? parseInt(options.idleTimeout, 10) : void 0,