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

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)