Storage ownership
Who owns which database, why PlanToCode never opens a Codex file itself, how a transcript source is identified, and the atomic identity claim behind the outbox.
Checked against the source on 17 September 2026
On this page
Separate the stores by owner
- PlanToCode datacodex-command-outputs/ · pasted-images/ · appdata.db
The desktop runtime opens these itself: SQLx for the database, plain file reads and writes for command output and chat attachments.
- Codex homecodex/profiles/<uuid>/home/
PlanToCode creates the folder, launches Codex with it as CODEX_HOME, and deletes the whole profile folder when you remove the profile, unless a session still links a rollout inside it. It never opens a file inside. History comes back through thread/items/list.
- Startup guard--allowed-storage-root = CODEX_HOME = sqlite_home
The bundled app-server refuses to start unless all three resolve to the same folder, so Codex can’t keep its databases anywhere else.
- Off this computerPostgreSQL · Redis
The server keeps accounts, devices, credits, and usage in PostgreSQL, and login handoffs, rate limits, and charge reservations in Redis. What the relay forwards isn’t stored.
| Store | Owner and content | Access rule |
|---|---|---|
| PlanToCode appdata.db | Desktop product metadata, session links, outbox operations, remote idempotency, and live overlay state. | PlanToCode Rust repositories use SQLx and SQLite. |
| App-owned Codex profile home | The Codex child owns its private databases, auth/config, and normal owned session storage. | Use the app-server protocol. PlanToCode must not query the child’s private SQLite schema. |
| An explicitly selected conversation source | The exact linked transcript path and thread identity define the source. | Access the selected source through path-aware app-server methods. Do not search other homes for a replacement. |
| Server PostgreSQL | Account and service data controlled by server repositories and database roles. | Separate migration, system-runtime, and tenant-runtime principals. |
| Phone state | View state, local copies of unsent messages, and preview and output caches. | Treat the desktop as authority for projects, execution, outbox, and files. |
The WebView asset protocol is scoped to media previews under the app cache only, so no private runtime file is reachable from the page.
Storage identity and execution identity differ
An app-owned Codex profile has a stable opaque UUID that must be canonical lowercase text; an email address and a filesystem path are not profile identity. codex_session_links keys the link by app session and carries profile_id, codex_thread_id, a nullable rollout_path, and a status of available, profile_required, or unavailable, with a partial unique index that allows exactly one available link per thread and profile. Indexed and Exact are design names; in code one predicate decides: a link whose rollout path lies outside the runtime home is an external exact source, reads pass path and historySources only when a rollout path is supplied, and the wire storageFormat stays indexed either way. Both routes call thread/items/list on the Codex app-server, and an exact read adds path and historySources. An operation resolves its route once and keeps it.
Exact source access has deliberate limits
- historySources[{ rolloutId, path }]
Built by the desktop from the absolute paths of the session’s other linked rollouts, which it records as Codex announces each child. It’s the only place the app-server looks for an ancestor.
- history_basethread_id · end_byte_offset
Session metadata at the top of the child’s file: the ancestor rollout and the byte where the inherited part ends. The app-server follows up to 256 ancestors and rejects a cycle or an ancestor shorter than that offset.
- reloadRequirederror data.code
The cursor carries a digest of every byte it covered. Appending keeps it valid. Changing those bytes fails the next page, which the desktop shows as an ordinary load error. Only sourceUnavailable offers to choose the source file again.
| Condition | Contract |
|---|---|
| Required ancestor source is absent | The app-server fails the read with missingHistorySource, even if the file sits next to the child. The desktop shows a generic load error. Only sourceUnavailable becomes CODEX_SOURCE_ERROR, which offers to choose the source file again. |
| Source is replaced or truncated | The app-server binds each cursor to a digest of the bytes it read. Appending keeps cursors valid. Truncating or rewriting fails the next page with reloadRequired, shown as a generic load error. “Timeline source changed; reload the timeline” is the desktop’s error when the linked source itself changed. |
| Current method takes only a thread ID | Do not offer that mutation for an exact external source if it cannot address the selected path. This applies to current goal, rollback, and persisted thread-settings operations. |
| Another bundled sidecar owns the writer | Release thread ownership before a different runtime resumes it. |
Before a send or an attach, the desktop releases the thread from every other profile server it runs. That coordinates the bundled sidecars of this process; it does not prevent an unrelated external Codex process from writing the same source, so stop an external writer before continuing that conversation in PlanToCode.
The outbox uses an atomic identity claim
workspace_chat_outbox
session_id → entries_json, updated_at
workspace_chat_outbox_operation_identity
(session_id, operation_id) → request_fingerprint
remote_rpc_idempotency
scope_key → request_fingerprint, state, response_json, lease, expiry
codex_live_timeline_overlays
timeline target → persisted overlay snapshot
codex_live_timeline_overlay_revisions
singleton → one global revision sequence
codex_chat_operation_ledger
(app_session_id, operation_id) → status, result_jsonsave_workspace_chat_outbox_with_operation_identity opens one transaction. Its INSERT … ON CONFLICT updates the identity row only when the existing fingerprint equals the submitted fingerprint. If that statement does not affect one row, the transaction rolls back. It then upserts the outbox entries JSON and commits. The identity row is keyed by the base session while the outbox JSON is saved under the alias session, so the same operation can be found from any alias in the scope.
The remote_rpc_idempotency table is the second ledger. It keeps the outcome of every listed remote mutation for 48 hours across desktop restarts, so a phone that retries after the desktop came back still gets the stored answer.
Project file access is a separate boundary
Remote file reads run with the desktop user’s filesystem access. resolve_desktop_file_read_path expands a home shortcut, joins a relative path onto the canonical project directory, and canonicalizes the result; it does not enforce project containment, and the design note says so on purpose. allowExternalFile and allowExternalMedia only decide which resolver is tried first, so an authenticated companion can open paths outside the selected project either way. ensure_path_within_project, which rejects parent components and checks the canonical root, exists for desktop-local operations.
App-managed chat attachments live in pasted-images under the app data directory, and an external media read for an attachment must classify as image, video, audio, or document. The phone never uses its own home directory.