@evident-ai/cli 3.3.1-dev.9eed666 → 3.3.1-dev.a4bb60b
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 +380 -58
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1700,13 +1700,22 @@ function buildNoProviderWarning(hasProvider) {
|
|
|
1700
1700
|
return "Warning: opencode has no authenticated model provider configured, so it won't be able to answer prompts. Run `opencode auth login` to set one up (see https://opencode.ai for details).";
|
|
1701
1701
|
}
|
|
1702
1702
|
|
|
1703
|
+
// src/lib/http-timeout.ts
|
|
1704
|
+
var REQUEST_TIMEOUT_MS = 6e4;
|
|
1705
|
+
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
1706
|
+
return ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1703
1709
|
// src/lib/opencode/session.ts
|
|
1710
|
+
function timedFetch(input, init) {
|
|
1711
|
+
return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
|
|
1712
|
+
}
|
|
1704
1713
|
function opencodeBase(port) {
|
|
1705
1714
|
return `http://127.0.0.1:${port}`;
|
|
1706
1715
|
}
|
|
1707
1716
|
async function getOpenCodeDirectory(port) {
|
|
1708
1717
|
try {
|
|
1709
|
-
const res = await
|
|
1718
|
+
const res = await timedFetch(`${opencodeBase(port)}/path`);
|
|
1710
1719
|
if (!res.ok) return null;
|
|
1711
1720
|
const body = await res.json();
|
|
1712
1721
|
const dir = typeof body.directory === "string" && body.directory || typeof body.worktree === "string" && body.worktree || typeof body.path?.cwd === "string" && body.path.cwd || typeof body.path?.directory === "string" && body.path.directory || null;
|
|
@@ -1757,7 +1766,7 @@ function isAssistantInFlight(m) {
|
|
|
1757
1766
|
}
|
|
1758
1767
|
async function getSessionMessages(port, sessionId) {
|
|
1759
1768
|
try {
|
|
1760
|
-
const res = await
|
|
1769
|
+
const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/message`);
|
|
1761
1770
|
if (!res.ok) return null;
|
|
1762
1771
|
const body = await res.json();
|
|
1763
1772
|
return Array.isArray(body) ? body : null;
|
|
@@ -1787,7 +1796,7 @@ function sessionLastActivityMs(session) {
|
|
|
1787
1796
|
}
|
|
1788
1797
|
async function listSessions(port) {
|
|
1789
1798
|
try {
|
|
1790
|
-
const res = await
|
|
1799
|
+
const res = await timedFetch(`${opencodeBase(port)}/session`);
|
|
1791
1800
|
if (!res.ok) return null;
|
|
1792
1801
|
const body = await res.json();
|
|
1793
1802
|
return Array.isArray(body) ? body : null;
|
|
@@ -1797,7 +1806,7 @@ async function listSessions(port) {
|
|
|
1797
1806
|
}
|
|
1798
1807
|
async function deleteSession(port, id) {
|
|
1799
1808
|
try {
|
|
1800
|
-
const res = await
|
|
1809
|
+
const res = await timedFetch(`${opencodeBase(port)}/session/${id}`, { method: "DELETE" });
|
|
1801
1810
|
return res.status >= 200 && res.status < 300;
|
|
1802
1811
|
} catch {
|
|
1803
1812
|
return false;
|
|
@@ -1805,7 +1814,7 @@ async function deleteSession(port, id) {
|
|
|
1805
1814
|
}
|
|
1806
1815
|
async function sessionExists(port, id) {
|
|
1807
1816
|
try {
|
|
1808
|
-
const res = await
|
|
1817
|
+
const res = await timedFetch(`${opencodeBase(port)}/session/${id}`);
|
|
1809
1818
|
if (res.status >= 200 && res.status < 300) return true;
|
|
1810
1819
|
if (res.status === 404) return false;
|
|
1811
1820
|
return null;
|
|
@@ -1815,7 +1824,7 @@ async function sessionExists(port, id) {
|
|
|
1815
1824
|
}
|
|
1816
1825
|
async function getSessionStatuses(port) {
|
|
1817
1826
|
try {
|
|
1818
|
-
const res = await
|
|
1827
|
+
const res = await timedFetch(`${opencodeBase(port)}/session/status`);
|
|
1819
1828
|
if (!res.ok) {
|
|
1820
1829
|
console.error(
|
|
1821
1830
|
`[getSessionStatuses] GET /session/status returned HTTP ${res.status} (port ${port})`
|
|
@@ -1848,7 +1857,7 @@ async function createOpenCodeSession(port, directory) {
|
|
|
1848
1857
|
if (directory && directory.trim()) {
|
|
1849
1858
|
url.searchParams.set("directory", directory.trim());
|
|
1850
1859
|
}
|
|
1851
|
-
const response = await
|
|
1860
|
+
const response = await timedFetch(url, {
|
|
1852
1861
|
method: "POST",
|
|
1853
1862
|
headers: { "Content-Type": "application/json" },
|
|
1854
1863
|
body: JSON.stringify({})
|
|
@@ -1862,7 +1871,7 @@ async function createOpenCodeSession(port, directory) {
|
|
|
1862
1871
|
}
|
|
1863
1872
|
async function getModelAttachmentCapability(port, model) {
|
|
1864
1873
|
try {
|
|
1865
|
-
const res = await
|
|
1874
|
+
const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
|
|
1866
1875
|
if (!res.ok) {
|
|
1867
1876
|
console.error(
|
|
1868
1877
|
`[getModelAttachmentCapability] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
@@ -1995,7 +2004,7 @@ async function sendPromptAsync(port, sessionId, content, options, attachments) {
|
|
|
1995
2004
|
};
|
|
1996
2005
|
}
|
|
1997
2006
|
}
|
|
1998
|
-
const res = await
|
|
2007
|
+
const res = await timedFetch(`${opencodeBase(port)}/session/${sessionId}/prompt_async`, {
|
|
1999
2008
|
method: "POST",
|
|
2000
2009
|
headers: { "Content-Type": "application/json" },
|
|
2001
2010
|
body: JSON.stringify(body)
|
|
@@ -2244,7 +2253,7 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
|
2244
2253
|
}
|
|
2245
2254
|
async function hasAnyConfiguredProvider(port) {
|
|
2246
2255
|
try {
|
|
2247
|
-
const res = await
|
|
2256
|
+
const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
|
|
2248
2257
|
if (!res.ok) {
|
|
2249
2258
|
console.error(
|
|
2250
2259
|
`[hasAnyConfiguredProvider] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
@@ -3413,8 +3422,13 @@ var B2_ABANDONMENT_MIN_PINNED_MS = 3 * 6e4;
|
|
|
3413
3422
|
var AMBIGUOUS_FINISH_MAX_PINNED_MS = 3 * 6e4;
|
|
3414
3423
|
var B2_ABANDONMENT_RECHECK_MS = HEARTBEAT_MS;
|
|
3415
3424
|
var POLL_MISS_GRACE_MS = HEARTBEAT_MS;
|
|
3425
|
+
var WATCHER_STALL_MS = 3 * POLL_MISS_GRACE_MS;
|
|
3426
|
+
var MAX_WATCHER_STALL_RESTARTS = 3;
|
|
3427
|
+
var MAX_RELEASED_OPENCODE_IDS = 256;
|
|
3416
3428
|
var MAX_SUPERSEDED_CONVERSATIONS = 256;
|
|
3417
3429
|
var MAX_IDENTICAL_REDRIVE_POLL_FAILURES = 5;
|
|
3430
|
+
var WEDGE_WARNING_INTERVAL_MS = 5 * 60 * 1e3;
|
|
3431
|
+
var MAX_WEDGED_CONVERSATIONS = 256;
|
|
3418
3432
|
var ChannelAuthError = class extends Error {
|
|
3419
3433
|
constructor(message) {
|
|
3420
3434
|
super(message);
|
|
@@ -3458,6 +3472,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3458
3472
|
fileSyncDirectories;
|
|
3459
3473
|
homeDir;
|
|
3460
3474
|
maxActiveSessions;
|
|
3475
|
+
watcherStallMs;
|
|
3476
|
+
wedgeWarningIntervalMs;
|
|
3461
3477
|
/** Cache of conversationId → opencode sessionId. */
|
|
3462
3478
|
sessions = /* @__PURE__ */ new Map();
|
|
3463
3479
|
/**
|
|
@@ -3488,6 +3504,40 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3488
3504
|
* bounded cost.
|
|
3489
3505
|
*/
|
|
3490
3506
|
supersededSessions = /* @__PURE__ */ new Map();
|
|
3507
|
+
/**
|
|
3508
|
+
* Local re-drive fence for a message force-released by the stall watchdog
|
|
3509
|
+
* (#1618, `reconcileWatchers`'s `unrecoverable_released` arm — the only writer,
|
|
3510
|
+
* see `recordReleasedOpencodeId`). The row's server-side `opencode_message_id`
|
|
3511
|
+
* is `null` for exactly this shape (its `markProcessing` never landed), so
|
|
3512
|
+
* without a local record of the id the driver last knew, the next drain's
|
|
3513
|
+
* `if (message.opencode_message_id)` re-drive-fence check at
|
|
3514
|
+
* `processConversation` would not engage and it would blind-`prompt_async`
|
|
3515
|
+
* a turn that may still be running in opencode — the one duplicate-turn
|
|
3516
|
+
* hazard this whole design exists to close (§3/D1 of the drain-wedge plan).
|
|
3517
|
+
* `processConversation` reads `message.opencode_message_id ?? this
|
|
3518
|
+
* .releasedOpencodeIds.get(id)?.opencodeMessageId` as the EFFECTIVE id and
|
|
3519
|
+
* threads it into `resolveRedrive`, which asks opencode itself whether the
|
|
3520
|
+
* turn is still ongoing before ever dispatching.
|
|
3521
|
+
*
|
|
3522
|
+
* Bounded FIFO, mirroring `supersededSessions` above (`MAX_RELEASED_OPENCODE_IDS`,
|
|
3523
|
+
* `recordReleasedOpencodeId`). Cleared by `clearRedriveUnresolved` (every
|
|
3524
|
+
* non-`unresolved` `resolveRedrive` outcome fires it, including a fresh
|
|
3525
|
+
* dispatch) and at the top-level fresh-dispatch site, so it does not outlive
|
|
3526
|
+
* the row it was recorded for.
|
|
3527
|
+
*/
|
|
3528
|
+
releasedOpencodeIds = /* @__PURE__ */ new Map();
|
|
3529
|
+
/**
|
|
3530
|
+
* Per-conversation throttle state for the #183 recurrence warning (#1618
|
|
3531
|
+
* WI-4) — see `reportWedgedConversation`'s doc comment for why this exists.
|
|
3532
|
+
* `firstWedgedAt` anchors `stuck_for_ms`; `lastWarnedAt` throttles both the
|
|
3533
|
+
* log line and the `dispatch_wedged` signal to at most once per
|
|
3534
|
+
* `wedgeWarningIntervalMs`; `consecutiveTicks` is reported in the log text
|
|
3535
|
+
* so the operator sees magnitude, not repetition. Cleared the moment the
|
|
3536
|
+
* conversation dispatches anything (a fresh wedge, if it recurs, is a new
|
|
3537
|
+
* incident). Bounded FIFO, mirroring `supersededSessions`
|
|
3538
|
+
* (`MAX_WEDGED_CONVERSATIONS`).
|
|
3539
|
+
*/
|
|
3540
|
+
wedgeWarnings = /* @__PURE__ */ new Map();
|
|
3491
3541
|
/**
|
|
3492
3542
|
* Per-opencode-session dispatch lock (Task 2.1a). `sendPromptAsync` is no
|
|
3493
3543
|
* longer idempotent (no caller-supplied `messageID`), and its read-back picks
|
|
@@ -3730,7 +3780,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3730
3780
|
this.retry = { ...DEFAULT_RETRY_POLICY, ...config.retry };
|
|
3731
3781
|
this.log = config.log ?? (() => {
|
|
3732
3782
|
});
|
|
3733
|
-
this.fetchImpl =
|
|
3783
|
+
this.fetchImpl = withRequestTimeout(
|
|
3784
|
+
config.fetchImpl ?? fetch,
|
|
3785
|
+
config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
|
|
3786
|
+
);
|
|
3734
3787
|
this.sleep = config.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
3735
3788
|
this.pausedPollIntervalMs = config.pausedPollIntervalMs ?? DEFAULT_PAUSED_POLL_INTERVAL_MS;
|
|
3736
3789
|
this.pausedMaxWaitMs = config.pausedMaxWaitMs ?? DEFAULT_PAUSED_MAX_WAIT_MS;
|
|
@@ -3739,6 +3792,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3739
3792
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
3740
3793
|
this.homeDir = config.homeDir ?? homedir2();
|
|
3741
3794
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
3795
|
+
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
3796
|
+
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
3742
3797
|
}
|
|
3743
3798
|
/** The IPv4-loopback base URL for the local `opencode serve`. */
|
|
3744
3799
|
get opencodeBase() {
|
|
@@ -3752,6 +3807,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3752
3807
|
* @returns the number of messages NEWLY dispatched to opencode's native queue.
|
|
3753
3808
|
*/
|
|
3754
3809
|
async drainPending() {
|
|
3810
|
+
try {
|
|
3811
|
+
this.reconcileWatchers();
|
|
3812
|
+
} catch (err) {
|
|
3813
|
+
this.log({
|
|
3814
|
+
level: "error",
|
|
3815
|
+
message: `Watchdog: reconcileWatchers threw unexpectedly (drain continues): ${err instanceof Error ? err.message : String(err)}`
|
|
3816
|
+
});
|
|
3817
|
+
}
|
|
3755
3818
|
if (this.stopped) return 0;
|
|
3756
3819
|
if (this.draining) return 0;
|
|
3757
3820
|
this.draining = true;
|
|
@@ -4005,8 +4068,15 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4005
4068
|
skippedAlreadyDispatched += 1;
|
|
4006
4069
|
continue;
|
|
4007
4070
|
}
|
|
4008
|
-
|
|
4009
|
-
|
|
4071
|
+
const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
|
|
4072
|
+
if (effectiveOpencodeMessageId) {
|
|
4073
|
+
const outcome = await this.resolveRedrive(
|
|
4074
|
+
conv,
|
|
4075
|
+
sessionId,
|
|
4076
|
+
message,
|
|
4077
|
+
sessionCreated,
|
|
4078
|
+
effectiveOpencodeMessageId
|
|
4079
|
+
);
|
|
4010
4080
|
if (outcome === "abandoned") {
|
|
4011
4081
|
continue;
|
|
4012
4082
|
}
|
|
@@ -4117,21 +4187,102 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4117
4187
|
}
|
|
4118
4188
|
this.unconfirmedDispatchFailures.delete(message.id);
|
|
4119
4189
|
this.dispatchNotStartedSignalled.delete(message.id);
|
|
4190
|
+
this.releasedOpencodeIds.delete(message.id);
|
|
4120
4191
|
this.dispatched.add(message.id);
|
|
4121
4192
|
this.registerInFlight(conv, sessionId, message, opencodeMessageId);
|
|
4122
4193
|
dispatched += 1;
|
|
4123
4194
|
void this.postSignal(conv.id, message.id, "dispatched");
|
|
4124
4195
|
}
|
|
4125
4196
|
if (messages.length > 0 && dispatched === 0 && skippedAlreadyDispatched === messages.length) {
|
|
4126
|
-
this.
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
conversation_id: conv.id
|
|
4130
|
-
});
|
|
4197
|
+
this.reportWedgedConversation(conv, messages);
|
|
4198
|
+
} else if (dispatched > 0) {
|
|
4199
|
+
this.wedgeWarnings.delete(conv.id);
|
|
4131
4200
|
}
|
|
4132
4201
|
this.ensureWatcherRunning(sessionId);
|
|
4133
4202
|
return dispatched;
|
|
4134
4203
|
}
|
|
4204
|
+
/**
|
|
4205
|
+
* The #183 "eyes but nothing sent" recurrence for `conv`, throttled and
|
|
4206
|
+
* escalated (#1618 WI-4). `messages` is the conversation's full pending list
|
|
4207
|
+
* on THIS tick — the caller has already confirmed every one of them is a
|
|
4208
|
+
* skip-because-already-`dispatched`, the exact signature of a message stuck
|
|
4209
|
+
* acknowledged-but-never-worked.
|
|
4210
|
+
*
|
|
4211
|
+
* Unthrottled, this fired every ~6s drain tick for as long as a wedge lasted
|
|
4212
|
+
* (52,843 occurrences observed in one incident) — burning the GLOBAL
|
|
4213
|
+
* 30-events/60s `runner-activity-telemetry.ts` budget that was itself
|
|
4214
|
+
* suppressing the diagnostics needed to debug the wedge. The `warn` log (and
|
|
4215
|
+
* the `dispatch_wedged` signal once the wedge has persisted past the same
|
|
4216
|
+
* interval) fire at most once per `wedgeWarningIntervalMs` per conversation,
|
|
4217
|
+
* naming the consecutive-tick count so the operator sees magnitude rather
|
|
4218
|
+
* than repetition.
|
|
4219
|
+
*
|
|
4220
|
+
* Deliberately does NOT trigger a release: WI-1's `reconcileWatchers` runs
|
|
4221
|
+
* unconditionally on this same tick and is already recovering anything it
|
|
4222
|
+
* can see. This is reporting only — see `countUntrackedIds`'s doc for the
|
|
4223
|
+
* one case it recovers nothing FOR (§3/D5 of the drain-wedge plan).
|
|
4224
|
+
*/
|
|
4225
|
+
reportWedgedConversation(conv, messages) {
|
|
4226
|
+
const now = this.now();
|
|
4227
|
+
const existing = this.wedgeWarnings.get(conv.id);
|
|
4228
|
+
const firstWedgedAt = existing?.firstWedgedAt ?? now;
|
|
4229
|
+
const consecutiveTicks = (existing?.consecutiveTicks ?? 0) + 1;
|
|
4230
|
+
const dueForWarn = !existing || now - existing.lastWarnedAt >= this.wedgeWarningIntervalMs;
|
|
4231
|
+
if (!dueForWarn) {
|
|
4232
|
+
this.wedgeWarnings.delete(conv.id);
|
|
4233
|
+
this.wedgeWarnings.set(conv.id, {
|
|
4234
|
+
firstWedgedAt,
|
|
4235
|
+
lastWarnedAt: existing.lastWarnedAt,
|
|
4236
|
+
consecutiveTicks
|
|
4237
|
+
});
|
|
4238
|
+
return;
|
|
4239
|
+
}
|
|
4240
|
+
const stuckForMs = now - firstWedgedAt;
|
|
4241
|
+
const untracked = this.countUntrackedIds(messages);
|
|
4242
|
+
this.log({
|
|
4243
|
+
level: "warn",
|
|
4244
|
+
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 for ${consecutiveTicks} consecutive tick(s) now (${stuckForMs}ms stuck). ` + (untracked > 0 ? `${untracked} of these id(s) are tracked by NO watcher \u2014 the dispatched/in-flight pairing invariant is violated for this conversation, which will NOT self-heal and needs a runner restart.` : `A watcher is tracking this work; the loop-liveness watchdog is already recovering it.`),
|
|
4245
|
+
conversation_id: conv.id
|
|
4246
|
+
});
|
|
4247
|
+
this.wedgeWarnings.delete(conv.id);
|
|
4248
|
+
this.wedgeWarnings.set(conv.id, { firstWedgedAt, lastWarnedAt: now, consecutiveTicks });
|
|
4249
|
+
while (this.wedgeWarnings.size > MAX_WEDGED_CONVERSATIONS) {
|
|
4250
|
+
const oldest = this.wedgeWarnings.keys().next().value;
|
|
4251
|
+
if (oldest === void 0) break;
|
|
4252
|
+
this.wedgeWarnings.delete(oldest);
|
|
4253
|
+
}
|
|
4254
|
+
if (stuckForMs >= this.wedgeWarningIntervalMs) {
|
|
4255
|
+
void this.postSignal(conv.id, messages[0].id, "dispatch_wedged", {
|
|
4256
|
+
stuck_for_ms: stuckForMs,
|
|
4257
|
+
untracked
|
|
4258
|
+
});
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
/**
|
|
4262
|
+
* How many of `messages`' ids are tracked by NO watcher's `inFlight` (#1618
|
|
4263
|
+
* WI-4) — the §3/D5 orphan discriminator. `reconcileWatchers` proves the
|
|
4264
|
+
* `dispatched`/`inFlight` pairing invariant holds by construction across
|
|
4265
|
+
* every `dispatched.add` site (see its own doc comment), so `> 0` here means
|
|
4266
|
+
* that invariant has actually been violated for this conversation: there is
|
|
4267
|
+
* no watcher for WI-1's watchdog to restart, so it will NOT self-heal.
|
|
4268
|
+
* `=== 0` means an ordinary stalled/exited watcher, which WI-1 is already
|
|
4269
|
+
* recovering. One pass over `this.watchers`, called only when the throttled
|
|
4270
|
+
* warning above is due to fire — not every tick.
|
|
4271
|
+
*/
|
|
4272
|
+
countUntrackedIds(messages) {
|
|
4273
|
+
let untracked = 0;
|
|
4274
|
+
for (const message of messages) {
|
|
4275
|
+
let tracked = false;
|
|
4276
|
+
for (const watcher of this.watchers.values()) {
|
|
4277
|
+
if (watcher.inFlight.has(message.id)) {
|
|
4278
|
+
tracked = true;
|
|
4279
|
+
break;
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
if (!tracked) untracked += 1;
|
|
4283
|
+
}
|
|
4284
|
+
return untracked;
|
|
4285
|
+
}
|
|
4135
4286
|
/**
|
|
4136
4287
|
* Poll a session's message list for the re-drive fence (#965), via the
|
|
4137
4288
|
* INJECTED `fetchImpl` — NOT the imported `getSessionMessages` helper, which
|
|
@@ -4200,9 +4351,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4200
4351
|
* for, `resolveRedriveUnresolved`'s own `pausedMaxWaitMs` bound below. Every
|
|
4201
4352
|
* other failure resolves to `unresolved` and is retried whole on the next
|
|
4202
4353
|
* ~2s drain tick.
|
|
4354
|
+
*
|
|
4355
|
+
* `effectiveOpencodeMessageId` (#1618) is the caller-resolved id: the real
|
|
4356
|
+
* server `opencode_message_id` when present, else the stall watchdog's local
|
|
4357
|
+
* `releasedOpencodeIds` fence. Read it here rather than re-deriving it from
|
|
4358
|
+
* `message` so every line below — and the signals this method posts —
|
|
4359
|
+
* keeps reporting the REAL server row; a shadow-copied `message` would
|
|
4360
|
+
* silently diverge from it.
|
|
4203
4361
|
*/
|
|
4204
|
-
async resolveRedrive(conv, sessionId, message, sessionCreated) {
|
|
4205
|
-
const ocId =
|
|
4362
|
+
async resolveRedrive(conv, sessionId, message, sessionCreated, effectiveOpencodeMessageId) {
|
|
4363
|
+
const ocId = effectiveOpencodeMessageId;
|
|
4206
4364
|
if (sessionCreated) {
|
|
4207
4365
|
this.clearRedriveUnresolved(message.id);
|
|
4208
4366
|
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
@@ -4432,7 +4590,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4432
4590
|
}
|
|
4433
4591
|
return "unresolved";
|
|
4434
4592
|
}
|
|
4435
|
-
/**
|
|
4593
|
+
/**
|
|
4594
|
+
* Clear all `unresolved`/failure-streak trackers for a row (any non-`unresolved`
|
|
4595
|
+
* outcome) — including the stall watchdog's local re-drive fence (#1618): once
|
|
4596
|
+
* `resolveRedrive` has resolved to dispatch/reattach/settle, the row either has
|
|
4597
|
+
* a real server-side `opencode_message_id` again or is no longer pending, so
|
|
4598
|
+
* the fence entry is no longer needed.
|
|
4599
|
+
*/
|
|
4436
4600
|
clearRedriveUnresolved(messageId) {
|
|
4437
4601
|
this.redriveUnresolvedSince.delete(messageId);
|
|
4438
4602
|
this.redriveUnresolvedSignalled.delete(messageId);
|
|
@@ -4440,6 +4604,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4440
4604
|
this.redriveOutcomeUnreportedSignalled.delete(messageId);
|
|
4441
4605
|
this.redriveOutcomeFailingSince.delete(messageId);
|
|
4442
4606
|
this.redriveOutcomeAbandonedSignalled.delete(messageId);
|
|
4607
|
+
this.releasedOpencodeIds.delete(messageId);
|
|
4443
4608
|
}
|
|
4444
4609
|
/**
|
|
4445
4610
|
* #1340: the dispatch loop reached a message and did NOT start a turn. Fires at
|
|
@@ -4594,6 +4759,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4594
4759
|
this.supersededSessions.delete(oldest);
|
|
4595
4760
|
}
|
|
4596
4761
|
}
|
|
4762
|
+
/**
|
|
4763
|
+
* Record the local re-drive fence for a message force-released without
|
|
4764
|
+
* completing (#1618) — see the `releasedOpencodeIds` field doc. Call BEFORE
|
|
4765
|
+
* `removeInFlight`, which is about to drop the `InFlightMessage` this reads
|
|
4766
|
+
* `opencodeMessageId` from. Hard-capped FIFO, same shape as `supersede`.
|
|
4767
|
+
*/
|
|
4768
|
+
recordReleasedOpencodeId(evidentMessageId, sessionId, opencodeMessageId) {
|
|
4769
|
+
this.releasedOpencodeIds.delete(evidentMessageId);
|
|
4770
|
+
this.releasedOpencodeIds.set(evidentMessageId, { sessionId, opencodeMessageId });
|
|
4771
|
+
while (this.releasedOpencodeIds.size > MAX_RELEASED_OPENCODE_IDS) {
|
|
4772
|
+
const oldest = this.releasedOpencodeIds.keys().next().value;
|
|
4773
|
+
if (oldest === void 0) return;
|
|
4774
|
+
this.releasedOpencodeIds.delete(oldest);
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4597
4777
|
/** Whether `sessionId` is the session this conversation has abandoned (#553). */
|
|
4598
4778
|
isSuperseded(conversationId, sessionId) {
|
|
4599
4779
|
return this.supersededSessions.get(conversationId) === sessionId;
|
|
@@ -4825,15 +5005,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4825
5005
|
registerInFlight(conv, sessionId, message, opencodeMessageId) {
|
|
4826
5006
|
let watcher = this.watchers.get(sessionId);
|
|
4827
5007
|
if (!watcher) {
|
|
4828
|
-
watcher =
|
|
4829
|
-
conv,
|
|
4830
|
-
inFlight: /* @__PURE__ */ new Map(),
|
|
4831
|
-
loop: null,
|
|
4832
|
-
reportedQuestions: /* @__PURE__ */ new Set(),
|
|
4833
|
-
reportedPermissions: /* @__PURE__ */ new Set(),
|
|
4834
|
-
lastGoodPollAt: this.now(),
|
|
4835
|
-
hadUsablePoll: false
|
|
4836
|
-
};
|
|
5008
|
+
watcher = this.newSessionWatcher(conv);
|
|
4837
5009
|
this.watchers.set(sessionId, watcher);
|
|
4838
5010
|
}
|
|
4839
5011
|
const now = this.now();
|
|
@@ -4864,6 +5036,27 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4864
5036
|
ambiguousResolved: false
|
|
4865
5037
|
});
|
|
4866
5038
|
}
|
|
5039
|
+
/**
|
|
5040
|
+
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
5041
|
+
* EQUAL (#1618) so a watcher whose loop has not started ticking yet is never
|
|
5042
|
+
* misread as stalled by the very first reconciliation that sees it.
|
|
5043
|
+
*/
|
|
5044
|
+
newSessionWatcher(conv) {
|
|
5045
|
+
const now = this.now();
|
|
5046
|
+
return {
|
|
5047
|
+
conv,
|
|
5048
|
+
inFlight: /* @__PURE__ */ new Map(),
|
|
5049
|
+
loop: null,
|
|
5050
|
+
reportedQuestions: /* @__PURE__ */ new Set(),
|
|
5051
|
+
reportedPermissions: /* @__PURE__ */ new Set(),
|
|
5052
|
+
lastGoodPollAt: now,
|
|
5053
|
+
hadUsablePoll: false,
|
|
5054
|
+
generation: 0,
|
|
5055
|
+
lastTickAt: now,
|
|
5056
|
+
lastObservedTickAt: now,
|
|
5057
|
+
consecutiveStallRestarts: 0
|
|
5058
|
+
};
|
|
5059
|
+
}
|
|
4867
5060
|
/**
|
|
4868
5061
|
* Register a RE-ADOPTED `processing` message with its session watcher
|
|
4869
5062
|
* (ADR-0046, WI-4). Mirrors `registerInFlight` but anchors the give-up
|
|
@@ -4893,15 +5086,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4893
5086
|
registerReadopted(conv, sessionId, message, opencodeMessageId, processedAtMs) {
|
|
4894
5087
|
let watcher = this.watchers.get(sessionId);
|
|
4895
5088
|
if (!watcher) {
|
|
4896
|
-
watcher =
|
|
4897
|
-
conv,
|
|
4898
|
-
inFlight: /* @__PURE__ */ new Map(),
|
|
4899
|
-
loop: null,
|
|
4900
|
-
reportedQuestions: /* @__PURE__ */ new Set(),
|
|
4901
|
-
reportedPermissions: /* @__PURE__ */ new Set(),
|
|
4902
|
-
lastGoodPollAt: this.now(),
|
|
4903
|
-
hadUsablePoll: false
|
|
4904
|
-
};
|
|
5089
|
+
watcher = this.newSessionWatcher(conv);
|
|
4905
5090
|
this.watchers.set(sessionId, watcher);
|
|
4906
5091
|
}
|
|
4907
5092
|
watcher.inFlight.set(message.id, {
|
|
@@ -4945,12 +5130,110 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4945
5130
|
ambiguousResolved: false
|
|
4946
5131
|
});
|
|
4947
5132
|
}
|
|
5133
|
+
/**
|
|
5134
|
+
* Loop-liveness watchdog (#1618). Runs once per `drainPending()` tick and
|
|
5135
|
+
* restarts any per-session watcher whose loop has exited or stopped ticking
|
|
5136
|
+
* — escalating to a bounded force-release only once
|
|
5137
|
+
* `MAX_WATCHER_STALL_RESTARTS` consecutive restarts have failed to recover
|
|
5138
|
+
* it. Fully synchronous: it only inspects in-memory state and calls the
|
|
5139
|
+
* synchronous `ensureWatcherRunning`/`removeInFlight`, which is what lets it
|
|
5140
|
+
* run from the very top of `drainPending()` — ahead of the un-timed
|
|
5141
|
+
* `getPendingConversations()` await that would otherwise be able to disable
|
|
5142
|
+
* it (`run.ts`'s poll loop is sequential, so a hung fetch there stops
|
|
5143
|
+
* `drainPending()` from being CALLED again at all, not just from finishing).
|
|
5144
|
+
*
|
|
5145
|
+
* Restarts the loop rather than releasing messages directly: a blind release
|
|
5146
|
+
* would let the next drain re-`prompt_async` a turn that may still be
|
|
5147
|
+
* running (ADR-0047; see `releasedOpencodeIds`'s doc). A restarted loop
|
|
5148
|
+
* re-polls with each message's `opencodeMessageId` still in hand and lets
|
|
5149
|
+
* the existing, audited `!activelyRunning` give-up decide, same as it always
|
|
5150
|
+
* has.
|
|
5151
|
+
*
|
|
5152
|
+
* Deliberately does NOT sweep `this.dispatched` for an id no watcher tracks:
|
|
5153
|
+
* that shape has no in-flight entry and therefore no `opencodeMessageId` to
|
|
5154
|
+
* fence a release with, so releasing it here would blind-re-POST a possibly-
|
|
5155
|
+
* running turn — and there is no conversation id in hand to signal with
|
|
5156
|
+
* either. Detection for that shape lives on WI-4's `dispatch_wedged` signal
|
|
5157
|
+
* instead, where a conversation id already exists. If you find yourself
|
|
5158
|
+
* wanting to add a `dispatched` sweep here, don't — read the drain-wedge
|
|
5159
|
+
* plan's §3/D5 first.
|
|
5160
|
+
*/
|
|
5161
|
+
reconcileWatchers() {
|
|
5162
|
+
const now = this.now();
|
|
5163
|
+
for (const [sessionId, watcher] of [...this.watchers]) {
|
|
5164
|
+
if (watcher.lastTickAt !== watcher.lastObservedTickAt) {
|
|
5165
|
+
watcher.consecutiveStallRestarts = 0;
|
|
5166
|
+
}
|
|
5167
|
+
watcher.lastObservedTickAt = watcher.lastTickAt;
|
|
5168
|
+
if (watcher.inFlight.size === 0 && watcher.loop === null) {
|
|
5169
|
+
this.watchers.delete(sessionId);
|
|
5170
|
+
continue;
|
|
5171
|
+
}
|
|
5172
|
+
if (watcher.loop === null && watcher.inFlight.size > 0) {
|
|
5173
|
+
if (now - watcher.lastTickAt < this.watcherStallMs) continue;
|
|
5174
|
+
this.log({
|
|
5175
|
+
level: "warn",
|
|
5176
|
+
message: `Watchdog: session ${sessionId.slice(0, 8)}'s watcher loop had exited with ${watcher.inFlight.size} message(s) still in flight (idle ${now - watcher.lastTickAt}ms) \u2014 restarting`,
|
|
5177
|
+
conversation_id: watcher.conv.id
|
|
5178
|
+
});
|
|
5179
|
+
this.ensureWatcherRunning(sessionId);
|
|
5180
|
+
for (const evidentMessageId of watcher.inFlight.keys()) {
|
|
5181
|
+
void this.postSignal(watcher.conv.id, evidentMessageId, "watcher_recovered", {
|
|
5182
|
+
recovery: "loop_exited"
|
|
5183
|
+
});
|
|
5184
|
+
}
|
|
5185
|
+
continue;
|
|
5186
|
+
}
|
|
5187
|
+
if (watcher.loop !== null && now - watcher.lastTickAt >= this.watcherStallMs) {
|
|
5188
|
+
const stalledForMs = now - watcher.lastTickAt;
|
|
5189
|
+
watcher.consecutiveStallRestarts += 1;
|
|
5190
|
+
if (watcher.consecutiveStallRestarts > MAX_WATCHER_STALL_RESTARTS) {
|
|
5191
|
+
this.log({
|
|
5192
|
+
level: "error",
|
|
5193
|
+
message: `Watchdog: session ${sessionId.slice(0, 8)}'s watcher loop stalled through ${watcher.consecutiveStallRestarts} restarts (last stall ${stalledForMs}ms) \u2014 releasing its ${watcher.inFlight.size} in-flight message(s)`,
|
|
5194
|
+
conversation_id: watcher.conv.id
|
|
5195
|
+
});
|
|
5196
|
+
for (const [evidentMessageId, inFlight] of [...watcher.inFlight]) {
|
|
5197
|
+
this.recordReleasedOpencodeId(evidentMessageId, sessionId, inFlight.opencodeMessageId);
|
|
5198
|
+
this.removeInFlight(watcher, evidentMessageId);
|
|
5199
|
+
void this.postSignal(watcher.conv.id, evidentMessageId, "watcher_recovered", {
|
|
5200
|
+
recovery: "unrecoverable_released"
|
|
5201
|
+
});
|
|
5202
|
+
}
|
|
5203
|
+
watcher.generation += 1;
|
|
5204
|
+
this.watchers.delete(sessionId);
|
|
5205
|
+
continue;
|
|
5206
|
+
}
|
|
5207
|
+
watcher.generation += 1;
|
|
5208
|
+
watcher.loop = null;
|
|
5209
|
+
watcher.lastGoodPollAt = now;
|
|
5210
|
+
watcher.lastTickAt = now;
|
|
5211
|
+
watcher.lastObservedTickAt = watcher.lastTickAt;
|
|
5212
|
+
this.ensureWatcherRunning(sessionId);
|
|
5213
|
+
this.log({
|
|
5214
|
+
level: "warn",
|
|
5215
|
+
message: `Watchdog: session ${sessionId.slice(0, 8)}'s watcher loop had not ticked in ${stalledForMs}ms \u2014 restarted under generation ${watcher.generation} (${watcher.consecutiveStallRestarts}/${MAX_WATCHER_STALL_RESTARTS})`,
|
|
5216
|
+
conversation_id: watcher.conv.id
|
|
5217
|
+
});
|
|
5218
|
+
for (const evidentMessageId of watcher.inFlight.keys()) {
|
|
5219
|
+
void this.postSignal(watcher.conv.id, evidentMessageId, "watcher_recovered", {
|
|
5220
|
+
recovery: "loop_stalled"
|
|
5221
|
+
});
|
|
5222
|
+
}
|
|
5223
|
+
}
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
4948
5226
|
/**
|
|
4949
5227
|
* Start (but do NOT await) the per-session watcher loop if it has in-flight
|
|
4950
5228
|
* work and is not already running. Single-flight per session. The loop is
|
|
4951
5229
|
* tracked on the watcher and cleared when it settles; it never rejects (fully
|
|
4952
5230
|
* guarded), so a failed poll/callback can never crash the run loop — the cron
|
|
4953
5231
|
* stays as the safety net.
|
|
5232
|
+
*
|
|
5233
|
+
* The generation started here (#1618) is captured in the `.finally` closure
|
|
5234
|
+
* so a RETIRED loop settling late — after `reconcileWatchers` has already
|
|
5235
|
+
* restarted this watcher under a newer generation — can neither null the new
|
|
5236
|
+
* loop's handle nor delete a watcher that still has live work.
|
|
4954
5237
|
*/
|
|
4955
5238
|
ensureWatcherRunning(sessionId) {
|
|
4956
5239
|
const watcher = this.watchers.get(sessionId);
|
|
@@ -4960,7 +5243,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4960
5243
|
this.watchers.delete(sessionId);
|
|
4961
5244
|
return;
|
|
4962
5245
|
}
|
|
4963
|
-
const
|
|
5246
|
+
const generation = watcher.generation;
|
|
5247
|
+
const loop = this.runWatcherLoop(sessionId, watcher, generation).finally(() => {
|
|
5248
|
+
if (watcher.generation !== generation) return;
|
|
4964
5249
|
watcher.loop = null;
|
|
4965
5250
|
if (watcher.inFlight.size === 0) {
|
|
4966
5251
|
this.watchers.delete(sessionId);
|
|
@@ -4980,11 +5265,25 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4980
5265
|
* `source_message_id`;
|
|
4981
5266
|
* 4. drops messages that completed or timed out from the in-flight set.
|
|
4982
5267
|
* Exits when the in-flight set empties. Never throws.
|
|
5268
|
+
*
|
|
5269
|
+
* `generation` (#1618) is the incarnation this call was started under.
|
|
5270
|
+
* `reconcileWatchers` can restart a stalled loop by bumping
|
|
5271
|
+
* `watcher.generation` and starting a NEW `runWatcherLoop` over the same
|
|
5272
|
+
* `SessionWatcher` object — the stalled promise itself cannot be cancelled,
|
|
5273
|
+
* so this loop instead checks at the top of every iteration, right after
|
|
5274
|
+
* waking from `sleep`, and right before servicing any message, and quietly
|
|
5275
|
+
* retires (returns without touching anything) the moment it is no longer the
|
|
5276
|
+
* watcher's current generation. Retiring mid-tick can still let ONE
|
|
5277
|
+
* `serviceInFlightMessage` pass complete first — acceptable, since that
|
|
5278
|
+
* method contains no non-idempotent action.
|
|
4983
5279
|
*/
|
|
4984
|
-
async runWatcherLoop(sessionId, watcher) {
|
|
5280
|
+
async runWatcherLoop(sessionId, watcher, generation) {
|
|
4985
5281
|
try {
|
|
4986
5282
|
while (watcher.inFlight.size > 0) {
|
|
5283
|
+
if (watcher.generation !== generation) return;
|
|
5284
|
+
watcher.lastTickAt = this.now();
|
|
4987
5285
|
await this.sleep(this.pausedPollIntervalMs);
|
|
5286
|
+
if (watcher.generation !== generation) return;
|
|
4988
5287
|
let messages = null;
|
|
4989
5288
|
try {
|
|
4990
5289
|
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
@@ -5005,6 +5304,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5005
5304
|
}
|
|
5006
5305
|
}
|
|
5007
5306
|
const { openQuestions, openPermissions, questionsPolledOk, permissionsPolledOk } = await this.pollInteractions(sessionId, watcher, messages);
|
|
5307
|
+
if (watcher.generation !== generation) return;
|
|
5008
5308
|
for (const inFlight of [...watcher.inFlight.values()]) {
|
|
5009
5309
|
await this.serviceInFlightMessage(
|
|
5010
5310
|
sessionId,
|
|
@@ -7418,23 +7718,46 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
7418
7718
|
return null;
|
|
7419
7719
|
}
|
|
7420
7720
|
let consecutiveFailures = 0;
|
|
7421
|
-
let
|
|
7721
|
+
let phase = "dormant";
|
|
7422
7722
|
let rearmRequested = false;
|
|
7423
|
-
const
|
|
7424
|
-
|
|
7425
|
-
|
|
7723
|
+
const armProbe = () => {
|
|
7724
|
+
phase = "probe-pending";
|
|
7725
|
+
state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);
|
|
7726
|
+
};
|
|
7727
|
+
const scheduleNextTick = (reportedSuccessfully) => {
|
|
7728
|
+
if (rearmRequested) {
|
|
7729
|
+
rearmRequested = false;
|
|
7730
|
+
armProbe();
|
|
7731
|
+
return;
|
|
7732
|
+
}
|
|
7733
|
+
phase = reportedSuccessfully ? "steady-pending-healthy" : "steady-pending-retry";
|
|
7426
7734
|
state.claudeUsageTimer = setTimeout(() => void tick(false), nextReportDelayMs());
|
|
7427
7735
|
};
|
|
7428
7736
|
const rearm = () => {
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7737
|
+
switch (phase) {
|
|
7738
|
+
case "tick-in-flight":
|
|
7739
|
+
rearmRequested = true;
|
|
7740
|
+
return;
|
|
7741
|
+
case "probe-pending":
|
|
7742
|
+
return;
|
|
7743
|
+
case "steady-pending-healthy":
|
|
7744
|
+
return;
|
|
7745
|
+
case "steady-pending-retry":
|
|
7746
|
+
if (state.claudeUsageTimer) {
|
|
7747
|
+
clearTimeout(state.claudeUsageTimer);
|
|
7748
|
+
state.claudeUsageTimer = null;
|
|
7749
|
+
}
|
|
7750
|
+
rearmRequested = false;
|
|
7751
|
+
armProbe();
|
|
7752
|
+
return;
|
|
7753
|
+
case "dormant":
|
|
7754
|
+
rearmRequested = false;
|
|
7755
|
+
armProbe();
|
|
7756
|
+
return;
|
|
7432
7757
|
}
|
|
7433
|
-
rearmRequested = false;
|
|
7434
|
-
armed = true;
|
|
7435
|
-
state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);
|
|
7436
7758
|
};
|
|
7437
7759
|
const tick = async (isProbe) => {
|
|
7760
|
+
phase = "tick-in-flight";
|
|
7438
7761
|
try {
|
|
7439
7762
|
const usage = await getClaudeUsage();
|
|
7440
7763
|
const result = await reportClaudeUsage(state.agentId, state.authHeader, usage);
|
|
@@ -7460,7 +7783,7 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
7460
7783
|
message: `Failed to report Claude usage: ${result.error}${claudeUsageFailureStreakSuffix(consecutiveFailures)}`
|
|
7461
7784
|
});
|
|
7462
7785
|
}
|
|
7463
|
-
scheduleNextTick();
|
|
7786
|
+
scheduleNextTick(result.ok);
|
|
7464
7787
|
} catch (error2) {
|
|
7465
7788
|
if (error2 instanceof ClaudeUsageError && isLocalCredentialProblem(error2)) {
|
|
7466
7789
|
if (mode === "on") {
|
|
@@ -7469,14 +7792,14 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
7469
7792
|
level: "warn",
|
|
7470
7793
|
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"
|
|
7471
7794
|
});
|
|
7472
|
-
scheduleNextTick();
|
|
7795
|
+
scheduleNextTick(false);
|
|
7473
7796
|
} else if (isProbe) {
|
|
7474
7797
|
logActivity(state, {
|
|
7475
7798
|
type: "info",
|
|
7476
7799
|
level: "debug",
|
|
7477
7800
|
message: `Claude usage reporting: ${error2.message}`
|
|
7478
7801
|
});
|
|
7479
|
-
|
|
7802
|
+
phase = "dormant";
|
|
7480
7803
|
if (rearmRequested) rearm();
|
|
7481
7804
|
} else {
|
|
7482
7805
|
logActivity(state, {
|
|
@@ -7484,7 +7807,7 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
7484
7807
|
level: "debug",
|
|
7485
7808
|
message: `Claude usage reporting: ${error2.message}`
|
|
7486
7809
|
});
|
|
7487
|
-
scheduleNextTick();
|
|
7810
|
+
scheduleNextTick(false);
|
|
7488
7811
|
}
|
|
7489
7812
|
} else {
|
|
7490
7813
|
consecutiveFailures++;
|
|
@@ -7494,12 +7817,11 @@ function scheduleClaudeUsageReporting(state, options) {
|
|
|
7494
7817
|
level: claudeUsageFailureLogLevel(consecutiveFailures),
|
|
7495
7818
|
message: `Claude usage reporting failed: ${message}${claudeUsageFailureStreakSuffix(consecutiveFailures)}`
|
|
7496
7819
|
});
|
|
7497
|
-
scheduleNextTick();
|
|
7820
|
+
scheduleNextTick(false);
|
|
7498
7821
|
}
|
|
7499
7822
|
}
|
|
7500
7823
|
};
|
|
7501
|
-
|
|
7502
|
-
state.claudeUsageTimer = setTimeout(() => void tick(true), FIRST_REPORT_DELAY_MS);
|
|
7824
|
+
armProbe();
|
|
7503
7825
|
return rearm;
|
|
7504
7826
|
}
|
|
7505
7827
|
async function notifyOffline(state) {
|