@evident-ai/cli 3.1.1-dev.2ab3b19 → 3.1.1-dev.2ea09d2
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 +311 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -997,6 +997,9 @@ import { homedir as homedir3 } from "os";
|
|
|
997
997
|
import { isAbsolute as isAbsolute2, join as join3, parse, resolve as resolvePath } from "path";
|
|
998
998
|
import chalk6 from "chalk";
|
|
999
999
|
|
|
1000
|
+
// ../../packages/types/src/agents/index.ts
|
|
1001
|
+
var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
|
|
1002
|
+
|
|
1000
1003
|
// ../../packages/types/src/telemetry/index.ts
|
|
1001
1004
|
var TelemetryEventTypes = {
|
|
1002
1005
|
// Agent activity events (shown in web UI activity log)
|
|
@@ -2688,6 +2691,10 @@ function nextReportDelayMs(random = Math.random) {
|
|
|
2688
2691
|
return BASE_REPORT_DELAY_MS - jitterRangeMs + random() * (2 * jitterRangeMs);
|
|
2689
2692
|
}
|
|
2690
2693
|
var FIRST_REPORT_DELAY_MS = 5e3 + Math.random() * 1e4;
|
|
2694
|
+
var CLAUDE_USAGE_FAILURE_REESCALATION_TICKS = 6;
|
|
2695
|
+
function claudeUsageFailureLogLevel(consecutiveFailures) {
|
|
2696
|
+
return consecutiveFailures === 1 || consecutiveFailures % CLAUDE_USAGE_FAILURE_REESCALATION_TICKS === 0 ? "warn" : "debug";
|
|
2697
|
+
}
|
|
2691
2698
|
|
|
2692
2699
|
// src/lib/channels/driver.ts
|
|
2693
2700
|
import { homedir as homedir2 } from "os";
|
|
@@ -3254,6 +3261,24 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3254
3261
|
* processing list, exactly like `dontRedispatch`/`doneUndeliverable`.
|
|
3255
3262
|
*/
|
|
3256
3263
|
readoptPollUnresolvedSignalled = /* @__PURE__ */ new Set();
|
|
3264
|
+
/**
|
|
3265
|
+
* "Already emitted `redrive_unresolved` for this row" (#965). Mirrors
|
|
3266
|
+
* `readoptPollUnresolvedSignalled`: `resolveRedrive`'s `unresolved` leaf recurs
|
|
3267
|
+
* every ~2s drain until opencode's status becomes readable, but the
|
|
3268
|
+
* server-visible signal is an OUTCOME, so it fires at most once per row. Cleared
|
|
3269
|
+
* on any non-`unresolved` outcome so the set cannot grow beyond the currently
|
|
3270
|
+
* unresolvable rows.
|
|
3271
|
+
*/
|
|
3272
|
+
redriveUnresolvedSignalled = /* @__PURE__ */ new Set();
|
|
3273
|
+
/**
|
|
3274
|
+
* First `now()` a `pending` row's re-drive was observed `unresolved` (#965). A
|
|
3275
|
+
* `pending` row is invisible to every cron arm (all require `status =
|
|
3276
|
+
* 'processing'`), so an indefinitely-`unresolved` row would be stranded with
|
|
3277
|
+
* nothing driving it. Once `now - since >= pausedMaxWaitMs`, `resolveRedrive`
|
|
3278
|
+
* takes `dispatch` instead of `unresolved` (reusing the existing knob — see
|
|
3279
|
+
* ADR-0047's own "unreachable ⇒ bounded" rule). Cleared on any other outcome.
|
|
3280
|
+
*/
|
|
3281
|
+
redriveUnresolvedSince = /* @__PURE__ */ new Map();
|
|
3257
3282
|
/**
|
|
3258
3283
|
* "A null-id re-adopt re-dispatch is in flight, awaiting its read-back" (WI-5
|
|
3259
3284
|
* Task 5.4, High-2). Since we no longer send a caller-supplied id, a re-dispatch
|
|
@@ -3593,6 +3618,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3593
3618
|
skippedAlreadyDispatched += 1;
|
|
3594
3619
|
continue;
|
|
3595
3620
|
}
|
|
3621
|
+
if (message.opencode_message_id) {
|
|
3622
|
+
const outcome = await this.resolveRedrive(conv, sessionId, message, refusedSessionId);
|
|
3623
|
+
if (outcome !== "dispatch") {
|
|
3624
|
+
break;
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3596
3627
|
const options = {
|
|
3597
3628
|
agent: message.opencode_agent ?? void 0,
|
|
3598
3629
|
model: message.opencode_model ?? void 0
|
|
@@ -3682,6 +3713,218 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3682
3713
|
this.ensureWatcherRunning(sessionId);
|
|
3683
3714
|
return dispatched;
|
|
3684
3715
|
}
|
|
3716
|
+
/**
|
|
3717
|
+
* Poll a session's message list for the re-drive fence (#965), via the
|
|
3718
|
+
* INJECTED `fetchImpl` — NOT the imported `getSessionMessages` helper, which
|
|
3719
|
+
* hits the global `fetch` and would bypass the same override every other
|
|
3720
|
+
* opencode poll in this file respects. Mirrors `readoptProcessing`'s own
|
|
3721
|
+
* snapshot fetch (`:3081-3111`). `null` = unreadable (non-OK response,
|
|
3722
|
+
* non-array body, or a network exception) — treated as "can't observe",
|
|
3723
|
+
* never as "confirmed gone".
|
|
3724
|
+
*/
|
|
3725
|
+
async pollSessionMessagesForRedrive(conv, message, sessionId) {
|
|
3726
|
+
try {
|
|
3727
|
+
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
3728
|
+
if (!res.ok) {
|
|
3729
|
+
this.log({
|
|
3730
|
+
level: "warn",
|
|
3731
|
+
message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned HTTP ${res.status} \u2014 treating as unreadable this tick`,
|
|
3732
|
+
conversation_id: conv.id,
|
|
3733
|
+
message_id: message.id
|
|
3734
|
+
});
|
|
3735
|
+
return null;
|
|
3736
|
+
}
|
|
3737
|
+
const body = await res.json();
|
|
3738
|
+
if (!Array.isArray(body)) {
|
|
3739
|
+
this.log({
|
|
3740
|
+
level: "warn",
|
|
3741
|
+
message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned a non-array message body \u2014 treating as unreadable this tick`,
|
|
3742
|
+
conversation_id: conv.id,
|
|
3743
|
+
message_id: message.id
|
|
3744
|
+
});
|
|
3745
|
+
return null;
|
|
3746
|
+
}
|
|
3747
|
+
return body;
|
|
3748
|
+
} catch (err) {
|
|
3749
|
+
this.log({
|
|
3750
|
+
level: "warn",
|
|
3751
|
+
message: `Re-drive: failed to poll session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
3752
|
+
conversation_id: conv.id,
|
|
3753
|
+
message_id: message.id
|
|
3754
|
+
});
|
|
3755
|
+
return null;
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
/**
|
|
3759
|
+
* The re-drive fence for a `pending` row that already carries a stored
|
|
3760
|
+
* `opencode_message_id` (#965) — i.e. it has already been handed to opencode at
|
|
3761
|
+
* least once (see the invariant at `QueuedMessage.opencode_message_id`'s doc).
|
|
3762
|
+
* The lifecycle cron can falsely reclaim a `processing` row back to `pending`
|
|
3763
|
+
* mid-turn (a 5-minute liveness-staleness check racing a still-running turn);
|
|
3764
|
+
* without this fence the drain loop would re-`prompt_async` the SAME turn a
|
|
3765
|
+
* second time against live GitHub state. Mirrors `readoptOne`'s job for the
|
|
3766
|
+
* `processing` re-adopt path, but simpler: no b1/b2 preamble cross-check is
|
|
3767
|
+
* needed here because `refusedSessionId` already handles the one case
|
|
3768
|
+
* (#553 abandoned session) that path exists for.
|
|
3769
|
+
*
|
|
3770
|
+
* Only `ChannelAuthError` propagates; every other failure resolves to
|
|
3771
|
+
* `unresolved` and is retried whole on the next ~2s drain tick.
|
|
3772
|
+
*/
|
|
3773
|
+
async resolveRedrive(conv, sessionId, message, refusedSessionId) {
|
|
3774
|
+
const ocId = message.opencode_message_id ?? null;
|
|
3775
|
+
if (refusedSessionId) {
|
|
3776
|
+
this.clearRedriveUnresolved(message.id);
|
|
3777
|
+
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
3778
|
+
return "dispatch";
|
|
3779
|
+
}
|
|
3780
|
+
const messages = await this.pollSessionMessagesForRedrive(conv, message, sessionId);
|
|
3781
|
+
if (messages == null || messages.length === 0) {
|
|
3782
|
+
return this.resolveRedriveUnresolved(conv, message);
|
|
3783
|
+
}
|
|
3784
|
+
const state = messageRunState(messages, ocId ?? "");
|
|
3785
|
+
if (state === "done" || state === "failed") {
|
|
3786
|
+
return this.settleRedrive(conv, sessionId, message, ocId, messages, state);
|
|
3787
|
+
}
|
|
3788
|
+
if (state === "running" || state === "queued") {
|
|
3789
|
+
const ongoing = await isSessionOngoing(this.port, sessionId);
|
|
3790
|
+
if (ongoing === true) {
|
|
3791
|
+
return this.reattachRedrive(conv, sessionId, message, ocId);
|
|
3792
|
+
}
|
|
3793
|
+
if (ongoing === false) {
|
|
3794
|
+
this.clearRedriveUnresolved(message.id);
|
|
3795
|
+
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
3796
|
+
return "dispatch";
|
|
3797
|
+
}
|
|
3798
|
+
return this.resolveRedriveUnresolved(conv, message);
|
|
3799
|
+
}
|
|
3800
|
+
this.clearRedriveUnresolved(message.id);
|
|
3801
|
+
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
3802
|
+
return "dispatch";
|
|
3803
|
+
}
|
|
3804
|
+
/**
|
|
3805
|
+
* The `reattached` outcome (Task 3.3): the prior turn is STILL ONGOING per
|
|
3806
|
+
* opencode's own status map — undo the false reclaim instead of starting a
|
|
3807
|
+
* second turn.
|
|
3808
|
+
*/
|
|
3809
|
+
async reattachRedrive(conv, sessionId, message, ocId) {
|
|
3810
|
+
let anchorMs;
|
|
3811
|
+
const parsed = message.processing_started_at ? Date.parse(message.processing_started_at) : NaN;
|
|
3812
|
+
if (!Number.isNaN(parsed)) {
|
|
3813
|
+
anchorMs = parsed;
|
|
3814
|
+
} else {
|
|
3815
|
+
anchorMs = this.now();
|
|
3816
|
+
this.log({
|
|
3817
|
+
level: "error",
|
|
3818
|
+
message: `Re-drive: message ${message.id.slice(0, 8)} has null/unparseable processing_started_at (${String(message.processing_started_at)}) \u2014 anchoring the watcher's absolute-age ceiling to now (defensive)`,
|
|
3819
|
+
conversation_id: conv.id,
|
|
3820
|
+
message_id: message.id
|
|
3821
|
+
});
|
|
3822
|
+
}
|
|
3823
|
+
const title = await this.resolveSessionTitle(sessionId, conv.id);
|
|
3824
|
+
try {
|
|
3825
|
+
await this.markProcessing(conv.id, message.id, sessionId, ocId, title);
|
|
3826
|
+
} catch (err) {
|
|
3827
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
3828
|
+
this.log({
|
|
3829
|
+
level: "warn",
|
|
3830
|
+
message: `Re-drive: failed to restore message ${message.id.slice(0, 8)} to processing (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
3831
|
+
conversation_id: conv.id,
|
|
3832
|
+
message_id: message.id
|
|
3833
|
+
});
|
|
3834
|
+
return "unresolved";
|
|
3835
|
+
}
|
|
3836
|
+
this.clearRedriveUnresolved(message.id);
|
|
3837
|
+
this.registerReadopted(conv, sessionId, message, ocId ?? "", anchorMs);
|
|
3838
|
+
this.dispatched.add(message.id);
|
|
3839
|
+
this.readopted.add(message.id);
|
|
3840
|
+
this.ensureWatcherRunning(sessionId);
|
|
3841
|
+
const watchedForMs = this.now() - anchorMs;
|
|
3842
|
+
void this.postSignal(conv.id, message.id, "redrive_reattached", {
|
|
3843
|
+
watched_for_ms: watchedForMs
|
|
3844
|
+
});
|
|
3845
|
+
this.log({
|
|
3846
|
+
level: "warn",
|
|
3847
|
+
message: `Re-drive: message ${message.id.slice(0, 8)} (session ${sessionId.slice(0, 8)}) was wrongly reclaimed to pending while its turn was still running (watched ${watchedForMs}ms) \u2014 restored to processing instead of re-dispatching`,
|
|
3848
|
+
conversation_id: conv.id,
|
|
3849
|
+
message_id: message.id
|
|
3850
|
+
});
|
|
3851
|
+
return "reattached";
|
|
3852
|
+
}
|
|
3853
|
+
/**
|
|
3854
|
+
* The `settled` outcome (Task 3.2): the prior turn already finished (or
|
|
3855
|
+
* errored) while nobody was watching — deliver/report it instead of re-running.
|
|
3856
|
+
* Mirrors `readoptOne`'s `done`/`failed` branches' error discipline, simplified
|
|
3857
|
+
* (no `doneUndeliverable` park: a terminal PATCH failure here just retries next
|
|
3858
|
+
* drain, same as any other non-auth failure).
|
|
3859
|
+
*/
|
|
3860
|
+
async settleRedrive(conv, sessionId, message, ocId, messages, state) {
|
|
3861
|
+
try {
|
|
3862
|
+
if (state === "done") {
|
|
3863
|
+
const title = await this.resolveSessionTitle(sessionId, conv.id);
|
|
3864
|
+
const usage = messageUsage(messages, ocId ?? "");
|
|
3865
|
+
this.log({
|
|
3866
|
+
level: "info",
|
|
3867
|
+
message: `Re-drive: message ${message.id.slice(0, 8)} completed while its row was wrongly reclaimed to pending \u2014 marking done instead of re-dispatching`,
|
|
3868
|
+
conversation_id: conv.id,
|
|
3869
|
+
message_id: message.id
|
|
3870
|
+
});
|
|
3871
|
+
await this.markDone(conv.id, message.id, sessionId, ocId, title, usage);
|
|
3872
|
+
} else {
|
|
3873
|
+
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
3874
|
+
const usage = messageUsage(messages, ocId ?? "");
|
|
3875
|
+
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
3876
|
+
this.log({
|
|
3877
|
+
level: "error",
|
|
3878
|
+
message: `Re-drive: message ${message.id.slice(0, 8)} errored while its row was wrongly reclaimed to pending \u2014 marking failed instead of re-dispatching: ${error2 ?? "(no error text)"}`,
|
|
3879
|
+
conversation_id: conv.id,
|
|
3880
|
+
message_id: message.id
|
|
3881
|
+
});
|
|
3882
|
+
await this.markFailed(conv.id, message.id, sessionId, error2, usage, failure);
|
|
3883
|
+
}
|
|
3884
|
+
} catch (err) {
|
|
3885
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
3886
|
+
this.log({
|
|
3887
|
+
level: "warn",
|
|
3888
|
+
message: `Re-drive: failed to report message ${message.id.slice(0, 8)} ${state} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
3889
|
+
conversation_id: conv.id,
|
|
3890
|
+
message_id: message.id
|
|
3891
|
+
});
|
|
3892
|
+
return "unresolved";
|
|
3893
|
+
}
|
|
3894
|
+
this.clearRedriveUnresolved(message.id);
|
|
3895
|
+
void this.postSignal(conv.id, message.id, "redrive_settled");
|
|
3896
|
+
return "settled";
|
|
3897
|
+
}
|
|
3898
|
+
/**
|
|
3899
|
+
* The bounded `unresolved` outcome (Task 3.4): opencode's state could not be
|
|
3900
|
+
* observed (snapshot unreadable/empty, or `isSessionOngoing` returned `null`).
|
|
3901
|
+
* A `pending` row is invisible to every cron arm (all require `status =
|
|
3902
|
+
* 'processing'`), so an indefinitely-`unresolved` row would be stranded with
|
|
3903
|
+
* nothing driving it — bound it to the existing `pausedMaxWaitMs` window
|
|
3904
|
+
* (reusing the knob, not a new constant) and take `dispatch` once elapsed.
|
|
3905
|
+
*/
|
|
3906
|
+
resolveRedriveUnresolved(conv, message) {
|
|
3907
|
+
const now = this.now();
|
|
3908
|
+
const since = this.redriveUnresolvedSince.get(message.id);
|
|
3909
|
+
if (since !== void 0 && now - since >= this.pausedMaxWaitMs) {
|
|
3910
|
+
this.clearRedriveUnresolved(message.id);
|
|
3911
|
+
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
3912
|
+
return "dispatch";
|
|
3913
|
+
}
|
|
3914
|
+
if (since === void 0) {
|
|
3915
|
+
this.redriveUnresolvedSince.set(message.id, now);
|
|
3916
|
+
}
|
|
3917
|
+
if (!this.redriveUnresolvedSignalled.has(message.id)) {
|
|
3918
|
+
this.redriveUnresolvedSignalled.add(message.id);
|
|
3919
|
+
void this.postSignal(conv.id, message.id, "redrive_unresolved");
|
|
3920
|
+
}
|
|
3921
|
+
return "unresolved";
|
|
3922
|
+
}
|
|
3923
|
+
/** Clear both `unresolved`-bound trackers for a row (any non-`unresolved` outcome). */
|
|
3924
|
+
clearRedriveUnresolved(messageId) {
|
|
3925
|
+
this.redriveUnresolvedSince.delete(messageId);
|
|
3926
|
+
this.redriveUnresolvedSignalled.delete(messageId);
|
|
3927
|
+
}
|
|
3685
3928
|
/**
|
|
3686
3929
|
* Record that `sessionId` is no longer a valid binding for `conversationId`
|
|
3687
3930
|
* (#553). Keyed by conversation and hard-capped, so it cannot grow with the
|
|
@@ -3965,9 +4208,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3965
4208
|
* opencode reports ACTIVELY `running` is watched to completion (its liveness
|
|
3966
4209
|
* heartbeat keeps the cron off its row), while a re-adopted turn that is paused
|
|
3967
4210
|
* awaiting a human — or queued/unreachable — is still bounded by `deadline` and
|
|
3968
|
-
* handed to the cron.
|
|
3969
|
-
*
|
|
3970
|
-
*
|
|
4211
|
+
* handed to the cron. Real invariant (#965): the cron MAY reclaim a row this
|
|
4212
|
+
* runner still holds; a reclaimed row that already ran is never re-dispatched
|
|
4213
|
+
* while opencode reports its turn ongoing (readopt's own gate here, and the
|
|
4214
|
+
* `pending`-row re-drive fence, `resolveRedrive`). `dispatchedAt` stays `now`
|
|
3971
4215
|
* (only the appear-guard uses it).
|
|
3972
4216
|
*
|
|
3973
4217
|
* `evidentMessageId` addresses the SERVER row (for markProcessing/markDone);
|
|
@@ -4869,7 +5113,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4869
5113
|
opencode_model: row.opencode_model,
|
|
4870
5114
|
source_message_id: row.source_message_id,
|
|
4871
5115
|
slack_user_id: row.slack_user_id,
|
|
4872
|
-
attachments: row.attachments ?? null
|
|
5116
|
+
attachments: row.attachments ?? null,
|
|
5117
|
+
opencode_message_id: row.opencode_message_id
|
|
4873
5118
|
};
|
|
4874
5119
|
}
|
|
4875
5120
|
/**
|
|
@@ -6121,10 +6366,15 @@ async function handleAuthError(state, error2) {
|
|
|
6121
6366
|
}
|
|
6122
6367
|
async function driveChannels(state, driver) {
|
|
6123
6368
|
let idlePolls = 0;
|
|
6369
|
+
let idleMs = 0;
|
|
6124
6370
|
let consecutiveDrainFailures = 0;
|
|
6371
|
+
let unreachableMs = 0;
|
|
6125
6372
|
let lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
|
|
6126
6373
|
let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
|
|
6127
6374
|
while (state.running) {
|
|
6375
|
+
const cycleStartedAtMs = performance.now();
|
|
6376
|
+
let idleThisCycle = false;
|
|
6377
|
+
let unreachableThisCycle = false;
|
|
6128
6378
|
if (state.connection?.reconnecting && state.connection.reconnectPromise) {
|
|
6129
6379
|
logActivity(state, { type: "info", message: "Waiting for tunnel reconnection..." });
|
|
6130
6380
|
if (state.interactive) displayStatus(state);
|
|
@@ -6140,17 +6390,22 @@ async function driveChannels(state, driver) {
|
|
|
6140
6390
|
try {
|
|
6141
6391
|
const processed = await driver.drainPending();
|
|
6142
6392
|
consecutiveDrainFailures = 0;
|
|
6393
|
+
unreachableMs = 0;
|
|
6143
6394
|
state.messageCount += processed;
|
|
6144
6395
|
const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
|
|
6145
6396
|
lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
|
|
6146
6397
|
const appliedFiles = driver.fileSyncActivity().appliedFiles;
|
|
6147
|
-
const
|
|
6398
|
+
const filesApplied = appliedFiles !== lastSeenAppliedFiles;
|
|
6399
|
+
const fileActivity = carriedOverFileSync || filesApplied;
|
|
6148
6400
|
lastSeenAppliedFiles = appliedFiles;
|
|
6401
|
+
if (filesApplied) state.claudeUsageRearm?.();
|
|
6149
6402
|
if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
|
|
6150
6403
|
idlePolls = 0;
|
|
6404
|
+
idleMs = 0;
|
|
6151
6405
|
if (processed > 0 && state.interactive) displayStatus(state);
|
|
6152
6406
|
} else if (state.idleTimeout !== null) {
|
|
6153
6407
|
idlePolls++;
|
|
6408
|
+
idleThisCycle = true;
|
|
6154
6409
|
if (idlePolls === 1) {
|
|
6155
6410
|
logActivity(state, {
|
|
6156
6411
|
type: "info",
|
|
@@ -6176,8 +6431,10 @@ async function driveChannels(state, driver) {
|
|
|
6176
6431
|
if (state.interactive) displayStatus(state);
|
|
6177
6432
|
if (driver.hasInFlightWatchers()) {
|
|
6178
6433
|
consecutiveDrainFailures = 0;
|
|
6434
|
+
unreachableMs = 0;
|
|
6179
6435
|
} else if (state.idleTimeout !== null) {
|
|
6180
6436
|
consecutiveDrainFailures++;
|
|
6437
|
+
unreachableThisCycle = true;
|
|
6181
6438
|
if (consecutiveDrainFailures === 1) {
|
|
6182
6439
|
logActivity(state, {
|
|
6183
6440
|
type: "info",
|
|
@@ -6188,25 +6445,22 @@ async function driveChannels(state, driver) {
|
|
|
6188
6445
|
}
|
|
6189
6446
|
}
|
|
6190
6447
|
await new Promise((resolve3) => setTimeout(resolve3, CHANNEL_POLL_INTERVAL_MS));
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
})
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6448
|
+
const cycleMs = performance.now() - cycleStartedAtMs;
|
|
6449
|
+
if (idleThisCycle) idleMs += cycleMs;
|
|
6450
|
+
if (unreachableThisCycle) unreachableMs += cycleMs;
|
|
6451
|
+
if (state.idleTimeout !== null && consecutiveDrainFailures >= 2 && unreachableMs > state.idleTimeout * 1e3) {
|
|
6452
|
+
logActivity(state, {
|
|
6453
|
+
type: "info",
|
|
6454
|
+
level: "warn",
|
|
6455
|
+
message: `Exiting: could not reach Evident for ${consecutiveDrainFailures} consecutive polls (${Math.round(unreachableMs / 1e3)}s)`
|
|
6456
|
+
});
|
|
6457
|
+
if (state.interactive) displayStatus(state);
|
|
6458
|
+
break;
|
|
6202
6459
|
}
|
|
6203
|
-
if (state.idleTimeout !== null && idlePolls >= 2) {
|
|
6204
|
-
|
|
6205
|
-
if (
|
|
6206
|
-
|
|
6207
|
-
if (state.interactive) displayStatus(state);
|
|
6208
|
-
break;
|
|
6209
|
-
}
|
|
6460
|
+
if (state.idleTimeout !== null && idlePolls >= 2 && idleMs > state.idleTimeout * 1e3) {
|
|
6461
|
+
logActivity(state, { type: "info", message: "Idle timeout reached" });
|
|
6462
|
+
if (state.interactive) displayStatus(state);
|
|
6463
|
+
break;
|
|
6210
6464
|
}
|
|
6211
6465
|
}
|
|
6212
6466
|
}
|
|
@@ -6285,6 +6539,9 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
6285
6539
|
);
|
|
6286
6540
|
state.sessionCleanupTimers.push(interval, firstSweep);
|
|
6287
6541
|
}
|
|
6542
|
+
function claudeUsageFailureStreakSuffix(consecutiveFailures) {
|
|
6543
|
+
return consecutiveFailures > 1 ? ` (${consecutiveFailures} consecutive failures)` : "";
|
|
6544
|
+
}
|
|
6288
6545
|
function scheduleClaudeUsageReporting(state, options) {
|
|
6289
6546
|
const { mode, warnings } = resolveClaudeUsageReportingMode(
|
|
6290
6547
|
options.claudeUsageReporting,
|
|
@@ -6303,13 +6560,26 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
6303
6560
|
level: "debug",
|
|
6304
6561
|
message: "Claude usage reporting is off (--claude-usage-reporting off)"
|
|
6305
6562
|
});
|
|
6306
|
-
return;
|
|
6563
|
+
return null;
|
|
6307
6564
|
}
|
|
6308
6565
|
let consecutiveFailures = 0;
|
|
6566
|
+
let armed = false;
|
|
6567
|
+
let rearmRequested = false;
|
|
6309
6568
|
const scheduleNextTick = () => {
|
|
6569
|
+
armed = true;
|
|
6570
|
+
rearmRequested = false;
|
|
6310
6571
|
state.claudeUsageTimer = setTimeout(() => void tick(false), nextReportDelayMs());
|
|
6311
6572
|
};
|
|
6312
|
-
const
|
|
6573
|
+
const rearm = () => {
|
|
6574
|
+
if (armed) {
|
|
6575
|
+
rearmRequested = true;
|
|
6576
|
+
return;
|
|
6577
|
+
}
|
|
6578
|
+
rearmRequested = false;
|
|
6579
|
+
armed = true;
|
|
6580
|
+
state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);
|
|
6581
|
+
};
|
|
6582
|
+
const tick = async (isProbe) => {
|
|
6313
6583
|
try {
|
|
6314
6584
|
const usage = await getClaudeUsage();
|
|
6315
6585
|
const result = await reportClaudeUsage(state.agentId, state.authHeader, usage);
|
|
@@ -6331,8 +6601,8 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
6331
6601
|
consecutiveFailures++;
|
|
6332
6602
|
logActivity(state, {
|
|
6333
6603
|
type: "info",
|
|
6334
|
-
level: consecutiveFailures
|
|
6335
|
-
message: `Failed to report Claude usage: ${result.error}`
|
|
6604
|
+
level: claudeUsageFailureLogLevel(consecutiveFailures),
|
|
6605
|
+
message: `Failed to report Claude usage: ${result.error}${claudeUsageFailureStreakSuffix(consecutiveFailures)}`
|
|
6336
6606
|
});
|
|
6337
6607
|
}
|
|
6338
6608
|
scheduleNextTick();
|
|
@@ -6345,12 +6615,14 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
6345
6615
|
message: "Claude usage reporting is forced on but no usable Claude Code login was found \u2014 run `claude` to sign in; reporting will keep retrying"
|
|
6346
6616
|
});
|
|
6347
6617
|
scheduleNextTick();
|
|
6348
|
-
} else if (
|
|
6618
|
+
} else if (isProbe) {
|
|
6349
6619
|
logActivity(state, {
|
|
6350
6620
|
type: "info",
|
|
6351
6621
|
level: "debug",
|
|
6352
6622
|
message: `Claude usage reporting: ${error2.message}`
|
|
6353
6623
|
});
|
|
6624
|
+
armed = false;
|
|
6625
|
+
if (rearmRequested) rearm();
|
|
6354
6626
|
} else {
|
|
6355
6627
|
logActivity(state, {
|
|
6356
6628
|
type: "info",
|
|
@@ -6364,14 +6636,16 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
6364
6636
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
6365
6637
|
logActivity(state, {
|
|
6366
6638
|
type: "info",
|
|
6367
|
-
level: consecutiveFailures
|
|
6368
|
-
message: `Claude usage reporting failed: ${message}`
|
|
6639
|
+
level: claudeUsageFailureLogLevel(consecutiveFailures),
|
|
6640
|
+
message: `Claude usage reporting failed: ${message}${claudeUsageFailureStreakSuffix(consecutiveFailures)}`
|
|
6369
6641
|
});
|
|
6370
6642
|
scheduleNextTick();
|
|
6371
6643
|
}
|
|
6372
6644
|
}
|
|
6373
6645
|
};
|
|
6646
|
+
armed = true;
|
|
6374
6647
|
state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);
|
|
6648
|
+
return rearm;
|
|
6375
6649
|
}
|
|
6376
6650
|
async function notifyOffline(state) {
|
|
6377
6651
|
if (!state.agentId || !state.authHeader) return;
|
|
@@ -6412,6 +6686,7 @@ async function cleanup(state, opts = {}) {
|
|
|
6412
6686
|
clearTimeout(state.claudeUsageTimer);
|
|
6413
6687
|
state.claudeUsageTimer = null;
|
|
6414
6688
|
}
|
|
6689
|
+
state.claudeUsageRearm = null;
|
|
6415
6690
|
if (opts.graceful && state.channelDriver) {
|
|
6416
6691
|
state.channelDriver.stop();
|
|
6417
6692
|
log2(state, "Draining in-flight channel work before shutdown...");
|
|
@@ -6493,6 +6768,7 @@ async function run(options) {
|
|
|
6493
6768
|
lastProxiedActivityAt: null,
|
|
6494
6769
|
sessionCleanupTimers: [],
|
|
6495
6770
|
claudeUsageTimer: null,
|
|
6771
|
+
claudeUsageRearm: null,
|
|
6496
6772
|
authHeader: ""
|
|
6497
6773
|
};
|
|
6498
6774
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
@@ -6572,6 +6848,7 @@ async function run(options) {
|
|
|
6572
6848
|
console.log(chalk6.dim("Or run `evident login` for interactive authentication"));
|
|
6573
6849
|
blank();
|
|
6574
6850
|
process.exit(1);
|
|
6851
|
+
return;
|
|
6575
6852
|
}
|
|
6576
6853
|
blank();
|
|
6577
6854
|
console.log(chalk6.yellow("You are not logged in to Evident."));
|
|
@@ -6616,6 +6893,7 @@ async function run(options) {
|
|
|
6616
6893
|
} else {
|
|
6617
6894
|
printError(resolved.error || "Failed to resolve runner ID from key");
|
|
6618
6895
|
process.exit(1);
|
|
6896
|
+
return;
|
|
6619
6897
|
}
|
|
6620
6898
|
} else {
|
|
6621
6899
|
printError(
|
|
@@ -6629,6 +6907,7 @@ async function run(options) {
|
|
|
6629
6907
|
);
|
|
6630
6908
|
blank();
|
|
6631
6909
|
process.exit(1);
|
|
6910
|
+
return;
|
|
6632
6911
|
}
|
|
6633
6912
|
}
|
|
6634
6913
|
telemetry.info(
|
|
@@ -6876,7 +7155,7 @@ async function run(options) {
|
|
|
6876
7155
|
throw error2;
|
|
6877
7156
|
}
|
|
6878
7157
|
scheduleSessionCleanup(state, channelDriver, options);
|
|
6879
|
-
scheduleClaudeUsageReporting(state, options);
|
|
7158
|
+
state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
|
|
6880
7159
|
if (!interactive || state.json) {
|
|
6881
7160
|
log2(state, "Driving channel messages...");
|
|
6882
7161
|
}
|
|
@@ -6961,7 +7240,7 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
6961
7240
|
[]
|
|
6962
7241
|
).option(
|
|
6963
7242
|
"--tunnel-ready-file <path>",
|
|
6964
|
-
"Path to write once the tunnel is connected (
|
|
7243
|
+
"Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
|
|
6965
7244
|
).action(
|
|
6966
7245
|
(options) => {
|
|
6967
7246
|
run({
|