fix: preserve explicit maxChars=0 (#796) (thanks @gabriel-trigo)

This commit is contained in:
Peter Steinberger
2026-01-13 02:29:48 +00:00
parent 56c406b19e
commit 46a694bbc7
5 changed files with 46 additions and 3 deletions

View File

@@ -7,6 +7,7 @@
- Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake) - Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake)
- Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs. - Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs.
- Update: run `clawdbot doctor --non-interactive` during updates to avoid TTY hangs. (#781 — thanks @ronyrus) - Update: run `clawdbot doctor --non-interactive` during updates to avoid TTY hangs. (#781 — thanks @ronyrus)
- Browser tools: treat explicit `maxChars: 0` as unlimited while keeping the default limit only when omitted. (#796 — thanks @gabriel-trigo)
- Tools: allow Claude/Gemini tool param aliases (`file_path`, `old_string`, `new_string`) while enforcing required params at runtime. (#793 — thanks @hsrvc) - Tools: allow Claude/Gemini tool param aliases (`file_path`, `old_string`, `new_string`) while enforcing required params at runtime. (#793 — thanks @hsrvc)
- Gemini: downgrade tool-call history missing `thought_signature` to avoid INVALID_ARGUMENT errors. (#793 — thanks @hsrvc) - Gemini: downgrade tool-call history missing `thought_signature` to avoid INVALID_ARGUMENT errors. (#793 — thanks @hsrvc)
- Messaging: enforce context isolation for message tool sends across providers (normalized targets + tests). (#793 — thanks @hsrvc) - Messaging: enforce context isolation for message tool sends across providers (normalized targets + tests). (#793 — thanks @hsrvc)

View File

@@ -89,4 +89,17 @@ describe("browser tool snapshot maxChars", () => {
}), }),
); );
}); });
it("skips the default when maxChars is explicitly zero", async () => {
const tool = createBrowserTool();
await tool.execute?.(null, {
action: "snapshot",
format: "ai",
maxChars: 0,
});
expect(browserClientMocks.browserSnapshot).toHaveBeenCalled();
const [, opts] = browserClientMocks.browserSnapshot.mock.calls.at(-1) ?? [];
expect(Object.hasOwn(opts ?? {}, "maxChars")).toBe(false);
});
}); });

View File

@@ -322,6 +322,7 @@ export function createBrowserTool(opts?: {
params.format === "ai" || params.format === "aria" params.format === "ai" || params.format === "aria"
? (params.format as "ai" | "aria") ? (params.format as "ai" | "aria")
: "ai"; : "ai";
const hasMaxChars = Object.hasOwn(params, "maxChars");
const targetId = const targetId =
typeof params.targetId === "string" typeof params.targetId === "string"
? params.targetId.trim() ? params.targetId.trim()
@@ -338,7 +339,9 @@ export function createBrowserTool(opts?: {
: undefined; : undefined;
const resolvedMaxChars = const resolvedMaxChars =
format === "ai" format === "ai"
? (maxChars ?? DEFAULT_AI_SNAPSHOT_MAX_CHARS) ? hasMaxChars
? maxChars
: DEFAULT_AI_SNAPSHOT_MAX_CHARS
: undefined; : undefined;
const interactive = const interactive =
typeof params.interactive === "boolean" typeof params.interactive === "boolean"
@@ -360,7 +363,9 @@ export function createBrowserTool(opts?: {
format, format,
targetId, targetId,
limit, limit,
...(resolvedMaxChars ? { maxChars: resolvedMaxChars } : {}), ...(typeof resolvedMaxChars === "number"
? { maxChars: resolvedMaxChars }
: {}),
interactive, interactive,
compact, compact,
depth, depth,

View File

@@ -1195,6 +1195,7 @@ export function registerBrowserAgentRoutes(
: "aria"; : "aria";
const limitRaw = const limitRaw =
typeof req.query.limit === "string" ? Number(req.query.limit) : undefined; typeof req.query.limit === "string" ? Number(req.query.limit) : undefined;
const hasMaxChars = Object.hasOwn(req.query, "maxChars");
const maxCharsRaw = const maxCharsRaw =
typeof req.query.maxChars === "string" typeof req.query.maxChars === "string"
? Number(req.query.maxChars) ? Number(req.query.maxChars)
@@ -1207,7 +1208,11 @@ export function registerBrowserAgentRoutes(
? Math.floor(maxCharsRaw) ? Math.floor(maxCharsRaw)
: undefined; : undefined;
const resolvedMaxChars = const resolvedMaxChars =
format === "ai" ? (maxChars ?? DEFAULT_AI_SNAPSHOT_MAX_CHARS) : undefined; format === "ai"
? hasMaxChars
? maxChars
: DEFAULT_AI_SNAPSHOT_MAX_CHARS
: undefined;
const interactive = toBoolean(req.query.interactive); const interactive = toBoolean(req.query.interactive);
const compact = toBoolean(req.query.compact); const compact = toBoolean(req.query.compact);
const depth = toNumber(req.query.depth); const depth = toNumber(req.query.depth);

View File

@@ -687,6 +687,25 @@ describe("browser control server", () => {
expect(stopped.stopped).toBe(true); expect(stopped.stopped).toBe(true);
}); });
it("skips default maxChars when explicitly set to zero", async () => {
const { startBrowserControlServerFromConfig } = await import("./server.js");
await startBrowserControlServerFromConfig();
const base = `http://127.0.0.1:${testPort}`;
await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json());
const snapAi = (await realFetch(
`${base}/snapshot?format=ai&maxChars=0`,
).then((r) => r.json())) as { ok: boolean; format?: string };
expect(snapAi.ok).toBe(true);
expect(snapAi.format).toBe("ai");
const [call] = pwMocks.snapshotAiViaPlaywright.mock.calls.at(-1) ?? [];
expect(call).toEqual({
cdpUrl: cdpBaseUrl,
targetId: "abcd1234",
});
});
it("validates agent inputs (agent routes)", async () => { it("validates agent inputs (agent routes)", async () => {
const { startBrowserControlServerFromConfig } = await import("./server.js"); const { startBrowserControlServerFromConfig } = await import("./server.js");
await startBrowserControlServerFromConfig(); await startBrowserControlServerFromConfig();