@evident-ai/cli 3.1.1-dev.45e3647 → 3.1.1-dev.490d9fc
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 +297 -28
- 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)
|
|
@@ -3258,6 +3261,24 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3258
3261
|
* processing list, exactly like `dontRedispatch`/`doneUndeliverable`.
|
|
3259
3262
|
*/
|
|
3260
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();
|
|
3261
3282
|
/**
|
|
3262
3283
|
* "A null-id re-adopt re-dispatch is in flight, awaiting its read-back" (WI-5
|
|
3263
3284
|
* Task 5.4, High-2). Since we no longer send a caller-supplied id, a re-dispatch
|
|
@@ -3597,6 +3618,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3597
3618
|
skippedAlreadyDispatched += 1;
|
|
3598
3619
|
continue;
|
|
3599
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
|
+
}
|
|
3600
3627
|
const options = {
|
|
3601
3628
|
agent: message.opencode_agent ?? void 0,
|
|
3602
3629
|
model: message.opencode_model ?? void 0
|
|
@@ -3686,6 +3713,218 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3686
3713
|
this.ensureWatcherRunning(sessionId);
|
|
3687
3714
|
return dispatched;
|
|
3688
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
|
+
}
|
|
3689
3928
|
/**
|
|
3690
3929
|
* Record that `sessionId` is no longer a valid binding for `conversationId`
|
|
3691
3930
|
* (#553). Keyed by conversation and hard-capped, so it cannot grow with the
|
|
@@ -3969,9 +4208,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3969
4208
|
* opencode reports ACTIVELY `running` is watched to completion (its liveness
|
|
3970
4209
|
* heartbeat keeps the cron off its row), while a re-adopted turn that is paused
|
|
3971
4210
|
* awaiting a human — or queued/unreachable — is still bounded by `deadline` and
|
|
3972
|
-
* handed to the cron.
|
|
3973
|
-
*
|
|
3974
|
-
*
|
|
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`
|
|
3975
4215
|
* (only the appear-guard uses it).
|
|
3976
4216
|
*
|
|
3977
4217
|
* `evidentMessageId` addresses the SERVER row (for markProcessing/markDone);
|
|
@@ -4873,7 +5113,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4873
5113
|
opencode_model: row.opencode_model,
|
|
4874
5114
|
source_message_id: row.source_message_id,
|
|
4875
5115
|
slack_user_id: row.slack_user_id,
|
|
4876
|
-
attachments: row.attachments ?? null
|
|
5116
|
+
attachments: row.attachments ?? null,
|
|
5117
|
+
opencode_message_id: row.opencode_message_id
|
|
4877
5118
|
};
|
|
4878
5119
|
}
|
|
4879
5120
|
/**
|
|
@@ -6125,10 +6366,15 @@ async function handleAuthError(state, error2) {
|
|
|
6125
6366
|
}
|
|
6126
6367
|
async function driveChannels(state, driver) {
|
|
6127
6368
|
let idlePolls = 0;
|
|
6369
|
+
let idleMs = 0;
|
|
6128
6370
|
let consecutiveDrainFailures = 0;
|
|
6371
|
+
let unreachableMs = 0;
|
|
6129
6372
|
let lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
|
|
6130
6373
|
let lastSeenAppliedFiles = driver.fileSyncActivity().appliedFiles;
|
|
6131
6374
|
while (state.running) {
|
|
6375
|
+
const cycleStartedAtMs = performance.now();
|
|
6376
|
+
let idleThisCycle = false;
|
|
6377
|
+
let unreachableThisCycle = false;
|
|
6132
6378
|
if (state.connection?.reconnecting && state.connection.reconnectPromise) {
|
|
6133
6379
|
logActivity(state, { type: "info", message: "Waiting for tunnel reconnection..." });
|
|
6134
6380
|
if (state.interactive) displayStatus(state);
|
|
@@ -6144,17 +6390,22 @@ async function driveChannels(state, driver) {
|
|
|
6144
6390
|
try {
|
|
6145
6391
|
const processed = await driver.drainPending();
|
|
6146
6392
|
consecutiveDrainFailures = 0;
|
|
6393
|
+
unreachableMs = 0;
|
|
6147
6394
|
state.messageCount += processed;
|
|
6148
6395
|
const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
|
|
6149
6396
|
lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
|
|
6150
6397
|
const appliedFiles = driver.fileSyncActivity().appliedFiles;
|
|
6151
|
-
const
|
|
6398
|
+
const filesApplied = appliedFiles !== lastSeenAppliedFiles;
|
|
6399
|
+
const fileActivity = carriedOverFileSync || filesApplied;
|
|
6152
6400
|
lastSeenAppliedFiles = appliedFiles;
|
|
6401
|
+
if (filesApplied) state.claudeUsageRearm?.();
|
|
6153
6402
|
if (processed > 0 || driver.hasInFlightWatchers() || proxiedActivity || fileActivity) {
|
|
6154
6403
|
idlePolls = 0;
|
|
6404
|
+
idleMs = 0;
|
|
6155
6405
|
if (processed > 0 && state.interactive) displayStatus(state);
|
|
6156
6406
|
} else if (state.idleTimeout !== null) {
|
|
6157
6407
|
idlePolls++;
|
|
6408
|
+
idleThisCycle = true;
|
|
6158
6409
|
if (idlePolls === 1) {
|
|
6159
6410
|
logActivity(state, {
|
|
6160
6411
|
type: "info",
|
|
@@ -6180,8 +6431,10 @@ async function driveChannels(state, driver) {
|
|
|
6180
6431
|
if (state.interactive) displayStatus(state);
|
|
6181
6432
|
if (driver.hasInFlightWatchers()) {
|
|
6182
6433
|
consecutiveDrainFailures = 0;
|
|
6434
|
+
unreachableMs = 0;
|
|
6183
6435
|
} else if (state.idleTimeout !== null) {
|
|
6184
6436
|
consecutiveDrainFailures++;
|
|
6437
|
+
unreachableThisCycle = true;
|
|
6185
6438
|
if (consecutiveDrainFailures === 1) {
|
|
6186
6439
|
logActivity(state, {
|
|
6187
6440
|
type: "info",
|
|
@@ -6192,25 +6445,22 @@ async function driveChannels(state, driver) {
|
|
|
6192
6445
|
}
|
|
6193
6446
|
}
|
|
6194
6447
|
await new Promise((resolve3) => setTimeout(resolve3, CHANNEL_POLL_INTERVAL_MS));
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
})
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
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;
|
|
6206
6459
|
}
|
|
6207
|
-
if (state.idleTimeout !== null && idlePolls >= 2) {
|
|
6208
|
-
|
|
6209
|
-
if (
|
|
6210
|
-
|
|
6211
|
-
if (state.interactive) displayStatus(state);
|
|
6212
|
-
break;
|
|
6213
|
-
}
|
|
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;
|
|
6214
6464
|
}
|
|
6215
6465
|
}
|
|
6216
6466
|
}
|
|
@@ -6310,13 +6560,26 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
6310
6560
|
level: "debug",
|
|
6311
6561
|
message: "Claude usage reporting is off (--claude-usage-reporting off)"
|
|
6312
6562
|
});
|
|
6313
|
-
return;
|
|
6563
|
+
return null;
|
|
6314
6564
|
}
|
|
6315
6565
|
let consecutiveFailures = 0;
|
|
6566
|
+
let armed = false;
|
|
6567
|
+
let rearmRequested = false;
|
|
6316
6568
|
const scheduleNextTick = () => {
|
|
6569
|
+
armed = true;
|
|
6570
|
+
rearmRequested = false;
|
|
6317
6571
|
state.claudeUsageTimer = setTimeout(() => void tick(false), nextReportDelayMs());
|
|
6318
6572
|
};
|
|
6319
|
-
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) => {
|
|
6320
6583
|
try {
|
|
6321
6584
|
const usage = await getClaudeUsage();
|
|
6322
6585
|
const result = await reportClaudeUsage(state.agentId, state.authHeader, usage);
|
|
@@ -6352,12 +6615,14 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
6352
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"
|
|
6353
6616
|
});
|
|
6354
6617
|
scheduleNextTick();
|
|
6355
|
-
} else if (
|
|
6618
|
+
} else if (isProbe) {
|
|
6356
6619
|
logActivity(state, {
|
|
6357
6620
|
type: "info",
|
|
6358
6621
|
level: "debug",
|
|
6359
6622
|
message: `Claude usage reporting: ${error2.message}`
|
|
6360
6623
|
});
|
|
6624
|
+
armed = false;
|
|
6625
|
+
if (rearmRequested) rearm();
|
|
6361
6626
|
} else {
|
|
6362
6627
|
logActivity(state, {
|
|
6363
6628
|
type: "info",
|
|
@@ -6378,7 +6643,9 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
6378
6643
|
}
|
|
6379
6644
|
}
|
|
6380
6645
|
};
|
|
6646
|
+
armed = true;
|
|
6381
6647
|
state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);
|
|
6648
|
+
return rearm;
|
|
6382
6649
|
}
|
|
6383
6650
|
async function notifyOffline(state) {
|
|
6384
6651
|
if (!state.agentId || !state.authHeader) return;
|
|
@@ -6419,6 +6686,7 @@ async function cleanup(state, opts = {}) {
|
|
|
6419
6686
|
clearTimeout(state.claudeUsageTimer);
|
|
6420
6687
|
state.claudeUsageTimer = null;
|
|
6421
6688
|
}
|
|
6689
|
+
state.claudeUsageRearm = null;
|
|
6422
6690
|
if (opts.graceful && state.channelDriver) {
|
|
6423
6691
|
state.channelDriver.stop();
|
|
6424
6692
|
log2(state, "Draining in-flight channel work before shutdown...");
|
|
@@ -6500,6 +6768,7 @@ async function run(options) {
|
|
|
6500
6768
|
lastProxiedActivityAt: null,
|
|
6501
6769
|
sessionCleanupTimers: [],
|
|
6502
6770
|
claudeUsageTimer: null,
|
|
6771
|
+
claudeUsageRearm: null,
|
|
6503
6772
|
authHeader: ""
|
|
6504
6773
|
};
|
|
6505
6774
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
@@ -6886,7 +7155,7 @@ async function run(options) {
|
|
|
6886
7155
|
throw error2;
|
|
6887
7156
|
}
|
|
6888
7157
|
scheduleSessionCleanup(state, channelDriver, options);
|
|
6889
|
-
scheduleClaudeUsageReporting(state, options);
|
|
7158
|
+
state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
|
|
6890
7159
|
if (!interactive || state.json) {
|
|
6891
7160
|
log2(state, "Driving channel messages...");
|
|
6892
7161
|
}
|
|
@@ -6971,7 +7240,7 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
6971
7240
|
[]
|
|
6972
7241
|
).option(
|
|
6973
7242
|
"--tunnel-ready-file <path>",
|
|
6974
|
-
"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)"
|
|
6975
7244
|
).action(
|
|
6976
7245
|
(options) => {
|
|
6977
7246
|
run({
|