Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
60 KiB
60 KiB
Troubleshooting: Toolchain, Browser, And Terminal
Standalone Agent provider login reports an internal error
- Symptom: A provider login request fails in a standalone Agent window even though a Workbench host appears to be bound, or an HTTP/file action unexpectedly opens an OS application.
- Quick checks:
Distinguish the AgentGUI
surface.hostfrom the standalone right-side tool host. Verify that the renderer registered the workspace-scoped Terminal, Browser, and Files presenters before announcing tool-host readiness. - Root cause: The AgentGUI surface host owns the Agent node, not Desktop tool nodes. Reusing it as a Workspace Workbench host makes Terminal launch return no node, while standalone-specific link/file shortcuts can bypass the in-app tools.
- Fix: Keep semantic launch coordinators shared and register Desktop presentation adapters per mode. The standalone adapter creates/selects sidebar tabs and reserves OS navigation for explicit external or reveal actions.
- Validation: Verify provider login opens a new Terminal tab, ordinary HTTP and file preview stay in right-side tools, explicit Open in Finder remains external, and Browser automation follows the canonical Turn's validated renderer claim (with Workspace fallback only for an unclaimed legacy Turn).
Go-only PR skips a repository contract that later fails
- Symptom: A Go-only PR is green, but a later TypeScript PR fails an unrelated repository-wide check or tool test against the Go change.
- Quick checks:
Inspect whether the failing command scans repository-wide files, generated
artifacts, workflows, hooks, or architecture boundaries. Then check whether
CI attached it to
run_tsonly because the checker is implemented in JavaScript or TypeScript. - Root cause: Check ownership was classified by implementation language instead of checked responsibility. Generated-source paths and non-code package assets could also be absent from inline workflow path predicates.
- Fix:
Register repository policy, tool contract, generated contract, or boundary
checks in
repository-checks.mjs. Keep TypeScript and Go jobs limited to language-owned lint and tests. Reusechange-classification.mjsin local and PR validation. - Validation: Add selector fixtures for every source-of-truth path and a narrow workflow contract test that verifies repository groups stay outside language jobs. Confirm a Go-only fixture does not select TypeScript validation.
- References: repository-checks.mjs change-classification.mjs testing.md
Goal recovery Go tests fail only in the full workspace lane
- Symptom:
services/tuttid/service/agentgoal recovery tests pass alone but fail in the pull-request Go workspace job withrecover goal operation ...: context deadline exceeded. The failing test can take several seconds even though its fake provider timeout is configured for 20–25 milliseconds. - Quick checks:
Inspect whether the test uses a small real
GoalOperationAttemptTimeoutaround the entire worker step. Compare the CI duration with the configured timeout and check whether the fake provider hook was reached before changing Host retry or lease semantics. - Root cause: The attempt context also covers actor acquisition and SQLite work before the fake provider call. Under concurrent Go workspace load, the runner may not schedule that work within a 20–25 millisecond test window. The context then expires before the intended fake deadline path, so the worker reports an infrastructure timeout instead of exercising retry persistence.
- Fix:
For deadline classification and retry-budget tests, return
context.DeadlineExceededdirectly from the fake provider. Use a test-controlled deadline context when cancellation propagation and detached lease persistence are the behavior under test. Reserve real wall-clock deadlines for end-to-end budget tests and give those assertions enough scheduling margin for the full workspace lane. Do not add retries or change production lifecycle timeouts to mask the test race. - Validation: Run the affected goal tests repeatedly with shuffled order, then run the full agent-service package and changed-aware validation.
- References: service_test.go Testing
gomobile Android AAR fails after Go compilation succeeds
- Symptom:
GOOS=android GOARCH=arm64 CGO_ENABLED=0 go build ./...succeeds, butgomobile bindeither reportsgobind was not foundaftergomobile init, or fails while linkinglibgojni.sowithgithub.com/wlynxg/anet: invalid reference to net.zoneCache. - Quick checks:
Confirm the module pins
golang.org/x/mobileat a version compatible with the repository Go baseline. Rungo mod why -m github.com/wlynxg/anet; Pion ICE reaches it through the Android network enumeration path. Check whethergobindis actually present onPATHduring the bind subprocess rather than assuminggomobile initinstalled it globally. - Root cause:
go tool gomobileruns the pinned tool without installing its siblinggobindexecutable. Separately,anetusesgo:linknamefor the Go network zone cache, and Go 1.23 or newer rejects that internal reference unless the linker compatibility flag is explicit. A no-CGO package compile does not exercise the same shared-library link step as an AAR build. - Fix:
Build the module-selected
golang.org/x/mobile/cmd/gobindinto an isolated temporaryGOBIN, prepend that directory toPATHonly forgomobile bind, and pass-ldflags=-checklinkname=0only to that gomobile build. Pin the NDK version throughANDROID_NDK_HOME; do not add a localanetfork or disable linker checks for ordinary host builds. - Validation:
Run
make android-crosscompile,make android-bindings-check, andmake android-aar. The final check must validate the Java binding andarmeabi-v7a,arm64-v8a,x86, andx86_64libgojni.soentries. A real Android device or emulator is still required to execute the loopback probe. - References: DeviceLink Makefile DeviceLink README
Android ICE probe gathers no candidates
- Symptom:
A gomobile DeviceLink probe loads its JNI library and enters Go successfully,
but
LocalParamsreports that the ICE agent gathered no candidates even when loopback candidates are enabled. - Quick checks:
Confirm the test or host app declares
android.permission.INTERNETin its manifest before changing ICE filters or Android interface-enumeration code. - Root cause:
Android applies the
INTERNETmanifest permission to network sockets. A minimal hand-built probe APK can omit it even though normal application templates usually include it, leaving Pion unable to bind a UDP candidate. - Fix:
Add the
INTERNETpermission to the probe or product manifest. Do not add an Android-only candidate fallback to the shared transport core for this case. - Validation:
Run the signed probe on an Android device or emulator and require a
TuttiDeviceLinkProbelog entry containingPASS epoch=1and the expected echoed payload. - References: Probe manifest DeviceLink README
Temporary Git fixture turns a linked worktree bare
- Symptom:
A test run leaves the shared repository config with
core.bare=true, writes fixture author identity into.git/config, or creates aninitcommit that deletes most tracked files from a linked-worktree branch. - Quick checks:
Run
git config --show-origin --get core.bare, inspect localuser.nameanduser.email, then inspect the affected branch reflog for a fixture-authored commit. Search the responsible test for temporary-repository Git commands whose child environment inheritsGIT_DIRorGIT_WORK_TREE. - Root cause:
mkdtempisolates files, not Git repository selection. An inherited linked-worktreeGIT_DIRoverrides the fixture cwd, sogit initreinitializes the caller's private worktree metadata and updates its shared common config. Later fixtureaddandcommitcommands can then stage the fixture tree against the real branch. - Fix:
Remove repository-local Git environment variables at the validation-lane
spawn boundary and for every fixture Git command using case-insensitive name
matching. Let normal lanes rediscover the repository from their cwd. For
fixtures, also set
GIT_CEILING_DIRECTORIESto the fixture root, stop on any command failure, verify--absolute-git-dirafter initialization, and pass fixture author identity through commit-local-carguments instead ofgit config. - Validation:
Run
run-validation-lanes.test.mjsto confirm spawned lanes remove poisoned Git selectors while preserving unrelated environment, then rungit-environment.test.mjs, which launches the temporary-repository test suites with a poisoned linked-worktreeGIT_DIRthat points only at a disposable repository. Confirm each fixture initializes its own.git, then verify the caller's config and branch remain unchanged. Keep the lower-level environment test coverage forGIT_WORK_TREEandGIT_CONFIG_*inputs. - References: git-environment.mjs run-validation-lanes.mjs check-agent-gui-degradation.test.mjs push-checked.test.mjs run-check-changed.test.mjs static-analysis.md
Dynamic CLI input rejects plausible flags
- Symptom:
A dynamic
tutti-devcommand prints normal-looking help, but invocation fails withinvalid input "<flag>"or an app-level invalid-argument error even though the flag name and shell syntax are correct. - Quick checks:
Inspect the command input struct tags under
services/tuttid/service/cli/providers. Confirmvalidate:"min=...,max=..."bounds and any finite string values such as status, priority, or source are represented in the framework input schema. - Root cause:
Dynamic CLI help and agent command guides are generated from daemon capability
schema, while actual invocation is bound and validated later by the daemon.
If the schema omits enum/range metadata, agents may guess plausible but
invalid values such as
--status openor an out-of-range page size. - Fix:
Keep finite string sets in
enum:"..."tags and numeric bounds invalidate:"min=...,max=...". The framework should reject invalid enum/range input with a reason before provider code sees the request. - Validation: Add provider tests that assert both advertised schema metadata and invalid input errors for the affected command.
- References: input.go issues.go
GitHub Actions pnpm setup fails with ERR_PNPM_BAD_PM_VERSION
- Symptom:
GitHub Actions jobs fail in the
pnpm/action-setupstep withERR_PNPM_BAD_PM_VERSIONor "Multiple versions of pnpm specified" afterpackage.jsongains an integrity-pinnedpackageManagervalue such aspnpm@10.11.0+sha512.... - Quick checks:
Inspect every workflow that uses
pnpm/action-setup. If the workflow passeswith.versionwhile the rootpackage.jsonalso declarespackageManager, the action sees two pnpm targets. - Root cause:
pnpm/action-setupreadspackageManagerfrompackage.jsonby default. Passing a separateversioninput duplicates the same version source, and an integrity-pinnedpackageManagerstring makes the mismatch explicit. - Fix:
Keep
package.jsonas the single pnpm version source. Remove thewith.versioninput frompnpm/action-setupsteps instead of weakening the rootpackageManagerintegrity pin. - Validation:
Search workflows for
pnpm/action-setupand confirm no step still passes aversioninput. Push a new commit to rerun the PR checks.
Multi-entry declaration build exhausts its worker heap or dominates release time
- Symptom:
A package build finishes its ESM phase, then fails during
DTS Build startwithERR_WORKER_OUT_OF_MEMORY. The failure is more frequent whencheck:changed --push-readyruns package builds beside tests and other builds, while an isolated retry may pass. - Quick checks:
Confirm the error comes from tsup's declaration worker after the JavaScript
output reports success. Measure an isolated build with
/usr/bin/time -land compare the package's declaration entry count; raising the parent Node heap does not prove the worker's declaration graph is bounded. - Root cause: tsup asks its declaration worker to typecheck and roll up every configured entry together. A package with many overlapping public entrypoints can keep repeated TypeScript and Rollup declaration graphs until it approaches the V8 heap limit. Splitting entries across workers bounds each heap, but every worker still repeats much of the same TypeScript analysis and can make the declaration phase dominate release time.
- Fix:
Keep the runtime bundle as one build. Pre-emit unbundled declarations once
with the repository-pinned native TypeScript compiler and a declaration-only
tsconfig whose root files exactly match the runtime entries. Then let a
bounded set of tsup workers roll up the emitted declarations and remove the
intermediate tree. Keep workspace source path mappings out of the
declaration-only config so dependency sources do not leak into the package
emit. Test that the runtime entries, declaration roots, rollup groups, and
published type paths stay aligned. Do not make a global
NODE_OPTIONSincrease the default fix; it preserves the oversized declaration graph and moves the failure threshold. - Validation: Measure the isolated package build before and after, run the entry-coverage test, typecheck imports from both the root and a subpath declaration, run the package pack check, and reproduce the original changed-aware concurrent build without a heap override.
- References: agentGuiBuildEntries.ts tsup.dts.config.ts tsup.dts.config.test.ts
Browser CLI cold start timeout looks like an unreachable daemon
- Symptom:
An agent runs a command such as
tutti-dev browser list-pagesand getsdaemon is not reachable, but desktop and daemon logs showtuttidis running and a Chrome or browser-use process may still appear. - Quick checks:
Confirm the listener file under the active
TUTTI_STATE_DIRhas a live address and token, then runtutti-dev status --json. If status succeeds but the browser command fails after roughly the CLI client timeout, inspect whether the first browser command is lazily startingchrome-devtools-mcpor another browser backend. For browser backend overrides, inspectTUTTI_BROWSER_MCP_COMMAND,TUTTI_BROWSER_MCP_ARGS, and the packaged desktop's internalTUTTI_BROWSER_MCP_ENTRY_PATHhandoff. Packaged desktop handoffs should launch that vendored entry with the daemon's managednode-staticruntime, not a barenodefrom the user'sPATH. - Root cause:
Browser commands can do a cold start on first use. The daemon may launch the
browser backend while the CLI HTTP request is still waiting for the daemon to
finish the tool call. If the CLI client times out first and collapses every
transport error into
daemon is not reachable, the message describes the timeout incorrectly instead of the daemon's actual reachability. - Fix: Keep the CLI daemon client timeout long enough for browser backend cold starts, and report request timeouts separately from connection failures. Avoid treating a visible browser window as proof that the browser tool call has completed.
- Validation:
Add CLI client tests for the default timeout and timeout-specific error
message. For a live smoke test, verify
tutti-dev status --jsonsucceeds and then run the browser command again after the first cold start settles. - References: client.go session.go command.go
Browser Agent retries plausible commands that the CLI rejects
- Symptom:
An Agent eventually opens the requested page but its transcript contains
repeated failures such as
browser openreturningcommand_not_found,json output is not supported, or a coldnavigatereturningtarget closed while handling command. - Quick checks: Inspect the session's exact tool calls rather than relying on the final Agent message. Compare the Browser command-group help with each leaf command's advertised output modes, and check whether the first operation tried to navigate a selected page before any workspace Browser page existed.
- Root cause:
openwas an intuitive but missing command, command-group help advertised a generic--jsonflag even when leaf commands rejected JSON, and the Browser skill led with page-scopednavigateinstead of page-creatingnew_page. Providers therefore converged on different retry sequences and surfaced avoidable failures before finding a working command. - Fix:
Provide
browser open --url <url>as the direct page-creation path, keepnew-pagefor explicit tab management, support JSON consistently across the Browser command family, and advertise--jsononly on leaf commands that support it. Lead the Browser skill withopen; reservenavigatefor an already selected page. - Validation:
Test that
browser.openmaps tonew_pagewith the exact workspace and Agent session, that every Browser command accepts JSON output, and that CLI help does not promise JSON for unsupported commands. In a live Agent session, send/browserwith a public URL and confirm the first Browser command succeeds without retries. - References: commands.go run.go browser-use skill template
Malformed user skill frontmatter breaks skill discovery
- Symptom:
Agent logs include
failed to load skill ... missing YAML frontmatter delimited by ---, and the composer skill picker may show partial or confusing skill results. - Quick checks:
Search daemon logs for
skill_frontmatter_invalid, then inspect the loggedskillPath. User-owned~/.codex/skills/*/SKILL.mdand~/.agents/skills/*/SKILL.mdfiles must start with a---line and include a closing---line before the body. - Root cause: Provider-native skill loaders expect delimited YAML frontmatter. If Tuttid exposes a malformed user skill into provider runtime state, or includes it in composer skill options, one bad local skill can pollute diagnostics around otherwise valid skills.
- Fix:
Skip user Codex skill folders with malformed frontmatter before exposing them
under the session
CODEX_HOME/skills, and skip malformed provider skills during composer skill option discovery so valid sibling skills continue to be recognized. Emit a structured warning witherror_code=skill_frontmatter_invalidwhenever a malformed skill is skipped. - Validation:
Add tests with malformed personal
.codexand.agentsskills beside valid skills, then runpnpm lint:goandcd services/tuttid && go test ./... && go build ./.... - References: codex.go skill_options.go
Browser Node failed navigation renders a blank panel
- Symptom:
Opening an unreachable URL or an HTTP error page in Browser Node shows an
empty panel or
about:blank/Chromium error state instead of the package error card. - Quick checks:
Inspect desktop logs for
Browser Node guest navigation failedorBrowser Node guest navigation returned HTTP error, then confirm the renderer runtime keepserrorafter laterstateevents. In DevTools, do not stop at the<webview src="about:blank">; verify whether the React error card is present in the DOM and whether a later state event removed it. - Root cause:
Electron emits several events for the same failed navigation. If
did-fail-loador an HTTP status error emits an error before a laterpublishState, a runtime reducer that clears errors from ordinary state updates can erase the error card and leave only the blank webview. Browser event subscriptions tied only to mounted node components can also miss events while the workbench node body is not mounted. - Fix:
Treat
did-fail-loadand HTTPdid-navigatestatus codes as Browser Node navigation failures, publish any immediate state first, and emit the final error after it. Keep runtime errors through non-loading state updates and ignore Chromium internal error URLs such aschrome-error://chromewebdata/when preserving the user-facing URL. Keep a workspace-level browser service connected to the Browser Node feature so host events are not owned only by React component mount effects. - Validation:
Add package tests that HTTP
>=400emitsnavigation-failed, that failed navigations leave the error as the final event, thatdid-fail-loadandloadURLrejection are not double-counted, and that runtime errors survive Chromium error-page state. For desktop integration, add coverage that browser events update runtime state without mounting the Browser Node component. - References: guestManager.ts runtimeStore.ts workspaceBrowserService.ts BrowserNode.tsx
Browser Node same-origin target-blank link does nothing
- Symptom:
Clicking a
target="_blank"link in the full workspace Browser leaves the current page unchanged and opens no tab. Desktop logs show the guest preload reportingaction=open-urlwithdefaultPrevented=true, followed by bothBrowser Node guest open-url IPC receivedand the guestopen-urlemission, but no navigation to the requested URL. Cross-origin popup links may still appear to work. A partial host fix can instead create a new tab whose first guest URL is the product home page, such as Google, for every requested link. - Quick checks:
Compare the current and requested origins. Confirm the event's
sourceNodeIdnames an existing:tab:*child and that the workspace Browser route is registered with sourcebrowser. - Root cause:
The guest preload correctly suppresses Chromium's native popup and delegates
the URL to the host. Reusing the Browser surface through Workbench activation
then changed only the active tab's
defaultUrl. Browser Node's passive host synchronization intentionally ignores same-origin differences to avoid fighting in-page and authentication redirects, so the explicit popup URL was never loaded. After routing the popup into a real tab, there is a second materialization boundary: the active child has no runtime state until its guest mounts. Falling straight back to the static product home page at that point synchronizes the home page over the new tab's requested URL. - Fix:
Keep the same-origin synchronization guard. Route Browser-owned
open-urlevents by their exact source child ID, create and select a new tab on that Browser surface, and retain the existing Workbench launch fallback for URLs emitted by Workspace Apps or unavailable Browser surfaces. Treat the route registration as a Workbench-session resource: replace an earlier route for the same workspace/source generation and dispose all workspace routes when its session closes. In the package Workbench adapter, resolve the active tab's stored URL before the static product home while runtime state is absent; explicit activation and restored runtime state must keep higher priority. - Validation: Cover a same-origin popup from an existing Browser child and assert that a second selected tab owns the requested URL while no Workbench launch occurs. Also resolve the new tab before it has runtime state and assert its requested URL wins over the product home page. Rebuild the Browser contribution and assert only the latest feature receives the popup, then dispose the workspace and assert its feature no longer receives events. Also retain coverage that Workspace App URLs still launch through the workspace Browser coordinator.
- References: workspaceBrowserService.ts tabsStore.ts nodeController.ts
Workspace App authorization opens two Browser windows
- Symptom: One Workspace App connection action opens two internal Browser windows for the same authorization URL.
- Quick checks:
Starting from one user action, count Electron window-open producer callbacks,
workspace app emitted open-urlevents, Workbench launch requests, and materialized Browser surfaces. Popup events should reportproducer=window-open-handler. Equal URLs do not prove duplicate delivery, and different OAuth query parameters do not prove separate user intent. - Root cause: Intercepting one cross-origin blank-link in preload while also installing Electron's window-open handler creates two popup transports. A downstream workspace, source node, local UUID, or URL cannot prove that independently produced events represent one user action. Renderer deduplication therefore masks the producer boundary and can merge intentional popup requests.
- Fix:
Leave blank links,
window.open, and popup forms to Electron'ssetWindowOpenHandler; preload must not globally intercept clicks or patch page APIs. Install one delegate while attaching the guest and give its host-provided Workspace App route priority. Browser NoderegisterGuestmay update only the delegate's ordinary Browser route; it must not install a second handler. Main compares accepted HTTP(S) targets with the current guest origin: internal URLs navigate the current guest after the handler returnsdeny, while external URLs emit one Browser event. Reject POST and empty/about:blankdeferred popups with localized feedback instead of losing the request body or later navigation. Keep the renderer as a stateless one-event/one-launch adapter. - Validation:
Start from one real internal popup and assert one producer callback, current
guest navigation, and zero Browser events, launches, surfaces, and native
child windows. Then start from one real external popup and assert producer
callback count one, Browser event count one, Workbench launch count one, and
surface count one.
Two events indicate duplicate production; one event with two launches
indicates duplicate routing or subscription; one launch with two surfaces
indicates a Workbench materialization race. Two real
window.openrequests must remain two independent launches even when their URLs are equal. The Electron integration fixture coversdid-attach-webviewfollowed byregisterGuest, installs the real Workspace App preload, and uses different loopback origins for the guest and popup target. It then covers blank links, internal blank links,window.openreturn semantics, external blank links, externalwindow.open, GET forms, deferred-popup rejection, and POST rejection. Its owner renderer runs the real workspace Browser event service, launch coordinator, presenter, and public Workbench host with the production Browser multi-instance launch handler, so one process asserts callback, Browser event, launch, materialized surface, rejection notification, and denied native-child counts. The package boundary test separately asserts that guest attachment installs one stable Electron handler. - References: workspaceAppWindowOpen.ts workspaceBrowserService.ts workbenchWorkspaceBrowserPresenter.ts
Agent opening several pages repeatedly steals workspace focus
- Symptom:
One Agent Turn asks to open several URLs. Every page is created successfully,
but each
new_pageresult reveals and focuses the workspace again, producing repeated foreground jumps during the same response. - Quick checks:
Confirm the calls carry the same Agent session and persisted active Turn ID.
Separately count
Browser automation host activatedlogs and workspace Browser surface-focus requests. One host-activation log with several visible Browser switches means the renderer reveal path is repeating even though Main's window activation is already deduplicated. - Root cause: There are two foreground effects: Desktop Main activates the owning Electron window, while the renderer focuses or opens the Browser surface before creating a tab. Deduplicating only Main's post-create activation still lets every renderer create request switch the workspace or standalone Agent panel back to Browser.
- Fix: Resolve the existing persisted active Turn in the daemon Browser CLI adapter, carry it opaquely through BrowserNode automation, and keep the reveal policy in Desktop Main. Mark only the first create request for each workspace, Agent session, and Turn as revealable. The renderer must still create and select every requested tab, but it focuses the workspace Browser surface or opens the standalone Browser panel only when that flag is set. Bound the remembered reveal keys and preserve the previous behavior when no exact Turn identity is available.
- Validation:
Issue three concurrent create requests for one Turn and assert that all three
pages are requested with reveal flags
true,false,falsewhile the host activates once. Assert the workspace and standalone renderer handlers do not focus/open their Browser surface forreveal=false. Then issue a create for a different Turn and assert it reveals again. Also cover turnless/manual requests, BrowserNode transport parsing, and daemon active-Turn propagation. - References: browserAutomationCoordinator.ts provider.go automationRegistry.ts
Standalone Agent Browser Node is blank and never attaches a guest
- Symptom:
The standalone Agent window opens its Browser sidebar with the expected
title and panel background, but no page, error card, or Browser Node guest
appears. Desktop logs contain no
Browser Node webview will attachentry for the standalone browser node. - Quick checks:
Inspect
window.tutti.browserin theview=agentrenderer before debugging BrowserNode lifecycle or network access. Compare the preload route gate forview=agentwithview=workspace. An absent browser API explains a panel that renders only host chrome and never reaches Electron guest attachment. - Root cause:
The desktop preload exposed browser and workspace-app bridges only when the
renderer query used
view=workspace. Standalone Agent windows useview=agent, so their renderer received noDesktopBrowserApi; the sidebar correctly reserved panel space but had no host API with which to activate or register a<webview>guest. - Fix:
Treat both
workspaceandagentas workspace surfaces in the preload route gate. Keep dashboard and unrelated window routes excluded. Because preload code is loaded when the Electron renderer is created, restart the Electron process after changing this gate; renderer HMR is insufficient. - Validation:
Unit-test the route predicate for
workspace,agent,dashboard, and an absent view. Run the desktop typecheck, Electron runtime-boundary check, and desktop build. Confirm the preload remains a self-containedindex.cjs, then open the Agent Browser panel and verify desktop logs record the shared Browser Node partition attaching with the browser guest preload. - References: main.ts workspaceSurfacePreload.ts StandaloneAgentToolSidebar.tsx
/browser with adjacent CJK text reaches the provider as an unknown ACP command
- Symptom:
Typing
/browser打开百度or/computer点击确认returns a provider error such asUnknown ACP command, while the equivalent command with a space works. The behavior can appear intermittent because selecting the Composer palette entry inserts a canonical command token with a trailing space, whereas directly typed text may not contain that boundary. - Quick checks:
Compare the submitted user prompt with the provider's
session/promptinput. If the raw slash text reaches ACP unchanged and its byte length matches the exact no-space draft, the local capability rewrite did not run. This is distinct from Browser Node launch, attachment, and authorization failures. - Root cause:
The local browser/computer capability parser previously treated the complete
non-whitespace token as the command name. Adjacent CJK arguments therefore
changed
browserinto the unknown namebrowser打开百度, bypassing the browser-use handoff and exposing the raw text to providers with native slash command parsing. - Fix:
Recognize an adjacent suffix as capability arguments when it begins outside
the ASCII command-name character set. Preserve exact matching for ASCII
command continuations so strings such as
/browseringand/computer2do not become local capability commands. Keep this logic provider-neutral and normalize the visible prompt to include the canonical separating space. - Validation: Cover English and localized browser/computer aliases with both spaced and adjacent CJK arguments, plus negative ASCII-prefix cases. At the Composer policy boundary, verify the draft is rewritten to the injected skill prompt and carries the required capability settings patch instead of reaching ACP as a raw slash command.
- References: agentCapabilityUseSubmit.ts agentSlashCommandProviderPolicy.ts
In-app Browser new-page fails to attach for every provider
- Symptom:
/browserfails for Tutti Agent, Codex, Kimi Code, Claude Code, or another Agent provider.tutti browser new-pagereturnsIn-app Browser page did not attach: browser:...:tab:1, whilelist-pagesremains empty. Desktop logs show the automation server listening and the workspace renderer returning a stable node id, but never show that node registering as a Browser automation target. - Quick checks:
Confirm the session advertises browser use and the slash command was rewritten
to the browser-use skill before debugging command discovery. If
new_pagereaches the desktop host and returns a node id, inspect that node's initial lifecycle and URL. Browser automation intentionally starts atcoldandabout:blank; the creation path must carry an explicit cold-materialization intent so the guest exists before the registry can attach request interception and navigate. - Root cause:
All Agent providers use the same BrowserNode automation authority.
new_pagedeliberately creates anabout:blanktab in the full workspace Browser before enabling its CDP request guard. Treating every cold blank page as a home surface suppresses the<webview>guest, so the automation registry can never observe the node id returned by the renderer. The request then times out identically for every provider. - Fix:
Materialize a cold Browser Node guest only when the automation creation path
marks that tab with the explicit materialization intent. Keep ordinary cold
tabs guest-free even though workspace Browser tabs also carry automation
target identity. Do not bypass the guarded
about:blanksequence or navigate before request interception is enabled. - Validation:
Assert that an explicitly materialized cold Browser Node renders a webview
while an ordinary automatable cold tab remains guest-free. Run the Browser
Node package tests and the
changed-aware repository checks. In a running desktop, execute
tutti browser new-page --url https://example.comfrom sessions belonging to multiple providers, verify the full workspace Browser appears with each page inlist-pages, and close the temporary pages. - References: automationRegistry.ts webviewController.ts workspaceBrowserService.ts
Agent reports a loaded page but the full workspace Browser is not visible
- Symptom:
An Agent reports that a page opened successfully, and
snapshotorlist-pagesreturns the expected title and URL, but the full workspace Browser does not come forward. Desktop logs show a Browser Node guest loading the requested URL; they may even showBrowser automation host activatedwhile the user still sees only the Agent conversation. - Quick checks:
Treat a successful snapshot as proof that the guest exists, not that its
owner window is visible. Confirm
new_pagerequestedsurfaceRole=user, the workspace renderer focused or launched the preferred Browser node, and Main activated the exact workspace window that returned the created tab id. - Root cause: Visibility has three independent gates. Page creation must route to the User Browser rather than the embedded Agent Browser, the workspace renderer must restore and focus the Browser node, and Main must reveal the exact owning workspace window. Missing any gate lets an Agent inspect a real loaded page while the user sees no full Browser surface.
- Fix:
Keep Agent session identity as the automation lease owner, but create the tab
on
surfaceRole=user. Focus or launch the exact full Browser node before responding, then reveal and focus its workspace window after creation succeeds. Do not activate windows for metadata reads, screenshots, select, close, or performance-headless runs. - Validation:
Test that Agent-owned creation is sent to a User Browser host, the workspace
renderer focuses the preferred Browser node or launches the first one, and
Main activates the responding workspace window. In a running Desktop, create
a page through the CLI, verify a large Browser page comes forward with the
loaded URL, and confirm the log records
Browser automation host activated. - References: browserAutomationCoordinator.ts browser.ts workspaceBrowserService.ts WorkspaceWorkbench.tsx
Browser Node action finds a webview but page injection does nothing
- Symptom:
A Browser Node toolbar action is visible and clickable, but moving the pointer
over the loaded page produces no expected guest-page behavior. Desktop logs
may report
The WebView must be attached to the DOM and the dom-ready event emitted before this method can be called, especially after HMR, navigation, or panel remount. - Quick checks:
Do not treat a matching
<webview>DOM element or a visibly rendered page as proof that Electron methods are callable. CallgetWebContentsId()inside atryblock and confirm it returns a finite id. Check whether the action found a detached element, ran beforedom-ready, or retained a stale element while React cleanup and BrowserNode guest teardown raced. - Root cause:
Electron exposes the webview element before its guest method bridge is ready,
and detaches that bridge before React passive cleanup necessarily runs. Direct
DOM lookup followed immediately by
executeJavaScript()therefore races the BrowserNode lifecycle. The method can also throw synchronously before it returns a Promise, so appending.catch()alone does not protect cleanup. - Fix:
Reuse BrowserNode's guest lifecycle rather than creating a second owner. Before
guest script execution, require a connected webview with a readable finite
web contents id; otherwise wait for its
dom-readyevent with a bounded timeout. Treat cancellation during navigation or unmount as best-effort and guard the full method call withtry/catch, including synchronous throws. For the element selector specifically, keep the selection session independent from one guest: consume BrowserNode's active-webview context, move the selector to the newly active webview, and re-arm it after navigationdom-ready. Increment an attempt token whenever the target changes so a late result from the previous page cannot finish the new page's selection. - Validation:
Test delayed
dom-ready, detached webviews, unmount cancellation, switching tabs while selecting, and navigating the active tab while selecting. Run the desktop typecheck, changed-aware checks, and production build. Confirm the guest action is bundled with the standalone Agent browser adapter, then reload the standalone Agent window before a manual page-selection smoke test. - References: browserElementWebview.ts BrowserElementContextAction.tsx webviewController.ts
Hidden Browser Node webview covers another panel
- Symptom: After switching from Browser Node to another panel in the same layout region, the new panel title or sidebar appears but the previous web page still covers part of its content. The panel selection state correctly identifies only the new panel as active. The same root cause can make a Browser Node header menu or dialog appear unresponsive: its trigger changes state, but the open Portal is visually covered by the guest page.
- Quick checks:
Inspect the mounted
BrowserNodeand its<webview>in DevTools. If the parent panel hasvisibility: hidden,display: none, or an inactive class butBrowserNodestill receiveshidden={false}, treat the guest surface as the likely overlay before changing the panel reducer. - Root cause: Electron webviews are guest surfaces with compositing behavior that cannot be treated as ordinary descendant DOM for visibility coordination. Keeping a Browser Node mounted preserves its local session, but hiding only an ancestor panel can leave the guest surface visible above the newly active sibling.
- Fix:
Keep one active panel id for tools that share the same region. Pass that
active state into every mounted Browser Node through its
hiddenprop, while retaining the mounted component when session preservation is required. Keep the App Center catalog and every workspace app listed in the persistedopenAppIdstab state as mounted sibling layers: selecting the permanent catalog tab clearsopenAppIdbut must not remove an app's Browser Node, and selecting another app tab must not replace the previous app's keyed Browser Node. Closing a tab removes its id fromopenAppIdsand intentionally releases that guest. Give each inline app a stable app-specific node id so Browser Node controllers and Electron guests cannot be rebound to a different app. A ready catalog snapshot may also close tabs for apps confirmed unavailable; loading or reconnecting snapshots are not proof that an app disappeared. Inactive app layers need both non-interactive DOM visibility andhidden={true}onBrowserNode, because ancestor visibility alone is insufficient for Electron guest compositing. Do not add an explicitvisibility: visibleutility to the active child layer: CSS descendants can override an inactive parent panel's inheritedvisibility: hiddenand leak the retained app or catalog over a newly selected sibling panel. Let active layers inherit visibility from their parent, and applyinvisibleonly to inactive layers. Keep tools in separate layout regions, such as a bottom terminal tray, on an independent visibility state. For Browser Node-owned dialogs, track open overlays by node id and mark the registered webview invisible until all modal overlay owners close; do not unmount the webview or discard its session. Render header menus inline through oneMenuSurfacepositioned from the browser header, and do not hide the webview for that inline menu. Keep nested action views inside the same surface instead of opening Radix or viewport-menu Portals above the guest. Portaled controls opened from a dialog, such asSelectContent, must use the--z-dialog-popoversemantic layer. The ordinary--z-popoverlayer renders behind dialog content and makes the control appear unresponsive even though its open state changed correctly. - Validation:
Cover every switch among panels in the shared region, verify the inactive
Browser Node receives
hidden={true}, and verify an independently placed terminal remains open throughout the same switches. For App Center, open two apps, switch through their tabs and the permanent catalog tab, and reopen both; page state and any running in-page Agent must continue while inactive Browser Nodes stay hidden. Close the active and inactive app tabs in turn and verify the adjacent/catalog fallback plus guest release. Also open the Browser Node overflow menu, its submenus, settings dialog, and clear-data confirmation above a loaded guest page; verify the webview returns after each overlay closes. Renderer-only visibility changes can use HMR; preload or Electron-main changes still require a process restart. - References: BrowserNode.tsx browserNodeHostOverlayStore.ts dropdown-menu.tsx StandaloneAgentToolSidebar.tsx
IME composition leaks native input into xterm terminals
- Symptom: Chinese, Japanese, or Korean text appears in a workspace terminal, but using Space or Enter to commit the candidate sends extra or strange input into the PTY, leaving the shell prompt or terminal display in an unexpected state.
- Quick checks:
Inspect custom
attachCustomKeyEventHandlerlogic before suspecting the PTY or websocket encoding. In xterm 6, the custom key handler runs beforeCompositionHelper.keydown; returningfalseskips that xterm path but does not automatically prevent the browser's hidden textarea from receiving native input. - Root cause:
A guard that suppresses a post-composition commit key by only returning
falsecan still allow the browser default action to mutate xterm's textarea. The later xterminputor delayed composition send can then forward polluted textarea content as terminal input. - Fix:
During active composition, do not call
preventDefault; the browser and IME need native composition behavior. For post-composition commit-key suppression, callpreventDefaultandstopPropagationbefore returningfalse, and keep the short suppression window open for repeated native key events. - Validation: Add unit coverage that active composition is not prevented, post-composition commit keys are prevented, and repeated native key events within the window stay suppressed. Manually verify Chinese IME candidate commit with Space and Enter in the workspace terminal.
- References: terminalImeInputGuard.ts terminalSurfaceRuntime.ts
Chinese input renders replacement and control characters in workspace terminals
- Symptom:
Chinese input reaches a local workspace terminal, but the shell prompt shows
replacement glyphs or control-byte markers such as
<0095>. ASCII input and commands continue to work, which can make the failure look like an xterm IME composition bug. - Quick checks:
Run
localeorlocale charmapinside a newly created terminal. IfLC_CTYPEresolves toCand the character map is not UTF-8, inspect the desktop andtuttidprocess environments forLC_ALL,LC_CTYPE, andLANGbefore changing xterm key handlers or terminal transport encoding. - Root cause:
Finder-launched macOS applications commonly start without locale variables.
The daemon inherited that environment and spawned the interactive shell
without a character-type locale, so zsh interpreted UTF-8 IME bytes under the
single-byte
Clocale and rendered invalid or control characters. - Fix:
When all locale variables are absent or effectively empty on macOS, append
LC_CTYPE=UTF-8to the terminal child environment. Preserve any explicitLC_ALL,LC_CTYPE, orLANGvalue. Restrict the fallback to the character type so message language, sorting, dates, and other locale categories do not change. - Validation:
Unit-cover missing, empty, explicit, and non-macOS environment cases. Start a
real macOS zsh PTY with empty locale variables and assert
locale charmapreportsUTF-8, then manually enter Chinese text in a newly created terminal. Existing terminal processes retain their original environment and must be replaced for the fix to take effect. - References: terminal_helpers.go terminal_helpers_test.go terminal_test.go
Post-composition suppression window swallows real terminal input
- Symptom: After committing Chinese IME text in a workspace terminal, the next quick keystroke is intermittently lost: Enter pressed right after the candidate commits does not execute the command (it must be pressed twice), fast typing drops the first letter of the next word, or a full-width punctuation mark typed immediately after a commit never reaches the PTY.
- Quick checks: Reproduce with fast input — commit a candidate with Space, then press Enter or type the next character within ~80ms. Losses that disappear when typing slowly point at the IME guard's post-composition window, not the PTY.
- Root cause:
The guard suppressed every unmodified key for a fixed window after
compositionendto swallow ghost commit-key events. Only keys that can commit a candidate (Enter, Escape, Space, digit selection keys) can replay aftercompositionend; blanket suppression also swallowed genuine next keystrokes, and blocking keyCode 229 keydowns kept xterm'sCompositionHelper._handleAnyTextareaChangesfrom forwarding IME punctuation entered right after a commit. - Fix: Inside the window, suppress only commit-capable keys (Enter, Escape, Space, digits); let all other keys through so xterm's own keyCode 229 handling still runs. Ghost events replay before the physical key is released, so close the window as soon as a keyup arrives outside composition — any later keydown is genuine user input, including genuine digits.
- Validation:
Unit-cover letters and
Processkeys passing through the window, keyup closing the window so a repeated Enter or digit is processed, and commit keys (including digits) staying suppressed. Manually commit with Space then immediately press Enter, select a candidate with a digit key, and type full-width punctuation right after a commit. - References: terminalImeInputGuard.ts
Published package runtime asset 404 because the consumer bundler never saw the file
- Symptom:
An external consumer installs a public
@tutti-os/*package, uses the package, and gets a browser or renderer 404 for an icon or image such asdist/assets/.... The same feature often works inside this monorepo because workspace source resolution or local build layout hides the packaging problem. In Vite development mode the failing URL may instead point atnode_modules/.vite/deps/assets/...: dependency prebundling moved the JavaScript module, then a preservednew URL("./assets/...", import.meta.url)resolved relative to the optimizer cache. A browser-only correction can surface asUnknown file extension ".png"in Vitest when the test runner externalizes the published dependency and Node evaluates its asset import directly. - Quick checks:
If the failing package entrypoint renders a package-local image or icon,
inspect whether the main runtime entrypoint still imports that asset directly
instead of leaving it to an explicit asset subpath such as
./assets/workspace-dock-website.png. Runpnpm release:pack:checkand confirm the packed tarball includes the exported asset file underdist/assets/.... Inspect the builtdistentrypoint and confirm the main runtime code no longer uses a module-relative asset URL. If the runtime intentionally imports the asset, verify the public export has both a browser asset target and a Node-executable target. - Root cause:
The public runtime entrypoint owned a default asset dependency instead of
exposing that asset as an explicit public subpath. The packed npm artifact
either did not ship the matching file layout or forced every consumer to pay
the asset cost even when the feature was unused.
A related failure occurs when a published bundle preserves a module-relative
new URL(...): consumer dependency optimization can relocate that bundle independently from its adjacent asset. - Fix:
Move the image or icon out of the main runtime entrypoint and export it
through an explicit package asset subpath such as
./assets/workspace-dock-website.png. Let the business consumer import that asset only when it needs the default visual, and keep the package build rule that copies the asset into the packeddist/assetsdirectory. When the package runtime owns an always-available default icon, prefer a code-owned UI-system SVG component so the runtime does not import an image at all. Conditional asset exports are not sufficient for this case: a browser-conditioned test environment can select the image target and then externalize the package for Node execution, which still fails on.png. Keep explicit public asset subpaths only for browser consumers that knowingly opt into the artwork. Do not require every consumer to add a test alias for the published package. Apply the same rule to every public runtime subpath in the package, not just the first failing icon. - Validation:
Build the affected package, inspect the built runtime entrypoint for the
absence of the old asset dependency, and rerun
pnpm release:pack:check. Import any intentionally supported packed asset subpath with Node and run a real Vite dependency-prebundle fixture. The optimized root runtime must contain the code-owned fallback icon and no fallback image dependency. If the package is consumed by desktop renderer code in this repo, also run the relevant desktop build to confirm the consumer bundler copies or emits the asset only when the business import is present. - References: docs/conventions/npm-package-release.md packages/browser/workbench-node/package.json packages/browser/workbench-node/src/workbench/index.ts packages/workspace/issue-manager/package.json packages/workspace/issue-manager/src/workbench/index.ts
New release CDN namespace returns an S3 403
- Symptom:
Release artifacts upload successfully and
s3api head-objectfinds them, but the corresponding CloudFront URL returns HTTP 403 withserver: AmazonS3andx-cache: Error from cloudfront. - Quick checks:
Compare the requested path with the distribution's ordered cache behaviors,
identify the selected origin, and inspect the origin bucket policy for a
matching
s3:GetObjectresource prefix. Do not treat a successful S3 upload or invalidation as proof that the CDN route exists. - Root cause: The new release namespace was uploaded before its CloudFront path behavior and S3 read policy were provisioned. The request fell through to an unrelated default origin, which correctly returned AccessDenied.
- Fix: Add a read-only cache behavior for the namespace that targets the intended S3 origin, append the narrow bucket-policy resource prefix, wait for the distribution deployment, and invalidate the new namespace. Preserve every unrelated distribution behavior and use the current distribution ETag when updating it.
- Validation: Download mutable index metadata, immutable release metadata, and the artifact from the public CDN. Require HTTP 200 and rerun signature, SHA-256, and byte size verification against those downloaded files.
- References: Agent Extensions and the concrete Agent repository's release workflow.
Browser Node focus pings miss iframe-hosted editors
- Symptom:
Clicking or typing inside a workspace app selects text or edits content, but
the owning Browser Node does not become the active node. This commonly shows
up in rich document editors that render the editable surface inside a
same-origin
iframeorsrcdocframe. - Quick checks:
Inspect whether the app portals or mounts its editor into an iframe document.
If the top-level workspace app preload listens on
window.documentonly, the host will not receive pointer, focus, or keyboard pings from that child frame. - Root cause:
DOM events do not bubble from iframe documents to the parent document. Electron
webview preloads also do not run in subframes unless the host enables
nodeIntegrationInSubFrames, so iframe-hosted editors can interact normally while the Browser Node focus bridge stays silent. - Fix:
Enable subframe preload execution only for host-controlled Browser Node or
workspace app guest preloads. Keep privileged workspace app bridges, such as
tuttiExternal, and behavior-changing guest logic, such as_blanklink interception, main-frame-only viaprocess.isMainFrame. Install only passive interaction forwarding in subframes. - Validation: Run Browser Node and desktop preload tests, desktop typecheck, and the desktop build. For workspace app preloads, inspect the built preload output so the guest files remain self-contained.
- References: webviewSecurity.ts workspaceApp.ts workspaceAppInteractionForwarding.ts