# Troubleshooting: Agent Providers And Setup [Agent runtime index](./agent-runtime.md) · [All troubleshooting](./README.md) Provider discovery, installation, authentication, models, configuration, and runtime reachability. ### Tutti Agent browser login succeeds but the desktop remains on the login screen - Symptom: Tutti account state is already visible in the desktop header and the browser login flow returns successfully, but the Tutti Agent surface does not react. Daemon logs show the token issue request succeeding, followed by `tutti-agent login --with-tutti-llm-tokens` failing because `node` cannot be found. - Quick checks: Compare the auth subprocess logs with the provider command resolution. A packaged npm launcher commonly starts with `#!/usr/bin/env node`; therefore finding the launcher binary is not enough. Check the resolved auth-command event for `managed_node_configured=true` and `managed_node_on_path=true`, then correlate it with the login process start and completion events. Never print token payloads while diagnosing this path. - Root cause: Tutti Agent sessions, status probes, and model discovery resolve the provider command through Tutti's managed-runtime resolver, which adds the bundled Node directory to the child environment. The dedicated auth bootstrap previously reused only a binary path and launched it with the daemon's ambient environment. On machines without a compatible system Node, `/usr/bin/env` could not execute the npm launcher even though the Rust program behind that launcher and the browser login itself were healthy. - Fix: Resolve the provider command and full managed-runtime environment at each auth entrypoint, then pass that same environment to the login subprocess. Keep the canonical Tutti Agent auth-home overrides authoritative. Do not special-case a guessed Node path or copy only `TUTTI_APP_NODE`: npm launchers consume `PATH`, and the shared resolver owns its construction. - Validation: Remove system Node from `PATH`, retain only Tutti's managed Node in the provider resolution, and execute an npm-style `/usr/bin/env node` launcher. Verify browser callback, model discovery, and session preparation all create ready auth material and leave the UI. Repeat provider command/environment resolution tests on Windows; on macOS/Linux retain an executable shebang integration test. - References: [auth_bootstrapper.go](../../../services/tuttid/service/tuttiagent/auth_bootstrapper.go) [service.go](../../../services/tuttid/service/tuttiagent/service.go) [wiring_daemon_api.go](../../../services/tuttid/wiring_daemon_api.go) ### A cold Agent Extension handoff fails at `acp session/new timed out after 30s` - Symptom: A handoff from another Agent reaches an enabled Agent Extension, but the destination conversation fails before its first Turn with `acp session/new timed out after 30s`. This was observed with Hermes on a busy Windows machine even though the same installation starts successfully on a later attempt. - Quick checks: Correlate the destination session with any hidden live-model discovery session for the same provider. If `initialize` succeeds, `session/new` is written, provider stderr continues showing cold-start work, and there is no JSON-RPC response before the exact 30-second boundary, treat it as a startup budget failure rather than a malformed handoff payload. Check separately for authentication, runtime exit, and invalid-path errors. - Root cause: Interactive Agent Extension sessions inherited the generic standard-ACP 30-second initialize/session-new bound. A cold managed runtime can still be loading provider configuration and discovering models at that boundary, especially when model discovery and other Agent sessions contend for host resources. The handoff only exposed that slow cold start; it did not corrupt the destination prompt. - Fix: Give adapters resolved through the Agent Extension boundary a bounded 60-second initialize/session-new budget. Keep generic ACP providers on their existing default, keep foreground composer discovery bounded independently, and do not retry a timed-out `session/new`: the provider may have created a session even when its response was not observed. Apply the policy to Agent Extensions declaratively rather than checking a Hermes provider ID. - Validation: Verify the Agent Extension resolver projects the larger startup budget, then repeat a cold handoff on Windows and confirm the destination session reaches its first Turn without a 30-second timeout. Confirm unrelated standard ACP adapters retain their default and no duplicate `session/new` is sent. - References: [runtime_resolver.go](../../../services/tuttid/service/agentextension/runtime_resolver.go) [standard_acp_session.go](../../../packages/agent/daemon/runtime/standard_acp_session.go) ### A fresh Hermes session waits on its first terminal or browser command - Symptom: Hermes starts normally, but the first terminal-backed capability in each new macOS session appears stuck for tens of seconds or minutes. A comparable Claude Code command opens the browser immediately, and Windows Hermes does not show the delay. - Quick checks: In the daemon's captured ACP stderr, find `tools.tirith_security: tirith not found` and compare it with `tirith installed` or `tirith download failed`. Attribute only that interval to the helper download; separately measure the following `tutti browser open` command. Check the session's effective `HERMES_HOME`, because a binary under another session home is not reusable. - Root cause: Hermes enables its Tirith pre-execution scanner on supported macOS/Linux targets and downloads the helper into `$HERMES_HOME/bin`. Windows currently has no Tirith build and Hermes skips that download. Tutti intentionally gives each Agent Extension session an isolated home; without an explicit shared directory declaration, every fresh Hermes home repeats the download. This can look like a browser regression when GitHub Release delivery becomes slow even though the actual browser command remains fast. Merely creating an empty stable `$HERMES_HOME/bin` is insufficient: previously downloaded helpers may still exist only in older session homes. - Fix: Keep the session home, config, credentials, skills, and state isolated. Let the signed extension declare `runtimePrep.home.sharedDirs: ["bin"]`; the provider-neutral preparer projects that directory from the stable source home into every session. If the stable directory is empty, seed it from the newest same-provider legacy session whose manifest owns the declared extension home; never overwrite an existing stable entry. Do not hardcode Hermes, Tirith, a version, a download URL, or a session ID in Tutti, and do not disable the upstream security scanner merely to hide latency. - Validation: Start with an empty stable directory and a manifest-owned legacy session cache; verify the next preparation seeds and projects that cache. Then create two fresh Hermes sessions on macOS and verify both session `bin` paths resolve to the stable source-home `bin`, neither first terminal command logs a Tirith download, and deleting either session does not remove the helper. On Windows, verify directory projection succeeds through a symlink or junction and Hermes continues using its upstream unsupported-platform fallback without a download. - References: [agent-runtime-preparation.md](../../architecture/agent-runtime-preparation.md) [extension_runtime.go](../../../packages/agent/runtimeprep/extension_runtime.go) ### Hermes is ready but a new Windows session reports Agent failed to start - Symptom: Hermes passes setup detection, but the new-conversation model picker is empty and sending the first message reports `Agent failed to start`. - Quick checks: In the daemon log, compare setup-probe configuration with the session-scoped `HERMES_HOME`. The failing session typically reports no `.env` in its `.tutti*/agent/runs//hermes` directory, followed by ACP `session/new` returning `No LLM provider configured`. Check whether the working user config is under `%LOCALAPPDATA%\hermes` while `%USERPROFILE%\.hermes` is absent. - Root cause: The signed extension profile uses the portable default `.hermes`, but the shared runtime preparer previously resolved it only relative to the Windows user profile. Hermes itself uses the native Windows user cache location, so discovery could see credentials while the isolated session home copied none. - Fix: Keep an explicit `HERMES_HOME` as the highest-priority source. Otherwise, preserve an existing `%USERPROFILE%\.hermes`; when it is absent, resolve the resolve the portable leading-dot directory through the Windows user cache root first, then fall back to a migrated `%USERPROFILE%\.hermes` only when the native directory is absent. Copy only the files declared by the signed runtime-preparation profile. Keep this behavior in the platform adapter rather than branching on `acp:hermes`. - Validation: With no user-level `HERMES_HOME`, place credentials in `%LOCALAPPDATA%\hermes`, create a new session, and verify the model picker is populated and the first message starts successfully. Confirm an explicit source environment variable still takes precedence, and that a migrated `%USERPROFILE%\.hermes` is used only when the native directory is absent. - References: [agent-runtime-preparation.md](../../architecture/agent-runtime-preparation.md) [windows-platform-support.md](../../architecture/windows-platform-support.md) [extension_runtime.go](../../../packages/agent/runtimeprep/extension_runtime.go) ### Focusing a workspace repeatedly starts provider CLIs and raises CPU usage - Symptom: Focusing or reopening a workspace makes the fan ramp up even when no Agent turn starts. The effect grows when more managed provider CLIs are installed. - Quick checks: Inspect GET requests to `/v1/agent-providers/status`. A focus-driven request with `refresh=true` bypasses the daemon status cache. Correlate the same time range with short-lived `codex`, `opencode`, `cursor`, `node`, `git`, and credential-helper processes. Use `pnpm perf:agent-gui -- --scenario provider-status-focus-refresh --all-process-time-profile` on macOS when a Chromium trace cannot see daemon child processes. - Root cause: Window lifecycle analytics requested a fresh availability snapshot by using the explicit-refresh path. Every pageview opportunity therefore bypassed the daemon cache and launched provider detection work; a focus arriving during a scan queued another forced scan. - Fix: Let availability analytics reuse the renderer's loaded status snapshot, and route only stale visibility reconciliation through a non-forced tuttid read. Let tuttid own cache expiry, credential fingerprints, and per-provider single-flight. Initialize providers in parallel for first-paint latency, but serialize later background stale reconciliation and cap the aggregate auth/version/adapter subprocess count. Keep `refresh=true` only for explicit user refreshes and provider-scoped confirmation after state-changing actions. A forced read must recheck auth/readiness, but can reuse successful executable-scoped facts while the binary fingerprint is unchanged; never cache failed version or adapter probes. Prefer validated credential files for OpenCode and Tutti Agent and allow at most one CLI fallback for malformed files. Reuse fields from a provider command instead of launching duplicate probes: Cursor `about --format json` returns both authentication details and `cliVersion`, so its status detection must not also launch `cursor-agent --version` unless the `about` output omitted the version. - Validation: Dispatch two focus events and assert they start no provider-status request, availability pageviews are unchanged, and an explicit refresh still sends exactly one forced request. The all-process trace should contain no focus-driven provider-detection child process burst. - References: [agent-gui-node.md](../../architecture/agent-gui-node.md) [desktopAgentProviderStatusService.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/desktopAgentProviderStatusService.ts) [agent-provider-status-performance-scenario.mjs](../../../tools/scripts/agent-provider-status-performance-scenario.mjs) ### Missing optional Agent CLIs inflate environment failure analytics - Symptom: `agent.env_detected` reports many `cli_not_found` outcomes for providers the user never selected, and an analytics view that treats every non-ready status as a failure makes the Agent environment failure rate look much higher than actual launch failures. - Quick checks: Group `agent.env_detected` by `provider`, `reason_code`, and `cli_installed`. If the dominant rows are `cli_not_found` with `cli_installed=false` across several managed providers for the same users, compare them with the desktop's background all-provider status request before investigating installation. - Root cause: Desktop intentionally discovers every managed provider so Agent pickers and setup surfaces can show local readiness. The automatic environment reporter previously treated every changed status as failure telemetry, so the expected absence of an optional CLI crossed the same reporting boundary as a provider that was installed but failed its runtime probe. - Fix: Keep `cli_not_found` in the canonical provider-status snapshot and continue full catalog discovery, but exclude the exact `not_installed` + `cli_not_found` + `cli.installed=false` state from automatic `agent.env_detected` reporting. Preserve explicit consent-gated problem reports and continue reporting installed-provider failures such as `acp_adapter_launch_failed`. - Validation: Reconcile one missing optional provider and one installed provider with a runtime launch failure in the same response. Assert both remain visible in the status snapshot while only the launch failure produces `agent.env_detected`. - References: [desktopAgentProviderStatusDiagnostics.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/desktopAgentProviderStatusDiagnostics.ts) [desktopAgentProviderStatusService.test.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/desktopAgentProviderStatusService.test.ts) ### Loading Agent Targets repeatedly starts Extension CLIs - Symptom: Opening a workspace, standalone Agent window, or Agent settings takes longer as more Agent Extensions are enabled. Repeated `GET /v1/agent-targets` requests start the same Extension CLI `--version` commands even though the executables did not change. - Quick checks: Time several authenticated `GET /v1/agent-targets` requests and correlate them with short-lived Extension CLI processes. Compare the total request duration with each configured discovery candidate's version command. - Root cause: Extension Target availability resolved every runtime from scratch. Package and executable validation are required, but the stable successful version result was discarded after every request. Concurrent catalog consumers could also run the same version command independently. - Fix: Cache only successful version results by resolved executable fingerprint, arguments, and version constraint, and coalesce concurrent probes. Keep failed probes uncached. Continue package authority and managed-runtime integrity checks on every resolution; catalog availability is not runtime launch authorization. - Validation: Resolve the same Extension runtime twice and assert one version subprocess. Cover concurrent callers, a failed probe followed by recovery, and executable replacement invalidation. Repeated `/v1/agent-targets` requests should retain the same availability while no longer starting unchanged Extension CLIs. - References: [agent-extensions.md](../../architecture/agent-extensions.md) [manager.go](../../../services/tuttid/service/agentextension/manager.go) [runtime_version_cache.go](../../../services/tuttid/service/agentextension/runtime_version_cache.go) ### A managed Extension runtime cannot be reused after an update on Windows - Symptom: The first managed Extension install reaches `ready`, but after Extension metadata changes the same compatible runtime falls back to `not_installed` only on Windows. Logs or focused tests report that renaming the legacy runtime directory failed because it is being used by another process. - Quick checks: Confirm the package version, discovery profile, and runtime identity are still compatible. Then inspect the adoption path around the legacy-directory rename; if the source `managedRuntimeDirectory` is still open, the failure is a Windows sharing violation rather than a package incompatibility. - Root cause: POSIX permits renaming a directory while the process retains an open directory handle. Windows does not. Keeping the verified candidate handle open across `rename` made the portability test pass on macOS while the native Windows lane rejected the same adoption. - Fix: Verify and fingerprint the candidate, write the new activation, close the source directory handle, rename it, then reopen the promoted directory and repeat the integrity check. Rollback must close the promoted handle before renaming it back and restoring the previous activation. - Validation: On native Windows, install a managed npm runtime, move it to a legacy identity, update the Extension without changing its runtime contract, and assert the runtime is adopted without reinstalling. Keep the batch-launcher execution and companion reconciliation assertions in the same Windows workflow. - References: [windows-platform-support.md](../../architecture/windows-platform-support.md) [managed_runtime.go](../../../services/tuttid/service/agentextension/managed_runtime.go) [setup_test.go](../../../services/tuttid/service/agentextension/setup_test.go) ### Workspace Apps repeatedly probe extension authentication - Symptom: Several Workspace Apps opening together repeatedly start the same Extension ACP process. A logged-in target can intermittently become unavailable when duplicate setup probes exhaust the caller timeout. - Root cause: Older Apps consume only the broad Agent catalog, while newer Apps refine an exact target. If the broad catalog exposes installation readiness without authentication, old Apps can also show an unconfigured extension as usable. - Fix: Resolve installed extension authentication for broad and exact `agent list` requests, run broad probes concurrently, coalesce them by workspace and target in the daemon, and retain the result for a short bounded interval. Preserve `auth_required` as the canonical reason code even when the runtime supplies a more specific diagnostic reason. Explicit refresh bypasses the short cache. - Validation: Broad and exact-target requests within the cache window should share one setup probe per target. A ready Kimi target reports `available`; an unconfigured Hermes target reports `unavailable` with `auth_required` in both response shapes. A refreshed broad request probes each installed extension once. ### An extension Agent is installed in the terminal but Tutti cannot detect it - Symptom: Running the Agent CLI in an interactive terminal works, while the extension setup dialog reports that no compatible runtime is installed. - Quick checks: Resolve the executable with `command -v` in the terminal and compare its parent directory with the desktop daemon's effective PATH. Check the signed extension `profiles/discovery.json` for a matching user-relative `searchPaths` entry and confirm that the reported CLI version satisfies its constraint. - Root cause: The desktop daemon inherits the GUI process environment and does not source `.zshrc` or other interactive shell startup files. Some official installers place a self-contained CLI in a vendor directory under the user's home, so the terminal can find it only after shell initialization. - Fix: Declare the vendor-owned location in the extension candidate, for example `{"scope":"user","path":".vendor/bin"}`. Keep the vendor path out of core and let the shared runtime resolver prepend the validated directory. Update the extension version constraint and managed package recipe to match the same official CLI generation. - Validation: Run discovery with a daemon PATH that omits the vendor directory and an injected user home containing the CLI. Assert that the binding is `local`, uses the absolute vendor executable, passes the version constraint, and retains the declared ACP launch arguments. Also reject absolute paths, parent traversal, and unsupported scopes. - References: [agent-extensions.md](../../architecture/agent-extensions.md) [runtime_contract.go](../../../services/tuttid/service/agentextension/runtime_contract.go) [manager.go](../../../services/tuttid/service/agentextension/manager.go) ### Clicking provider login repeatedly opens terminals and browser auth pages - Symptom: One login click opens many terminal nodes and repeatedly launches the provider's browser authentication page. The repeats may continue at the provider-status polling interval and resume after reopening the app. - Quick checks: Count `agent-provider.terminal-command.start` events for one provider and compare their timestamps with provider status requests. If every status snapshot is followed by another login command, inspect whether a React effect reattaches the setup workflow when a status-derived callback changes identity. Also check whether reattachment resets the request-sequence dedup marker and whether login is excluded from pending/single-flight tracking. - Root cause: The panel lifecycle and the setup workflow had competing ownership. A status refresh replaced the status object, rebuilt the login callback, reran the effect, reset wizard state, and accepted the same automatic login again. Each command opened a new terminal; the CLI then opened another browser page. A single terminal-handle map entry also allowed newer launches to overwrite the only handle that could later be closed. - Fix: Inject the panel-open host command into AgentGUI and let a window-scoped Agent Env service/controller own the request session, automatic-action idempotency, reveal/report state, and provider-status subscription. Route account and CLI login through the provider-status service. Its per-provider login lifecycle must reuse automatic requests, coalesce launches, replace an awaiting attempt only for an explicit retry, reject stale terminal handles, and own one poll. React should only subscribe and forward commands. - Validation: Replay at least 60 provider-status ticks for one request and assert one automatic login call. Cover rapid user clicks, explicit replacement, delayed terminal resolution, account login without a terminal, timeout/dispose cleanup, and a new request sequence. Verify one terminal and one browser page for the original click. - References: [agent-gui-node.md](../../architecture/agent-gui-node.md) [desktop-layering.md](../desktop-layering.md) [agentEnvService.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/agentEnvService.ts) [desktopAgentProviderLoginLifecycle.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/desktopAgentProviderLoginLifecycle.ts) ### Codex `/status` shows a 5h limit for a weekly-only account window - Symptom: Opening `/status` before starting a Codex conversation labels the only quota as `5h limit`, while the upstream usage response reports a seven-day window. An active conversation may show a different label. - Quick checks: Inspect `agent.usage_probe.result` in desktop logs, then inspect the Codex `/wham/usage` response shape. If `primary_window.limit_window_seconds` is `604800` and `secondary_window` is absent, the primary slot is carrying the weekly window. Compare this with daemon app-server telemetry, where the same duration is `windowDurationMins: 10080`. - Root cause: Empty-session `/status` loads account quotas through the desktop provider probe, while active sessions receive canonical runtime usage from the daemon. Both paths once inferred quota type from `primary`/`secondary` position, but Codex may put the weekly-only quota in `primary`. - Fix: Classify known Codex windows by duration in both mappers: five hours is `session`, seven days is `weekly`. Use the positional type only when duration is missing or unknown. Keep additional named rate limits typed as `model`. - Validation: Cover a desktop probe response whose primary and secondary durations are opposite their conventional positions, plus daemon mapper cases for a weekly-only primary window. Verify both empty and active `/status` views. - References: [agentProviderUsageProbe.ts](../../../apps/desktop/src/main/agentProviderUsageProbe.ts) [codex_appserver_event_state.go](../../../packages/agent/daemon/runtime/codex_appserver_event_state.go) ### Provider setup notice flashes after switching to an already-connected agent - Symptom: Opening or restarting Tutti, then switching to an existing Claude Code, Cursor, or other managed-provider session, briefly shows the toast-like "connect provider before sending" notice even though automatic readiness recovery succeeds and messages can be sent after the status refresh settles. - Quick checks: Compare the desktop provider-status snapshot for the active provider with the AgentGUI view model. An active conversation must project no provider-readiness gate. If provider `checking`, `auth_required`, or `not_installed` disables its composer or renders a setup notice, catalog readiness leaked into session recovery ownership. - Root cause: Startup or daemon restart may temporarily expose an uncaptured or stale provider catalog status. AgentGUI projected that target-creation readiness into an already-open session, creating a second owner beside canonical session/runtime recovery. Transient catalog reconciliation then blocked the active composer and rendered a misleading connect action. - Fix: Keep the structured readiness gate only on the empty new-conversation surface. Active sessions always project a null provider gate; canonical session/runtime state owns recovery, submit, queue, and cancel capability. Remove active setup notices and all composer conditions derived from provider catalog readiness. Desktop may still refresh stale catalog status for future session creation. - Validation: Cover active-session null gate, empty-surface gate selection, and explicit install/login action mapping. Also run desktop readiness-gate tests, AgentGUI tests, and desktop/AgentGUI typechecks. - References: [agentGuiProviderReadiness.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/model/agentGuiProviderReadiness.ts) [useAgentGUIViewAssembly.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUIViewAssembly.ts) [useDesktopAgentGUIReadiness.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/ui/useDesktopAgentGUIReadiness.ts) [desktopAgentProviderNotReadyRecheck.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/ui/desktopAgentProviderNotReadyRecheck.ts) [agent-gui-node.md](../../architecture/agent-gui-node.md) ### Agent provider picker shows only Claude Code and Codex - Symptom: Desktop settings, App Center, Issue Manager, or an installed workspace app only shows Claude Code and Codex even after Cursor/OpenCode are enabled. - Quick checks: For host-owned pickers, compare `/v1/agent-targets` with `/v1/agent-providers/status`. The target list must include enabled `local:cursor`/`local:opencode`, and provider status must report them as `ready`. For workspace apps, inspect the app server's provider detection; host preferences are not injected into app-owned provider lists. - Root cause: Host-owned app/workbench pickers are derived from daemon agent targets plus provider readiness and visibility preferences. The desktop default provider preference is a separate OpenAPI/event schema enum. Workspace apps own their runtime provider policy through `@tutti-os/agent-acp-kit`, so generated or packaged app UIs can still be limited to the providers the app implements. - Fix: Keep the host default-provider enum, desktop settings options, daemon validation, and generated clients/protocol schemas in sync. For installed workspace apps, update the app's provider detection/runtime integration instead of expecting host settings to expand the app UI. - Validation: Run `pnpm generate:api`, `pnpm generate:event-protocol`, `pnpm check:api-generated`, `pnpm check:event-protocol-generated`, desktop typecheck, and focused daemon preferences/API tests. If local `pnpm` resolves to the wrong version inside generator subprocesses, run the checks with a temporary `pnpm` PATH shim that delegates to `corepack pnpm@10.11.0`. - References: [core.ts](../../../apps/desktop/src/shared/preferences/core.ts) [model.go](../../../services/tuttid/biz/preferences/model.go) [tuttid.v1.yaml](../../../services/tuttid/api/openapi/tuttid.v1.yaml) [workspace-app-runtime.md](../workspace-app-runtime.md) ### Claude composer model list stays stale after credential switch - Symptom: After an external credential switcher rewrites Claude Code auth or config files, the AgentGUI composer still shows the previous model list even though `tuttid.log` contains `agent.model_catalog.invalidated` for `claude-code`. - Quick checks: Search `tuttid.log` for `CLAUDE_MODEL_CATALOG_INVALIDATION_DEBUG`. If `live_composer_models_invalidated` is followed by `running_session_model_options_reused`, inspect that session's `createdAtUnixMs` and `updatedAtUnixMs` against the invalidation timestamp. - Root cause: Claude composer model discovery reuses model options from a live Claude runtime session to avoid spawning overlapping credential-touching processes. After a credential switch, a pre-switch runtime session can still carry the old `runtimeContext.configOptions`; reusing it repopulates the just-cleared live model cache with stale models. - Fix: Track provider model-catalog invalidation time in `tuttid`. When loading Claude composer options, skip running-session model options whose session timestamp is older than the provider invalidation, and allow hidden live discovery to query the current credentials. - Validation: Add daemon service coverage where invalidation happens after a Claude session has advertised old model options; the next composer options request must start hidden discovery and return the freshly discovered model list. Run `cd services/tuttid && go test ./service/agent`. - References: [composer_live_model_discovery.go](../../../services/tuttid/service/agent/composer_live_model_discovery.go) [composer_live_model_cache.go](../../../services/tuttid/service/agent/composer_live_model_cache.go) ### Cursor keeps the previous account after login changes - Symptom: A Cursor Agent session started under one account still reports that account's entitlement after `cursor-agent login` switches `~/.cursor/cli-config.json` to another account. A fresh session may use the new account while the existing conversation does not. - Quick checks: Compare the `cursor-agent` process start time with the login attempt and the later prompt. If the same process accepts prompts both before and after the credential file changed, provider status refresh alone did not replace the credential-bearing ACP connection. - Root cause: Provider readiness and model-cache invalidation do not mutate an already running ACP process. Cursor reads its credentials at process startup, so the live connection retains the previous account until it is reprepared. - Fix: Watch the Cursor auth marker by full content fingerprint. Mark that provider's live connections stale without interrupting active Turns; the next ordinary idle send uses Host's atomic runtime reprepare-and-send operation, preserving the canonical Session, provider session id, and history. - Validation: Use a fake Cursor ACP process that records the auth generation at startup. Replace the marker through a native-path rename, then prove the same canonical session's next idle prompt runs in a new process. Also cover identical-content replacement, another provider, an active Turn followed by idle retry, and a second auth change racing with reprepare. - References: [provider_auth_watcher.go](../../../services/tuttid/service/agent/provider_auth_watcher.go) [provider_auth_runtime_rebind.go](../../../services/tuttid/service/agent/provider_auth_runtime_rebind.go) ### Claude SDK context window shows 200k for 1M models - Symptom: Claude Code GUI usage shows a 200k context window for a model that should have 1M context, such as Claude Sonnet 5. The inverse can also happen after a model switch: a 200k model such as Haiku keeps showing the prior 1M total. - Quick checks: Inspect the session runtime context for `usage.contextWindow.totalTokens`, then trace the Claude SDK sidecar `usage_updated` payload and daemon `agent_session.claude_sdk.usage_update` log. If the payload keys include `modelUsage` but `raw_total_tokens` is `0`, the daemon did not parse the model-usage context window. If `previous_context_model` and `current_context_model` differ but `current_total_tokens` equals `previous_total_tokens`, daemon usage normalization reused a stale context window across models. If switching models without sending a message makes the usage entry disappear, inspect whether a forced session-control reload returned `runtimeContext` without `usage` and replaced the active control state. - Root cause: AgentGUI only renders `runtimeContext.usage`; the total comes from the daemon and Claude SDK sidecar. Claude SDK result messages expose model usage as a map keyed by model id, for example `modelUsage["claude-sonnet-5"].contextWindow`. If either sidecar or daemon only parses array-shaped `modelUsage`, the context-window total is missing and daemon normalization falls back to 200k. - Fix: Parse `modelUsage` recursively as both arrays and maps before using fallback context-window values. Track the model associated with a cached context window, and only reuse the previous total for the same model or when the model is unknown. Treat `runtimeContext.usage` as incremental telemetry in AgentGUI reload races: a full session-control snapshot that omits usage should not clear the previous usage display. Do not hard-code alias-to-model mappings in Tutti. - Validation: Add sidecar and daemon coverage with map-shaped `modelUsage` carrying `contextWindow: 1_000_000`, plus daemon coverage for Haiku -> Sonnet5 -> Haiku usage updates where the last payload lacks `totalTokens`. Add AgentGUI coverage for session-control reloads that omit `runtimeContext.usage`. Then run the Claude SDK sidecar tests, daemon Go tests, AgentGUI tests, and typechecks. - References: [main.ts](../../../packages/agent/claude-sdk-sidecar/src/main.ts) [main.test.ts](../../../packages/agent/claude-sdk-sidecar/src/main.test.ts) [claude_sdk_adapter.go](../../../packages/agent/daemon/runtime/claude_sdk_adapter.go) ### Bun-installed Codex works in a terminal but Tutti cannot use it - Symptom: `codex --version` works in an interactive terminal after `bun add -g @openai/codex`, while the desktop reports `cli_not_found`, `codex_platform_pkg_incomplete`, or a generic app-server launch failure. - Quick checks: Compare the terminal's `command -v codex` and `bun pm bin -g` with the daemon's effective PATH. Then run the exact resolved command with `app-server` and verify the formal `initialize` response followed by the `initialized` notification. Do not infer runtime failure from the absence of npm's nested `@openai/codex/node_modules/@openai/codex-` path: inspect the package root and ancestor `node_modules` directories as well. - Root cause: Desktop processes do not source interactive shell startup files. Bun's default global binary directory is `~/.bun/bin`, `BUN_INSTALL` can relocate the Bun installation, and `globalBinDir` can point somewhere else entirely. Bun may also hoist the Codex platform package or place it behind an isolated `.bun` store, so an npm-nested-only check can reject a working CLI. Finally, the actionable missing-optional-dependency message is written to stderr after the app-server process starts; classifying only the protocol error loses that evidence. - Fix: Resolve Codex from the effective PATH, `BUN_INSTALL/bin`, the default `~/.bun/bin`, and, only when those fail, a bounded `bun pm bin -g` discovery. Cache a successful configured-bin result by the Bun executable fingerprint. Rewrite the provider command to the resolved absolute Codex launcher and pass the same environment to status probes and real sessions. Treat a completed app-server initialize handshake as the readiness source of truth. Scan the package from the resolved launcher using Node-style ancestor `node_modules` lookup across npm, Bun, and pnpm layouts; layout evidence must never override a successful protocol probe. Classify the current platform package from bounded stderr even when the message says `Missing optional dependency` without an `ENOENT` token. Preserve Bun/pnpm provenance and never run npm repair in those package-manager-owned directories. - Validation: Use a minimal GUI PATH (`/usr/bin:/bin`) and cover default `~/.bun/bin`, `BUN_INSTALL/bin`, and a custom `bun pm bin -g` result. Include a real single-process initialize/initialized handshake, Bun hoisted and isolated package layouts, missing optional-dependency stderr without `ENOENT`, unsupported `app-server`, and a broken Bun install that must not invoke npm. - Multiple installations: Do not stop at the first PATH result. Enumerate PATH, Bun, pnpm, npm, and Homebrew launchers; deduplicate logical package roots; then validate each launcher's version, package layout, and app-server handshake. A single ready candidate is used implicitly. Two or more ready candidates are not ranked by PATH or package-manager order: Tutti blocks startup and asks the user to choose one in the Agent environment panel. Persist that choice as its absolute launcher path (not an ephemeral list id), selected from the current catalog revision. A stale, broken, or unsupported saved choice also requires a new user selection; never silently fall back to another installation or run an installer. Read `GET /v1/agent-providers/codex/runtime-candidates` before changing it with `PUT /v1/agent-providers/codex/runtime-selection`. - References: [resolver.go](../../../packages/agent/daemon/runtimecmd/resolver.go) [codex_bun_discovery.go](../../../services/tuttid/service/agentstatus/codex_bun_discovery.go) [codex_layout_scan.go](../../../services/tuttid/service/agentstatus/codex_layout_scan.go) [codex_protocol_probe.go](../../../services/tuttid/service/agentstatus/codex_protocol_probe.go) [codex_appserver_probe.go](../../../packages/agent/daemon/runtime/codex_appserver_probe.go) ### Codex npm install misses the platform package - Symptom: The Codex environment dialog says the CLI is installed, but the adapter or `codex app-server` probe is still missing. Logs may show `Missing optional dependency @openai/codex-darwin-arm64`, a long wait on an npm registry, or a later repair failure such as `ENOTEMPTY` while moving an existing `@openai/codex` directory. Another form is an immediate launcher failure such as `env: node: No such file or directory` after the JavaScript `codex` shim has been installed. - Quick checks: Inspect the npm debug log under the install cache for `reify failed optional dependency`, then check whether the matching platform package directory contains both `package.json` and the vendor `codex` executable. Compare the selected registry with a temporary prefix/cache install before changing the user's real install. - Root cause: `@openai/codex` installs a JavaScript launcher plus a per-platform optional package such as `@openai/codex-darwin-arm64`. npm can exit successfully even when an optional dependency fetch failed, which leaves the launcher installed but unable to start. A registry can also be reachable but too slow for the platform tarball, so retrying the same source burns the install timeout before mirrors are tried. The launcher itself uses `#!/usr/bin/env node`, so daemon-run Codex commands (`--version`, login, and `app-server`) need a usable Node on `PATH`. Tutti should prefer the user's Node environment, but fall back to the managed Node runtime when the visible `codex` shim exists and no user Node is resolvable. - Fix: Keep Codex installs on the Tutti-managed Node/npm runtime, install with optional dependencies included, and rank configured npm registries with a lightweight package metadata probe before attempting the install. Preserve `TUTTI_AGENT_NPM_REGISTRY` as an explicit single-registry pin with no mirror fallback. Provider command resolution should leave the user's Node first when it is available, and only append managed Node runtime env (`TUTTI_APP_NODE`, `TUTTI_APP_NPM`, managed `PATH`) when user Node is missing. Ensure the Codex app-server adapter consumes that provider command resolution; otherwise status probes can pass while session startup still fails with `env: node: No such file or directory`. A failed `codex app-server` probe is diagnostic evidence, not repair authorization by itself. Require a fresh, platform-specific missing-dependency classification plus a package-relative scan that confirms the same package or binary is missing. Keep npm repair limited to npm-owned layouts; Bun and pnpm installations must be repaired by their owning package manager rather than overwritten in place by npm. If the supported CLI and platform binary are complete but the formal protocol handshake still fails, report a runtime bug and do not reinstall. - Validation: Reproduce in a temporary prefix/cache using the Tutti-managed npm. Confirm `codex --version`, the platform package metadata and vendor binary, and a short `codex app-server` probe before touching the user's real install. Include a case where the visible `codex` shim uses `#!/usr/bin/env node` and the normal user `PATH` does not contain `node`. - References: [npm_registry.go](../../../services/tuttid/service/agentstatus/npm_registry.go) [installer_codex_cli.go](../../../services/tuttid/service/agentstatus/installer_codex_cli.go) [codex_platform.go](../../../services/tuttid/service/agentstatus/codex_platform.go) [provider_resolution.go](../../../services/tuttid/service/agentstatus/provider_resolution.go) [codex_appserver_adapter.go](../../../packages/agent/daemon/runtime/codex_appserver_adapter.go) ### Tutti Agent npm install misses the platform package - Symptom: The Tutti Agent provider setup reaches the login screen or reports the CLI as installed, but `tutti-agent login` or `tutti-agent app-server` fails with `Missing optional dependency @tutti-os/tutti-agent-`. - Quick checks: Check the selected registry for both `@tutti-os/tutti-agent` and the exact alias target version, such as `@tutti-os/tutti-agent@0.0.1-darwin-arm64`. Do not treat a successful aggregate package metadata fetch as proof that the platform tarball is available. - Root cause: `@tutti-os/tutti-agent` follows the Codex npm layout: a JavaScript launcher plus per-platform optional dependencies expressed as npm aliases. npm can complete the aggregate install even when a mirror has not synced the platform optional dependency version. - Fix: Keep the package layout aligned with Codex and use registries that carry the platform optional dependency versions. The daemon default chain intentionally excludes mirrors that only sync the aggregate package. Preserve `TUTTI_AGENT_NPM_REGISTRY` as an explicit single-registry pin with no fallback. Before a managed global npm retry, remove only the selected package's sibling staging directories (for example, `@tutti-os/.tutti-agent-`), and repeat that cleanup after a failed or canceled attempt. Do not remove the global `node_modules` tree because the selected prefix can contain unrelated user-installed packages. This lets a later daemon restart recover from a desktop-close cancellation instead of repeatedly failing with `ENOTEMPTY`. - Validation: Install into a temporary prefix/cache and verify the provider probe, not only npm's exit code. Confirm `tutti-agent app-server` can start far enough to pass the daemon readiness probe. - References: [npm_registry.go](../../../services/tuttid/service/agentstatus/npm_registry.go) [runtimeprep tutti_agent.go](../../../packages/agent/runtimeprep/tutti_agent.go) [tuttid tuttiagent service.go](../../../services/tuttid/service/tuttiagent/service.go) ### Managed npm install fails before reaching every registry - Symptom: A Codex or Tutti Agent managed npm install fails immediately with exit code `126`. On macOS or another Unix host, stderr contains `dirname: command not found` followed by a malformed `node` path. Switching npm registries produces the same failure without meaningful network delay. - Quick checks: Inspect the prepared npm path and the install process `PATH`. Run the managed npm launcher once with that exact environment. If adding `/usr/bin:/bin` makes it work, the failure is local process setup rather than registry or package availability. On Windows, confirm the structured runner uses `cmd.exe /D /S /C call` for `npm.cmd` and retains the inherited `System32` path. - Root cause: Managed runtime overrides put the bundled Node directory first, but an already-installed runtime fast path can accidentally build the override from an empty base environment. Direct structured execution then exposes the truncated `PATH`: the Unix npm launcher cannot resolve tools such as `dirname`, while Windows batch launchers can lose commands supplied by the host environment. A login shell can hide this defect by rebuilding `PATH`. - Fix: Keep structured argv execution and the platform process adapters. When no environment provider is injected, inherit the daemon process environment before prepending managed runtime directories. Do not switch back to shell command strings or hardcode a Unix path into shared installer policy. - Validation: Execute a real POSIX npm-style launcher that calls `dirname` with the production managed-runtime composition, test Windows `.cmd` argument preservation, build the daemon natively, and cross-compile the Windows agentstatus tests. - References: [installer_codex_cli.go](../../../services/tuttid/service/agentstatus/installer_codex_cli.go) [provider_resolution.go](../../../services/tuttid/service/agentstatus/provider_resolution.go) [service_helpers.go](../../../services/tuttid/service/agentstatus/service_helpers.go) [install_command_windows.go](../../../services/tuttid/service/agentstatus/install_command_windows.go) ### Tutti Agent unexpectedly loses login after a host auth read failure - Symptom: Tutti Agent was previously authenticated, but provider preparation or model discovery changes it to `auth_required` after the desktop account auth file is temporarily missing, unreadable, or malformed. A token issue rejection may produce the same symptom. - Root cause: Provider preparation used to treat one failed observation of the host Account session as a completed logout and removed the durable `~/.tutti-agent/auth.json`. The token issue 401 path used the same cleanup helper, even though neither condition proves that the user requested logout. - Invariants: Missing, unreadable, malformed, or session-less host auth retains existing Tutti Agent credentials. Failed token issue, validation, provider login, or verification safely restores the previous auth file. Bootstrap and explicit logout resolve a symlinked auth file to the same final target as Tutti Agent, then use its sibling `auth.json.refresh.lock`. Go `flock` and Rust `fs2` coordinate through the same OS advisory lock on a local filesystem; this is not a distributed lock. Only the completed Account logout callback may delete local provider auth and revoke its refresh token. Logs identify these decisions with `event=tutti_agent.auth_bootstrap`, `action`, and `reason`, without including cookies or tokens. - Validation: Run the `service/tuttiagent` tests covering `RetainsAuthWithoutHostSession`, `RetainsAuthWhenHostAuthIsInvalidJSON`, `RetainsAuthWhenHostAuthIsUnreadable`, `RetainsAuthAfterUnauthorizedTokenIssue`, and `LogoutTuttiAgentUserAuthRemovesAuthAndRevokesToken`, plus the reconciliation restoration and refresh-lock serialization tests in the same package. - References: [service.go](../../../services/tuttid/service/tuttiagent/service.go) [tutti-agent-readiness-bootstrap.md](../../architecture/tutti-agent-readiness-bootstrap.md) ### Tutti Agent stays on login while the desktop account is signed in - Symptom: The desktop account avatar is present, but Tutti Agent remains `auth_required`. Repeated bootstrap attempts log `stage=login`, while the Account service successfully issues and then compensates the short-lived LLM token. - Root cause: Desktop Account auth and provider auth are separate. The daemon scopes the provider login subprocess to the canonical user `TUTTI_AGENT_HOME`. Tutti Agent requires an explicitly configured home to exist during configuration loading, even though its later credential writer can create the default home. Passing a missing explicit directory therefore used to fail before the writer ran. - Fix: Create the canonical provider auth home before launching `tutti-agent login --with-tutti-llm-tokens`. Keep the explicit environment override so session-scoped homes cannot capture durable user credentials. Log auth-home creation without the local path, and include only bounded, access-token- and refresh-token-redacted CLI diagnostics on login failure. - Validation: Run the `service/tuttiagent` tests covering `RunTuttiAgentTokenLoginPreparesCanonicalAuthHomeBeforeLaunch`, `PrepareTuttiAgentAuthHomeRejectsFile`, and `SanitizeTuttiAgentLoginOutputRedactsTokensAndTruncates`. Confirm a clean user home gains `.tutti-agent/auth.json`, provider status changes from `auth_required` to `ready`, and no token value appears in daemon logs. - References: [service.go](../../../services/tuttid/service/tuttiagent/service.go) [tutti-agent-readiness-bootstrap.md](../../architecture/tutti-agent-readiness-bootstrap.md) ### Agent sandbox cannot reach local daemon - Symptom: An AgentGUI-backed Codex-compatible turn runs a dynamic Tutti CLI command such as `tutti agent get --session-id ` and gets `reasonCode=daemon_unavailable` or `daemon is not reachable`, while the listener file exists and the desktop daemon is serving other requests. - Quick checks: Inspect the turn context in the provider session JSONL. If `network_access=false`, a plain `exec_command` cannot reach localhost/IPC. Identify the executing provider from that same session instead of inferring it from a queried session ID. For Codex sessions, also confirm the command was not rerun with `sandbox_permissions=require_escalated`. Other providers need their own local-daemon-capable shell/runtime path, not Codex-specific sandbox syntax. - Root cause: Dynamic CLI scopes fetch command capabilities from the local daemon before printing scope help. In a sandboxed provider command environment, localhost access can be blocked even though the daemon is reachable from the host. For a Codex-compatible app-server, omitting `sandboxPolicy.networkAccess=true` from a `readOnly` or `workspaceWrite` turn creates this exact split between successful host requests and failed in-sandbox CLI requests. - Fix: Keep the CLI's transport failure message explicit about the sandbox but provider-neutral. The Tutti Desktop host explicitly enables command network access for the built-in `codex` and `tutti-agent` app-servers through `agentdaemon.Config.CommandNetworkAccessPolicy`. Keep this an explicit provider-registry Desktop opt-in instead of branching on provider identity or granting every future app-server network access. This preserves the permission-mode filesystem sandbox and approval policy. Codex should run the CLI normally first and use `sandbox_permissions=require_escalated` only as a fallback for hosts that do not grant command networking. ACP providers should use an execution environment with localhost/IPC access and not invent Codex flags. - Validation: Verify the default adapter policy enables `sandboxPolicy.networkAccess=true` for Codex and `tutti-agent` read-only and workspace-write turns. Verify the Desktop host policy rejects Claude Code, external ACP IDs, and empty provider IDs. Retain CLI daemon-client coverage for provider-neutral agent hints and Codex fallback coverage for `sandbox_permissions=require_escalated`. - References: [client.go](../../../apps/cli/internal/daemon/client.go) [run.go](../../../apps/cli/internal/app/run.go) [agent daemon runtime.go](../../../packages/agent/daemon/runtime.go) [tuttid command network policy](../../../services/tuttid/agent_command_network_policy.go) ### Codex provider install fails with missing npm - Symptom: Agent setup or the onboarding flow repeatedly reports Codex install failures, and `tuttid` logs show `installerKind=codex_cli_latest`, `exitCode=127`, and `stderr="zsh:1: command not found: npm"` for every npm registry attempt. - Quick checks: Search `tuttid.log` for `agent provider install step failed` and `codex_cli_latest`. If each registry fails in milliseconds with exit code `127`, stop investigating registry reachability; the command never reached npm networking. - Root cause: The Codex CLI installer is daemon-owned but shells out through the daemon environment. Packaged desktop launches may not expose a user-managed `npm` on `PATH`, even though Tutti already has a managed Node runtime for workspace app and external-agent npm work. - Fix: Resolve user `npm` first for compatibility, then fall back to the Tutti managed Node runtime's `npm` before running `npm install -g --prefix @openai/codex --include=optional`. Keep the install prefix in a resolver-searched user directory such as `~/.local` so the installed `codex` remains discoverable after install. - Validation: Add or run service coverage for a daemon environment with no user `npm` and a ready managed Node runtime. Then run `pnpm lint:go` and `cd services/tuttid && go test ./service/agentstatus`. - References: [installer_codex_cli.go](../../../services/tuttid/service/agentstatus/installer_codex_cli.go) [runtime.go](../../../services/tuttid/service/managedruntime/runtime.go) ### Codex ACP warns about user-level config as project-local config - Symptom: Codex ACP startup logs include `Ignored unsupported project-local config keys` for user-level Codex config keys such as `model_provider`, `model_providers`, or `notify`. - Quick checks: Inspect the session cwd and its parents for an accidental project root, such as a `.git` directory under `$HOME`, plus a sibling `.codex/config.toml`. Inspect the generated `codex-home/config.toml`; it should be a session-scoped file, not a symlink to the user's global Codex config. - Root cause: Codex walks upward from the session cwd to identify the project root. If it reaches a parent directory that also contains `.codex/config.toml`, Codex can read the user's global config as project-local config, where user-level keys are unsupported. - Fix: Codex sidecar preparation must treat `CODEX_HOME` as the run-scoped user-level Codex home for application-wide injection, not as a project root. Copy the user's `config.toml` into the run-scoped `codex-home`, then merge `project_root_markers = []` there so ACP sessions do not read accidental parent `.codex/config.toml` files as project-local config. Do not symlink the config, because the run may need session-specific config that must not mutate the global Codex config. Do not create marker files or directories in the user's cwd. - Validation: Add or update `runtimeprep` tests that verify no cwd marker is created, the generated Codex config preserves user-level provider settings while disabling project root markers, and the user's global config is not modified. Run `pnpm lint:go` plus `cd services/tuttid && go test ./... && go build ./...`. - References: [codex.go](../../../packages/agent/runtimeprep/codex.go) [preparer_test.go](../../../packages/agent/runtimeprep/preparer_test.go) ### Cursor sessions create project `.cursor/skills` or `AGENTS.md` changes - Symptom: Starting a Cursor Agent session through Tutti leaves new Tutti-managed skill directories under the repository, such as `.cursor/skills/tutti-cli` or `.cursor/skills/tutti-cli-tutti-6`, or appends a `BEGIN TUTTI-RUNTIME` managed block to the tracked project `AGENTS.md`. The directories may accumulate across runs and the managed block can appear as a tracked working tree change. - Quick checks: Inspect the session sidecar manifest for `provider-skill` entries pointing inside the workspace cwd. For current sessions, `TUTTI_CURSOR_PLUGIN_DIR` should point under the session runtime root, for example `~/.tutti-dev/agent/runs//cursor-plugin/tutti-cli`, and the Cursor ACP command should include `--plugin-dir ` before `acp`. The project `AGENTS.md` should not receive a `TUTTI-RUNTIME` managed block for Cursor sessions. - Root cause: Cursor supports local plugins via `cursor-agent --plugin-dir`, but the previous sidecar path reused project-local native skill installation and wrote Tutti injected skills to `cwd/.cursor/skills`. Repeated runs then allocated suffixed names instead of overwriting the session-owned materialization. The same Cursor preparation path also wrote provider instructions into `cwd/AGENTS.md`, which dirtied tracked repositories. - Fix: Materialize Tutti Cursor skills as a session-scoped Cursor plugin with `.cursor-plugin/plugin.json` and `skills/*/SKILL.md`. Generate the canonical runtime policy and its materialized Skill catalog from the same resolved capability profile instead of maintaining a Cursor-specific Skill catalog. Reconcile the session-owned root on every prepare so resume replaces current managed Skills and removes stale managed entries without touching unmanaged directories. Expose the plugin through `TUTTI_CURSOR_PLUGIN_DIR`, start Cursor ACP as `cursor-agent --plugin-dir acp`. Keep user/project `.cursor/skills` discoverable for composer options, but never write Tutti injected skills or Tutti runtime instructions into the workspace cwd for Cursor sessions. Cursor ACP does not project plugin Skills or Rules into the model context, so append the prepared policy and dynamic catalog to the first provider-only ACP prompt; never project it as user-visible content. Cursor Agent `2026.07.01-41b2de7` does not load plugin hooks in ACP mode, so do not advertise the dormant background-Task guard in the plugin manifest and do not claim that background Task is blocked. Do not install the hook into user or project Cursor configuration as a workaround. - Validation: Add `runtimeprep` coverage that Cursor prepare creates the runtime plugin and dynamic prompt context while leaving project `.cursor/skills` and `AGENTS.md` untouched; add runtime coverage that Cursor ACP includes `--plugin-dir` and injects the prepared context only on its first provider prompt, and agent service coverage that Cursor composer skill discovery includes plugin skills. Then run `cd packages/agent/runtimeprep && go test ./...`, `cd services/tuttid && go test ./service/agent`, and `go test ./packages/agent/daemon/runtime`. - References: [cursor.go](../../../packages/agent/runtimeprep/cursor.go) [acp_provider_cursor.go](../../../packages/agent/daemon/runtime/acp_provider_cursor.go) [skill_options.go](../../../services/tuttid/service/agent/skill_options.go) ### Cursor ACP Glob/Grep fails for absolute Windows search roots - Symptom: A Cursor ACP session on Windows reports that Glob or Grep failed for an absolute path such as `C:/Users/...`. The underlying provider output may be `rg: : IO error for operation on` with an empty operand and `The system cannot find the path specified`, while the same repository is searchable with no explicit path, a relative path, or a native terminal command. - Quick checks: Compare the `cwd` in `agent_session.acp.session_new.start` with the `cwd`/`protocol_cwd` in `agent_session.acp.process_start.start`. Tutti must pass the native Windows workspace path through unchanged. Run the Cursor version's bundled `rg.exe` directly against the same absolute path; if it succeeds, the filesystem and Windows `rg` receiver are healthy. Set `TUTTI_ACP_TOOL_DEBUG=1` before starting the daemon when the raw and normalized ACP tool inputs are needed; treat paths in that diagnostic as sensitive. - Root cause: Tutti owns the ACP process working directory and the `session/new` payload, but Cursor owns the built-in Glob/Grep implementation and constructs its internal `rg` invocation from the provider tool call. In the incident, the provider's error names an empty `rg` operand even though the requested directory was absolute, so the path was lost inside Cursor after Tutti had already passed the native cwd. The ACP event normalizer only projects the provider's completed call; changing its display fields cannot repair the provider's search request. A separate Tutti defect also used POSIX `/` as the generic ACP protocol cwd when no cwd was supplied; Windows now falls back to the process's native cwd instead. - Fix: Cursor's Windows-only prepared prompt context requires workspace-relative roots for its built-in Glob/Grep tools, uses `.` for the workspace root, and routes workspace-external or uncertain absolute paths through native `rg`/PowerShell. If a provider search still fails, the agent must retry via that route and report the retry's actual output; it must not turn the failed call into a successful result. - Validation: Run the runtime preparation and standard ACP tests on Windows. With a real Cursor ACP session, search for a known file using (1) a relative workspace root and (2) an absolute path outside the workspace. Verify that the first uses the provider search successfully and the second uses native terminal search with returned file content. Inspect the ACP diagnostic input if the provider still emits an empty `rg` operand. - Limitation: The Cursor provider owns the failing internal path conversion. Tutti's prompt policy prevents the known trigger for model-driven searches, but it cannot guarantee behavior when Cursor ignores that policy. Do not call this provider defect fixed based only on an error card becoming visible; update or report the Cursor runtime if the raw tool input still loses the path. - References: [standard_acp_session.go](../../../packages/agent/daemon/runtime/standard_acp_session.go), [cursor.go](../../../packages/agent/runtimeprep/cursor.go), [acp_tool_normalizer.go](../../../packages/agent/daemon/runtime/acp_tool_normalizer.go) ### Cursor read-only mode still creates files without approval - Symptom: AgentGUI shows Cursor as Read-only, but the transcript reports a successful `Switch Mode` followed by `Edit`, the file appears on disk, and no approval interaction is shown. - Quick checks: Inspect the exported session settings and ACP startup logs together. If the durable `permissionModeId` is `read-only` while `agent_session.acp.permission_mode.start` reports `mode_id=plan`, then the permission tier was mapped to Cursor's planning workflow rather than its read-only execution mode. Confirm the turn contains no `session/request_permission` before investigating AgentGUI approval cards. - Root cause: Cursor `plan` and `ask` are different workflow modes. Plan can transition into implementation, including a provider-owned `Switch Mode`, whereas Ask retains read/search tools without making changes. Mapping Tutti's durable read-only permission tier to Plan therefore delegated a security boundary to a workflow that could advance into editing. A provider-owned tool call is historical activity, not a canonical approval Interaction, so AgentGUI correctly had no approval request to render. - Fix: Map Cursor `read-only` to ACP `ask`. Keep the independent AgentGUI plan-mode control mapped to ACP `plan`, and when plan mode is turned off, restore the runtime mode derived from the durable permission tier instead of assuming `agent`. - Validation: Cover startup tier mapping (`read-only -> ask`) and a read-only plan-mode round trip (`ask -> plan -> ask`). With a real Cursor ACP binary, verify Ask can search and read project files, a request to create a file does not write one, and no provider-owned mode switch silently enables Edit. - References: [providers.go](../../../packages/agent/daemon/providerregistry/providers.go) [acp_provider_cursor.go](../../../packages/agent/daemon/runtime/acp_provider_cursor.go) [standard_acp_adapter_test.go](../../../packages/agent/daemon/runtime/standard_acp_adapter_test.go) ### Cursor CLI can ask questions but Cursor ACP cannot - Symptom: `cursor-agent` in its interactive CLI can pause and ask the user a structured question, while the same prompt through `cursor-agent acp` either continues without asking or reports that no question tool is available. - Quick checks: Probe the raw ACP `initialize` and `session/new` exchange before changing Tutti's interaction projection. Changing ACP `clientInfo`, advertising extra client capabilities, or passing a root `-H x-cursor-client-type: cli` header does not add the tool. If an injected HTTP MCP exposes `AskUserQuestion`, confirm Cursor emits `session/request_permission` with kind `other`, then a Tutti question interaction whose metadata reports `providerMethod=mcp/tools/call`. - Root cause: Cursor's ACP transport identifies itself internally as client type `acp` and currently omits the interactive CLI's built-in question tool. The ACP bridge contains a native `cursor/ask_question` request path, but changing initialize metadata does not enable it; the transport middleware also overwrites a caller-supplied client-type header. - Fix: Preserve the native `cursor/ask_question` handler for Cursor versions that expose it. For affected versions, bind Tutti's `AskUserQuestion` through the session-scoped HTTP MCP capability advertised by ACP. Keep the bridge on a loopback listener with a per-session bearer token, activate it only for the exact running turn, and route the blocking call through the existing Tutti interactive state machine. Auto-approve only Cursor's exact non-mutating permission titles for this Tutti-owned tool; all other tool permissions must continue through the configured permission tier. - Validation: First assert `session/new` contains the HTTP MCP binding and that a real HTTP `tools/call` blocks until `SubmitInteractive` supplies structured answers. Keep native question and normal permission-tier regression coverage. Finally, run a real Cursor ACP turn: require the model to call `AskUserQuestion`, answer the emitted Tutti interaction, and verify the provider turn completes. On Windows, this also verifies the loopback listener and local process transport; retain POSIX coverage for the same platform-neutral socket path. - References: [cursor_acp_question_mcp.go](../../../packages/agent/daemon/runtime/cursor_acp_question_mcp.go) [cursor_acp_interactive.go](../../../packages/agent/daemon/runtime/cursor_acp_interactive.go) [standard_acp_session.go](../../../packages/agent/daemon/runtime/standard_acp_session.go) ### Codex is installed but Tutti keeps asking to connect or install it - Symptom: Tutti shows the Codex setup surface even though the Codex executable exists. Provider diagnostics report `no_ready_candidate`, and an install action may start repairing the existing npm package. - Quick checks: Correlate `agent_session.acp.request.sent method=initialize` with a later `agent_session.acp.response.unmatched` for the same request ID. A response arriving after the former three- or five-second probe window proves a slow cold start rather than a missing CLI. - Root cause: Codex app-server startup may take longer than the generic fast health-probe budget. Runtime and authentication probes could expire before a valid initialize response, causing the discovered executable to be classified as unavailable and routed into installation. - Fix: Give only Codex runtime and authentication probes a ten-second cold-start budget. Other providers retain their existing bounds, and a faster Codex response still returns immediately. - Validation: Cover the Codex-specific timeout policy, its descriptor-projected auth timeout, and preservation of a longer explicit test/operator timeout. Run `cd services/tuttid && go test ./service/agentstatus`. ### Codex provider appears logged in with an empty auth.json - Symptom: Provider management reports Codex as logged in when `~/.codex/auth.json` is `{}`, but starting Codex fails during TUI bootstrap with `account/read failed: plan type is required for chatgpt authentication`. - Quick checks: Compare `codex login status` with a direct app-server `account/read`. The former may print `Logged in using ChatGPT` and exit successfully solely because `auth.json` exists, while the latter rejects the incomplete account. - Root cause: Codex provider status used the generic text-command runner and accepted `codex login status` as authoritative. That command's file-level check is weaker than the account validation performed by the TUI and app-server. - Fix: Use the descriptor-owned `codex_app_server_account` auth runner. It performs the formal app-server initialize handshake and calls `account/read`; only a structurally valid returned account is authenticated. A failed or malformed response remains unknown and never falls back to auth-file existence. - Validation: Cover authenticated, login-required, and account/read-error responses in the shared app-server probe, then verify tuttID does not authenticate from a present marker when account/read is unknown. ### Codex provider shows login required when global service tier is legacy - Symptom: The workspace dock popup shows Codex as needing login even though `~/.codex/auth.json` contains OAuth tokens. - Quick checks: Run `codex login status`. If it prints `Error loading configuration: ... unknown variant ... expected fast or flex`, inspect the top-level `service_tier` in `~/.codex/config.toml`. - Root cause: Newer Codex CLIs only accept `service_tier = "fast"` or `"flex"` in global config. Older values such as `"default"` or `"priority"` make the status command fail before it can report auth state, so tuttid classifies auth as unknown and the renderer shows login/refresh. - Fix: Provider status and login commands should pass a temporary Codex config override such as `-c 'service_tier="fast"'` instead of mutating the user's global config. Session-scoped Codex homes should continue sanitizing copied config through `codexConfigWithSupportedServiceTier`. - Validation: Add or update `agentstatus` tests for the Codex status/login command shape, then run `cd services/tuttid && go test ./service/agentstatus`. ### Codex provider shows login required when only an API key is configured - Symptom: The environment wizard / dock marks Codex as needing login ("未登录") even though an API key is configured and Codex sessions can already run successfully. Common sources are `OPENAI_API_KEY` in the environment, `api_key` in `~/.codex/config.toml`, or `OPENAI_API_KEY` inside `~/.codex/auth.json` as written by custom-provider switchers. - Quick checks: Run `codex login status` (often prints `Not logged in`). Confirm an API credential exists via `echo $OPENAI_API_KEY`, `grep -E 'api_key' ~/.codex/config.toml`, or a non-empty `OPENAI_API_KEY` field in `~/.codex/auth.json`. - Root cause: `codex login status` only reflects a ChatGPT OAuth session. API-key billing from the environment, config, or `auth.json` is invisible to that command, so tuttid used to treat the provider as `auth_required` and block the wizard even though the runtime can authenticate with the key. - Fix: Provider status should call `providerHasAPICredential` for Codex the same way it does for Claude Code, including `auth.json` `OPENAI_API_KEY`. When an API key is present, report auth as authenticated with method `apiKey` / label `API Usage Billing` instead of requiring login. A bare custom base URL without a credential must not trigger this override. - Validation: Add or update `agentstatus` tests for environment, config.toml, and auth.json API-key-without-login readiness, then run `cd services/tuttid && go test ./service/agentstatus`. ### Codex session fails with not connected when config file dependencies are relative - Symptom: Codex is installed and `codex login status` reports logged in, but Tutti chat fails with `agent session is not connected`. Daemon logs show `thread/start` failing with `failed to load configuration: No such file or directory (os error 2)`. Tools such as CC Switch often set `model_catalog_json = "cc-switch-model-catalog.json"` in `~/.codex/config.toml`. User-managed instruction files can reproduce the same failure with `model_instructions_file = "gpt5.5-unrestricted.md"`. If that catalog file is missing entirely, the same config error can also make provider status show login required (`auth_unknown`) even though OAuth tokens exist. - Quick checks: `grep -E 'model_catalog_json|model_instructions_file' ~/.codex/config.toml` and confirm each relative referenced file exists under `~/.codex/`. Inspect the run-scoped `~/.tutti-dev/agent/runs//codex-home/` (or `~/.tutti/...` in prod): `config.toml` is copied, but relative catalog and instruction files must also be present there. - Root cause: Tutti prepares a run-scoped `CODEX_HOME` and copies only `config.toml` (plus auth/plugin/skill exposure). Relative `model_catalog_json` and `model_instructions_file` paths resolve against that sandbox home, so the dependency is missing unless Tutti mirrors it. - Fix: After copying `config.toml`, resolve top-level `model_catalog_json` and `model_instructions_file`. For relative paths under `~/.codex`, symlink (or copy) the file into the run-scoped `CODEX_HOME` at the same relative path. Absolute paths need no mirror but must be validated in place. Missing, unreadable, non-regular, or illegal dependencies should fail preparation before provider startup with a safe `agent.config_dependency_unavailable` diagnostic. Do not mutate the user's global config. - Validation: Add or update `runtimeprep` tests that set relative catalog and instruction files beside `config.toml` and assert the sandbox exposes them. Run `cd packages/agent/runtimeprep && go test ./...`. - References: [codex.go](../../../packages/agent/runtimeprep/codex.go) [preparer_test.go](../../../packages/agent/runtimeprep/preparer_test.go) ### Codex model picker collapses to the configured model - Symptom: With the default OpenAI provider, the composer model picker contains only the top-level `model` from `~/.codex/config.toml`, while a directly initialized `codex app-server` connection returns multiple models from `model/list`. - Quick checks: Compare the composer options with a directly initialized Codex app-server `model/list` response. The returned catalog remains authoritative when `model_provider` is custom; `model_catalog_json` changes the catalog Codex returns but is not required to prevent Tutti from trimming it. Search daemon logs for `composer model catalog lookup failed`, `Not initialized`, or a Codex `model/list` timeout, then compare the request sequence with the app-server initialization contract. - Root cause: Model discovery sent `initialize` and `model/list` back to back without reading the initialize response or sending the `initialized` notification. An app-server that enforces the connection handshake can reject or withhold `model/list`. The failed catalog then falls back to the configured model, making the protocol failure look like a valid one-option picker. - Fix: Keep one stdout scanner for the exchange: send `initialize`, read and validate the matching response, send `initialized`, and only then send `model/list`. Preserve the configured-model fallback for genuine discovery failures. - Validation: Use a fake app-server that rejects `model/list` until it has returned the initialize response and received `initialized`. Run `cd services/tuttid && go test ./service/agent -run TestCodexCLIModelLister` plus `pnpm lint:go`. - References: [codex_model_catalog.go](../../../services/tuttid/service/agent/codex_model_catalog.go) [codex_model_catalog_test.go](../../../services/tuttid/service/agent/codex_model_catalog_test.go) ### Codex composer model and reasoning selectors stay loading - Symptom: The empty Codex composer shows loading placeholders for both model and reasoning, even though provider status reports Codex as ready. - Quick checks: Correlate a ready Codex provider snapshot in `tuttid.log` with `agent.composer_options.load` in `tutti-desktop.log`. A duration near 15 seconds with `errorCode=ETIMEDOUT` means the Desktop request deadline expired before Composer Options returned. If tuttid later logs `superfluous response.WriteHeader`, or a detached `codex app-server` remains after the request, the canceled handler did not finish cleaning up its discovery subprocess. - Root cause: Codex Composer Options has two independent waits: `model/list` feeds the model, reasoning, and speed controls, while app-server capability discovery feeds skills and capability entries. A legacy combined response can still wait for both, and repeated capability failures can otherwise start another eight-second probe for every refresh. - Fix: Desktop requests `section=core` for model controls. It requests `section=capabilities` only when the user opens or uses a capability surface, so the capability response and its eight-second provider timeout never block the model controls. The legacy `section=full` response still starts both catalogs concurrently. Capability loads use single-flight sharing plus a short negative cache so identical callers do not stampede a broken app-server. Run every short-lived Codex app-server in its own process group, begin process reaping immediately, and make timeout cancel the entire group. The daemon keeps one initialized Codex app-server session warm per provider for up to two minutes, and refreshes the five-minute model catalog in the background after expiry or an auth/config invalidation. Identical atomic rewrites of Codex auth/config do not invalidate the catalog because the watcher compares file content. Keep the Desktop deadline unchanged so a genuinely stuck daemon request still fails closed. - Validation: Block both catalog fixtures and assert both start before either is released. Use a fake app-server whose child retains stdout and assert model and capability timeouts return promptly with no surviving child. Assert concurrent cold catalog callers share one fetch, stale options remain visible while the background refresh runs, and repeated requests reuse one app-server process. Finally, time a cold Composer Options request and confirm it completes within the Desktop deadline. Useful logs are `agent.model_catalog.fetch_start`, `stage_settled`, `fetch_settled`, `request_settled`, `agent.composer_options.load`, and `process_idle_close`. Composer telemetry reports section/stage, outcome, duration, and bounded model identifiers without paths or settings. When an auth/config watcher causes invalidation, `agent.model_catalog.invalidated` also includes the exact changed file and change kind; use that field to distinguish Codex auth/config churn from a provider-side fetch failure. - References: [composer_options.go](../../../services/tuttid/service/agent/composer_options.go) [codex_appserver_process.go](../../../services/tuttid/service/agent/codex_appserver_process.go) [codex_model_catalog.go](../../../services/tuttid/service/agent/codex_model_catalog.go) [codex_capability_catalog.go](../../../services/tuttid/service/agent/codex_capability_catalog.go) ### Codex custom model_provider hides app-server models, duplicates replies, or shows metadata warnings - Symptom: With `model_provider` set to a custom endpoint and `model` set to a vendor model id, the composer may show only the configured model even though Codex `model/list` returns several models and their reasoning efforts. A turn may also show the same assistant reply twice, or the transcript may repeatedly display `Model metadata for ... not found. Defaulting to fallback metadata`. - Quick checks: Inspect top-level `model_provider`, `model`, and `model_catalog_json` in `~/.codex/config.toml`, then compare Tutti's composer options with a direct app-server `model/list` response. In persisted session messages, look for two completed assistant rows with equivalent text but different message ids in one turn. The composer should preserve the models and `supportedReasoningEfforts` returned by app-server regardless of whether the active `model_provider` is OpenAI or custom. - Root cause: A Tutti post-processing rule treated any non-default `model_provider` as proof that Codex's discovered catalog was unrelated and collapsed it to the top-level configured model. This also discarded per-model reasoning metadata. Separately, Codex can finalize an assistant item after an early stream boundary and replay the answer again in `turn/completed`, sometimes with whitespace polish; treating each report as a new segment creates duplicate bubbles. The model-metadata warning is runtime diagnostic noise rather than an actionable user error. Persisted skill-context warnings may omit their optional `source` metadata, and Codex has emitted both percentage and non-percentage variants of that wording. - Fix: Treat Codex app-server `model/list` as the authoritative catalog regardless of `model_provider`. Preserve the full returned list and reasoning metadata; use the top-level configured model only to select the default or as the existing fallback when discovery fails. Preserve the assistant message id for whitespace-equivalent item-finalization text and ignore turn-final text after an assistant segment has already completed. Filter the metadata fallback warning through the same AgentGUI diagnostic-notice projection used for skills-context-budget warnings. Match the optional percentage in the skills warning as diagnostic context rather than as part of its identity; accept a missing source only for that exact warning, preserve explicitly non-runtime notices, and keep the metadata fallback warning runtime-only. - Validation: Run `go test ./packages/agent/daemon/runtime -run 'TestApplyAssistantFinalText|TestApplyAssistantTurnFinalText|TestCodexAppServerAdapterExecStreamsTurn'`, `cd services/tuttid && go test ./service/agent -run TestAgentModelCatalog`, and the focused AgentGUI projection test. ### Claude SDK Grep or Glob unavailable despite Claude Code preset - Symptom: Claude emits `Grep` or `Glob`, but the SDK returns `No such tool available` and suggests using shell `grep` or `find`. - Root cause: Some Claude Code SDK native builds expose search through `Bash` by default. The `claude_code` tool preset may not register dedicated `Grep`/`Glob` tools unless the host also lists them in `allowedTools` or `tools`. - Fix: Keep the `claude_code` preset as the base tool set, and explicitly include `Grep` and `Glob` in Claude SDK `allowedTools`. Avoid replacing `tools` with a short string list unless the host intentionally wants to narrow every built-in tool available to Claude. - Validation: Assert the sidecar start payload carries `allowedTools: ["Grep", "Glob"]`, and typecheck against the local `@anthropic-ai/claude-agent-sdk` definitions. ### Provider process loses final stdout or a sidecar fails during startup - Symptom: A short-lived provider helper exits successfully but its final stdout frame is absent, or a long-lived SDK sidecar intermittently appears to exit before its startup response is consumed. - Root cause: `os/exec.Cmd.StdoutPipe` and `StderrPipe` make pipe draining and `Cmd.Wait` ordering the caller's responsibility. Waiting for readers before `Cmd.Wait` makes process reaping depend on pipe EOF, while calling `Cmd.Wait` first can close a short-lived process's pipes before its last bytes are delivered. - Fix: Give `Cmd.Stdout` and `Cmd.Stderr` frame writers instead of managing `StdoutPipe`/`StderrPipe` directly. `os/exec` then owns the copy goroutines and `Cmd.Wait` returns only after the final writes complete, without delaying startup-time streaming for a live sidecar. - Validation: Run the local-process transport tests together with the Claude SDK sidecar start, approval, and controller tests repeatedly. Keep explicit assertions that final stdout arrives before the exit frame and that a live process can exchange frames before it exits. ### Concurrent agent CLI installs corrupt shared npm global state - Symptom: Two agent-provider installs started close together can leave global npm bins or package directories half-written. Follow-up probes may report `cli_not_found`, `acp_adapter_not_found`, or a binary that exists but fails immediately after install. - Quick checks: Confirm whether more than one `tuttid` agent-provider install action or desktop install button fired at roughly the same time for commands shaped like `npm install -g ...`. Inspect the daemon run-state lock path under `TUTTI_STATE_DIR/run/locks/npm-global-install.lock` while an install is in progress to verify later installs are waiting instead of running in parallel. - Root cause: npm global installs mutate shared package and bin locations. Without a daemon-owned cross-process lock, concurrent `npm install -g` commands can race while writing the same global state and leave a corrupted runtime. - Fix: Serialize agent-provider `npm install -g` commands behind the daemon install lock and keep the lock path under daemon-owned state. Start the install timeout only after the lock is acquired so queued installs do not consume their npm execution budget while waiting. Do not auto-delete the lock on a timer. Instead, recover the lock during daemon startup only when the recorded owner pid is no longer running. If recovery is still needed manually, clear `npm-global-install.lock` only after verifying no install is still running. - Validation: Run `pnpm lint:go` plus `cd services/tuttid && go test ./... && go build ./...`. Then trigger two install actions in quick succession and confirm the second waits for the first instead of starting another global npm mutation. ### Agent provider install looks idle while a non-Codex installer is running - Symptom: Provider setup appears stuck or idle even though `tuttid.log` has an `agent provider install step started` entry and no matching completed/failed line yet. This is most visible for Claude Code CLI or ACP adapter installs. - Quick checks: Compare the install start timestamp with the log export timestamp before calling it hung. Also check for a later completed install log line and the provider binary path in the rechecked runtime log. If `tuttid.log` shows `active_action.output_appended` but desktop diagnostics keep reporting `logLines=0`, check whether the status request copied `activeAction` before installer output arrived, or whether the renderer stopped refreshing while the install action was still pending. - Root cause: The provider installer is daemon-owned and can legitimately run for minutes, but renderer progress must come from the generic provider `activeAction` status field. Do not special-case long-running install progress to Codex. - Fix: Set, stream stdout into, expose, and clear `ActiveAction` for every provider install action. Keep provider-specific installer details inside `services/tuttid/service/agentstatus` and project only the transport-safe active action shape through the API seam. Refresh the provider's active action snapshot at the end of `List`, and short-poll provider status while a daemon install action is pending so live installer output can reach the wizard. - Validation: Run `cd services/tuttid && go test ./service/agentstatus ./api` and `pnpm check:api-generated`. Trigger a Claude Code install and confirm status responses include `activeAction` while the CLI or adapter step is in flight. ### Legacy Claude ACP adapter appears stale after external registry migration - Symptom: With `TUTTI_CLAUDE_CODE_RUNTIME=acp`, Claude Agent provider status is not ready, or live ACP options do not match the package version advertised by the ACP External Agent Registry. Another form is Claude Code context usage briefly showing `0%` during a running session or around compaction, then returning to the prior nonzero value on the next usage update. A third form is new Claude Code sessions failing during startup with `Invalid value for config option fast: standard`. - Quick checks: First confirm the runtime is legacy ACP. The default Claude Code runtime is SDK; SDK provider availability checks the `claude` CLI plus the Claude SDK sidecar entry and must not require `claude-acp`. Inspect `/agent-providers/external-agent-registry/cache/registry.json` and the package manifest under `/agent-providers/external-agent-registry/packages/claude-acp/node_modules/@agentclientprotocol/claude-agent-acp/package.json`. `which claude-agent-acp` only describes a user/global shim and is no longer the Tutti-owned Claude adapter source. For usage flicker, inspect that package's `dist/acp-agent.js` for `sessionUpdate: "usage_update"` near `compact_boundary`; it must not publish `used: 0` when the SDK `getContextUsage()` probe fails. For speed failures, inspect the live `fast` config option values advertised by the managed package; supported native Claude ACP packages that fall back to select options use `off` and `on`. - Root cause: Tutti resolves Claude ACP from the external agent registry and installs the npm adapter into a daemon-owned prefix with managed npm. A stale or missing prefix package, stale registry cache, or unavailable managed Node runtime can make the adapter unavailable even when a global `claude-agent-acp` exists. Usage flicker can also come from the managed bridge bundle itself publishing an invalid zero context usage after a failed compact-boundary usage probe; AgentGUI only displays the normalized runtime context it receives. Speed failures come from treating Tutti's internal `standard` / `fast` speed tier values as ACP wire values; supported Claude ACP packages advertise native `fast` config values as `off` / `on`. - Fix: Run the provider install action so tuttid refreshes the registry, resolves the managed Node runtime, and installs the npm package into the per-agent prefix. Do not compensate by changing static model catalogs for behavior that should come from the live ACP package. Keep the Tutti claude-agent-acp patch script authoritative for bridge behavior and apply it to the managed package; do not mask invalid usage in AgentGUI. Keep Tutti's internal speed tiers stable, but translate Claude ACP `fast` config values at the adapter boundary according to the live advertised options, and normalize the live value back before projecting runtime settings. - Validation: Run `go test ./services/tuttid/service/agentstatus`, then confirm a stale global adapter is ignored and the install action uses managed npm with `--prefix /agent-providers/external-agent-registry/packages/claude-acp`. For usage flicker, run `node services/tuttid/service/agentstatus/assets/patch-claude-agent-acp.mjs --dist ` twice and confirm the second run reports no changes, then inspect the bundle and confirm `lastAssistantTotalUsage = usedTokens ?? 0` is absent. For speed compatibility, run the Claude ACP adapter tests that cover native `off` / `on` advertised values and confirm legacy `standard` / `fast` advertised values are ignored. - References: [service.go](../../../services/tuttid/service/agentstatus/service.go) [store.go](../../../services/tuttid/service/externalagentregistry/store.go) [patch-claude-agent-acp.mjs](../../../services/tuttid/service/agentstatus/assets/patch-claude-agent-acp.mjs) ### Cursor ACP context ring stays empty or usage looks wrong - Symptom: A Cursor AgentGUI session shows an empty context ring, `0%`, or stale context usage while the session is actively running. Check & Settings may show the Cursor subscription tier from `cursor-agent about`, but account quota still reads as unsupported. - Quick checks: Grep tuttid logs for `event=agent_session.acp.usage_update` while reproducing the session. Inspect `provider`, `parsed_ok`, `context_known`, `raw_used`, `raw_size`, `used_tokens`, `total_tokens`, and `quota_count` on each event. If no events appear, Cursor is not pushing ACP `usage_update` for that session. If events appear with `parsed_ok=false` or missing `raw_used` / `raw_size`, inspect the raw ACP payload shape before changing AgentGUI. - Root cause: Tutti's standard ACP adapter already normalizes `usage_update` into runtime context, but Cursor may omit the event or publish a different payload than Codex/Claude bridges. Subscription tier display comes from auth probing, not from `usage_update`. - Fix: Use the diagnostic log fields to decide whether to fix adapter parsing or wait for Cursor to publish usage updates. Do not mask missing usage in AgentGUI when the provider never sent `usage_update`. - Validation: Run `go test ./packages/agent/daemon/runtime -run UsageUpdate` and start a Cursor session while tailing tuttid logs for `agent_session.acp.usage_update`. - References: [standard_acp_adapter.go](../../../packages/agent/daemon/runtime/standard_acp_adapter.go) [acp_live_state.go](../../../packages/agent/daemon/runtime/acp_live_state.go) [service_helpers.go](../../../services/tuttid/service/agentstatus/service_helpers.go) ### Claude SDK model aliases resolve to configured Anthropic defaults - Symptom: A Claude Code SDK session shows a Tutti composer model alias such as `sonnet` or `haiku`, but the model response or error mentions a different concrete model such as `mimo-v2.5-pro`. - Quick checks: Inspect the effective Claude Code settings env from `$CLAUDE_CONFIG_DIR/settings.json` or `~/.claude/settings.json`, especially `ANTHROPIC_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, `ANTHROPIC_DEFAULT_HAIKU_MODEL`, `ANTHROPIC_DEFAULT_OPUS_MODEL`, and `ANTHROPIC_BASE_URL`. Also inspect the session `runtime_context_json` for `providerConfig.baseUrl` and `model` before assuming the UI selected the concrete provider model directly. - Root cause: The SDK sidecar passes Tutti's Claude Code aliases to `@anthropic-ai/claude-agent-sdk`, while the SDK/Claude Code runtime still resolves those aliases through the user's Claude settings env. A proxy such as MiMo may map `sonnet` to a configured concrete model, and provider access errors then mention that concrete model instead of the Tutti alias. - Fix: Keep the sidecar inheriting the user's Claude settings so credentials and base URL keep working. Fix provider access by changing the user's Claude settings or managed provider model config, not by hard-coding Tutti's static alias list. Report a typed `SessionStateSnapshot.Capabilities` value before projecting the session to AgentGUI so stale provider-private runtime context cannot disable prompt-image paste. - Validation: Confirm the session runtime context shows `adapter: claude-agent-sdk`, the expected `providerConfig.baseUrl`, and confirm the typed session capability snapshot includes `imageInput`. Then run the Claude SDK adapter tests plus the agent service capability-projection tests. - References: [claude_sdk_adapter.go](../../../packages/agent/daemon/runtime/claude_sdk_adapter.go) [service_helpers.go](../../../services/tuttid/service/agent/service_helpers.go) [composer_live_model_discovery.go](../../../services/tuttid/service/agent/composer_live_model_discovery.go) ### Claude SDK rejects live bypassPermissions mode - Symptom: A Claude Code SDK session starts in `default`, `auto`, or plan mode, then live switching to `bypassPermissions` fails with `Cannot set permission mode to bypassPermissions because the session was not launched with --dangerously-skip-permissions`. Or the session state already shows `permissionModeId=bypassPermissions`, but ordinary tools such as Bash still surface AgentGUI approval prompts. - Quick checks: Inspect the SDK query options emitted by `packages/agent/claude-sdk-sidecar`. `allowDangerouslySkipPermissions` must be enabled when the query is created, not only when the initial permission mode is already `bypassPermissions`. In root/sandboxed runtimes, confirm the sidecar process receives `IS_SANDBOX=1`. If the query launched correctly, inspect the sidecar `canUseTool` callback path; bypass mode should short-circuit ordinary tools after preserving special handling for `AskUserQuestion` and `ExitPlanMode`. - Root cause: Claude SDK treats bypass permission support as a session launch capability. `query.setPermissionMode("bypassPermissions")` cannot enable that capability after the query has already started. Tutti's sidecar also owns the `canUseTool` callback; if that callback always requests AgentGUI approval, it can reintroduce prompts even after the SDK permission mode is bypass. - Fix: Gate bypass availability with the same rule as Claude Agent ACP: non-root processes can bypass, and root processes can bypass only when `IS_SANDBOX` is set. Launch the SDK query with `allowDangerouslySkipPermissions` whenever that gate passes, regardless of the current permission mode. In `canUseTool`, handle `AskUserQuestion` and `ExitPlanMode` first, then directly allow ordinary tools when the effective permission mode is `bypassPermissions`. - Validation: Add sidecar coverage for a `default` session whose query still receives `allowDangerouslySkipPermissions: true`, plus daemon runtime coverage that Claude SDK sidecar process env includes `IS_SANDBOX=1`. Add callback coverage proving bypass mode allows an ordinary Bash request without `approval_requested`, while `AskUserQuestion` still surfaces user input. ### Extension session create rejects a semantic permission id - Symptom: An extension exposes a full-access option whose id is provider-specific, such as `bypassPermissions`, but session creation fails after a caller sends `full-access`. The Tutti CLI reports `reasonCode=unsupported_permission_mode_id` and lists the currently accepted ids. - Quick checks: Query Composer Options for the exact Agent target. Compare `permissionConfig.modes[].id` with `permissionConfig.modes[].semantic` and inspect the submitted `permissionModeId`. Multiple ids may legitimately have the same semantic. - Root cause: `semantic` is provider-neutral classification metadata. It is only a launch id for extensions whose signed Composer profile declares semantic launch permission mapping. Runtime-id extensions require the exact provider-owned id returned by Composer Options. - Fix: Refresh Composer Options and round-trip the selected `id` verbatim. Do not hard-code a provider permission alias or collapse modes by semantic. Extension implementations keep the signed Composer profile as the launch contract; runtime config options may enrich current state and labels but cannot rewrite ids. Composer Options ignores a persisted default that is no longer in the signed profile and falls back to live runtime state, then the profile default; an explicit obsolete id still fails. Exact runtime ids also take precedence over semantic and historical aliases in the Standard ACP lookup. - Validation: Confirm Composer Options preserves every declared runtime id in profile order, including multiple ids with the same semantic, and that the exact selected id reaches the runtime start input and final Standard ACP mapping. Test an exact runtime id that collides with another mode's semantic alias, plus an obsolete persisted default. An invalid explicitly supplied semantic alias must fail before hidden discovery or visible session creation. - References: [extension_composer_options.go](../../../services/tuttid/service/agent/extension_composer_options.go) [composer_runtime_context.go](../../../services/tuttid/service/agent/composer_runtime_context.go) ### Claude Code logs out after sending a message (invalid_grant, credentials wiped) - Symptom: Inside the desktop app, sending a message around the OAuth token expiry window leaves Claude Code in a "Not logged in · Please run /login" state. The keychain entry (`Claude Code-credentials`) has empty `accessToken`/`refreshToken` and `expiresAt: 0`, while the plaintext `~/.claude/.credentials.json` may still hold a valid token. The Claude CLI alone does not reproduce it. - Quick checks: Capture `/v1/oauth/token` traffic (mitmproxy). A failure may show `Client disconnected` immediately before later refresh attempts return `400 invalid_grant`. Search `tuttid.log` for `event=agent.model_discovery.hidden_session_triggered provider=claude-code`; this info-level event confirms that Tutti actually triggered a hidden Claude live-model discovery session. The event intentionally includes only the provider and random discovery-session id. - Root cause: Composer-options loading spawned a hidden, `visible:false` Claude live-model discovery session that shares the on-disk credential store with the real conversation session and deleted it as soon as the model list was read. When it performs an OAuth refresh near expiry, the server can rotate the refresh token even if the local client disconnects before receiving or persisting the response. The real session later refreshes with the now-consumed refresh token, gets `400 invalid_grant`, and Claude Code wipes the stored credentials. Because `fallbackStorage` prefers the (now empty) keychain entry over the still-valid plaintext file, the user is locked out. - Fix: Cold composer options must always have a static Claude fallback (`default`, `opus`, `sonnet`, `haiku`, plus any configured custom model) so the UI never depends on live discovery. A cold-start live discovery may run at most once per provider/workspace/cwd cache key, but it must be hidden, serialized with other Claude startups, and deleted only after a delayed grace period rather than immediately after the model list appears. Successful discovery updates the daemon live-model cache; later composer-options calls prefer cached models or model options reported by a real running Claude session over the static fallback. Claude Create model validation should only use cached live-model options; it must not start discovery. If the daemon exits before the delayed cleanup timer fires, later persisted-session reads must delete the stale hidden discovery session instead of restoring it as a real conversation. - Validation: Add daemon service tests for Create cache-only validation, SendInput waiting on the Claude startup slot before runtime exec, static Claude cold-start model options, reusing model options from a running Claude session, cold-start discovery running once, delayed hidden discovery cleanup, and stale persisted hidden discovery cleanup after restart. Run targeted agent service Go tests plus the daemon Go lint/test/build lanes. - References: [composer_live_model_discovery.go](../../../services/tuttid/service/agent/composer_live_model_discovery.go) [model_validation.go](../../../services/tuttid/service/agent/model_validation.go) ### Claude Code `Not logged in` renders as a file link instead of sign-in guidance - Symptom: A Claude Code turn renders the plain text `Not logged in · Please run /login`. Clicking `/login` tries to open a local file and may show a missing-file toast instead of the Agent login flow. - Quick checks: Inspect the durable assistant message and owning Turn. If both are `completed` even though the body is the standalone login notice, the Claude SDK returned an authentication failure as successful assistant output. The generic failed-message recovery path will not run for that shape. - Root cause: The Claude SDK can pair its standalone login notice with a successful result. AgentGUI previously recovered plain authentication errors only when the message status was `failed`, so the completed notice fell through to Markdown rendering, where `/login` also matched the local absolute-path link rule. - Fix: Preserve provider-agnostic recovery for failed messages. Additionally, for completed messages, recognize the short, whole-message standalone login notice and recover it as `auth_required`. Match the provider-owned output shape rather than branching on provider identity; keep the matcher length-bounded and anchored so ordinary answers that discuss login text are not reclassified. - Validation: Cover the completed Claude notice rendering the authentication card and a normal completed answer that quotes the notice remaining ordinary content. - References: [agentErrorPresentation.ts](../../../packages/agent/gui/shared/agentEnv/agentErrorPresentation.ts) [AgentMessageBlock.tsx](../../../packages/agent/gui/shared/agentConversation/components/AgentMessageBlock.tsx) ### Model Plan check succeeds but Kimi Claude Code turns wait and then return 401 - Symptom: An Anthropic-protocol Model Plan using `https://api.kimi.com/coding/` passes model discovery/inference checks, but a custom Agent backed by Claude Code stays `running` for roughly three minutes and then appends `Failed to authenticate. API Error: 401`. - Quick checks: Confirm the submit claim is `accepted`, the user message exists, and the runtime reached `runtime.turn_goroutine_started`. Inspect only environment variable names and whether each is non-empty, never credential values. If the child has a non-empty `ANTHROPIC_AUTH_TOKEN` but no non-empty `ANTHROPIC_API_KEY`, the plan credential was injected with the wrong auth shape. - Root cause: Model Plan detection sends Anthropic requests with `x-api-key`. Runtime preparation previously treated every non-`api.anthropic.com` endpoint as a bearer-token relay and launched Claude Code with `ANTHROPIC_AUTH_TOKEN`. Kimi Coding's Claude Code contract requires `ANTHROPIC_API_KEY`, so the same valid credential passed detection and failed only in the real Agent process. - Fix: Keep the existing bearer default for relay endpoints, but classify `api.kimi.com` alongside the official Anthropic endpoint for `ANTHROPIC_API_KEY` injection. Also preserve Claude SDK `assistant.error` and `result.is_error` as failed message/turn state; a result subtype of `success` is not authoritative when either error signal is present. - Validation: Run `go test ./packages/agent/runtimeprep ./packages/agent/daemon/runtime` and `pnpm --dir packages/agent/claude-sdk-sidecar test`. Verify a newly created Kimi-backed session has a non-empty `ANTHROPIC_API_KEY`, an empty `ANTHROPIC_AUTH_TOKEN`, and no 401. Existing running sessions retain their launch environment and must be recreated. - References: [Kimi Claude Code setup](https://www.kimi.com/code/docs/en/third-party-tools/claude-code.html) [model_endpoint.go](../../../packages/agent/runtimeprep/model_endpoint.go) [messageRouter.ts](../../../packages/agent/claude-sdk-sidecar/src/messageRouter.ts) ### Kimi Code shows no models and silently ends every turn - Symptom: The Kimi Code model picker is empty. Sending a prompt appends the user message, but no assistant message or visible error appears and the Turn settles immediately. - Quick checks: Inspect the ACP `session/new` result. Kimi may advertise the model selector through `configOptions[id="model"]` with an empty `currentValue` and empty `options`, rather than through the top-level `models` object. If `session/prompt` then returns `stopReason: "end_turn"` without an assistant chunk or tool call, treat it as a hidden provider failure rather than a successful empty answer. Check that the signed Kimi Extension routes `/status` to the native status panel with `showStatus` and `/usage` to the runtime with `submitImmediate`. - Root cause: Kimi Code can create an ACP session while no model is configured. Its ACP adapter maps some underlying model, authentication, plan, and balance failures to a normal `end_turn` with no output because ACP has no failed stop reason. The setup guard previously recognized only the top-level `models` shape. - Fix: Reject both empty ACP model shapes during generic setup. A normal ACP terminal with neither assistant output nor tool activity must settle as `provider_empty_response`, producing a visible conversation error card that points users back to model and account setup. Turns with only thinking or a system notice remain valid because they produced observable assistant output. Keep Kimi's slash-command behavior declarative in the signed Extension. - Validation: Cover empty and populated `models`/`configOptions` selectors, thinking-only and notice-only ACP turns, and an otherwise normal empty ACP `end_turn`. Validate in the Extension repository that `/status` uses `showStatus` and `/usage` uses `submitImmediate` against the pinned real runtime. - References: [standard_acp_setup.go](../../../packages/agent/daemon/runtime/standard_acp_setup.go) [standard_acp_turn.go](../../../packages/agent/daemon/runtime/standard_acp_turn.go) [createDesktopAgentStatusSource.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/createDesktopAgentStatusSource.ts) [Agent Extensions](../../architecture/agent-extensions.md) [Kimi Code Agent Extension](https://github.com/tutti-os/agent-extension-kimi-code) ### CodeBuddy account panel does not distinguish API billing from Coding Plan - Symptom: CodeBuddy `/status` opens the native account panel, but its limits row says the current Agent does not provide quota limits. Ordinary API keys and Coding Plan credentials also have the same presentation. - Quick checks: Confirm the provider is `acp:codebuddy`. Inspect CodeBuddy's effective `CODEBUDDY_API_KEY`, `CODEBUDDY_BASE_URL`, `CODEBUDDY_AUTH_TOKEN`, and `apiKeyHelper` presence without copying credential values into logs. Coding Plan keys use the `sk-sp-` prefix or an endpoint whose path contains `coding`. - Root cause: CodeBuddy's ACP `usage_update` reports context usage and per-request cost, but the pinned runtime does not expose or document a complete account-level credit-balance contract. Private product endpoints, filtered package queries, and a single page of resource rows cannot prove a complete account balance. Tutti previously had no CodeBuddy account probe, so the native panel treated the provider as unsupported and could not identify the billing mode. - Fix: CodeBuddy's Extension declares a separately published Provider-owned account usage companion. The companion owns only CodeBuddy's effective configuration precedence and billing-mode classification. Tutti executes the generic `accountUsage` capability and consumes a closed Provider-neutral snapshot. Ordinary API keys report `api` with quota state `not_applicable`; Coding Plan reports `coding_plan` with quota state `unavailable`; native authentication reports `provider_account` with quota state `unavailable`. The UI renders API `not_applicable` as `—`; Coding Plan and native accounts render the localized account-quota-unavailable copy. Neither presentation claims a verified credit balance. Until the Provider publishes a contract that proves a complete snapshot, the companion emits no exact Credits. It does not enumerate or read native session files, decode JWTs, execute credential helpers, inspect local `expiresAt`, or call private account endpoints. Runtime/ACP therefore remains the only login and refresh authority. Tokens, account identifiers, paths, raw Provider responses, and Provider messages never cross the daemon API or renderer IPC and never enter logs. - Validation: Cover ordinary API keys, Coding Plan keys, authentication tokens, `apiKeyHelper`, effective settings precedence, and stable errors. Prove that simultaneous expired and valid session files are never read, Runtime token refresh cannot affect the probe, private package endpoints are never called, incomplete package pagination cannot be presented as exact Credits, concurrent probes coalesce at the Tutti boundary, and credentials or raw errors are never projected. Also cover generic renderer labels and exact amount validation for future Providers with a complete contract. Run the Extension's Linux and Windows companion checks plus Tutti's daemon, Desktop, typecheck, i18n, and changed-aware push-ready gates. - References: [CodeBuddy Agent Extension](https://github.com/tutti-os/agent-extension-codebuddy) [account_usage.go](../../../services/tuttid/service/agentextension/account_usage.go) [agentTargetAccountUsageProbe.ts](../../../apps/desktop/src/main/agentTargetAccountUsageProbe.ts) ### Kimi Code account panel says the Agent provides no quota limits - Symptom: Kimi Code `/status` opens the native account panel, but its limits row says the current Agent does not provide quota limits even though the selected account uses Coding Plan. - Quick checks: Confirm the provider is `acp:kimi-code`, the active model in `$KIMI_CODE_HOME/config.toml` (or `~/.kimi-code/config.toml`) resolves to `managed:kimi-code`, and the matching OAuth credential exists under the Kimi-owned `credentials/` directory. Search Desktop logs for `agent.usage_probe.result`; logs contain only strategy/error metadata and must never contain credentials or raw responses. - Root cause: Kimi Code 0.28 exposes session/context usage through ACP, but it does not expose Coding Plan account windows as structured ACP data. The native panel therefore had no account-level source and treated the extension provider as unsupported. - Fix: Kimi's Extension declares a separately published Provider-owned account usage companion. It resolves the selected model's provider and Kimi credentials, while Tutti executes the generic `accountUsage` capability and consumes only Provider-neutral billing/quota fields. API-key providers return `api` with no quota rows. The managed provider requests Coding Plan `/usages` and emits normalized quota percentages/reset times. Kimi owns OAuth refresh, so the companion does not treat point-in-time `expires_at` or an authorization rejection as Agent login authority. It reloads the credential once after 401/403 and retries only when the token changed. Tokens, API keys, paths, raw responses, and Provider messages never cross the daemon API or renderer IPC. - Validation: Cover managed inline/nested OAuth configuration, Coding Plan summary/window mapping, API billing without an account request, a concurrent credential refresh, an unchanged unauthorized token that does not become `session_expired`, and the renderer's localized API billing label. Run the Desktop tests, typecheck, i18n check, build, and changed-aware push-ready gate. - References: [Kimi Code Agent Extension](https://github.com/tutti-os/agent-extension-kimi-code) [account_usage.go](../../../services/tuttid/service/agentextension/account_usage.go) [agentTargetAccountUsageProbe.ts](../../../apps/desktop/src/main/agentTargetAccountUsageProbe.ts) [agentProviderUsageProbe.ts](../../../apps/desktop/src/main/agentProviderUsageProbe.ts) [createDesktopAgentStatusSource.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/createDesktopAgentStatusSource.ts) ### Claude Code sessions fail with `effectiveSource: "none"` when CC-Switch or similar proxy tools are used - Symptom: Tutti desktop sessions for the `claude-code` provider never connect. The UI reports `agent session is not connected` even though the same Claude CLI works fine when run from a terminal session that loaded CC-Switch (or a similar `~/.claude/settings.json` proxy). - Quick checks: In `tuttid.log` search for `CLAUDE_CODE_AUTH_REFRESH_DEBUG`. If `credentials.effectiveSource` is `"none"` and both `keychain.found` and `plaintext.found` are `false`, but `~/.claude/settings.json` contains an `env` block with `ANTHROPIC_AUTH_TOKEN` and `ANTHROPIC_BASE_URL`, the sidecar never propagated the file's `env` to the Claude SDK. - Root cause: CC-Switch writes proxy credentials into `~/.claude/settings.json`'s `env` field. The native Claude CLI picks them up because the user shell exports them into `process.env`, but the Tutti `claude-sdk-sidecar` is launched directly without going through a shell, so those variables are missing. The sidecar previously only merged `process.env` with the ACP payload `env` and never read the file. - Fix: Read the Claude settings files in the sidecar and merge their `env` blocks into the Claude SDK query options, between `process.env` (lower priority) and the ACP payload `env` (higher priority). The merge covers `${CLAUDE_CONFIG_DIR}/settings.json` (defaulting to `~/.claude`) plus project-level `.claude/settings.json` / `.claude/settings.local.json` walking from the filesystem root to the session `cwd`, matching the native CLI's layering. See `claudeSettingsEnv` and `ensureQuery` in [claude-sdk-sidecar main.ts](../../../packages/agent/claude-sdk-sidecar/src/main.ts). The agentstatus probe reads the same `$CLAUDE_CONFIG_DIR`-aware location (`claudeSettingsDeclares` in [provider_custom_config.go](../../../services/tuttid/service/agentstatus/provider_custom_config.go)) so the environment wizard and the runtime agree on whether credentials exist. - Validation: Run `pnpm --filter @tutti-os/claude-sdk-sidecar test` and `cd services/tuttid && go test ./service/agentstatus/`. Unit tests cover reading, non-string value skipping, missing file, malformed JSON, missing `env` field, user/project/local layering, and `CLAUDE_CONFIG_DIR` resolution. - Note: `credentials.effectiveSource` only tracks OAuth material (keychain or `.credentials.json`). For API-key/proxy users it stays `"none"` even after the fix; a connected session is the success signal, not that field. ### Cursor free plan shows a red error on the next send after upgrade copy - Symptom: A Cursor free / exhausted account first returns plain assistant text `Upgrade your plan to continue`. Sending again shows a scary red turn-failed card (often with an “Open setup” escape hatch) instead of the same calm plan-gate copy. - Root cause: `cursor-agent` soft-surfaces the first plan gate as an assistant chunk + `end_turn`. Later attempts may fail `session/prompt` with the same fixed copy (`upgrade` / `payment` actions). Tutti previously treated that ACP call failure as a generic `turn.failed` / `provider_error` danger card, and the visible-error classifier did not recognize the Cursor phrases as a quota / plan limit. - Invariants: When ACP `session/prompt` fails with Cursor plan/payment gate copy, soft-settle the turn: emit a warning system notice with that copy and complete the turn (`planLimit=true`) so the composer stays usable without a danger card. Keep residual visible-error classification of those phrases in the `quota_or_rate_limit` bucket, and render that bucket with warning tone rather than danger. Do not route plan gates into the env-wizard “Open setup” path. - Validation: Run `go test ./packages/agent/daemon/runtime -run 'PlanLimit|VisibleFailureCodeRecognizesCursorPlanLimit'` and the AgentGUI visible-error / `classifyFailedAgentMessage` specs that cover `Upgrade your plan to continue`. - References: [acp_plan_limit.go](../../../packages/agent/daemon/runtime/acp_plan_limit.go) [standard_acp_turn.go](../../../packages/agent/daemon/runtime/standard_acp_turn.go) [visible_error.go](../../../packages/agent/daemon/runtime/visible_error.go) [AgentMessageBlock.tsx](../../../packages/agent/gui/shared/agentConversation/components/AgentMessageBlock.tsx) ### Tutti Agent retries a 402 and shows generic provider setup - Symptom: A request with insufficient Tutti credits displays `Reconnecting... 5/5`, then falls back to a generic provider error card whose action opens local setup instead of account plans. - Root cause: The billing boundary collapsed every commerce pre-deduct error into HTTP 402, the agent protocol treated every unexpected HTTP status as retryable, and the daemon classifier did not distinguish the resulting payment failure from a generic provider failure. - Invariants: Preserve machine-readable billing codes across commerce, token usage, the LLM gateway, and the agent runtime. Commerce exposes depleted credits as `ResourceExhausted/CREDITS_INSUFFICIENT`; token usage translates only that decision to the OpenAI-compatible `429 usage_limit_reached` envelope with code `insufficient_credits`, which legacy Tutti Agent releases already treat as terminal. The gateway adds the Tutti plans promo header so the parsed terminal error retains actionable account context. Dependency failures remain 5xx. Classify actionable account failures before generic quota/provider buckets, and route account actions through the host link-action boundary rather than opening URLs directly from transcript UI. - Validation: Cover the commerce RPC error mapping, token-usage envelope, gateway promo header, daemon visible-error classification, and rendered plans-page action as separate boundary tests. ### OpenCode effort changes fail with `effort not found` - Symptom: An OpenCode session starts successfully, but changing reasoning effort fails through `session/set_config_option` with `Invalid params: effort not found`. Big-Pickle is a common example. - Quick checks: Run `opencode models --verbose` and inspect the selected model's `variants` object. Compare those keys with the model-specific reasoning profile returned by the composer-options endpoint. Also inspect the live ACP `configOptions[id="effort"]`; a UI option that is absent from both sources must never be submitted. - Root cause: OpenCode's top-level `capabilities.reasoning` says the model can reason, but it does not mean the model exposes selectable reasoning variants. Models use different variant sets, and some models return an empty `variants` object. A provider-wide static `low` / `medium` / `high` / `xhigh` list therefore creates controls that the current model cannot honor. - Fix: Parse `opencode models --verbose`, preserve an explicitly empty variants profile, clear remembered effort values that are unsupported by the selected model, and refresh composer options after model changes. Before sending a live effort update, require the current ACP descriptor to advertise the exact value. - Validation: Cover a model with empty variants, a model with ordered `low` / `medium` / `high` / `max` variants, remembered-setting sanitization, and runtime rejection before any ACP call for an unadvertised value. ### Codex or Tutti Agent model/capability catalog times out on cold start - Symptom: The provider status is ready, but the composer model or capability catalog temporarily falls back, reports `model/list timed out`, or records an app-server capability discovery timeout after the daemon has been idle. - Quick checks: Search the daemon log for `agent.model_catalog.fetch_settled` and inspect `provider`, `durationMs`, `stage`, and `error`. A provider-process timeout near 8 seconds points to the old cold-start boundary. Also inspect the provider stderr for `codex_models_manager` refresh failures and distinguish those from Tutti's own `context deadline exceeded`. - Root cause: Codex and Tutti Agent both launch a Codex app-server. On Windows the `.cmd` shim, managed Node startup, and the provider's own model metadata refresh can make the first `model/list` or capability request slower than the previous 8-second process bound, even though a later request succeeds and status detection remains ready. `codex_runtime_selection_stale` and a provider-owned child-process refresh timeout are separate runtime-selection failures; increasing Tutti's request bound does not mask them. - Fix: Keep the app-server process/request bound at 30 seconds for model and capability catalogs, and give Codex/Tutti Agent's outer model-catalog fetch a 35-second bound. Keep these values provider-specific; do not widen Claude, Cursor, generic ACP, or interactive session timeouts without corresponding boundary evidence. - Validation: Confirm a cold catalog fetch either returns a non-empty model/capability list or reports the provider-owned error after the bounded window. Verify subsequent persistent/cache-backed requests succeed, and that status detection and interactive session timeout values remain unchanged. Run `cd services/tuttid && go test ./service/agent` and `pnpm check:changed`. - References: [codex_model_catalog.go](../../../services/tuttid/service/agent/codex_model_catalog.go) [codex_capability_catalog.go](../../../services/tuttid/service/agent/codex_capability_catalog.go) [model_catalog.go](../../../services/tuttid/service/agent/model_catalog.go) ### Windows Agent process remains after ACP startup timeout - Symptom: A Codex, Cursor, Kimi, Claude SDK, or other local process Agent times out, and a later retry reports cleanup pending, `process did not exit after kill`, or an apparently missing provider. The same provider may work again after a daemon restart. - Quick checks: Search the daemon log for `agent_session.live_resource_cleanup.failed`, `agent_session.live_release.failed`, and `agent_session.process_start.group_attach_failed`. On Windows, inspect the provider's `.cmd`/`.bat` wrapper and its Node or Python descendants rather than checking only the wrapper PID. - Root cause: A Windows command shim can exit while a descendant keeps the ACP stdout or stderr pipe open. A later `taskkill` by the exited root PID cannot reliably reach that descendant, so the transport close remains pending and the adapter retains ownership to prevent an unsafe replacement. - Fix: Local process launches attach their process tree to a Windows Job Object with `KILL_ON_JOB_CLOSE`. Graceful `taskkill` remains the first close attempt; the owned Job Object is the fallback when the shim PID has already exited. Hosts that reject nested Job Object assignment retain the existing taskkill fallback and emit `agent_session.process_start.group_attach_failed`. - Validation: Reproduce a `.cmd` launch whose child outlives the shim, close the transport, and verify that the child is gone or signaled as exited. Repeat with startup timeout, cancellation, replacement, daemon shutdown, and direct `.exe` providers. No `live_resource_cleanup.failed` should remain after successful cleanup. - References: [process_transport.go](../../../packages/agent/daemon/runtime/process_transport.go) [process_command_windows.go](../../../packages/agent/daemon/runtime/process_command_windows.go) ### OpenCode model picker has fewer models than the terminal - Symptom: `opencode models --verbose` lists more models in a local terminal than the OpenCode model picker in Agent GUI. Custom provider ids or recently published model variants are commonly absent. A related presentation symptom shows a provider-qualified recent item such as `newapi/deepseek-v4-pro`, while the searchable catalog shows only the ambiguous model name `DeepSeek V4 Pro`. - Quick checks: Run `opencode models --verbose` from the same workspace cwd passed to the composer. Count exact `provider/model` lines and compare them with the composer-options model config. Confirm daemon logs do not contain `composer model catalog lookup failed`. A `models.dev` cache hit only affects image-input metadata and cannot remove a model option. - Root cause: OpenCode resolves project configuration relative to the command cwd. The daemon previously ran model discovery in its own inherited cwd and stored the resulting provider-wide list for six hours. A smaller result from the wrong project context therefore remained visible even after the terminal catalog changed. Separately, verbose catalog normalization previously used only the model metadata `name` as the display label even though the exact launch identity remained the provider-qualified `provider/model` id. - Fix: Pass the composer workspace cwd through the daemon model-catalog request and set it as the `opencode models --verbose` process directory. Cache a successful catalog for five minutes and a failed fetch for 30 seconds, with the cwd included in the in-memory key; never reuse one workspace's project catalog in another workspace or persist these cwd-scoped entries. Keep one request-scoped catalog projection so a composer-options request starts the CLI only once. Preserve the auth/config invalidation event so an already-open composer refreshes when global OpenCode credentials or config files change. Append a non-built-in provider id to verbose model labels while preserving the exact provider-qualified id as the selection value. Keep the built-in `opencode` provider suffix hidden so ordinary catalog entries stay concise, and avoid renderer-side provider branches. - Validation: Cover cwd propagation, repeated same-cwd cached OpenCode lookups, separate fetches for different cwds, failed-fetch throttling, all provider/model prefixes from verbose output, one catalog lookup per composer-options request, duplicate model names under different provider ids, and unchanged cache policies for Codex and Tutti Agent. Run `cd services/tuttid && go test ./service/agent` and `pnpm check:changed`. - References: [opencode_model_catalog.go](../../../services/tuttid/service/agent/opencode_model_catalog.go) [model_catalog.go](../../../services/tuttid/service/agent/model_catalog.go) [composer_options.go](../../../services/tuttid/service/agent/composer_options.go) ### Agent slash palette only shows Browser - Symptom: Typing `/` in a Claude Code, Codex, or OpenCode composer shows only the Browser capability. Provider commands such as `compact`, `status`, `goal`, `review`, or `plan` are missing. - Quick checks: Call the provider composer-options endpoint and inspect `slashCommandPolicy`. If Codex or Claude returns a policy but the UI still shows only Browser, trace the new-session creation guard and the target-scoped composer-options cache. If one provider returns no policy, inspect its provider registry descriptor. - Root cause: Composer-options loading can be intentionally skipped while a new session is being created. A mount-time creation ref that never follows current engine state leaves loading permanently disabled after creation settles. Browser still appears because it is independently projected from session capabilities. A provider descriptor missing its slash policy produces the same symptom for that provider even when loading succeeds. - Fix: Keep the creation guard synchronized with current engine state and reload composer options on the creating-to-settled transition. Keep fallback commands and local effects in the provider registry descriptor; do not add provider-name branches in Agent GUI. - Validation: Cover creation settling followed by a composer-options request, provider descriptor policy projection, and slash palette composition alongside the Browser capability. Run Agent GUI, provider registry, and agent service tests. - References: [agent-activity-packages.md](../architecture/agent-activity-packages.md) [useAgentGUIComposerOptionsSync.ts](../../packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUIComposerOptionsSync.ts) [opencode.go](../../packages/agent/daemon/providerregistry/opencode.go) ### Local capability slash command reaches the provider as unknown - Symptom: A local capability command appears in the Agent slash palette and updates its composer setting, but submitting text such as `/computer click Confirm` produces a provider response such as `Unknown command: /computer`. - Quick checks: Confirm the resolved slash catalog contains a capability entry rather than a provider command. Then trace both palette selection and form submission; the latter must resolve a local capability submit effect before the generic provider slash-command path. - Root cause: The palette selection path enabled the capability and filled the canonical token, but the form submission path had no matching local interceptor. The raw slash invocation therefore crossed the runtime boundary and was parsed as a provider-native command. - Fix: Route every local capability entry through the shared capability submission parser and handoff projection. Preserve the slash invocation as `displayPrompt`, then dispatch one semantic submit carrying the handoff prompt plus a `requiredSettingsPatch`. New-session activation merges the patch into initial settings; existing-session delivery retains it in the activity queue and applies it at the host command port before sending. Do not sequence a settings mutation and a submit in a React hook. Keep provider-native command behavior descriptor authoritative; do not add provider-name branches. - Validation: Cover slash and alias forms, capability-disabled rejection, visible prompt normalization, handoff prompt construction, new-session setting activation, queued-prompt patch retention, and settings-before-prompt host ordering. - References: [agent-gui-node.md](../../architecture/agent-gui-node.md) [agentCapabilityUseSubmit.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/model/agentCapabilityUseSubmit.ts) [agentSlashCommandProviderPolicy.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/model/agentSlashCommandProviderPolicy.ts) [promptQueue.reducer.ts](../../../packages/agent/activity-core/src/engine/promptQueue.reducer.ts) ### Standard ACP tools show generic cards and no-project file links do nothing - Symptom: OpenCode or another standard ACP provider completes tool calls, but Agent GUI renders generic raw-payload cards instead of terminal, edit, read, search, or todo UI. In a session without a selected project, clicking an absolute HTML or source-file path in an assistant message has no effect. - Quick checks: Inspect persisted tool payloads. If `toolName` contains a command, absolute path, or result sentence while `input.kind`, `input.title`, or `rawInput` identifies the actual operation, canonicalization happened too late. For file links, compare the selected project root with the durable session cwd. - Root cause: Standard ACP terminal updates may replace the display title with dynamic output. Persisting that title as tool identity prevents shared specialized renderers from matching the call. Older events may also retain protocol envelopes under `rawInput`, `rawOutput`, and output metadata. Separately, requiring a selected project root discards valid link actions for no-project sessions even though their cwd is authoritative. - Fix: Canonicalize standard ACP tools before persistence, retain the started call's identity through terminal updates, and promote protocol envelopes into the shared tool payload. Keep a provider-neutral historical projection for rows already stored in the old shape. Resolve conversation files against the selected project root or, when absent, the session cwd. - Validation: Cover dynamic ACP start/terminal titles, historical payload projection, specialized renderer data, direct link resolution, and a transcript click from a no-project session. Run the Agent GUI tests and daemon runtime tests. - References: [agent-gui-node.md](../architecture/agent-gui-node.md) [acp_tool_normalizer.go](../../packages/agent/daemon/runtime/acp_tool_normalizer.go) [workspaceLinkActions.ts](../../packages/agent/gui/actions/workspaceLinkActions.ts) ### Enabled Agent Extension is missing from AgentGUI - Symptom: The extension feature gate is enabled and its release is reachable, but no extension target appears. The daemon log may only mention a missing local `active.json` fallback. - Quick checks: Confirm the daemon process inherited the feature-gate environment variable, inspect `/agent/extensions/`, and query `agent_targets` for `extension:`. Verify the public ZIP's signature, digest, size, entry modes, and package structure using the same daemon installation path. A version containing `+local.` is a development snapshot; it is intentionally ineligible once the matching package-directory override is removed. - Root cause: A failed remote reconciliation can be obscured when the subsequent offline fallback error replaces the original error. ZIP directory entries commonly use mode `0755`; treating their search bits as executable file content rejects an otherwise valid data-only package before it can be registered. Runtime discovery can fail similarly when the daemon's strict JSON decoder does not model a signed profile field such as the standard `probe` declaration. A previously active local snapshot must also not silently become the offline fallback for a source that is now configured as signed remote. - Fix: Preserve both the remote reconciliation error and the offline fallback error. Reject symlinks for every entry, accept safe directory entries before checking executable bits, and reject executable bits only on non-directory files. Keep the daemon discovery DTO aligned with the release profile contract, including optional probe metadata, even while a later migration phase owns executing the ACP readiness probe. Treat local and remote installations as different source modes: removing the local override removes a stale local Target and requires a compatible signed remote installation, while a verified remote installation remains eligible for normal offline fallback. - Validation: Cover a release ZIP with explicit `0755` directory entries and non-executable data files, retain a separate executable-file rejection test, and confirm a failed remote request remains visible when no offline installation exists. Then install the published artifact in an isolated state directory and verify both `active.json` and `extension:`. - References: [manager.go](../../../services/tuttid/service/agentextension/manager.go) [manager_test.go](../../../services/tuttid/service/agentextension/manager_test.go) ### Extension composer controls stay on Loading and environment setup says unsupported - Symptom: An extension Target is `ready` and its home composer is visible, but model or permission controls never leave `Loading`. Opening Environment Check says the agent has no managed environment setup. - Quick checks: Call the target-scoped composer-options endpoint with both the extension provider and `agentTargetId`. A `400 malformed_request` while `/v1/agent-targets` reports the Target as ready points to provider identity normalization, not runtime discovery. Also confirm the config menu is not offering the desktop-managed environment wizard for an extension Target. If the endpoint returns `200` but has no models, inspect whether the ACP agent reports standard `models` state rather than legacy `configOptions`, and confirm its hidden no-project session has a daemon-managed discovery CWD. - Root cause: After the Agent Target had authoritatively resolved an open provider identity such as `acp:gemini`, composer-options normalized it again through the closed built-in provider catalog. The identity became empty and the request failed, while the renderer kept its loading projection. Separately, the config menu exposed the built-in managed-environment action for every provider even though extension readiness belongs to the Agent Target lifecycle. A second failure path used an empty CWD for no-project discovery and only understood `configOptions`, while Gemini reports its catalog through ACP `models`. - Fix: Preserve open provider identities only after successful Agent Target launch resolution. Keep direct provider-only requests on the closed built-in path. Show the desktop environment wizard only for providers owned by the built-in provider catalog; extension installation and readiness remain Target-owned. Give hidden extension probes a daemon-owned CWD and normalize standard ACP `models` into the same shared composer model descriptor. - Validation: Cover target-scoped composer options for an extension provider, verify the real endpoint returns `200`, and confirm the extension config menu omits the desktop environment action while retaining general Agent settings. Verify the response contains the runtime-advertised model IDs without a provider-specific catalog in Tutti. - References: [composer_options.go](../../../services/tuttid/service/agent/composer_options.go) [AgentGUINodeView.tsx](../../../packages/agent/gui/agent-gui/agentGuiNode/AgentGUINodeView.tsx) ### Extension login returns to the login button without an error - Symptom: Target setup opens the browser and waits for authentication, then silently returns to `auth_required`. The runtime detection row may appear to restart during each progress poll, and a pending `Ready` row can look like an incorrect success state while login is still running. - Quick checks: Inspect the setup snapshot's durable `action.status`, `errorCode`, and `errorMessage`, then correlate its timestamp with the ACP `authenticate` request in the daemon log. A successful browser callback does not prove that the runtime accepted the account; the ACP response remains authoritative. - Root cause: Setup polling reused the foreground refresh state, so every background request set the whole panel to loading. Explicit ACP authentication failures were also normalized into a successful `auth_required` probe result, losing the provider error before the durable action was written. AgentGUI then rendered errors only for top-level setup failure, not a failed authenticate action. The terminal `Ready` row could never become successful because the setup dialog closes as soon as the authoritative snapshot becomes ready. - Fix: Keep background polling non-disruptive, preserve errors from explicit ACP `authenticate` calls, and show failed/interrupted action details in both the existing host toast and the setup dialog while keeping retry available. Fire the toast only when the current action moves from running to failed so polling cannot repeat it and restoring an old failure cannot replay it. Do not render a terminal readiness row that only exists while setup is non-ready. - Validation: Cover background polling without detection loading, an ACP authenticate rejection retaining provider text through the durable setup action, one toast per current-action failure, the persistent GUI failure presentation, and absence of the misleading pending readiness row. Run Agent daemon, setup service, AgentGUI, desktop watcher, i18n, typecheck, and desktop build checks. - References: [standard_acp_setup.go](../../../packages/agent/daemon/runtime/standard_acp_setup.go) [setup.go](../../../services/tuttid/service/agentextension/setup.go) [AgentTargetSetupGate.tsx](../../../packages/agent/gui/agent-gui/agentGuiNode/view/AgentTargetSetupGate.tsx) [agentTargetSetupNotificationController.ts](../../../packages/agent/gui/shared/agentEnv/agentTargetSetupNotificationController.ts) ### Standard ACP first-time configuration is reported as runtime installation failure - Symptom: A managed standard ACP runtime installs and initializes successfully, but `session/new` reports that no model or inference provider is configured. Target setup then labels the installed runtime as failed instead of offering the runtime's advertised terminal setup action. - Root cause: Authentication classification covered credential and login failures but not a runtime whose first-use gate is model-provider configuration. The setup probe therefore discarded the terminal method learned from `initialize`. - Fix: Treat an explicit missing-provider response as `auth_required` only when the same `initialize` response advertised a usable terminal configuration method. Keep the provider response as a hard runtime failure when no such method exists, so unrelated launch failures are not hidden by the setup gate. - Validation: Emulate `initialize` with a terminal setup method followed by a missing-provider `session/new` error and assert that setup preserves the method and returns `auth_required`. Also cover the no-method and unrelated error boundaries. - References: [standard_acp_setup.go](../../../packages/agent/daemon/runtime/standard_acp_setup.go) [standard_acp_setup_test.go](../../../packages/agent/daemon/runtime/standard_acp_setup_test.go) ### Extension uv runtime install selects an incompatible system Python - Symptom: Reinstalling an Agent Extension runtime fails with `install command uv failed: exit status 1`. Replaying the signed `uv tool install` command reports that the current Python version does not satisfy the package's `Requires-Python` constraint. A common macOS example is the system Python 3.9 being selected for a package that requires Python 3.11 or newer. - Quick checks: Inspect the Extension manifest's exact uv package pin and the package's `Requires-Python` metadata. Reproduce with the Tutti-managed uv binary and the same `UV_TOOL_DIR`, `UV_TOOL_BIN_DIR`, `UV_PYTHON_INSTALL_DIR`, `UV_CACHE_DIR`, and `UV_NO_CONFIG=1` values. If adding `UV_PYTHON=3.12` resolves the package and downloads managed CPython beneath the install root, the package pin and network path are not the root cause. - Root cause: Setting `UV_PYTHON_INSTALL_DIR` confines Python versions that uv downloads, but it does not request an interpreter version. Without an explicit request, `uv tool install` may select the daemon's system Python before it resolves the package metadata, making a valid pinned package appear unsatisfiable. - Fix: Tutti's uv install environment requests Python 3.12 with `UV_PYTHON=3.12` and requires uv-managed interpreters with `UV_MANAGED_PYTHON=1`. Keep `UV_PYTHON_INSTALL_DIR` inside the final runtime root so the tool remains isolated and does not depend on or mutate a system Python installation. - Validation: Cover the two Python selection variables in the uv installer test. Re-run the real signed package installation in an isolated root, assert the executable reports the pinned package version and Python 3.12, then run the changed-aware repository validation. - References: [installer_uv.go](../../../services/tuttid/service/agentextension/installer_uv.go) [installer_uv_test.go](../../../services/tuttid/service/agentextension/installer_uv_test.go) [Agent Extensions](../../architecture/agent-extensions.md) ### Extension runtime installation stays failed after restart - Symptom: An Agent Extension runtime install fails once. Reopening Tutti restores the same failure, and checking again never offers or starts another installation. - Quick checks: Inspect the setup snapshot. If it contains a failed or interrupted install action plus a non-null install plan, the daemon has enough information to retry. Confirm the visible action starts `setup/install` with a new `clientActionId`; a setup GET only refreshes the persisted failure. - Root cause: Failed setup actions are durable by design. The setup panel rendered its install button only for `not_installed`, while mapping failed/interrupted install actions to `failed`. Its generic check-again action only fetched the same durable snapshot, so restart could not change the state. - Fix: Keep the failed action and provider error visible. When a failed or interrupted install snapshot retains a plan, offer an explicit reinstall action that submits the plan digest with a fresh client action ID. Do not erase the previous failure or treat a GET refresh as a retry. - Validation: Mount the setup panel from an initial persisted failed-install snapshot, verify the error remains visible, and assert reinstall submits the retained plan with a client action ID different from the failed action. - References: [setup.go](../../../services/tuttid/service/agentextension/setup.go) [AgentTargetSetupGate.tsx](../../../packages/agent/gui/agent-gui/agentGuiNode/view/AgentTargetSetupGate.tsx) [agentTargetSetupController.tsx](../../../packages/agent/gui/shared/agentEnv/agentTargetSetupController.tsx) ### Vertex setup reports ready but the first prompt cannot load credentials - Symptom: Selecting Gemini `vertex-ai` completes Target setup and creates a session, but the first prompt fails with `Could not load the default credentials` or reports missing `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION`, or an API key. The empty-home setup gate is gone, leaving no obvious way to change the login method. - Quick checks: Correlate the durable authenticate action with ACP traffic. If `authenticate` returns an empty result and `session/new` succeeds, but the later formal `session/prompt` returns the credential error, setup observed a runtime false positive rather than losing the selected method. - Root cause: Gemini can defer Vertex ADC and project/location validation until a real prompt. ACP exposes no credential-validation request stronger than the runtime's own `authenticate` plus `session/new`, so setup cannot prove request usability without sending user-visible work. Extension Target setup was also mounted only in empty-home state, unlike the persistent built-in provider environment entry. - Fix: Classify the formal prompt's credential error as an authentication failure and feed it into Target setup detection. Override a later otherwise-ready ACP probe to `auth_required`. Expose the same Target setup dialog from the selected provider's config menu in both ready and non-ready states, permit explicit re-authentication from ready, and reuse one Target watch across the two UI hosts. - Validation: Cover the real Vertex ADC error text, auth invalidation overriding a ready probe, re-authentication clearing invalidation, ready-state auth method selection, and one cached desktop watch per Target. Do not add a hidden synthetic prompt to setup. - References: [agent_run_outcome_reporter.go](../../../services/tuttid/agent_run_outcome_reporter.go) [setup.go](../../../services/tuttid/service/agentextension/setup.go) [AgentGUINodeView.tsx](../../../packages/agent/gui/agent-gui/agentGuiNode/AgentGUINodeView.tsx) [AgentTargetSetupGate.tsx](../../../packages/agent/gui/agent-gui/agentGuiNode/view/AgentTargetSetupGate.tsx) ### Extension messages appear sent but show no running or failure state - Symptom: A new extension conversation displays the user message, but the composer immediately looks idle and no provider response or error card appears. - Quick checks: Trace one Agent Session from `runtime.submitted` through the standard ACP `session/prompt` call. If the adapter logs a provider error and `runtime.events_emitted` reports empty event types, inspect provider identity normalization before debugging renderer polling or streaming. - Root cause: The Agent Target and runtime accepted the extension-owned provider ID, but the shared activity event context still resolved providers through the closed built-in catalog. Turn-started, user-message, and turn-failed events for identities such as `acp:gemini` became empty events, so neither running state nor the real provider error reached durable conversation state. - Fix: Centralize the canonical open-provider format in the provider registry and reuse it for both authorized service requests and activity event identities. Keep launch authorization separate: accepting an identity as event metadata does not authorize a runtime without a fixed Agent Target reference. - Validation: Cover open extension identities in provider-registry and activity-event tests, then project `turn.started` and `turn.failed` for an extension session and assert both retain the extension provider ID. - References: [registry.go](../../../packages/agent/daemon/providerregistry/registry.go) [activity_types.go](../../../packages/agent/daemon/activity/events/activity_types.go) [activity_projection.go](../../../packages/agent/daemon/runtime/activity_projection.go) ### Extension sessions show an open provider ID or disappear from mentions - Symptom: An extension works in AgentGUI, but message-center cards or `@session` rows show a raw identity such as `acp:gemini` with the generic multi-Agent icon. The same extension may be absent from the `@agent` Agents tab. - Quick checks: Read the extension Agent Target from `/v1/agent-targets` or the local target store and confirm it has the expected name and signed icon URL. Then compare the affected session's `agentTargetId`. If both are correct, inspect whether the renderer projection still calls the built-in provider catalog or provider icon resolver instead of the Agent Directory. - Root cause: Runtime `provider` and product `agentTargetId` are different identities. Built-in providers happened to render correctly when older consumers used `provider` for both, but an open extension provider has no built-in catalog entry and therefore degrades to raw text/generic artwork or is filtered out. - Fix: Resolve session and message-center presentation by exact `agentTargetId` against the shared Agent Directory. Build `@agent` candidates directly from ready, enabled Agent Targets; use the built-in provider catalog only for optional built-in visibility gates, never as extension authorization or display metadata. - Validation: Cover an enabled `extension:*` Target with an `acp:*` provider and assert the Agents tab, Agent Session rows, and message-center cards all use the Target name and icon. Also retain coverage for historical provider-only sessions. - References: [desktopRichTextAtAgentContributors.ts](../../../apps/desktop/src/renderer/src/features/rich-text-at/services/internal/desktopRichTextAtAgentContributors.ts) [workspaceAgentMessageCenterModel.ts](../../../packages/agent/gui/agent-message-center/workspaceAgentMessageCenterModel.ts) [agent-gui-node.md](../../architecture/agent-gui-node.md) ### Extension failure card appears while processing never stops - Symptom: A standard ACP extension displays the provider error card, but the transcript still shows the processing indicator and the conversation remains busy. - Quick checks: Compare the terminal `turn.failed` runtime log with the streamed session projection. If the runtime reports `failed / settled` while the renderer still has the same `activeTurnId` in `running`, inspect the lifecycle data on the terminal activity event rather than changing the processing-row UI. - Root cause: The standard ACP adapter emitted explicit turn events without authoritative lifecycle snapshots. Built-in providers could still settle through their registered event projection policy, but an extension provider was not in that closed catalog. Its error message persisted while the prior running turn reference remained active. - Fix: Stamp every standard ACP turn transition with a sequenced adapter-origin lifecycle snapshot. The reporter then copies the provider-independent snapshot, so a terminal failure atomically records the error outcome, marks the turn settled, clears `activeTurnId`, and re-enables submission. - Validation: Cover a standard ACP start/failure pair and assert adapter-origin snapshots progress from `running` with an active turn ID to `settled / failed` with no active turn ID. Run the runtime and service regression suites. - References: [standard_acp_turn.go](../../../packages/agent/daemon/runtime/standard_acp_turn.go) [turn_lifecycle_stamp.go](../../../packages/agent/daemon/runtime/turn_lifecycle_stamp.go) [reporter_state.go](../../../packages/agent/daemon/runtime/reporter_state.go) ### Extension slash palette is empty or ignores its command filter - Symptom: Typing `/` in an extension conversation opens no command or Skill list, while the ACP process otherwise starts successfully. A related failure shows every provider-advertised command instead of the signed profile's smaller catalog. - Quick checks: Inspect the persisted session `internal_runtime_context_json`. If `commands` contains provider command names, the ACP command update was received and the remaining fault is command hydration or filtering. Confirm the composer request uses the active Session's exact `agentTargetId`, then compare the response `commands` and `slashCommandPolicy` with the installed `profiles/composer.json`. Skills remain empty unless the profile declares validated roots and the matching capabilities profile advertises Skill support. - Root cause: Runtime command updates were available only through a transient renderer event. A renderer that subscribed after the startup update, or reloaded an existing session, had no command catalog even though the daemon retained it. The slash palette also discarded every provider command when no built-in slash-command policy existed. That condition is normal for an open extension provider, so a valid ACP command catalog could still render as empty after hydration succeeded. Open extension providers also have no built-in composer profile, so the built-in provider Skill discovery table correctly returned no roots. Active-session composer reads could also fall back to node-level provider metadata and miss the extension Target. Conversely, an authoritative signed catalog that repeated every ACP command correctly preserved every command; that was a package declaration error, not a renderer filtering failure. - Fix: Persist the detailed ACP command catalog in session runtime context and let composer options restore it when no live engine snapshot is present. Treat provider-advertised commands as runtime capabilities even without a built-in policy, and keep their selection provider-native. Scope active-session composer reads and cache lookup by the Session's exact `agentTargetId`. Declare only the intended product command subset in an authoritative signed catalog; do not add a provider-name filter in AgentGUI. Declare extension Skill roots, invocation, and trigger prefix in the signed composer profile; resolve only safe relative workspace/user paths. - Validation: Cover startup command projection, legacy command-name recovery, composer option parsing, active-session Target selection, authoritative command narrowing, declared extension Skill roots, and unsafe path rejection. - References: [standard_acp_settings.go](../../../packages/agent/daemon/runtime/standard_acp_settings.go) [composer_commands.go](../../../services/tuttid/service/agent/composer_commands.go) [profiles.go](../../../services/tuttid/service/agentextension/profiles.go) [agentSlashCommandProviderPolicy.ts](../../../packages/agent/gui/agent-gui/agentGuiNode/model/agentSlashCommandProviderPolicy.ts) ### Codex Model Plan turns fail or stay waiting against a Chat-only endpoint - Symptom: A Codex session bound to an OpenAI-protocol Model Plan fails immediately, stays working without output, or loses tool-call messages. The Plan's connection check can still pass because detection calls `/v1/chat/completions` directly. Another immediate-failure shape is a terminal provider error such as `metadata value too long: ... (578 > 512)` after a short prompt that produced no assistant content. - Quick checks: Inspect the session-scoped Codex `config.toml`. The `tutti-model-plan` provider must use a loopback `base_url`, a temporary `TUTTI_MODEL_PLAN_API_KEY`, and `wire_api = "responses"`. Verify the upstream server receives `/v1/chat/completions`, not `/v1/responses`. A direct Chat-only Base URL paired with `wire_api = "responses"` is incomplete. For the metadata failure, inspect the exported Session's terminal Turn error and compare the reported value length with 512. Codex workspace diagnostics can grow with Git remotes and usage-attribution fields. Adjacent model-list 404s are not the terminal cause when the thread and Turn both start successfully. - Root cause: Current Codex emits Responses-shaped requests and requires terminal Responses SSE events. A Chat-only provider neither owns `/v1/responses` nor emits the `response.output_item.*` and `response.completed` state machine. Renaming Chat deltas or changing only `wire_api` leaves Codex waiting or discarding output. Current Codex can also advertise its built-in hosted `web_search` tool by default, and later versions may advertise other hosted tools that Chat Completions cannot execute. Codex also sends Responses `developer` messages; Chat-compatible providers that only recognize `system`/`user`/`assistant`/`tool` can reject the otherwise valid request during tokenization. The gateway also used to forward Responses `metadata`/`client_metadata` unchanged. Codex can encode its workspace diagnostics as one optional metadata value larger than the 512-byte limit enforced by common Chat-compatible endpoints, so the upstream rejects the request before model execution. - Fix: Keep Codex on `wire_api = "responses"` and route the session through tuttid's loopback Model Gateway. The gateway authenticates the temporary session token, converts supported Responses inputs to Chat Completions, forwards with the daemon-held Plan credential, and reconstructs complete Responses JSON/SSE output. The gateway filters non-translatable entries only from the per-request tool registration list and removes orphaned `tool_choice`/`parallel_tool_calls` controls. It still rejects explicit selection of a filtered tool and hosted call/output history. This avoids version-specific Codex config mutations while preserving fail-closed semantics for requested or recorded tool use. Its Codex role normalization matches cc-switch: `developer` becomes `system`, `latest_reminder` and unknown internal roles become `user`, text-only content-part arrays are newline-joined, and textual system messages are merged in order at message index zero. This preserves instruction precedence without requiring newer OpenAI-only roles or mid-conversation system roles from the upstream tokenizer. OpenCode continues to use the Plan endpoint directly. Before sending the converted Chat request, omit only metadata values larger than 512 bytes. Do not truncate them, because a truncated diagnostic JSON value is misleading and may be invalid. Keep the original Responses metadata for local response reconstruction; metadata at or below the limit and Responses-over-client key precedence remain unchanged. - Validation: Cover request/tool conversion, interleaved parallel tool arguments, UTF-8 and arbitrary SSE byte boundaries, large arguments, usage, upstream errors, timeout/cancel/disconnect paths, route isolation, token replacement, cleanup, immutable-revision resume, mixed supported/hosted registrations, future unknown registration types, explicit hosted tool choices/history, and internal-role normalization and system-message collapse. A real smoke test must complete two Codex turns and one tool call while the upstream records `/v1/chat/completions` without any upstream `developer` role or `system` message after index zero. Cover the metadata boundary explicitly: a 512-byte value is forwarded, a 513-byte value is omitted, and an omitted Responses value is not replaced by lower-priority client metadata with the same key. - References: [model-access-plans.md](../../architecture/model-access-plans.md) [gateway.go](../../../services/tuttid/service/modelgateway/gateway.go) [responses_request.go](../../../services/tuttid/service/modelgateway/responses_request.go) [stream_converter.go](../../../services/tuttid/service/modelgateway/stream_converter.go) [model_endpoint.go](../../../packages/agent/runtimeprep/model_endpoint.go) ### Enabled Agent Extensions delay every daemon startup - Symptom: `tutti.parent_monitor.started` is followed by a multi-second silent gap before `tutti.managed_runtime.profile_preload_started` and `tutti.listen`. The gap grows as more Agent Extension feature flags are enabled. - Quick checks: Compare the two timestamps and inspect `feature_flags_json` in the active `desktop_preferences` row. Time each enabled source's signed `versions.json`; the old startup path fetched the enabled indexes serially before constructing the daemon API. - Root cause: Agent Extension reconciliation combined two different jobs: restoring an already verified local installation and checking its remote release index. The daemon needed the first job before serving the Agent Target catalog, but synchronously waited for the second job too. Multiple CloudFront TLS and response waits therefore accumulated on every restart. - Fix: Restore and verify cached active installations synchronously, register their Targets, and move remote release refresh after successful daemon API construction into the background. Keep synchronous reconciliation when an enabled source has no usable local installation, and for explicit preference activation changes, so the initial or newly enabled Target does not disappear from the next catalog read. Release the reconciliation lock between background source refreshes so a preference change does not wait for the complete remote batch. - Validation: Cover cached restore without any network request, missing-cache fallback to synchronous reconciliation, disabled Target removal, offline fallback, and preference-driven enable/disable. On a state root with cached enabled extensions, verify `tutti.agent_extension.refresh_started` no longer delays `tutti.listen` and later reaches `tutti.agent_extension.refresh_completed`. - References: [agent-extensions.md](../../architecture/agent-extensions.md) [manager.go](../../../services/tuttid/service/agentextension/manager.go) [wiring_daemon_api.go](../../../services/tuttid/wiring_daemon_api.go) ### Kimi setup opens a browser before showing the platform selector - Symptom: Clicking the Kimi setup action immediately opens the Kimi website instead of showing the Kimi Code TUI selector for OAuth or a Platform API key. - Quick checks: Inspect the exact terminal launch. `kimi login` and starting `kimi` followed by `/login` are different interfaces: the former may start a device-code flow and open a browser, while the latter opens the interactive platform selector inside the running TUI. Confirm the installed runtime's `login --help` and verify the welcome marker still appears before assuming the two paths are equivalent. - Root cause: A terminal authentication profile projected the TUI slash command as a CLI subcommand. Provider-owned commands with the same spelling do not necessarily share behavior across those two command surfaces. - Fix: Declare the signed authentication method as `runtime-slash-command`, with one safe command name and a bounded literal ready marker. Launch the bare runtime, wait for that marker on the matching terminal session, then submit the Desktop-generated slash command through the terminal transport. The daemon and AgentGUI Host boundary must carry one typed startup action rather than independent raw input and marker fields. Do not put raw terminal input or shell source in the extension profile. - Validation: Cover output split across terminal events, output received before the session is armed, unrelated terminal sessions, timeout, and transport failure. In a fresh isolated Kimi home, verify setup first shows the in-TUI platform selector and opens a browser only after the user chooses OAuth. - References: [agent-extensions.md](../../architecture/agent-extensions.md) [runtime_probe.go](../../../services/tuttid/service/agentextension/runtime_probe.go) [workbenchTerminalLoginPresenter.ts](../../../apps/desktop/src/renderer/src/features/workspace-workbench/services/workbenchTerminalLoginPresenter.ts) ### Kimi Code remains in setup or reports login after authentication - Symptom: Kimi Code remains blocked after terminal login, has no selectable model, or reports an authentication problem when the account actually lacks an eligible plan, has insufficient balance, or reached its billing-cycle limit. - Quick checks: Build the extension package first, then pass its unpacked package directory: ```sh cd /path/to/agent-extension-kimi-code pnpm package:tutti-agent cd /path/to/tutti DEV_GUI_KIMI_CODE_PACKAGE_DIR=/path/to/agent-extension-kimi-code/build/tutti-agent/package \ make dev-gui ``` An explicit directory must exist and contain `tutti.agent.json`; `dev-gui.sh` now rejects stale source-tree paths before Electron starts. Inspect the setup snapshot `reason` and the structured conversation error `code`, not only raw text that may mention API keys or OAuth credentials. - Root cause: Kimi's model endpoint may wrap membership, model-plan, balance, and quota responses in an authentication-shaped error. Classifying the credential words first sends the user back through login and hides the actionable account state. Separately, restoring a cached local extension snapshot can hide an invalid or outdated development package path. - Fix: Classify subscription, model access, HTTP 402 balance, and quota markers before generic authentication. Project the stable reason through setup and localized AgentGUI copy without rendering raw provider payloads. For local overrides, synchronously snapshot the configured directory on every daemon start and remove the stale Target if validation fails. - Validation: Cover official membership/plan/quota message shapes that also mention credentials, setup reason projection, localized account-state presentation, invalid local paths, changed local package bytes, and preservation of the Target enabled preference after a successful resnapshot. - References: [agent-extensions.md](../../architecture/agent-extensions.md) [runtime-overrides.md](../runtime-overrides.md) [visible_error.go](../../../packages/agent/daemon/runtime/visible_error.go) [setup.go](../../../services/tuttid/service/agentextension/setup.go) ### Terminal login succeeds but the setup terminal remains open - Symptom: The provider's terminal login reports success and returns to its normal TUI, but AgentGUI keeps the setup terminal open and continues polling the Target as `auth_required`. - Quick checks: Confirm the Desktop terminal diagnostic reports the startup action as `submitted`, then inspect repeated Target setup probes. Compare the fresh ACP `initialize` response with `session/new`: some runtimes keep advertising a terminal login method even after authentication succeeds. - Root cause: ACP `authMethods` is a catalog of available authentication methods, not the current authentication state. Treating a terminal-only catalog as an immediate `auth_required` verdict skips `session/new`, so a successfully configured runtime can never become `ready` and the Host never closes its login-terminal handle. - Fix: Preserve the advertised methods for presentation, but verify readiness with the bounded setup `session/new` probe. Continue mapping explicit authentication, missing-model, missing-provider, and terminal-method timeout outcomes to `auth_required`. Do not scrape terminal output or inject an exit command to infer login completion. - Validation: Cover a runtime that still advertises only a terminal login method while `session/new` returns a usable model, plus unconfigured runtimes whose `session/new` returns no usable model, a missing-provider error, or a timeout. - References: [standard_acp_setup.go](../../../packages/agent/daemon/runtime/standard_acp_setup.go) [desktopTerminalLoginReadinessMonitor.ts](../../../apps/desktop/src/renderer/src/features/workspace-agent/services/internal/desktopTerminalLoginReadinessMonitor.ts) ### Cursor intermittently fails to start or reports a failed file read as completed - Symptom: Cursor ACP sometimes reports `Cursor failed to start` even though `initialize` succeeded. Newer Cursor releases can instead reach the generic 30-second `session/new` deadline while attaching a session-scoped HTTP MCP server. A file-read tool can also appear completed while its result is `Error: Aborted` and the raw diagnostic reports a disconnected TLS socket. - Quick checks: Correlate the same `agent_session` in the daemon log. The startup signature is `session/new` returning JSON-RPC `-32603` with `Failed to initialize session services`. For the read signature, inspect the ACP tool update's `result` and `rawErrorMessages`; do not rely only on `isError`/`is_error`. A timeout exactly 30 seconds after `session/new` with no provider response is the slower MCP-client initialization signature. - Root cause: Cursor can transiently fail its session services after the ACP initialize handshake. Standard ACP startup previously returned that first failure immediately. Cursor 2026.08 can also take longer than the generic ACP startup budget while it establishes the supplied HTTP MCP transport. Cursor can return a successful-looking tool envelope with the failure text in provider-specific fields, which the status and error projection did not inspect or preserve. - Fix: Retry only the matching Cursor `session/new` error once on the same initialized connection, before a provider session id or user Turn exists. Give Cursor a provider-scoped 75-second initialize/session-new window; do not increase the shared ACP timeout for every provider. Normalize known aborted/socket-disconnect markers in output/error envelopes as `call.failed`, and promote both the result and raw transport diagnostics into the failed payload. Do not automatically replay an arbitrary file tool operation. - Validation: Cover a transient Cursor session-service error followed by success, assert one process and two `session/new` calls, and reject retries for authentication, unrelated internal errors, and other methods. Assert the Cursor-only startup bound without changing the generic ACP bound. Cover aborted/TLS tool output, preserve its diagnostic text, and keep normal output and input-side error strings from changing the status. Run the native Windows runtime lane for process/ACP behavior. - References: [acp_provider_cursor.go](../../../packages/agent/daemon/runtime/acp_provider_cursor.go) [standard_acp_session.go](../../../packages/agent/daemon/runtime/standard_acp_session.go) [acp_tool_error_status.go](../../../packages/agent/daemon/runtime/acp_tool_error_status.go) [acp_tool_normalizer.go](../../../packages/agent/daemon/runtime/acp_tool_normalizer.go)