Files
tutti/docs/conventions/local-state-storage.md
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

605 lines
30 KiB
Markdown

# Local State Storage
`tutti` local state must follow one root-directory convention.
The repository-owned default names for these paths now live in:
- `config/tutti.defaults.json`
Runtime code should consume generated defaults from that source instead of duplicating literal file names in multiple implementations.
## Default Roots
- production defaults to `~/.tutti`
- local development defaults to `~/.tutti-dev`
This rule applies to local databases, logs, caches, temporary runtime metadata, and other daemon-owned state.
## Environment Rules
- `TUTTI_ENV=development` uses `~/.tutti-dev`
- `TUTTI_ENV=production` uses `~/.tutti`
- `TUTTI_STATE_DIR=/custom/path` overrides both defaults
- `TUTTI_AGENT_RUNTIME_DIR=/custom/path` overrides the managed Agent Extension
runtime root
- `TUTTI_DESKTOP_USER_DATA_DIR=/custom/path` overrides Electron `userData`
only, for isolated desktop diagnostics
These environment variables are for development, test, packaging, and diagnostics overrides.
They are not the primary source of product defaults.
Per-file overrides such as `TUTTID_DB_PATH` are still allowed for narrow operational needs, but new local storage code should derive paths from the shared generated defaults and shared state root first.
`TUTTID_RUN_DIR` and `TUTTID_PID_PATH` redirect runtime metadata only. They do
not redirect the state ownership lock, which is always derived from
`TUTTI_STATE_DIR` as `<state-dir>/run/tuttid.pid.lock`.
## Allowed Override Surface
Current supported override surface for local state and closely-related runtime paths:
- `TUTTI_ENV`
- `TUTTI_STATE_DIR`
- `TUTTI_AGENT_RUNTIME_DIR`
- `TUTTI_LOG_DIR`
- `TUTTI_DESKTOP_USER_DATA_DIR`
- `TUTTID_DB_PATH`
- `TUTTID_RUN_DIR`
- `TUTTID_PID_PATH`
- `TUTTID_LISTENER_INFO_PATH`
- `TUTTI_AGENT_CONTEXT_CONFIG`
Rules:
- treat these variables as developer and operator escape hatches, not product settings
- prefer `TUTTI_STATE_DIR` over adding new per-file overrides
- keep `TUTTI_DESKTOP_USER_DATA_DIR` paired with an isolated
`TUTTI_STATE_DIR`; it does not redirect daemon-owned state
- `TUTTI_STATE_DIR` does not redirect the managed Agent Extension runtime,
whose compatibility location is `~/.local/share/tutti/agent-runtimes`; pair
isolated Desktop diagnostics with an explicit `TUTTI_AGENT_RUNTIME_DIR`
- do not add a new environment variable when an existing shared root or generated default can express the same rule
- if a new override is truly needed, update this document and the matching transport or logging convention document in the same change
## Standard Layout
Production:
```text
~/.tutti/
tuttid.db
bin/
tutti
tutti-dev
logs/
tuttid.log
tutti-desktop.log
run/
tuttid.listener.json
tuttid.pid
tuttid.pid.lock
```
Local development:
```text
~/.tutti-dev/
tuttid.db
bin/
tutti
tutti-dev
logs/
tuttid.log
tutti-desktop.log
run/
tuttid.listener.json
tuttid.pid
tuttid.pid.lock
```
`tuttid.listener.json` is runtime endpoint metadata. It contains the loopback
address and per-run bearer auth needed by local clients such as the bundled
CLI, and should be written with restrictive file permissions.
## Daemon State Ownership
One state root has exactly one live `tuttid` owner. The daemon must acquire and
hold an exclusive operating-system lock on `<state-dir>/run/tuttid.pid.lock` before
initializing logging, recovering runtime locks, opening SQLite, running
migrations, seeding system records, or publishing listener metadata. A second
daemon targeting the same root must fail before touching durable state, even if
its pid or runtime metadata path is overridden. The PID
text remains available for desktop supervision and also protects upgrades from
an older live daemon that did not yet hold the operating-system lock. Before a
legacy PID blocks startup, the daemon verifies that the positive PID still
identifies a `tuttid` executable; a reused PID owned by an unrelated process is
stale metadata. Shutdown leaves the PID marker in place instead of racing a
legacy writer with a non-atomic read-then-remove. The next owner validates and
replaces that stale marker while holding the state-root lock.
Arguments that only inspect the daemon executable, such as `--help`, must exit
before state-path creation or ownership acquisition. Unknown arguments must
also fail without starting a daemon. Building or probing `tuttid` from an agent
or terminal therefore cannot silently become another production daemon.
Bare daemon execution defaults to production state. Development commands must
set `TUTTI_ENV=development`, set an explicit `TUTTI_STATE_DIR`, or use the
repository's managed development entry points. Environment separation and
single-owner locking are complementary: separation prevents unintended access;
locking prevents concurrent mutation after a root has been selected.
## Desktop Preferences
Device-global desktop preferences are durable daemon state in the
`desktop_preferences` row of `tuttid.db`. They are not workspace settings and
must be changed through the preferences service/API so the daemon can persist,
normalize, and publish the authoritative preferences event.
Fresh-profile creation is daemon-owned. A client uses the desktop preferences
`initializeIfAbsent` write mode. Before the atomic write, the preferences
service applies the daemon's fresh-profile workspace-mode default to the
normalized client candidate, so the Agent default cannot vary by caller while
other candidate fields and feature flags are preserved. The store writes that
complete row with a single `INSERT ... ON CONFLICT DO NOTHING` and returns the
authoritative stored row. If a concurrent initializer created the row first,
that stored row wins without being overwritten. Only `initializeIfAbsent` may
create this identity row. Field-specific writers fail without side effects when
the row is missing, so a partial patch can never decide whether the identity is
new or accidentally replace the complete initialization contract. The normal
omitted/`replace` write mode remains the full preference update. The daemon-owned
complete default uses the stable desktop update channel; packaged RC builds may
subsequently align that preference from their installed version. Existing rows
are never backfilled by initialization.
`agent_cli_update_check_enabled` stores the
`agentCliUpdateCheckEnabled` preference as a non-null SQLite boolean and
defaults to `true`, including for existing databases upgraded by migration. It
controls only the daemon's periodic managed-provider CLI update discovery. A
false value cancels scheduling and any in-flight discovery; it does not remove
cached metadata, change local readiness, or disable an explicit user-requested
update action.
## Model Access Plans
Workspace model access plans and custom Agents are daemon-owned rows in
`tuttid.db`. `model_plans` stores the plan configuration and verification
projection; API keys are encrypted in `api_key_ciphertext` and must never be
returned through public plan DTOs. `workspace_agents` is the current writable
Harness-to-Plan/default-model mapping. `agent_target_model_bindings` retains
the older target-to-plan shape only for historical sessions, rollback, and
legacy API compatibility; a forward migration idempotently materializes any
late binding rows as `source=legacy_binding` WorkspaceAgents before Desktop
removes the binding editor. Plan reference protection includes both current
WorkspaceAgents and remaining legacy consumers.
The retired `first_use_json` column and
`model_plan_first_use_candidates` table remain only for database downgrade
compatibility; current model-plan readiness ends at successful connection
detection and no session attribution is written or reconciled. The historical
`model_plan_first_use_candidates_v1` migration still repairs databases that
recorded `model_plans_v1` before this table existed. Migration identifiers are
immutable once any development or production database can record them; later
required tables, columns, or indexes must use a new forward migration rather
than extending the SQL hidden behind an existing marker.
The initial migration copies existing managed model-provider credentials into
stable `mp-migrated-<provider>` plans without removing the legacy rows, because
the workspace-app credential broker still owns that legacy surface. Normal
reads use the SQLite read pool and mutations use the single writer connection.
Migrated agent runtime state should derive from the same root:
```text
~/.tutti[-dev]/
tutti-mode-plans/
<workflow-id>/
revisions/
<sha256>.md
agent/
discovery/
claude-code/
extensions/
<agent-key>/
active.json
<extension-version>/
installation.json
tutti.agent.json
profiles/
locales/
assets/
sessions/
<date>-<sequence>/
worktrees/
<worktree-id>/
.metadata/
<worktree-id>.json
runs/
<agent-session-id>/
sidecar-manifest.json
codex-home/
tutti-agent-home/
attachments/
<agent-session-id>/
<attachment-id>.<ext>
agent-prompt-assets/
issues/
<attachment-id>.<ext>
codex/
tutti/
current/
agent-context.json
agent-providers/
claude-code/
current.json
external-agent-registry/
cache/
registry.json
packages/
<agent-id>/
binaries/
<agent-id>/
apps/
packages/
<app-id>/
<version>/
installations/
<app-id>/
<installation-scope>/
runtime/
data/
database/
logs/
factory/
jobs/
<factory-job-id>/
draft/
runtime/
data/
logs/
app-toolchains/
```
`agent/discovery/claude-code` is the fixed, project-neutral working directory
for Claude Code capability discovery. Discovery must not run from `/` or from a
user project directory, and its cache identity must not vary with a caller's
cwd or workspace. Agent-target identity and the non-secret auth fingerprint do
remain part of the identity. `agent/sessions` stores daemon-created working directories for agent sessions
that do not receive an explicit cwd. `agent/runs` stores per-session provider
sidecar state that can be recreated or cleaned up when the owning agent session
is deleted. Provider-specific homes, generated skills, and cleanup manifests
live under the matching run directory. Codex sessions use `codex-home` and
receive it through `CODEX_HOME`; Tutti Agent sessions use `tutti-agent-home`
and receive it through `TUTTI_AGENT_HOME`. `agent/skill-bundles/v1/<digest>`
stores immutable, rebuildable Tutti-managed Skill bundles shared by equal
content across Tutti Agent sessions; session cleanup never removes these
entries. `agent/system-skill-bundles/v1/<digest>/.system` stores immutable,
rebuildable snapshots of the embedded Skills installed by the active Tutti
Agent binary; run-scoped homes link to the matching digest before thread
startup. Session cleanup never removes either shared cache. `agent/attachments`
stores persisted prompt attachments by agent session.
`agent-prompt-assets/issues` stores daemon-managed PNG, JPEG, and WebP source
files attached to Qute Issues. Their metadata and Issue/Task ownership remain
in SQLite ContextRefs. The source root is intentionally inside the Agent prompt
attachment allowlist: when an Issue Run starts, the prompt attachment store
copies each image into `agent/attachments/<agent-session-id>` before provider
preparation. Removing the owning Issue, Task, or ContextRef removes only
matching files inside this managed directory. New image bytes are staged
before the Issue and its ContextRefs are committed together in one SQLite
transaction. Daemon startup reconciles the directory against durable SQLite
ContextRefs and prepared/leased Issue Run payload snapshots. A launch snapshot
pins its managed image paths until delivery is confirmed or the Run is settled,
so editing an Issue cannot change an admitted prompt. Reconciliation removes
files orphaned before the Issue transaction, or by a prior cleanup failure.
## Developer Agent Session Cassettes
The daemon keeps mutable Recording metadata and the rebuildable Cassette catalog
in SQLite. It does not persist Replay Workspace, Replay Surface, playback, or
verification state. It writes a Recording candidate under
`<TUTTI_STATE_DIR>/agent-session-recordings/<recording-id>/candidate/`.
A recording is a capture window over one recursive SessionGraph.
`create-session` has no initial state. `continue-session` captures the selected
root graph and related Tutti Mode, Workflow, and Issue business state in
`initial-state.json`. Explicit stop writes `expected-state.json`, finalizes the
provider tape, assigns a distinct
Cassette id, audits the candidate, and atomically publishes it under
`<TUTTI_STATE_DIR>/agent-session-cassettes/<cassette-id>`. Recording metadata
does not enter the published Cassette. Canceling removes the candidate. Turn
settlement does not stop recording. A daemon restart marks an active recording
incomplete and discards its candidate. Deleting an inactive recording removes
its Recording row, Cassette catalog row, and candidate or published directory.
`pnpm e2e:agent-gui -- --record <directory> --scenario <id>` exercises one
named scenario in an isolated runtime and copies the completed recording to
the requested directory. The runner has no raw prompt or generic
`--continue-session` recording mode; those semantics belong to the scenario
and the finalized cassette action.
The isolated daemon database and Electron `userData` live under the repository
`.tmp` root and are removed unless `--keep-runtime` is set.
The schema-v7 cassette contains `cassette.json`, `activity-events.jsonl`,
`checkpoint-plan.json`, optional `initial-state.json`,
`provider/{manifest.json,frames.jsonl}`, `expected-state.json`,
`blobs/manifest.json`, and any referenced content-addressed files under
`blobs/`.
Published files contain no source Workspace ID, database table/column names,
row IDs, migrations, or absolute paths. The daemon creates the transient
Workspace, restores semantic state before Host recovery, and captures actual
semantic state for verification.
Publication rejects every unrecognized file, including logs, screenshots,
databases, credentials, unrelated Sessions, and Workspace copies. Provider
payloads are capped at 8 MiB per frame and 256 MiB stored per tape; referenced
blobs are capped at 20 MiB each; the full audited file set is capped at 384 MiB.
The manifests report per-kind and per-file byte counts and hashes. Provider
frames and selected blobs may still contain private prompts, output, and files.
The blob manifest selects persisted prompt attachments explicitly referenced by
semantic Agent messages; replay verifies their SHA-256 and restores them into the
isolated attachment store. It does not scan the Workspace. Do not commit,
upload, or share a cassette without reviewing those scoped artifacts.
## Deleted Agent Conversation Retention
Soft-deleted Agent conversations remain recoverable canonical tombstones until
the device-global retention period expires. The supported values are 15 and 30
days, with 30 days as the default. Existing tombstones use their original
deletion timestamp; upgrades do not add another grace period. The daemon runs
small permanent-removal batches only while Agent work is idle, no more than one
successful automatic sweep per 24 hours. The desktop setting also exposes an
explicitly confirmed manual cleanup that targets all current topmost tombstone
components in the selected Workspace, regardless of the visible search or
project filter.
Only tombstones written by the lossless deletion generation are restorable.
They retain the complete root/child Session tree and its transcript, history,
provider resume identity, copied prompt attachments, and worktree reference.
Legacy tombstones are listed but remain unavailable for restore because their
Turn graph was already removed. Restore clears the exact tree tombstone without
rewriting its original Session metadata or starting provider work.
The Workspace store records a durable cleanup item in the same SQLite
transaction that hard-deletes canonical and Tutti Mode state for these
Tutti-owned Session components:
- `agent/attachments/<agent-session-id>`: persisted prompt attachments owned by
the purged session.
- `agent/runs/<agent-session-id>`: the isolated provider sidecar state retained
while the Session is recoverable.
The queue remains keyed by Workspace and Session so each purge transaction has
its own durable work item and is the only producer of that item. After Host
returns, maintenance only drains the committed queue; it never re-enqueues the
purge result. Cleanup is attempted immediately and failures remain durable for
a later idle retry. A deliberately queue-less composition may perform direct
best-effort cleanup instead. The physical roots above are currently keyed only
by agent Session ID, however, so any pending queue row globally fences insertion
of that Session ID in every Workspace. Before hard resource deletion, the
cleanup worker queries canonical live and tombstoned state across the full
database and retires the item without touching files if any Workspace still
owns the ID. Multiple queue rows for the same ID are safe: the first may remove
the roots and later attempts are idempotent. During recoverable deletion,
Workspace-keyed runtime and Model Gateway cleanup still runs, but the global
Agent/browser resource releaser is skipped when another Workspace has a live
canonical owner with that ID. Canonical deletion remains successful when
filesystem cleanup fails. Cleanup is limited to the two Session-scoped roots
above; it never deletes a user project, arbitrary Session cwd, provider
installation, shared provider home, or custom external provider home. A future
migration to Workspace-scoped physical paths is outside this change.
Managed worktrees are independent Workspace resources. Recoverable deletion,
restore, hard deletion, and purge do not retain, release, or remove them.
Deleted SQLite pages are immediately reusable by the database. After an
explicit manual sweep only, the daemon may additionally run a three-second
best-effort `VACUUM` when the whole database is no larger than 64 MiB, at least
8 MiB and one quarter of its pages are free, and Agent work is still idle.
Automatic maintenance never performs this compaction; a busy, timed-out, or
failed attempt does not roll back the committed purge.
`agent/worktrees/<worktree-id>` is a daemon-managed Git checkout with its own
resource identity and explicit lifecycle. The tuttid agent adapter owns the
corresponding `.metadata/<worktree-id>.json` record used for enumeration,
creation-transaction recovery, and explicit deletion. It records the Workspace,
repository root, branch, base commit, selected repository-relative working
directory, and opaque creation-request hash; new records never persist a
Session id. Metadata moves from `creating` to `ready` after `git worktree add`
and cwd validation. An exact retry repairs an interrupted `creating` transaction
or reuses its ready checkout; a Workspace, repository, or relative-directory
mismatch fails closed. Legacy Session-keyed records remain readable but their
Session id is compatibility data, not ownership.
Canonical Session isolation JSON references the independent worktree id and Git
coordinates as runtime facts. Fork copies the prepared cwd and runtime context
without validating a Session-to-worktree ownership relation. Session deletion,
restore, and purge never remove a worktree, and Host startup or periodic work
never sweeps this directory. The Workspace managed-worktree API is the only
normal deletion entrypoint. It refuses dirty or ahead resources and deletes the
branch with an expected-object-id compare so a concurrent commit is preserved.
`agent/extensions` is daemon-owned verified Agent Extension state. Version
directories are immutable after installation; `active.json` selects the
currently registered client-pinned version and is replaced atomically. A remote
active record from another client pin is not a valid offline fallback.
Extension ZIPs do not contain runtimes or executables. Cached assets and
profiles remain under each fixed installation for integrity checks and future
session-pinned resume.
Development-only local package overrides are copied into the same state as
content-addressed `+local.<digest>` versions; the daemon never launches against
the mutable source directory. Only the `data/agentextension` installation
adapter derives these paths or persists `installation.json` and `active.json`;
the service layer retains verification and activation workflow ownership. The
managed-first decision is derived from the running client's exact source pin,
not persisted as a separate mutable installation fact. Readers discard the
legacy `preferManagedRuntime` field after decoding older records.
Agent Extension executables are user-local programs rather than daemon state:
```text
~/.local/bin/
<agent-command> -> ~/.local/share/tutti/agent-runtimes/<agent-key>/bin/<agent-command>
~/.local/share/tutti/agent-runtimes/
<agent-key>/
bin/
<agent-command> -> ../<runtime-identity>/<runtime-executable>
<runtime-identity>/
activation.json
node_modules/
claude-code/
versions/
<claude-version>/
claude
```
For client-pinned remote Extensions, one matching managed Runtime is installed
automatically and reused across production state and all workspaces; a
compatible user-local executable is only a fallback until that convergence
succeeds. Development package snapshots remain local-first. Automatic
installation never modifies a user-owned executable or writes under a user
project. Retry state is process-local and keyed by Target installation:
transient failures back off independently to a 30-minute ceiling, while
permanent contract or command-ownership conflicts wait for a source or
preference wake, or an explicit setup action.
Setup action records, extension packages, discovery CWDs, and session state
remain under the selected `~/.tutti[-dev]` state root. The Claude SDK sidecar's
`current.json` pointer is state metadata, while its pinned native executable
uses the shared user-local runtime root.
Agent Extension activation publishes its command through the stable two-link
chain above. Development and production share that command and underlying
versioned runtime; environment separation applies only to daemon state. Tutti
never replaces a pre-existing regular file or foreign symlink in
`~/.local/bin`. Its own published link remains classified as managed for
fingerprint checks, persists across feature disablement and daemon shutdown,
and requires explicit reinstall if either link is missing or invalid.
Agent Extension setup uses these daemon-owned state paths:
```text
<state-dir>/agent/extension-runtime-actions/<scope-sha256>.json
<state-dir>/agent/extension-account-usage-companion-failures/<scope-sha256>.json
<state-dir>/agent/account-usage-node-snapshots/<node-sha256>.exe
<state-dir>/agent/discovery/agent-extensions/
```
Account-usage companion failure records contain only stable identities, an
error code, a failure count, and retry timestamps. Raw installer/provider
errors, paths, output, and credentials are never persisted. Recovery deletes
the record. Windows stores content-addressed Node snapshots here and keeps each
verified snapshot read-locked while it is reusable.
Connector CLI packages use daemon-owned state rather than the user's global
npm prefix. All CLI Connector installations bind to the one signed
`connector-node-static` runtime under `<state-dir>/connectors/runtimes` and use:
```text
<state-dir>/connectors/node-packages/shared/pnpm-store/
<state-dir>/connectors/node-packages/shared/corepack/
<state-dir>/connectors/node-packages/shared/npm-cache/
<state-dir>/connectors/node-packages/shared/pnpm-home/
<state-dir>/connectors/node-packages/packages/<connector-key>/<release-digest>/
```
The shared directories deduplicate registry downloads and physical dependency
content. The release directory remains isolated and contains only its lock,
links/hardlinks, package metadata, generated bin entry, and installation
receipt. A release-scoped rollback removes only the target release. Explicit
Connector uninstall removes every private release directory and prepared or
download-cached artifact for that Connector, while preserving shared package
content, account authorization, and user/workspace state.
The action filename hashes exact Target plus fixed extension installation
identity; workspace identity remains inside the record, not in a directory
segment. The data adapter owns path derivation, strict JSON decoding, scope
validation, `0700` directories, `0600` temporary files, sync, and atomic rename.
Session-level runtime/profile pinning remains tracked in the Agent Extension
architecture migration; `active.json` alone is not a durable session pin.
Filesystem paths under `<state-dir>` must not expose `workspaceId` as a
directory segment. Workspace ownership belongs in the SQLite database and
transport/domain contracts; local file paths should use user-meaningful or
session-scoped names. Workspace app installation state uses an opaque
`<installation-scope>` derived from the workspace/app identity so separate
workspace installations stay isolated without exposing workspace IDs in the
filesystem.
Pre-release layouts that exposed workspace IDs as state-directory segments are
intentionally unsupported by runtime fallback or automatic migration. Internal
testers who need to keep data should move it to the current layout before
upgrading.
The exact files may appear gradually as features are implemented, but new daemon-owned local files should follow this layout.
`agent-providers/external-agent-registry` stores the ACP External Agent Registry
cache plus daemon-managed adapter artifacts. npm-based adapters use
`packages/<agent-id>` as their npm prefix so global npm shims cannot affect
Tutti provider startup.
## Current Usage
- `tuttid` SQLite database defaults to `<state-dir>/tuttid.db`
- immutable Tutti mode plan revisions live under `<state-dir>/tutti-mode-plans/<workflow-id>/revisions/<sha256>.md`; the daemon writes each revision atomically and verifies its content digest when reading it
- desktop-managed local development starts `tuttid` with `TUTTI_ENV=development`
- packaged desktop builds start `tuttid` with `TUTTI_ENV=production`
- path helpers reserve `<state-dir>/logs` and `<state-dir>/run` for daemon log, listener-info, and pid files
- `tuttid` holds an exclusive lock on `<state-dir>/run/tuttid.pid.lock` for its full state-owning lifetime
- desktop main-process operational logging defaults to `<state-dir>/logs/tutti-desktop.log`
- desktop-to-daemon listener publication defaults to `<state-dir>/run/tuttid.listener.json`
- the bundled CLI discovers the managed daemon by reading `<state-dir>/run/tuttid.listener.json`
- packaged desktop shim install or repair uses `<state-dir>/bin/tutti` as the canonical user-level command path and points it at the packaged CLI binary; on macOS and Linux, when the login-shell `PATH` already contains writable `~/.local/bin` or `~/bin`, desktop also maintains a Tutti-owned forwarding shim there without replacing third-party commands
- packaged Windows uninstall preserves the `.tutti` state root by default but
removes canonical and user-PATH `tutti.cmd` shims only when their marker line
identifies them as Tutti-owned; uninstall must not delete an unmarked
third-party command with the same name; interactive uninstall asks whether
to preserve or delete all user state, while silent uninstall preserves it
unless explicitly invoked with `--delete-app-data`; the delete path removes
daemon state, Electron user data, updater cache, and the per-user `tutti`
protocol registration
- local development scripts install or repair `<state-dir>/bin/tutti-dev` as the development CLI command and default it to `TUTTI_ENV=development`
- workspace app package cache, per-installation runtime/data/database/log state, and
app factory job working directories live under `<state-dir>/apps`
- each workspace app installation receives a host-local durable `database/`
directory for active SQLite databases and other files that require local
filesystem locking; uninstalling the installation removes it with the rest
of that installation's state
- workspace apps receive `<state-dir>/app-toolchains` as the shared cache root
for reusable app-managed binaries
## SQLite Connection Governance
The daemon owns one SQLite database file and opens separate `database/sql`
pools for writes and reads. The write pool has exactly one connection because
SQLite serializes writers and several migrations rely on connection-scoped
PRAGMA state. The read-only pool uses WAL snapshots and may grow on demand to a
small bounded number of connections; its connections use SQLite read-only and
`query_only` modes.
Route independent queries through the read pool. Writes, migrations,
read-modify-write sequences, and reads that must share a write transaction's
snapshot stay on the write connection or its `sql.Tx`. Configure
connection-scoped PRAGMAs in the SQLite DSN so every dynamically opened
connection receives the same settings; executing a PRAGMA once through
`sql.DB` is not sufficient for a multi-connection pool. Long-lived read
transactions must be avoided because they can delay WAL checkpoints.
## Validation
The repository includes a transport smoke test:
- `pnpm smoke:desktop-transport`
Use it after changing local transport, listener setup, or state path derivation.
## Logging
`tuttid` default operational logging writes to:
- `<state-dir>/logs/tuttid.log`
See [Logging](./logging.md) for output mode and level rules.
## Rule Of Thumb
When adding a new local file path:
1. start from the shared state root
2. create a domain-specific subpath under that root
3. avoid writing new daemon-owned files directly under `$HOME`