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
594 lines
33 KiB
Markdown
594 lines
33 KiB
Markdown
# Troubleshooting: Desktop And Release
|
|
|
|
[Back to troubleshooting index](./README.md)
|
|
|
|
### A published stable release is missing from the public changelog
|
|
|
|
- Symptom:
|
|
The stable GitHub Release and its installers are public, but
|
|
`https://tutti.sh/changelog` skips that version and the aggregate
|
|
`changelog.json` does not contain its tag.
|
|
- Quick checks:
|
|
Inspect the public `changelog.json`, then confirm the GitHub Release is a
|
|
published non-prerelease with a `release-summary.json` asset. Check the
|
|
original promotion run's `Update stable changelog metadata` step and any
|
|
later release or recovery runs that rewrote the mutable aggregate.
|
|
- Root cause:
|
|
The public page renders the release-owned aggregate rather than deriving
|
|
entries from GitHub on each request. If that mutable aggregate loses an
|
|
entry, later releases preserve the incomplete baseline even though the
|
|
missing release and its immutable summary remain valid.
|
|
- Fix:
|
|
Run `Repair Desktop Release Changelog` with the exact missing stable tag.
|
|
The workflow validates the published release and staged summary metadata,
|
|
rebuilds the public entry from the current human-reviewed Release Notes,
|
|
preserves every existing entry, and upserts the corrected summary without
|
|
moving release pointers or republishing assets. Do not rerun promotion for an
|
|
older stable tag because its rollback guard correctly rejects moving the
|
|
public channel behind the current release.
|
|
- Validation:
|
|
Confirm the workflow's S3 round-trip verification passes, then verify the
|
|
public aggregate and `https://tutti.sh/changelog` both contain the restored
|
|
version after the configured cache window.
|
|
- References:
|
|
[.github/workflows/desktop-release-changelog-repair.yml](../../../.github/workflows/desktop-release-changelog-repair.yml)
|
|
[upsert-release-changelog.mjs](../../../apps/desktop/scripts/upsert-release-changelog.mjs)
|
|
|
|
### Packaged Tutti starts but external shells cannot find `tutti`
|
|
|
|
- Symptom:
|
|
The packaged desktop app starts normally and `~/.tutti/bin/tutti status`
|
|
succeeds, but a new terminal reports `command not found: tutti`.
|
|
- Quick checks:
|
|
Run `command -v tutti`, print the login-shell `PATH`, and inspect
|
|
`~/.tutti/bin/tutti`. Check `~/.tutti/logs/tutti-desktop.log` for
|
|
`tutti cli shim is not discoverable on user PATH` and check whether an
|
|
unrelated `tutti` executable already exists earlier on `PATH`.
|
|
- Root cause:
|
|
Creating the canonical shim under the state root does not make it shell
|
|
discoverable unless `<state-dir>/bin` is already on `PATH`. Desktop apps
|
|
launched from Finder may also inherit a smaller `PATH` than the user's login
|
|
shell, so using only the Electron process environment misses user bin
|
|
directories.
|
|
- Fix:
|
|
Reuse the cached login-shell environment resolved for the managed daemon.
|
|
Keep `<state-dir>/bin/tutti` canonical, and install a forwarding shim only in
|
|
writable `~/.local/bin` or `~/bin` directories already present on the login
|
|
shell's `PATH`. Repair Tutti-owned shims on later startups, preserve unrelated
|
|
commands, and do not edit shell profiles or write `/usr/local/bin`. Keep this
|
|
installation best-effort so a slow or invalid shell environment cannot delay
|
|
desktop window creation.
|
|
- Validation:
|
|
Start packaged Tutti, open a new login shell, and verify `command -v tutti`
|
|
resolves to a user bin directory and `tutti status` succeeds. Cover creation,
|
|
repair, canonical-PATH, no-supported-directory, and third-party-command
|
|
conflict cases in desktop tests.
|
|
- References:
|
|
[cliInstaller.ts](../../../apps/desktop/src/main/cli/cliInstaller.ts)
|
|
[userShellEnv.ts](../../../apps/desktop/src/main/daemon/userShellEnv.ts)
|
|
[desktop-transport.md](../../architecture/desktop-transport.md)
|
|
|
|
### Desktop stable release alias disappears or is not first on Releases
|
|
|
|
- Symptom:
|
|
The desktop release workflow publishes a concrete release, but the
|
|
`Refresh stable release alias` step fails with `Committer identity unknown`,
|
|
the GitHub Releases page no longer has a `stable` entry after a failed RC
|
|
publish, or the GitHub Releases list still puts a newer RC above `stable`.
|
|
- Quick checks:
|
|
Inspect the failed `Desktop Release` run's `Refresh stable release alias`
|
|
step. If the log shows `git tag -a` or `gh release delete stable --cleanup-tag`,
|
|
the workflow is using the unsafe annotated-tag refresh path. Also check
|
|
`gh release view stable` and `git ls-remote --tags origin stable` to confirm
|
|
whether the release, tag, or both are missing. For ordering failures, list
|
|
public prereleases with `gh api 'repos/$GITHUB_REPOSITORY/releases?per_page=100'
|
|
--jq '.[] | select(.prerelease and (.draft | not)) | .tag_name'` and confirm
|
|
the workflow ran `Archive public GitHub prereleases`.
|
|
- Root cause:
|
|
Annotated tags require a configured Git committer identity in GitHub Actions.
|
|
Deleting the old floating release and tag before creating the replacement
|
|
leaves the repository in a half-refreshed state if tag creation fails.
|
|
GitHub's public Releases list has no supported pin or explicit order field.
|
|
Recreating the alias and assigning it a newer commit timestamp does not
|
|
reliably place it above public prereleases.
|
|
- Fix:
|
|
Keep RC and beta GitHub Releases as drafts and distribute them through the
|
|
S3 preview-channel metadata instead. The workflow must archive any older
|
|
public prereleases with `PATCH draft=true`, then refresh the floating
|
|
`stable` release. Delete only the old stable alias (`gh release delete stable
|
|
--yes`) and never pass `--cleanup-tag`; keep the concrete stable release as
|
|
`Latest`.
|
|
- Validation:
|
|
Run `node --test ./tools/scripts/desktop-release-config.test.mjs` and verify
|
|
the workflow test checks that stable is the only release promoted from draft,
|
|
archives legacy public prereleases, checks the alias tree and parent, and
|
|
rejects `git tag -a`, `--cleanup-tag`, and deleting `refs/tags/stable`. After
|
|
a live release, confirm the GitHub Releases page lists `stable` first while
|
|
`/releases/latest` still resolves to the concrete stable semver release.
|
|
- References:
|
|
[.github/workflows/desktop-release.yml](../../../.github/workflows/desktop-release.yml)
|
|
[desktop-release-config.test.mjs](../../../tools/scripts/desktop-release-config.test.mjs)
|
|
|
|
### Desktop release notes exceed GitHub's body limit
|
|
|
|
- Symptom:
|
|
All architecture builds and asset uploads succeed, but `Stage Release Draft`
|
|
fails in `Update release notes with summary` with HTTP 422 and
|
|
`body is too long (maximum is 125000 characters)`. Promotion and notification
|
|
jobs are then skipped.
|
|
- Quick checks:
|
|
Inspect the failed step rather than rebuilding the installers. Run
|
|
`gh release view <tag> --json body --jq '.body | length'` and check whether
|
|
the generated `What's Changed` section contains repository-wide history.
|
|
- Root cause:
|
|
GitHub's implicit release-note comparison selects published releases. Tutti
|
|
intentionally keeps RC and beta GitHub Releases as drafts, so the implicit
|
|
base can remain far behind and every prerelease accumulates the same old pull
|
|
requests until the Release body reaches GitHub's limit.
|
|
- Fix:
|
|
Resolve the previous same-channel tag explicitly and pass it as
|
|
`softprops/action-gh-release`'s `previous_tag`. Keep the shared Release body
|
|
limiter as a fallback: it preserves managed summary and direct-download
|
|
sections and trims only generated detail entries. Re-running an old Actions
|
|
run still uses its original workflow revision; merge the workflow fix before
|
|
starting a new release, or repair the existing draft notes before manually
|
|
promoting that draft.
|
|
- Validation:
|
|
Generate notes with the failing tag and its resolved previous tag and confirm
|
|
the body is bounded to that comparison range. Run
|
|
`node --test tools/scripts/desktop-release-summary.test.mjs tools/scripts/desktop-release-download-links.test.mjs tools/scripts/desktop-release-config.test.mjs`.
|
|
- References:
|
|
[.github/workflows/desktop-release.yml](../../../.github/workflows/desktop-release.yml)
|
|
[resolve-previous-release-tag.mjs](../../../apps/desktop/scripts/resolve-previous-release-tag.mjs)
|
|
[githubReleaseBody.mjs](../../../apps/desktop/scripts/lib/githubReleaseBody.mjs)
|
|
|
|
### Desktop release stalls after all packages finish building
|
|
|
|
- Symptom:
|
|
Every macOS and Windows build succeeds and the GitHub draft assets are
|
|
staged, but `Stage Release Draft` remains in `Install AWS CLI` until GitHub
|
|
cancels the job near its six-hour runtime limit. The last output comes from
|
|
`apt-get update` and contains repeated `Ign` lines for Ubuntu package indexes.
|
|
- Quick checks:
|
|
Inspect the start of `Install AWS CLI`. If the shell still runs
|
|
`sudo apt-get update`, the Actions run used a workflow revision from before
|
|
the bounded bootstrap fix. Confirm no later `curl` or `aws --version` line
|
|
appears; that distinguishes an APT mirror stall from an AWS CLI download or
|
|
credential failure.
|
|
- Root cause:
|
|
The old bootstrap refreshed Ubuntu package indexes before checking for AWS
|
|
CLI. A transient runner mirror failure could therefore block an otherwise
|
|
complete release, and the step had no deadline below GitHub's job limit.
|
|
- Fix:
|
|
Start a new release run from a revision that skips APT, extracts the official
|
|
AWS CLI archive with `python3 -m zipfile`, retries bounded downloads, and
|
|
limits the complete install step to five minutes. Re-running the original
|
|
Actions run is insufficient because GitHub reuses that run's old workflow
|
|
revision.
|
|
- Validation:
|
|
Run `node --test tools/scripts/desktop-release-config.test.mjs`. Confirm both
|
|
release workflows reject `apt-get`, require the download retry/connect/total
|
|
time limits, use Python extraction, and set the step deadline.
|
|
- References:
|
|
[.github/workflows/desktop-release.yml](../../../.github/workflows/desktop-release.yml)
|
|
[.github/workflows/desktop-release-promote.yml](../../../.github/workflows/desktop-release-promote.yml)
|
|
[desktop-release-config.test.mjs](../../../tools/scripts/desktop-release-config.test.mjs)
|
|
|
|
### Desktop dev GUI exits before opening
|
|
|
|
- Symptom:
|
|
`make dev-gui` exits during startup before the desktop window is usable. The
|
|
early form reports `pnpm <version> installation did not succeed`; the later
|
|
form reaches `start electron app...` and then `make` exits while desktop logs
|
|
say `secondary tutti instance detected`. A database form reports
|
|
`no such table: model_plan_first_use_candidates`, followed by
|
|
`tuttid exited before it published its listener info`. Another early form
|
|
exits while checking prerequisites because a stale `pnpm` shim reports that
|
|
its bundled `../node/bin/node` no longer exists. A catalog form starts
|
|
successfully but selects a legacy workspace-app release because the daemon
|
|
reports the development package placeholder version `0.0.0`.
|
|
- Quick checks:
|
|
Run `DEV_GUI_SKIP_START=1 make dev-gui` to isolate prerequisite setup from
|
|
Electron startup. If full startup exits after `start electron app...`, inspect
|
|
`~/.tutti-dev/logs/tutti-desktop.log` and check whether `/Applications/Tutti.app`
|
|
or another Tutti instance is already running.
|
|
- Root cause:
|
|
Shells launched by tools can put another `pnpm` earlier on `PATH` than
|
|
corepack's shim, so `corepack prepare` succeeds but the script still validates
|
|
the wrong `pnpm`. That earlier shim can also be a symlink into a relocated
|
|
runtime cache, so invoking `pnpm --version` fails before the script has a
|
|
chance to run Corepack. Electron's single-instance lock also follows Electron
|
|
userData; if development and production share userData, a running production
|
|
app makes the dev app quit as a secondary instance. Agent shells launched from
|
|
the packaged app may inherit `TUTTI_ENV=production`, so `make dev-gui` must
|
|
force the development environment instead of preserving that inherited value.
|
|
Electron development reads the placeholder version from
|
|
`apps/desktop/package.json`; passing that value to the daemon makes
|
|
compatibility-gated catalog releases ineligible.
|
|
The missing-table variant comes from a development database that already
|
|
recorded `model_plans_v1` before the candidate table was added behind that
|
|
same marker. SQLite correctly skips the recorded migration, so startup
|
|
reconciliation reaches a physical schema that the marker does not describe.
|
|
- Fix:
|
|
Probe `pnpm --version` without letting a broken shim abort startup, discover
|
|
Corepack from the active or locally installed Node runtime, prefer that
|
|
Corepack shim directory before checking or running `pnpm`, and set development
|
|
Electron userData to an environment-specific path before requesting the
|
|
single-instance lock. Ensure the dev-gui script exports
|
|
`TUTTI_ENV=development` before resolving pid files, installing the dev CLI, or
|
|
launching Electron. Resolve and export `TUTTI_APP_VERSION` from the repository
|
|
Git description before launch, while preserving an explicit developer
|
|
override.
|
|
Apply a new, idempotent `model_plan_first_use_candidates_v1` forward migration
|
|
after `model_plans_v1`; do not require deleting the development database and
|
|
do not rewrite or reuse the recorded migration identifier.
|
|
- Validation:
|
|
Run `DEV_GUI_SKIP_START=1 make dev-gui` and confirm it logs a SemVer-compatible
|
|
Git-derived desktop version rather than `0.0.0`. Then run full `make dev-gui`
|
|
while the packaged app is open and confirm the renderer dev server and
|
|
development `tuttid` start. Also run `pnpm --filter @tutti-os/desktop test`,
|
|
`pnpm --filter @tutti-os/desktop typecheck`, and
|
|
`pnpm check:electron-runtime-boundaries`.
|
|
Retain a migration test that starts with `model_plans_v1` recorded and the
|
|
candidate table absent, then verifies a full migration pass restores the table
|
|
and index exactly once.
|
|
- References:
|
|
[dev-gui.sh](../../../tools/scripts/dev-gui.sh)
|
|
[bootstrap.ts](../../../apps/desktop/src/main/bootstrap.ts)
|
|
[defaults.ts](../../../apps/desktop/src/main/defaults.ts)
|
|
[migrations_model_plans.go](../../../services/tuttid/data/workspace/migrations_model_plans.go)
|
|
|
|
### Running a development tuttid breaks the production Agent session
|
|
|
|
- Symptom:
|
|
Production Tutti is used to develop Tutti itself. After an Agent runs a newly
|
|
built `tuttid` command, sending in a new conversation fails and the workspace
|
|
returns to the previously selected conversation. Daemon logs may report an
|
|
unsupported Agent Target launch-ref type immediately after a second daemon
|
|
starts.
|
|
- Quick checks:
|
|
List live `tuttid` processes and compare their command paths. Inspect
|
|
`~/.tutti/logs/tuttid.log` for overlapping startup records, then check the
|
|
command run by the Agent for a bare daemon binary without
|
|
`TUTTI_ENV=development` or `TUTTI_STATE_DIR`. A historical `tuttid --help`
|
|
invocation is significant because older binaries treated it as normal
|
|
startup.
|
|
- Root cause:
|
|
Bare daemon execution selects the production root. Older `tuttid` binaries
|
|
did not parse `--help` and overwrote the shared PID file instead of claiming
|
|
exclusive state ownership. The second process could open the production
|
|
SQLite database and reseed system Agent Targets with a newer launch-ref
|
|
discriminator while the packaged daemon still expected the older value.
|
|
Session creation then failed, and renderer recovery restored the previous
|
|
conversation.
|
|
- Immediate recovery:
|
|
Quit Tutti completely, terminate any remaining `tuttid` processes after
|
|
verifying their command paths, then reopen production Tutti. Do not delete
|
|
`tuttid.db`; normal daemon startup reseeds its own system records. For further
|
|
local daemon work, use the managed development command or explicitly set
|
|
`TUTTI_ENV=development`/`TUTTI_STATE_DIR`.
|
|
- Fix:
|
|
Parse help and reject unknown arguments before creating state. Acquire the
|
|
PID sidecar as an exclusive operating-system lease before logging, lock
|
|
recovery, database wiring, migrations, or listener publication. Keep the PID
|
|
text check so a new daemon also refuses a state root owned by a live older
|
|
daemon, but validate process identity so PID reuse by an unrelated process
|
|
does not block recovery. Leave the marker for stale-owner recovery instead of
|
|
deleting it through a read/remove race with an older lockless daemon.
|
|
- Validation:
|
|
Run focused daemon tests. Verify `tuttid --help` exits successfully without
|
|
creating the selected state root, invalid arguments exit nonzero without
|
|
state, a live legacy PID is rejected, a stale PID is recovered, and a second
|
|
lease cannot be acquired while the first is held.
|
|
- References:
|
|
[main.go](../../../services/tuttid/main.go)
|
|
[pid_file.go](../../../services/tuttid/pid_file.go)
|
|
[local-state-storage.md](../local-state-storage.md)
|
|
|
|
### macOS updates fail from a mounted DMG
|
|
|
|
- Symptom:
|
|
A packaged macOS build can check for and download an update, but clicking
|
|
install appears to do nothing or logs an updater error such as
|
|
`Cannot update while running on a read-only volume`. The desktop log shows
|
|
the app executable under `/Volumes/.../Tutti.app`, and the daemon may stop
|
|
briefly because the update install flow began before the updater rejected the
|
|
read-only volume.
|
|
- Quick checks:
|
|
Inspect `tutti-desktop.log` for `process.execPath`, updater errors, or
|
|
managed daemon start lines that point under `/Volumes`. Confirm whether the
|
|
user launched Tutti directly from a mounted `.dmg` instead of the copy in
|
|
`/Applications`.
|
|
- Root cause:
|
|
macOS mounts compressed DMG installers as read-only volumes. Electron's macOS
|
|
updater cannot replace an app bundle that is running from that volume, so the
|
|
failure is an install-location problem rather than a dead `tuttid` process.
|
|
- Fix:
|
|
In packaged macOS builds, detect `/Volumes` startup before desktop services
|
|
and managed `tuttid` are created. Prompt the user to move Tutti to
|
|
`/Applications`, call Electron's application-folder move when accepted, and
|
|
quit rather than continuing from the mounted image. Development builds must
|
|
skip this guard so local Electron runs keep working.
|
|
- Validation:
|
|
Cover the guard with tests for development mode, non-macOS platforms,
|
|
`/Applications`, `/Volumes`, declined installation, successful automatic
|
|
move, and failed automatic move. Run the desktop tests, desktop typecheck, and
|
|
i18n check because the guard uses Electron dialog copy.
|
|
- References:
|
|
[macosApplicationInstallGuard.ts](../../../apps/desktop/src/main/macosApplicationInstallGuard.ts)
|
|
[bootstrap.ts](../../../apps/desktop/src/main/bootstrap.ts)
|
|
[desktop-release.md](../desktop-release.md)
|
|
|
|
### macOS Gatekeeper dialogs appear during Codex provider probing
|
|
|
|
- Symptom:
|
|
Opening an app or surface that reads agent composer options, provider status,
|
|
or capability catalog triggers repeated macOS warnings that `codex` may harm
|
|
the computer.
|
|
- Quick checks:
|
|
Resolve the active Codex binary from `tuttid` logs or by running with the
|
|
same daemon environment. Then inspect the native binary behind the npm shim
|
|
with `spctl --assess --type execute -vv <native-codex-path>`. If it reports
|
|
`CSSMERR_TP_CERT_REVOKED`, remove or reinstall that specific Codex package.
|
|
- Root cause:
|
|
Provider status and composer capability discovery intentionally start Codex
|
|
commands such as `codex app-server`. If the daemon
|
|
resolves an older nvm global Codex package whose Developer ID certificate has
|
|
been revoked, each otherwise harmless background probe can become a
|
|
Gatekeeper dialog. This can happen even when `which codex` in the user's shell
|
|
points at a newer working Codex if the daemon command resolver places scanned
|
|
nvm fallback directories before the real PATH.
|
|
- Fix:
|
|
Respect the daemon PATH before scanned nvm fallback directories, and sort nvm
|
|
fallback directories by Node version so fallback resolution does not pick the
|
|
oldest installed Node first. Do not automatically remove attributes or delete
|
|
arbitrary user-managed Codex binaries from Tutti; user repair scripts should
|
|
only move Codex packages that `spctl` explicitly reports as certificate
|
|
revoked, and should keep a backup.
|
|
- Validation:
|
|
Run `go test ./runtimecmd` and `go test ./runtime` from
|
|
`packages/agent/daemon`, plus `go test ./service/agentstatus` from
|
|
`services/tuttid`. Verify provider status logs resolve `codex` to the same
|
|
npm shim the user expects from PATH, unless PATH lacks Codex and the resolver
|
|
intentionally falls back to a scanned nvm install.
|
|
- References:
|
|
[resolver.go](../../../packages/agent/daemon/runtimecmd/resolver.go)
|
|
|
|
### Electron main/preload crashes on a workspace package `.ts` export
|
|
|
|
- Symptom:
|
|
Desktop development starts the renderer, then Electron throws
|
|
`TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"` for a
|
|
path under `packages/.../src`.
|
|
- Quick checks:
|
|
Inspect the package named in the stack trace and any bundled workspace package
|
|
that imports it.
|
|
Run `pnpm check:electron-runtime-boundaries` to confirm whether the package is
|
|
being externalized in Electron main/preload instead of bundled.
|
|
- Root cause:
|
|
Local workspace packages intentionally export source files for monorepo
|
|
development. Electron main/preload can only execute those packages when
|
|
`electron-vite` bundles them. If a bundled workspace package imports another
|
|
workspace package that is not also listed in the `externalizeDepsPlugin`
|
|
`exclude` list, Node may try to load the transitive package's raw `.ts`
|
|
export at runtime.
|
|
- Fix:
|
|
Add the source-exporting workspace package, including transitive workspace
|
|
dependencies reached by bundled main/preload code, to
|
|
`apps/desktop/electron.vite.config.ts` `externalizeDepsPlugin({ exclude })`.
|
|
Prefer narrow non-UI package subpaths for Electron runtime imports so the
|
|
bundle does not pull React-facing barrels into main/preload.
|
|
- Validation:
|
|
Run `pnpm check:electron-runtime-boundaries` and
|
|
`pnpm --filter @tutti-os/desktop build`.
|
|
- References:
|
|
[apps/desktop/electron.vite.config.ts](../../../apps/desktop/electron.vite.config.ts)
|
|
[tools/scripts/check-electron-runtime-boundaries.mjs](../../../tools/scripts/check-electron-runtime-boundaries.mjs)
|
|
|
|
### Desktop restart leaves an orphan tuttid
|
|
|
|
- Symptom:
|
|
The desktop logs `Timed out waiting for tuttid listener info: daemon runtime
|
|
information is not available yet`, or the first restart reports a daemon
|
|
startup failure while a second restart succeeds. `ps`, `lsof`, or Task Manager
|
|
may still show an older `tuttid` process holding the database or a loopback
|
|
listener.
|
|
- Quick checks:
|
|
Inspect `~/.tutti-dev/run/tuttid.pid`, run `lsof` on
|
|
`~/.tutti-dev/tuttid.db`, and check whether the daemon process
|
|
has parent PID `1`. That combination means the Electron parent no longer owns
|
|
the process even though the daemon survived. On Windows, align
|
|
`desktop parent process disappeared`, `http server shutdown error`,
|
|
`process did not exit after kill`, `stopping stale tuttid process`, and
|
|
`disk I/O error (1546)` across `tuttid.log` and `tutti-desktop.log`. A later
|
|
`managed tuttid restarted` after `failed to start managed tuttid` identifies
|
|
two competing startup owners rather than a second user action fixing data.
|
|
- Root cause:
|
|
In development, launching through `go run` can create a wrapper process and a
|
|
compiled daemon child. Killing only the direct child can leave the compiled
|
|
daemon alive. If the desktop also removes the listener info file before the
|
|
next launch, the orphan can keep local state busy while the new managed daemon
|
|
never publishes runtime info within the startup timeout. The same symptom can
|
|
occur in packaged builds when HTTP shutdown returns before active handlers and
|
|
live Agent transports finish closing. A stale-daemon handoff can transiently
|
|
fail SQLite WAL initialization with `SQLITE_IOERR_TRUNCATE`. Initial startup
|
|
must own recovery until the daemon first becomes healthy; runtime supervision
|
|
must not independently recover the same child exit while bootstrap propagates
|
|
the original failure.
|
|
- Fix:
|
|
Prefer a prebuilt `apps/desktop/build/tuttid/tuttid` binary in development
|
|
when present, kill managed daemon process groups during desktop shutdown,
|
|
write and clear `tuttid.pid`, and inject `TUTTI_DESKTOP_PARENT_PID` so
|
|
`tuttid` can self-shutdown when its desktop parent disappears. Keep one
|
|
startup recovery owner until health succeeds, wait for HTTP shutdown before
|
|
closing daemon wiring, and retry only the bounded transient
|
|
`SQLITE_IOERR_TRUNCATE` WAL handoff error. Do not delete WAL/SHM files or retry
|
|
arbitrary SQLite failures.
|
|
- Validation:
|
|
Repeatedly quit and restart the desktop, then confirm there is at most one
|
|
`tuttid` process and that `~/.tutti-dev/run/tuttid.pid`
|
|
matches it. Fault-inject the first WAL initialization attempt and verify the
|
|
original desktop bootstrap continues after one recovery loop. Hold an HTTP
|
|
request open during cancellation and verify daemon `Run` does not return until
|
|
the request drains. Also run the desktop daemon-manager tests and
|
|
`cd services/tuttid && go test ./...`.
|
|
- References:
|
|
[tuttidManager.ts](../../../apps/desktop/src/main/daemon/tuttidManager.ts)
|
|
[main.go](../../../services/tuttid/main.go)
|
|
|
|
### Switching agent permission mode flashes Checking for updates
|
|
|
|
- Symptom:
|
|
In a packaged build, changing composer permission mode (or other remembered
|
|
composer defaults) briefly shows the top-right **Checking for updates** /
|
|
**正在检查更新** badge. Rapid switches may spam updater checks in
|
|
`tutti-desktop.log` (`Checking for update` next to
|
|
`agent.gui.composer_defaults.remembered`). Local unpackaged dev usually hides
|
|
this because update checks are unsupported unless a shared
|
|
`DESKTOP_UPDATE_ADMISSION_*` development scenario is enabled.
|
|
- Quick checks:
|
|
Confirm packaged/`supportsUpdates`. Correlate
|
|
`agent.gui.composer_defaults.remembered` with
|
|
`checking for application updates` / `application updater static feed
|
|
configured`. Verify `updateChannel` / `updatePolicy` did not change.
|
|
- Root cause:
|
|
Remembering composer defaults writes desktop preferences and emits
|
|
`preferences.desktop.updated`. The main-process host preferences stream used
|
|
to call `updateService.configure()` on every preferences event; `configure()`
|
|
always runs a background update check and surfaces the checking status.
|
|
- Fix:
|
|
Only call `updateService.configure()` when `updateChannel` or `updatePolicy`
|
|
actually changed. Other preference syncs (composer defaults, locale, rail,
|
|
theme) must not reconfigure the updater.
|
|
- Validation:
|
|
In a packaged build, switch permission mode and confirm the update badge does
|
|
not appear and desktop logs do not emit a new update check. Cover
|
|
composer-default vs channel/policy changes in
|
|
`desktopHostPreferencesEventStream` tests.
|
|
- References:
|
|
[desktopHostPreferencesEventStream.ts](../../../apps/desktop/src/main/desktopHostPreferencesEventStream.ts)
|
|
[appUpdateService.ts](../../../apps/desktop/src/main/update/appUpdateService.ts)
|
|
|
|
### App update diagnostics flood with identical download progress states
|
|
|
|
- Symptom:
|
|
Renderer diagnostics show hundreds of
|
|
`app_update.state_applied` events per minute during update downloads, often
|
|
with identical payloads. `AppUpdateStatus` may re-render without visible UI
|
|
changes.
|
|
- Quick checks:
|
|
Compare consecutive diagnostic payloads for `status`, `downloadPercent`, and
|
|
`downloadedBytes`. Inspect whether main-process `applyState` and renderer
|
|
`applyUpdateState` both commit every IPC event.
|
|
- Root cause:
|
|
`electron-updater` can emit high-frequency download progress callbacks. Without
|
|
a shared `AppUpdateState` equality guard at the commit boundary, identical
|
|
states still emit IPC, write the valtio store, and log diagnostics.
|
|
- Fix:
|
|
Compare incoming state with the current snapshot via
|
|
`isSameAppUpdateState()` before committing in both main `applyState` and
|
|
renderer `applyUpdateState`. Keep diagnostics on successful commits only.
|
|
- Validation:
|
|
Run desktop app-update tests and confirm repeated identical progress events
|
|
produce a single state change.
|
|
- References:
|
|
[appUpdateState.ts](../../../apps/desktop/src/shared/contracts/appUpdateState.ts)
|
|
[appUpdateService.ts](../../../apps/desktop/src/main/update/appUpdateService.ts)
|
|
[appUpdateService.ts](../../../apps/desktop/src/renderer/src/features/app-update/services/internal/appUpdateService.ts)
|
|
|
|
### macOS in-app update closes Tutti but does not install the new version
|
|
|
|
- Symptom:
|
|
After downloading a desktop update on macOS, clicking **Install** closes or
|
|
relaunches Tutti, but reopening the app still shows the old version. ShipIt
|
|
logs may contain `SQRLInstallerErrorDomain Code=-9` or
|
|
`App Still Running Error`.
|
|
- Quick checks:
|
|
Confirm the packaged build is signed (unsigned or ad-hoc builds disable in-app
|
|
updates). Inspect `~/Library/Caches/sh.tutti.desktop.ShipIt/ShipIt_stderr.log`
|
|
for `Aborting update attempt because there are 1 running instances of the
|
|
target app`. Compare `/Applications/Tutti.app/Contents/Info.plist` with the
|
|
cached update under `~/Library/Caches/@tutti-osdesktop-updater/pending`.
|
|
- Root cause:
|
|
Squirrel.Mac refuses to replace `/Applications/Tutti.app` while any target
|
|
app instance is still running. Stopping `tuttid` before `quitAndInstall()` is
|
|
not sufficient because the Electron main process and helper windows still need
|
|
to complete the app quit path.
|
|
- Fix:
|
|
Keep daemon shutdown in the desktop lifecycle instead of the update service.
|
|
`installUpdate()` should mark the install pending and call
|
|
`quitAndInstall()`. The `before-quit` gate should still run for pending update
|
|
installs: prevent the first quit, stop managed `tuttid`, destroy all windows,
|
|
then call `app.quit()` again so the app process exits and ShipIt can replace
|
|
the bundle.
|
|
- Validation:
|
|
Run `src/main/desktopAppLifecycle.test.ts` and
|
|
`src/main/update/appUpdateService.test.ts`, including updater error and
|
|
synchronous `quitAndInstall()` failure cases. Then install a downloaded update
|
|
in a packaged macOS build; the app should relaunch on the new version and
|
|
ShipIt should not log `App Still Running Error`.
|
|
- References:
|
|
[appUpdateService.ts](../../../apps/desktop/src/main/update/appUpdateService.ts)
|
|
[desktopAppLifecycle.ts](../../../apps/desktop/src/main/desktopAppLifecycle.ts)
|
|
[desktopAppServices.ts](../../../apps/desktop/src/main/desktopAppServices.ts)
|
|
|
|
### Desktop Performance trace export runs out of memory
|
|
|
|
- Symptom:
|
|
Chrome DevTools Performance export or trace parsing fails with
|
|
`Maximum call stack size exceeded` or V8 `CALL_AND_RETRY_LAST` OOM while the
|
|
desktop app is running through `make dev-gui`.
|
|
- Quick checks:
|
|
Keep the trace short and disable renderer diagnostics that inflate tracks:
|
|
`VITE_TUTTI_WHY_DID_YOU_RENDER=0 make dev-gui`. For CDP-based trace capture,
|
|
launch with
|
|
`TUTTI_ELECTRON_REMOTE_DEBUGGING_PORT=9223 TUTTI_ELECTRON_JS_FLAGS=--max-old-space-size=8192`.
|
|
Confirm the port with `curl http://127.0.0.1:9223/json/version`.
|
|
- Root cause:
|
|
DevTools can run out of stack or old-space memory while processing large trace
|
|
payloads. Passing extra CLI args through `electron-vite` is not reliable enough
|
|
for these diagnostics, so the desktop main process owns the Electron command
|
|
line switches.
|
|
- Fix:
|
|
Prefer CDP `Tracing.start` with `transferMode: "ReturnAsStream"` for large
|
|
captures instead of DevTools UI export. The repository helper uses that path:
|
|
`pnpm trace:desktop -- --duration 15`. Record only the smallest repro window.
|
|
- Validation:
|
|
Restart the desktop app, confirm the remote debugging endpoint responds, record
|
|
a short trace, and verify the trace JSON is written without opening the
|
|
Performance export path.
|
|
|
|
### macOS screenshot selector leaves the menu bar and Dock uncovered
|
|
|
|
- Symptom:
|
|
The screenshot selector darkens only the macOS work area. The real menu bar
|
|
and Dock remain visible while a second captured copy of each appears inside
|
|
the selector preview.
|
|
- Quick checks:
|
|
Find `screenshot selector presented` in the Desktop log. Compare
|
|
`windowBounds` with `displayBounds` and `displayWorkArea`, then inspect
|
|
`simpleFullScreen`. A work-area-sized window with
|
|
`simpleFullScreen: false` confirms that the native selector never entered
|
|
simple full-screen; renderer `fixed inset-0` styles cannot draw outside that
|
|
native window.
|
|
- Root cause:
|
|
Electron's macOS `simpleFullscreen` constructor option chooses the pre-Lion
|
|
implementation for a later full-screen request. It does not enter
|
|
full-screen by itself. Combining `simpleFullscreen: true` with
|
|
`fullscreen: false` therefore leaves a normal frameless window that macOS can
|
|
present within the work area.
|
|
- Fix:
|
|
Request `fullscreen: true` together with macOS `simpleFullscreen: true`, then
|
|
explicitly call or verify `setSimpleFullScreen(true)` before showing the
|
|
selector. Keep the renderer preview sized to the native viewport and exit
|
|
full-screen before resizing the same window into the floating Composer.
|
|
- Validation:
|
|
On macOS, invoke capture from an external application and confirm the selector
|
|
covers the physical display, including the menu bar and Dock. The Desktop log
|
|
should report `simpleFullScreen: true` and `windowBounds` equal to
|
|
`displayBounds`. Complete a crop and verify the floating Composer still uses
|
|
its remembered work-area-clamped position.
|
|
- References:
|
|
[desktopCaptureService.ts](../../../apps/desktop/src/main/capture/desktopCaptureService.ts)
|
|
[captureSelectionFullscreen.ts](../../../apps/desktop/src/main/capture/captureSelectionFullscreen.ts)
|