From 20f30172413b640989dfb8606d766d796ae10a66 Mon Sep 17 00:00:00 2001 From: Claudia Date: Mon, 9 Mar 2026 16:20:59 +0100 Subject: [PATCH 1/2] fix: guard estimateMessageCharsCached against null/undefined messages After upgrading to v2026.3.7, orphaned null/undefined entries can appear in the session message array (likely from the new compaction lifecycle). These cause a TypeError in estimateMessageCharsCached when WeakMap.get() is called on a non-object, crashing active sessions. Changes: - Add null/typeof guard at the top of estimateMessageCharsCached() - Add .filter() in estimateContextChars() to skip null entries before reduce - Add test coverage for null, undefined, and primitive inputs Regression since v2026.3.7. Related to #35146 and #35143 (null guards for thinking/text blocks by @Sid-Qin), but this addresses null entries in the message array itself rather than within message content blocks. --- .../tool-result-char-estimator.test.ts | 92 +++++++++++++++++++ .../tool-result-char-estimator.ts | 7 +- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts diff --git a/src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts b/src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts new file mode 100644 index 00000000000..1fade385fed --- /dev/null +++ b/src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts @@ -0,0 +1,92 @@ +import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import { describe, expect, it } from "vitest"; +import { castAgentMessage } from "../test-helpers/agent-message-fixtures.js"; +import { + createMessageCharEstimateCache, + estimateContextChars, + estimateMessageCharsCached, +} from "./tool-result-char-estimator.js"; + +function makeUser(text: string): AgentMessage { + return castAgentMessage({ + role: "user", + content: text, + timestamp: Date.now(), + }); +} + +describe("estimateMessageCharsCached", () => { + it("returns a positive estimate for a valid message", () => { + const cache = createMessageCharEstimateCache(); + const msg = makeUser("hello world"); + expect(estimateMessageCharsCached(msg, cache)).toBeGreaterThan(0); + }); + + it("returns 0 for null", () => { + const cache = createMessageCharEstimateCache(); + expect(estimateMessageCharsCached(null as unknown as AgentMessage, cache)).toBe(0); + }); + + it("returns 0 for undefined", () => { + const cache = createMessageCharEstimateCache(); + expect(estimateMessageCharsCached(undefined as unknown as AgentMessage, cache)).toBe(0); + }); + + it("returns 0 for a non-object primitive", () => { + const cache = createMessageCharEstimateCache(); + expect(estimateMessageCharsCached(42 as unknown as AgentMessage, cache)).toBe(0); + }); + + it("caches the estimate on second call", () => { + const cache = createMessageCharEstimateCache(); + const msg = makeUser("cached test"); + const first = estimateMessageCharsCached(msg, cache); + const second = estimateMessageCharsCached(msg, cache); + expect(first).toBe(second); + expect(first).toBeGreaterThan(0); + }); +}); + +describe("estimateContextChars", () => { + it("sums estimates for valid messages", () => { + const cache = createMessageCharEstimateCache(); + const messages = [makeUser("one"), makeUser("two")]; + const total = estimateContextChars(messages, cache); + expect(total).toBeGreaterThan(0); + }); + + it("skips null entries without crashing", () => { + const cache = createMessageCharEstimateCache(); + const messages = [ + makeUser("valid"), + null as unknown as AgentMessage, + makeUser("also valid"), + ]; + const total = estimateContextChars(messages, cache); + expect(total).toBeGreaterThan(0); + }); + + it("skips undefined entries without crashing", () => { + const cache = createMessageCharEstimateCache(); + const messages = [ + undefined as unknown as AgentMessage, + makeUser("valid"), + ]; + const total = estimateContextChars(messages, cache); + expect(total).toBeGreaterThan(0); + }); + + it("handles an entirely null/undefined array", () => { + const cache = createMessageCharEstimateCache(); + const messages = [ + null as unknown as AgentMessage, + undefined as unknown as AgentMessage, + ]; + expect(estimateContextChars(messages, cache)).toBe(0); + }); + + it("handles an empty array", () => { + const cache = createMessageCharEstimateCache(); + expect(estimateContextChars([], cache)).toBe(0); + }); +}); diff --git a/src/agents/pi-embedded-runner/tool-result-char-estimator.ts b/src/agents/pi-embedded-runner/tool-result-char-estimator.ts index 16bdc5e43eb..2745f1a0eb5 100644 --- a/src/agents/pi-embedded-runner/tool-result-char-estimator.ts +++ b/src/agents/pi-embedded-runner/tool-result-char-estimator.ts @@ -145,6 +145,9 @@ export function estimateMessageCharsCached( msg: AgentMessage, cache: MessageCharEstimateCache, ): number { + if (msg == null || typeof msg !== "object") { + return 0; + } const hit = cache.get(msg); if (hit !== undefined) { return hit; @@ -158,7 +161,9 @@ export function estimateContextChars( messages: AgentMessage[], cache: MessageCharEstimateCache, ): number { - return messages.reduce((sum, msg) => sum + estimateMessageCharsCached(msg, cache), 0); + return messages + .filter((m): m is AgentMessage => m != null) + .reduce((sum, msg) => sum + estimateMessageCharsCached(msg, cache), 0); } export function invalidateMessageCharsCacheEntry( From 1efff47fcf580424a98e566c2e62299ce01433de Mon Sep 17 00:00:00 2001 From: Albert Hild Date: Tue, 10 Mar 2026 11:57:59 +0100 Subject: [PATCH 2/2] style: format test file with oxfmt --- .../tool-result-char-estimator.test.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts b/src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts index 1fade385fed..6e0ab2adad3 100644 --- a/src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts +++ b/src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts @@ -57,31 +57,21 @@ describe("estimateContextChars", () => { it("skips null entries without crashing", () => { const cache = createMessageCharEstimateCache(); - const messages = [ - makeUser("valid"), - null as unknown as AgentMessage, - makeUser("also valid"), - ]; + const messages = [makeUser("valid"), null as unknown as AgentMessage, makeUser("also valid")]; const total = estimateContextChars(messages, cache); expect(total).toBeGreaterThan(0); }); it("skips undefined entries without crashing", () => { const cache = createMessageCharEstimateCache(); - const messages = [ - undefined as unknown as AgentMessage, - makeUser("valid"), - ]; + const messages = [undefined as unknown as AgentMessage, makeUser("valid")]; const total = estimateContextChars(messages, cache); expect(total).toBeGreaterThan(0); }); it("handles an entirely null/undefined array", () => { const cache = createMessageCharEstimateCache(); - const messages = [ - null as unknown as AgentMessage, - undefined as unknown as AgentMessage, - ]; + const messages = [null as unknown as AgentMessage, undefined as unknown as AgentMessage]; expect(estimateContextChars(messages, cache)).toBe(0); });