유대선
프로젝트로
·기술 회고·5 ·리뷰 필요

Startup ≠ pane ≠ tmux session: the three layers behind 'same app, different data'

Running the dev build (tauri dev) next to the installed app showed a completely different startup list — same code, different data. Untangling that confusion surfaces DalkkakAI's actual layering: a startup is a frontend localStorage group, a pane is one tmux session, and there is exactly one shared tmux server. Frontend state is per-origin (separate between dev and prod); the tmux backend is shared. That asymmetry is the whole story — and it's why the orphan-session GC must run in prod only.

AI 버전

The confusion that started it

While dogfooding, I ran the dev build (pnpm tauri dev) next to the installed app to test a change. The dev window opened with a completely different startup listdefault / TEst / dafa / test / da instead of my real Dalkkak / ki-clash / OverDrive / …. Same program, launched the same way, yet different data. Naturally: "is it broken? did something get wiped? is each startup a separate tmux session?"

None of those. The honest answer is that "what you see" is assembled from three independent layers, and the confusion comes from collapsing them into one. Here they are.

Layer 1 — where the UI loads from (origin)

The app is a Tauri (WebKit/WKWebView) window rendering a React UI. Where that UI is loaded from differs by build:

  • Dev (tauri dev) serves the UI from a live Vite dev server: http://localhost:1430 (so edits hot-reload). tauri.conf.json"devUrl": "http://localhost:1430".
  • Prod (installed .app) has the UI compiled into files inside the bundle (frontendDist: "../dist") and loads them over Tauri's asset protocol. No port, no server.

So "the dev build has a port" is just a dev-tooling detail — it is not an app feature, and it has nothing to do with tmux. But it has one big consequence, which is Layer 3.

Layer 2 — the tmux backend (one server, one session per pane)

Every terminal pane is backed by tmux, and the structure is deliberately simple:

tmux server   =  exactly ONE, dedicated:  tmux -L dalkkak
   └─ pane (terminal) 1  =  one tmux session:  dalkkak-<paneId>
   └─ pane (terminal) 2  =  one tmux session:  dalkkak-<paneId>
   ...
  • A dedicated -L dalkkak server (not the user's default tmux), so DalkkakAI's shells are isolated and run in the app's own TCC context. (See the earlier post-mortem on why a GUI app embedding tmux must use its own -L <name> server.)
  • One tmux session per pane, named dalkkak-<paneId>. The pane id is a random slug minted in the renderer.

Crucially, the server is keyed only by its -L name — so dev and prod share the same -L dalkkak server. Both builds' panes live side by side on it. (Measured: 38 sessions on that server at one point, mixing dev panes and prod panes.)

Layer 3 — what a "startup" actually is

This is the one that trips people up:

A startup is NOT a tmux session. A startup is a frontend grouping stored in localStorage — a saved layout (a tree of panes) plus its name/emoji.

startup "dafa"   ── stored in localStorage (key: dalkkak.startups.v1 + a per-startup layout)
   ├─ pane tyqi  →  tmux session dalkkak-…-tyqi
   └─ pane dqmf  →  tmux session dalkkak-…-dqmf

So one startup contains N panes, and each pane is the thing that maps to a tmux session. The startup itself doesn't exist in tmux at all — it's purely a UI/organisation concept. Opening a startup mounts its layout, and each pane then spawns (or re-attaches to) its dalkkak-<paneId> session.

The asymmetry that explains everything

Put the two storage facts next to each other:

WhatStored whereDev vs prod
Startups + layouts (the sidebar)frontend localStorage — and localStorage is per-originseparate (dev = localhost:1430, prod = bundle origin)
Panes / tmux sessionsthe -L dalkkak servershared (one server, both builds)

That's the whole "mystery." The startup list differs between dev and prod only because localStorage is scoped to the origin the UI loaded from — and dev (localhost:1430) and prod (the bundled protocol) are different origins, so they keep different stores. It has nothing to do with tmux; in fact the tmux sessions are shared. Same code, different notebook — like opening the same website in Chrome vs Safari and finding different saved logins.

This is also reassuring: the dev instance is a sandbox for the real app's data. Testing in dev can't touch your prod startups, because they live in a different store entirely.

Why this matters: the GC footgun

We added a garbage collector that reaps orphaned tmux sessions (detached + not referenced by any saved layout — leftovers from past app quits, since the app intentionally doesn't kill sessions on quit so they survive a relaunch). The "not referenced by any saved layout" check reads the startup layouts from localStorage.

The asymmetry makes this dangerous across instances: the dev build's localStorage is separate (and often near-empty), but the tmux server is shared. A GC run from dev would see prod's detached-but-recoverable sessions as "not in my layouts" and could reap them. The fix follows directly from the layering: only the prod build runs the GC (it owns the real localStorage), and the GC additionally skips any attached session (open in any running instance). Layer the guards the same way the system is layered.

The one-line model

  • Port = how the UI is loaded (dev server vs bundle). Frontend-only. Not tmux.
  • tmux session = one per pane, on a single shared -L dalkkak server.
  • Startup = a localStorage group of panes (UI), per-origin — not a session.

Frontend state is per-origin and separate; the tmux backend is one shared server. When a tool hosts other processes, knowing which state is shared and which is partitioned isn't trivia — it's exactly what decides whether a "cleanup" is safe.

리뷰 필요

내 시각이 아직 안 들어간 entry.