DSH storage layout reference
What DSH writes to disk, measured on a real install (0.2.1-rc.2). The payload, the list entry and the title are three separate things on disk. Treat them as one and you get an empty-looking session list while every file is still there.
Measured 2026-11-13 on DSH 0.2.1-rc.2, Windows.
Session formats
| v4 | legacy | |
|---|---|---|
| File name | session.v4.jsonl.zstd | session.jsonl.zstd |
| Produced by | DSH 0.2.x web / CLI / TUI | Older desktop runtime |
| Frame layout | Multi-frame, append-only | Multi-frame, append-only |
| Coexistence | A single session folder can hold both files | |
| Largest observed | 22,832,359 bytes in one file | 21,360,396 bytes in one file |
Both formats are streams of independently decompressible zstd frames, appended as the session grows. Tools should treat them as opaque bytes.
Directory layout
The workspace directory name is a slug of the absolute workspace path, not the workspace id. Two machines with different paths get different directory names for the same workspace.
~/.dsh DSH data root (web / CLI / TUI)
├─ sessions/
│ └─ --C-Users-you-my~0020project--/ slug of "C:\Users\you\my project"
│ └─ session-<uuid>/ one folder per session, name used verbatim
│ ├─ session.v4.jsonl.zstd
│ └─ session.jsonl.zstd (legacy, may coexist)
└─ storages/
├─ workspace.json workspaces + session list
├─ session_projcache.json titles + per-turn stats (aggregate)
├─ session_projcache/sessions/<id>.json same data, per session
└─ schedule.json device-local reminders (do not sync)Path slug rule
Derived from the absolute path: drop the colon, turn backslashes into hyphens, encode spaces as ~0020, wrap in double hyphens. C:\Users\you\my project → --C-Users-you-my~0020project--
workspace.json (unit version 2)
This is the file that decides whether a session appears in the list. Syncing payloads without it makes sessions invisible.
| Field | Meaning | Merge rule |
|---|---|---|
global.workspaceIds | Registered workspaces | Union |
global.archivedSessionIds | Hidden sessions | Union (idempotent) |
global.pinnedSessionIds | Pinned sessions | Union |
tables.workspaces.<id>.path | Absolute path on this machine | Keep local; never take the remote value |
tables.workspaces.<id>.title | Display name | Local if set |
tables.workspaces.<id>.sessionIds | Ordered session list | Union |
tables.workspaces.<id>.createdAt / updatedAt | Timestamps | min / max |
session_projcache.json (unit version 3)
Titles, turn counts and token usage live here, not in workspace.json. DSH keeps an aggregate table and a per-session file for the same data; both count as “has a title”. Rows carry {ver, seq, val}, so merging compares ver first, then seq.
| Field | Meaning |
|---|---|
tables.sessions.<id>.identity.cwd | Absolute path of the session workspace (authoritative for mapping sessions to workspaces) |
tables.sessions.<id>.rows.title | Display title; null means untitled |
tables.sessions.<id>.rows.sessionStats | turns, steps, llmMs, decodeTokens… |
tables.sessions.<id>.rows.tokenUsage | Cumulative and last-turn token buckets |
Symptom → cause
| Symptom | Cause |
|---|---|
| Session file exists, DSH does not list it | The session id is missing from tables.workspaces.<id>.sessionIds |
| Session listed but untitled | No projcache row (aggregate or per-session) for that id |
| Entry opens into nothing | Listed id has no payload folder on disk |
| Session shows an old state | Two formats coexist; DSH reads one, the other went stale |
| Archived session still takes space | Archiving hides, it does not delete |
| A sync “worked” but the list is empty | storages/ was not synced with sessions/ |
How to check your own install
The Session Index Doctor runs these checks locally: pick your DSH data root, get the mismatched ids and the one-line fix for each.