From d21f571e21cbba8a79d7a9efdaa89835e3856214 Mon Sep 17 00:00:00 2001 From: Stephen Schoettler Date: Thu, 26 Feb 2026 12:08:28 -0800 Subject: [PATCH 1/5] fix: add null guards to usage sort comparators Prevents crash when totals is undefined in byModel/byProvider/byAgent sort comparators. Fixes 'Cannot read properties of undefined (reading totalTokens)' crash that causes context overflow in active sessions. --- src/gateway/server-methods/usage.ts | 10 +++++----- src/infra/session-cost-usage.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/gateway/server-methods/usage.ts b/src/gateway/server-methods/usage.ts index e40af58f5fe..3546c25723c 100644 --- a/src/gateway/server-methods/usage.ts +++ b/src/gateway/server-methods/usage.ts @@ -844,22 +844,22 @@ export const usageHandlers: GatewayRequestHandlers = { .toSorted((a, b) => b.count - a.count), }, byModel: Array.from(byModelMap.values()).toSorted((a, b) => { - const costDiff = b.totals.totalCost - a.totals.totalCost; + const costDiff = (b.totals?.totalCost ?? 0) - (a.totals?.totalCost ?? 0); if (costDiff !== 0) { return costDiff; } - return b.totals.totalTokens - a.totals.totalTokens; + return (b.totals?.totalTokens ?? 0) - (a.totals?.totalTokens ?? 0); }), byProvider: Array.from(byProviderMap.values()).toSorted((a, b) => { - const costDiff = b.totals.totalCost - a.totals.totalCost; + const costDiff = (b.totals?.totalCost ?? 0) - (a.totals?.totalCost ?? 0); if (costDiff !== 0) { return costDiff; } - return b.totals.totalTokens - a.totals.totalTokens; + return (b.totals?.totalTokens ?? 0) - (a.totals?.totalTokens ?? 0); }), byAgent: Array.from(byAgentMap.entries()) .map(([id, totals]) => ({ agentId: id, totals })) - .toSorted((a, b) => b.totals.totalCost - a.totals.totalCost), + .toSorted((a, b) => (b.totals?.totalCost ?? 0) - (a.totals?.totalCost ?? 0)), ...tail, }; diff --git a/src/infra/session-cost-usage.ts b/src/infra/session-cost-usage.ts index 230ebd60c2e..4c021bcc72f 100644 --- a/src/infra/session-cost-usage.ts +++ b/src/infra/session-cost-usage.ts @@ -707,11 +707,11 @@ export async function loadSessionCostSummary(params: { const modelUsage = modelUsageMap.size ? Array.from(modelUsageMap.values()).toSorted((a, b) => { - const costDiff = b.totals.totalCost - a.totals.totalCost; + const costDiff = (b.totals?.totalCost ?? 0) - (a.totals?.totalCost ?? 0); if (costDiff !== 0) { return costDiff; } - return b.totals.totalTokens - a.totals.totalTokens; + return (b.totals?.totalTokens ?? 0) - (a.totals?.totalTokens ?? 0); }) : undefined; From f1eed3f875668402aaa0d8f8f84c88e1041038cc Mon Sep 17 00:00:00 2001 From: Stephen Schoettler Date: Thu, 26 Feb 2026 12:16:28 -0800 Subject: [PATCH 2/5] fix(browser): prevent stdio buffer blocking in Docker environments --- src/browser/chrome.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/chrome.ts b/src/browser/chrome.ts index 9501d1e4d98..6e994cdcda3 100644 --- a/src/browser/chrome.ts +++ b/src/browser/chrome.ts @@ -226,7 +226,7 @@ export async function launchOpenClawChrome( args.push("about:blank"); return spawn(exe.path, args, { - stdio: "pipe", + stdio: ["ignore", "ignore", "ignore"], env: { ...process.env, // Reduce accidental sharing with the user's env. From 098d276fe02062fb3ecff9c6090cf7d7e0452846 Mon Sep 17 00:00:00 2001 From: Stephen Schoettler Date: Sun, 1 Mar 2026 18:42:02 -0800 Subject: [PATCH 3/5] fix(delivery-queue): increment retryCount on deferred entries when time budget exceeded When delivery recovery ran out of the 60s time budget, remaining pending entries were silently deferred to the next restart with no retryCount increment. This caused them to loop forever across restarts, never hitting MAX_RETRIES and never moving to failed/. Fix: call failDelivery() on each remaining entry before breaking out of the recovery loop (both the deadline check and the backoff-exceeds-deadline check). This increments retryCount so that entries eventually exhaust MAX_RETRIES and are permanently skipped. Fixes #24353 --- src/infra/outbound/delivery-queue.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/infra/outbound/delivery-queue.ts b/src/infra/outbound/delivery-queue.ts index 1e954ea8e39..9b09aef4550 100644 --- a/src/infra/outbound/delivery-queue.ts +++ b/src/infra/outbound/delivery-queue.ts @@ -303,8 +303,19 @@ export async function recoverPendingDeliveries(opts: { for (const entry of pending) { const now = Date.now(); if (now >= deadline) { - const deferred = pending.length - recovered - failed - skippedMaxRetries - deferredBackoff; - opts.log.warn(`Recovery time budget exceeded — ${deferred} entries deferred to next restart`); + // Increment retryCount on remaining entries so they eventually hit MAX_RETRIES + const remaining = pending.slice(pending.indexOf(entry)); + for (const r of remaining) { + try { + await failDelivery(r.id, "Recovery time budget exceeded — deferred", opts.stateDir); + } catch { + /* best-effort */ + } + } + const deferred = remaining.length; + opts.log.warn( + `Recovery time budget exceeded — ${deferred} entries deferred (retryCount incremented)`, + ); break; } if (entry.retryCount >= MAX_RETRIES) { From d8876ec76ba56565a5a2f627888d26be4525a489 Mon Sep 17 00:00:00 2001 From: Stephen Schoettler Date: Sun, 1 Mar 2026 20:13:40 -0800 Subject: [PATCH 4/5] fix(delivery-queue): break immediately on deadline instead of failing all remaining entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1-C: After now >= deadline, the old code would iterate all remaining queue entries and call failDelivery() on each — O(n) work that nullified the maxRecoveryMs wall-clock cap on large queues. Fix: break out of the recovery loop immediately when the deadline is exceeded. Remaining entries are picked up on next startup unchanged (retryCount not incremented). The deadline means 'stop here', not 'fail everything remaining'. --- src/infra/outbound/delivery-queue.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/infra/outbound/delivery-queue.ts b/src/infra/outbound/delivery-queue.ts index 9b09aef4550..897963f264b 100644 --- a/src/infra/outbound/delivery-queue.ts +++ b/src/infra/outbound/delivery-queue.ts @@ -303,19 +303,7 @@ export async function recoverPendingDeliveries(opts: { for (const entry of pending) { const now = Date.now(); if (now >= deadline) { - // Increment retryCount on remaining entries so they eventually hit MAX_RETRIES - const remaining = pending.slice(pending.indexOf(entry)); - for (const r of remaining) { - try { - await failDelivery(r.id, "Recovery time budget exceeded — deferred", opts.stateDir); - } catch { - /* best-effort */ - } - } - const deferred = remaining.length; - opts.log.warn( - `Recovery time budget exceeded — ${deferred} entries deferred (retryCount incremented)`, - ); + opts.log.warn(`Recovery time budget exceeded — remaining entries deferred to next startup`); break; } if (entry.retryCount >= MAX_RETRIES) { From ce864cdb6eb79ba71a6cd29071ebb19b1f1e14f1 Mon Sep 17 00:00:00 2001 From: Stephen Schoettler Date: Sun, 1 Mar 2026 20:49:04 -0800 Subject: [PATCH 5/5] fix(delivery-queue): align test assertion and JSDoc with 'next startup' log message --- src/infra/outbound/delivery-queue.ts | 2 +- src/infra/outbound/outbound.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/infra/outbound/delivery-queue.ts b/src/infra/outbound/delivery-queue.ts index 897963f264b..e009f4c96d8 100644 --- a/src/infra/outbound/delivery-queue.ts +++ b/src/infra/outbound/delivery-queue.ts @@ -280,7 +280,7 @@ export async function recoverPendingDeliveries(opts: { log: RecoveryLogger; cfg: OpenClawConfig; stateDir?: string; - /** Maximum wall-clock time for recovery in ms. Remaining entries are deferred to next restart. Default: 60 000. */ + /** Maximum wall-clock time for recovery in ms. Remaining entries are deferred to next startup. Default: 60 000. */ maxRecoveryMs?: number; }): Promise { const pending = await loadPendingDeliveries(opts.stateDir); diff --git a/src/infra/outbound/outbound.test.ts b/src/infra/outbound/outbound.test.ts index 4bb00b4db04..9ea7fd239e2 100644 --- a/src/infra/outbound/outbound.test.ts +++ b/src/infra/outbound/outbound.test.ts @@ -445,7 +445,7 @@ describe("delivery-queue", () => { expect(remaining).toHaveLength(3); // Should have logged a warning about deferred entries. - expect(log.warn).toHaveBeenCalledWith(expect.stringContaining("deferred to next restart")); + expect(log.warn).toHaveBeenCalledWith(expect.stringContaining("deferred to next startup")); }); it("defers entries until backoff becomes eligible", async () => {