Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

3021 lines
174 KiB
Markdown

# 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/<session>/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-<platform>` 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-<platform>`.
- 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-<hash>`), 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 <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 <stable-user-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/<session>/cursor-plugin/tutti-cli`, and the Cursor
ACP command should include `--plugin-dir <that-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 <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/<session>/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 `<state-dir>/agent-providers/external-agent-registry/cache/registry.json`
and the package manifest under
`<state-dir>/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 <state-dir>/agent-providers/external-agent-registry/packages/claude-acp`.
For usage flicker, run
`node services/tuttid/service/agentstatus/assets/patch-claude-agent-acp.mjs --dist <managed-acp-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 <provider> --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 `<state>/agent/extensions/<agentKey>`, and query `agent_targets` for
`extension:<agentKey>`. 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:<agentKey>`.
- 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)