* fix: robust compaction with Pi-style token counting + overflow middleware
Root cause: getCurrentTokenCount() returned stale inputTokens from the
previous step, ignoring new tool results added to messages since that
step. A large tool output (DOM snapshot, page content) caused a token
jump that bypassed the compaction threshold check, leading to
context_length_exceeded errors (322K tokens sent, model max 262K).
Layer 1 — Accurate token counting (proactive):
- Adopt Pi coding agent's additive approach: base(inputTokens) +
outputTokens + estimate(trailing tool results)
- Trailing tool results are estimated by walking backwards from end of
messages array until a non-tool message is found
- Falls back to full estimation with safety multiplier when no real
usage data is available (first step of a turn)
Layer 2 — Context overflow middleware (reactive):
- LanguageModelV3Middleware that wraps doGenerate/doStream
- Catches context_length_exceeded errors at the model call level
- Truncates prompt (keeps system messages + most recent non-system
messages targeting 60% of context window)
- Retries the model call once
Verified end-to-end with real model (Gemini Flash Lite via OpenRouter)
on 16K context window: 4 compactions triggered correctly across 8
steps, no context_length_exceeded errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: adopt Pi-style overflow detection patterns + fix truncation edge case
- Replace 6 generic substring matches with 17 provider-specific regex
patterns from Pi coding agent (Anthropic, OpenAI, Google, xAI, Groq,
OpenRouter, Bedrock, Copilot, llama.cpp, LM Studio, MiniMax, Kimi,
Mistral, z.ai)
- Fix truncatePrompt edge case: when the last message alone exceeds the
target, keepFrom was never updated → empty non-system messages. Now
always keeps at least the most recent non-system message.
- Add runtime guard for LanguageModelV3 cast in ai-sdk-agent.ts
- Add tests for false-positive rejection and truncation edge case
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The Kimi K2.5 model supports a 256,000 token context window, not
128,000. Updated the provider template and model config to reflect
the correct value.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: return element coordinates in tool responses and DPR in screenshots
- click, hover, fill, drag now return resolved coordinates in response text
- take_screenshot returns devicePixelRatio for mapping coordinates to pixels
- Coordinates are in CSS pixels; multiply by DPR to get screenshot pixels
* fix: use Promise.allSettled in screenshot to prevent DPR eval from aborting capture
Runtime.evaluate for devicePixelRatio can fail on PDF pages or
chrome-extension pages. Using Promise.allSettled ensures the screenshot
still succeeds, falling back to DPR=1.
* feat: gate Moonshot AI provider behind VITE_PUBLIC_KIMI_LAUNCH flag
Hide all Moonshot/Kimi provider UI when the launch flag is off:
- Filter moonshot from provider templates and type dropdown
- Gate Kimi flare badges in HubProviderRow
- Gate Kimi auto-insertion in LLM hub storage
- Add analytics events for Kimi API key configuration and guide clicks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: allow editing existing moonshot providers when launch flag is off
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add search provider settings page with 5 engine options
Allow users to select their preferred search engine (Google, DuckDuckGo,
Bing, Brave Search, Yahoo) from a new settings page. The selected provider
drives search suggestions, search URL navigation, placeholder text, and
analytics tracking. Replaces all hardcoded Google references with the
stored preference. Adds Brave Search support, replacing Yandex.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add error handling for search provider storage writes
Write to storage before updating React state so UI never diverges from
persisted value on failure. Add try/catch in the settings page to show
an error toast if the write fails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: migrate stale 400k context window for browseros provider
Existing installations cached the old 400k default in extension storage.
Always normalize the browseros provider's contextWindow to 200k on load,
matching the current default and preventing compaction from failing.
* fix: add browseros-auto model with 200k context length
* fix: setup migrations using the migrations api for context window size
---------
Co-authored-by: Dani Akash <DaniAkash@users.noreply.github.com>
* fix: anchor agent to active tab page ID from browser context
Generalize the scheduled-task page anchoring instruction to all tasks.
The agent now always uses the page ID from Browser Context instead of
calling get_active_page or list_pages, preventing it from operating
on the wrong tab.
* fix: add chatMode guard and scope windowLine to scheduled tasks
- Skip page-context section in chat mode where list_pages is allowed
- Only show windowId instruction for scheduled tasks (hidden window)
* feat: integrate models.dev registry for auto-populated model defaults
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: fall back to upstream provider for model registry lookup
When the browseros meta-provider is used, the registry lookup now
also tries the upstream provider (e.g., openrouter, anthropic) so
that BrowserOS-hosted models get correct context window and image
support defaults.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add Object.hasOwn guards to prevent prototype chain lookup
Addresses Greptile review: bracket notation on the registry object
could return prototype-chain properties for keys like __proto__ or
constructor, bypassing the 404 guard in the route handler.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add browseros-cli Go CLI for browser automation
Implements a full-featured CLI that communicates with the BrowserOS MCP
server over JSON-RPC 2.0 / StreamableHTTP. Covers all 54 MCP tools across
10 categories with a hybrid command structure (flat verbs for hot-path
commands, grouped noun-verb for resource management).
- MCP client with initialize + tools/call pattern, thread-safe request IDs
- Dual output: human-readable default, --json for structured/piped usage
- Implicit active page resolution with --page override
- 21 command files: open, nav, snap, click, fill, scroll, eval, ss, pdf,
dom, wait, dialog, pages, window, bookmark, history, group, health, info
- Cobra CLI framework with fatih/color for terminal formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add end-to-end integration tests for browseros-cli
Go integration tests gated by `//go:build integration` that exercise the
CLI binary against a running BrowserOS server. Tests build the binary,
run commands via exec.Command, and verify JSON output.
Covers: health, version, page lifecycle (open → text → snap → eval →
screenshot → nav → reload → close), active page, info, error handling,
and invalid page ID rejection. Skips gracefully when no server is running.
Run with: go test -tags integration -v ./...
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add init command and fix MCP client bugs
- Add `browseros-cli init` command that prompts for the server URL,
verifies connectivity, and saves to ~/.config/browseros-cli/config.json
- Config priority: --server flag > BROWSEROS_URL env > config file > default
- Fix Accept header: include text/event-stream (required by StreamableHTTPTransport)
- Fix nil args: send empty object {} instead of null for tools with no params
- Update error messages to suggest `browseros-cli init` on connection failure
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add README for browseros-cli with setup, usage, and testing guide
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: always send arguments object in MCP tools/call
Go's json omitempty omits empty maps, causing the arguments field to be
missing from tools/call requests. The MCP SDK requires arguments to be
an object (even empty {}), not undefined. Remove omitempty from the tag.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: update help menu to be have groups
* refactor: replace hand-rolled MCP client with official Go SDK
Switch from custom JSON-RPC implementation to the official
github.com/modelcontextprotocol/go-sdk. This removes all hand-rolled
protocol types (jsonrpcRequest, jsonrpcResponse, RPCError, etc.) and
uses the SDK's StreamableClientTransport with DisableStandaloneSSE
for clean CLI process lifecycle.
Also adds URL normalization/validation, config command, and
updates init/README to reference YAML config.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add server-level instructions that get injected into the LLM system
prompt when external MCP clients (Claude Desktop, Cursor, Gemini CLI)
connect. Covers browser automation workflow, Klavis integration
discovery, and auth flow guidance.
* feat: add inline chat experience to new tab page
Bring the full sidepanel chat experience to the new tab page. When
users select an AI suggestion from the search bar, the page transitions
inline to a full chat view instead of opening the sidepanel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove unnecessary comments from NewTab.tsx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address PR review comments
- Move NEWTAB_CHAT_STARTED_EVENT tracking to startInlineChat where it
actually fires (was dead code in NewTabChat handleSubmit)
- Add NEWTAB_CHAT_RESET_EVENT tracking to handleNewConversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: gate newtab chat behind NEWTAB_CHAT_SUPPORT feature flag
When the flag is off (BrowserOS < 0.40.0), falls back to opening the
sidepanel via openSidePanelWithSearch (previous behavior). In dev mode
all features are enabled, so inline chat works during development.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add newtab origin context to chat system prompt
When chatting from the new tab page, the AI is instructed to open
content in new tabs rather than navigating the current tab, keeping
the user's new tab page accessible.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The AI SDK agent (v2) was allowing all 54 browser tools in chat mode,
while the Gemini agent correctly restricted to 6 read-only tools.
Extract CHAT_MODE_ALLOWED_TOOLS to a shared constant and filter
browser tools in AiSdkAgent.create() when chatMode is true.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: expose Klavis MCP tools to external MCP clients
Connect to Klavis Strata at server startup and register discovered tools
on each per-request McpServer instance. This lets external MCP clients
(Claude Code, Gemini CLI) access Klavis-proxied integrations (Gmail,
Slack, GitHub, etc.) alongside browser tools.
- Add register-klavis-mcp.ts with connectKlavisProxy() and registerKlavisTools()
- Wire KlavisProxyHandle through server.ts -> mcp routes -> mcp-server
- Use structured logging and proper type imports
* fix: forward Klavis tool schemas and add shutdown cleanup
- Use zod-from-json-schema to convert Strata's JSON Schema to Zod,
so MCP clients see proper parameter names, types, and required fields
- Close Klavis proxy transport on server shutdown
- Move per-request Klavis tool registration logging to debug level
- Use proper type imports instead of inline import() types
- Fix connectKlavisProxy return type (never returns null)
* fix: add timeout to Klavis MCP connect/listTools and log shutdown errors
* fix: clear timeout timer and pre-compute Klavis tool schemas at startup
* fix: use client.close() instead of transport.close() for proper cleanup
Add SOUL_SUPPORT feature flag to capabilities system requiring
minServerVersion 0.0.67. Hides "Agent Soul" nav item in settings
sidebar for older servers that lack the /soul endpoint.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- Add `VITE_PUBLIC_KIMI_LAUNCH` feature flag controlling Kimi partnership branding
- BrowserOS provider card shows "Powered by Kimi K2.5 from Moonshot AI" badge and "Extended usage limits for the next 2 weeks!" when flag is on
- Moonshot/Kimi highlighted as "Recommended" in provider templates
- LLM Hub defaults to Kimi, ChatGPT, Claude, Gemini (with legacy defaults migration)
- Kimi hub row shows "Powered by Moonshot AI" flare
- Model selector locked to kimi-k2.5
- "How to get a Kimi API key" link in provider dialog
- Moonshot provider fully integrated across frontend and backend
* fix: refactor SDK BrowserService to use Browser class directly
The tools system was completely rewritten with new tool names and response
formats. BrowserService was calling non-existent MCP tools (browser_get_active_tab,
browser_navigate, etc.) that returned structuredContent which no longer exists.
Replaced MCP HTTP client calls with direct Browser class method calls:
- getActiveTab → browser.getActivePage() / browser.listPages()
- getPageContent → browser.contentAsMarkdown()
- getScreenshot → browser.screenshot()
- navigate → browser.goto() with tabId/windowId resolution
- getPageLoadStatus → browser.listPages() with isLoading check
- getInteractiveElements → browser.snapshot() / browser.enhancedSnapshot()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address PR review — consistent tabId guard and remove dead PageContent type
- Change `if (tabId)` to `if (tabId !== undefined)` in navigate() to match
the guard style used for windowId and elsewhere in the file
- Remove orphaned PageContent interface no longer imported after refactor
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
SIGQUIT (Ctrl+\) was not in the signal notify list, causing Go's default
handler to dump goroutines. On macOS ARM64 this triggers a known runtime
bug where semasleep panics on the signal stack.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add "don't show again" checkbox to JTBD survey popup
Mirrors the ImportDataHint pattern — adds a checkbox that permanently
suppresses the survey popup when checked and dismissed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: persist dontShowAgain when user clicks Take Survey
Addresses Greptile review — if the checkbox is checked and the user
clicks "Take Survey", persist the flag before opening the survey so
the popup won't reappear if the survey tab is closed without starting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: show "don't show again" only after 2nd popup, increase interval to 10 msgs
- Track shownCount in storage, only show checkbox on 3rd+ appearance
- Increase MESSAGE_THRESHOLD from 5 to 10 messages between popups
- Add DONT_SHOW_AGAIN_AFTER constant (2) for configurability
- Pass showDontShowAgain through the component chain
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: thread dontShowAgain through onTakeSurvey to avoid duplicate analytics
Addresses Greptile review — previously clicking "Take Survey" with the
checkbox checked would fire both dismissed and clicked events. Now the
dontShowAgain flag is threaded through onTakeSurvey, which persists it
without firing a dismiss event.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The --new flag creates a fresh temp profile directory but WXT's
chromiumProfile was hardcoded to /tmp/browseros-dev, ignoring it.
Pass BROWSEROS_USER_DATA_DIR env var from the Go dev tool and read
it in web-ext.config.ts.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: filter out messages with empty parts to prevent follow-up crash
When an assistant response is interrupted or errors before producing content,
a UIMessage with empty parts remains in the chat state. On the next send, the
AI SDK validates all messages and rejects the empty-parts message with
"Message must contain at least one part". This filters them out when not
streaming and adds a safety guard in formatConversationHistory.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: filter empty-parts messages before persisting to storage
Addresses race condition where the save effect could persist messages
with empty parts before the cleanup effect's state update applies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: limit claude code review to PR creation and @claude comments
Reduces unnecessary action runs and token usage by only triggering the
review on initial PR open, and re-running when @claude is mentioned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: restrict @claude trigger to trusted contributors
Only repo owners, org members, and collaborators can invoke the review
via @claude comments, preventing external users from consuming token quota.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: consolidate claude workflows and auto-run on PR creation
Remove separate claude-code-review.yml and add pull_request trigger
to claude.yml so it runs automatically on PR open without needing
@claude in the body.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: restore author_association guard on issue_comment trigger
The consolidation commit dropped the author_association check from the
issue_comment condition. Without it, any external commenter could invoke
Claude and consume token quota. Restores the guard to limit triggers to
OWNER, MEMBER, and COLLABORATOR.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: apply author_association guard to review comment triggers
Extends the OWNER/MEMBER/COLLABORATOR check to pull_request_review_comment
and pull_request_review events, preventing external users from triggering
Claude via review comments.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: gate previousConversation array format behind BrowserOS 0.41.0.0
Older servers reject the array format for previousConversation with a
ZodError ("Expected string, received array"). Gate the feature behind
BrowserOS >= 0.41.0.0 which bundles server >= 0.0.64 that accepts both
array and string formats.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use minServerVersion 0.0.64 for previousConversation gate
Server version is the direct indicator of schema support, more accurate
than using BrowserOS version as a proxy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: fall back to string format for previousConversation on old servers
Instead of omitting previousConversation entirely on servers < 0.0.64,
serialize the conversation history as a "role: content" string which
old servers accept via their z.string() schema.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add get_dom and search_dom tools for HTML DOM inspection
Add two new observation tools:
- get_dom: Returns raw HTML of a page or scoped element via CSS selector
- search_dom: Fuzzy searches DOM elements by text, attributes, IDs, and
class names using Fuse.js with extended search syntax support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: use CDP DOM protocol instead of injected scripts for DOM tools
Replace Runtime.evaluate-based approach with native CDP DOM methods:
- get_dom uses DOM.getDocument + DOM.querySelector + DOM.getOuterHTML
- search_dom uses DOM.performSearch + DOM.getSearchResults + DOM.describeNode
- Remove fuse.js dependency (CDP performSearch handles text/CSS/XPath natively)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add comprehensive tests for get_dom and search_dom tools
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve text nodes to parent elements in searchDom
CDP performSearch returns text nodes (nodeType 3) for plain text queries.
describeNode does not populate parentId, so use resolveNode + callFunctionOn
to get parentElement, then requestNode to obtain the parent's nodeId.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add limit bounds validation and searchId leak prevention
- Add .int().min(1).max(200) to search_dom limit parameter
- Wrap searchDom result processing in try/finally to ensure
discardSearchResults is always called
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Tests were passing raw Chrome tabIds to group_tabs and ungroup_tabs tools,
but the Zod schemas expect pageIds (MCP-layer page IDs). The tabIds field
was silently stripped during validation, causing both tests to fail.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add new CDP tools for links, hidden pages/windows, show/move
- get_page_links: extract deduplicated links from a page via evaluate
- new_hidden_page: open a hidden tab for background automation
- create_hidden_window: create a hidden window for background automation
- show_page: restore a hidden page back into a visible window
- move_page: move a tab to a different window or position
- Default includeLinks to false in get_page_content
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: use AX tree for get_page_links, add tests, fix test scripts
- Refactor get_page_links to use accessibility tree instead of raw JS
evaluate — more reliable for role="link" elements and shadow DOM
- Add extractLinkNodes() to snapshot.ts and getPageLinks() to browser.ts
- Add tests for get_page_links (constructed HTML with dedup/filtering),
new_hidden_page, show_page, move_page, create_hidden_window
- Fix root package.json test scripts to match server's actual scripts
- Update CLAUDE.md test docs to reflect current structure
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: move ChatV2Service to api/services layer and add resolvePageIds
Move ChatV2Service from agent/tool-loop/ to api/services/ where it
belongs as a service-layer concern. Add resolvePageIds() to convert
Chrome tab IDs to internal page IDs before they reach the agent,
fixing undefined pageId issues in browser automation tools.
Clean up server.ts by removing the USE_TOOL_AGENT flag, SessionManager,
and old chat route import — both /chat and /chat-v2 now directly use
createChatV2Routes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review comments for chat-v2-service
- Fix TOCTOU race: derive isNewSession inside the creation block
instead of separate has()/get() calls
- Log warning when resolvePageIds can't map a tab ID
- Deduplicate tab IDs with Set before resolving
- Remove redundant null check on session in onFinish
- Add license header
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update bun.lock
* fix: skip resolvePageIds for scheduled tasks to prevent pageId corruption
Scheduled tasks build browserContext with internal page IDs from
browser.newPage(), not Chrome tab IDs. The unconditional second
resolvePageIds() call was passing these internal IDs to resolveTabIds()
which expects Chrome tab IDs, causing the lookup to fail and overwrite
correct pageIds with undefined.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add biome-ignore comments for noExcessiveCognitiveComplexity on compaction.ts
and grep.ts, and noExplicitAny on filesystem test helpers.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: generalized compaction prompts with split turn handling
Replace browser-specific XML prompts with domain-agnostic markdown format.
Add split turn detection and parallel summarization for large single-turn
conversations. Switch compaction from generateText to streamText for
Fireworks API compatibility. Add comprehensive unit and E2E tests (84 total).
* fix: address code review issues for compaction (PR #391)
Enforce COMPACTION_MAX_SUMMARIZATION_INPUT cap, extract shared
callSummarizer helper, add runtime type guard for experimental_context,
move magic constants to AGENT_LIMITS, and remove dead constants.
* fix: cap truncatedTurnPrefix input to maxSummarizationInput
Apply the same sliding window cap to turn prefix messages that was
already applied to toSummarize, preventing unbounded LLM input for
long single-turn conversations with many tool calls.
* fix: reduce browseros-auto default context window to 200K
The 400K setting caused compaction to trigger at ~383K, but the actual
model limit is 262K. Conversations hit the hard limit before compaction
could kick in.