Skip to content

Latest commit

 

History

History
126 lines (114 loc) · 13.3 KB

state-file.md

File metadata and controls

126 lines (114 loc) · 13.3 KB

The state file — sessions.tsv, line by line

What agliteterm writes to restore itself from, field by field, so a file can be read, hand-edited or seeded by a test without reading saveSessionState() / parseStateFile() in src/main.cpp. Session restore is the user-facing half (where the file is, the .bak generation, how to recover by hand); this is the format.

Path: %LOCALAPPDATA%\agliteterm\sessions.tsv for the default instance, sessions-<instance>.tsv for a --pipe <instance> window. One file per window process. Tab-separated UTF-8 text, \n line ends (\r is tolerated on read), no BOM. A field never contains a tab, a newline or a carriage return. Ordinary line types replace each with a space (tsvField). P9's R/B and P10b's K2 command fields instead use JSON string-content escapes, without the surrounding quotes: \\, \", \t, \r, \n, and \u00XX preserve the command exactly. An empty command field means none. Malformed escaped command fields are dropped with a warning. C still uses line absence for none.

Line types, in the order they are written

Line Fields Meaning
V1 — The header. Always first. The number has never changed: the format grows by adding line types, not by bumping it.
W name One workspace, in tree order. The first W is workspace 0.
S ws name app cwd arg… One visible session, in tree order. ws indexes the W lines; name is empty when the session was never named (it shows as session N); app, cwd and the args are the launch spec — what is relaunched, never what was running inside. cwd is the shell's live directory when it was readable and fits the pty-host's 260-byte field, the creation directory otherwise.
F i… The indices, among the S lines, of the flagged sessions. Absent when none.
E i… The indices, among the S lines, of sessions whose empty argument list is exact: a session new --command-mode direct launch of a program with no arguments (#79). Without it an empty argument list means "no arguments given", and a PowerShell app relaunches with its usual -NoLogo -NoExit -EncodedCommand wrapper. A non-empty argument list is always relaunched exactly, so it is never listed. Absent when none; older builds skip it.
D id… The pty-host id of every S line's shell (the session's paneId), in order. Lets a relaunch after a kill adopt a shell the host still holds instead of starting a new one. It is the session id too, except for a session promoted after its own shell closed (P4): that one kept the closed shell's id as its session id, and the adopting relaunch brings it back under its shell's id — the file records shells, not promotions. Absent in files from before 0.17.x builds that wrote it; the count must equal the S count or the whole line is ignored.
C i text The context of the session at S index i (parity batch P3, session context). One line per session that has one; no line for a session without one, because an empty field could not tell "no context" from a context that is empty. Written after D, before P.
P owner app cwd arg… The split shell of the session at S index owner: its own launch spec. One line per session that has a split.
L owner axis order The layout of the split of the session at S index owner (parity batch P4): axis is vertical (left/right panes) or horizontal (top/bottom panes) — the arrangement of the panes, never the divider, case-sensitive; order is 0 when the session's own shell sits in slot 0 (left/top) or 1 after a session swap put it in slot 1. Written only when the layout is not the default (horizontal, or swapped, or both), so a vertical unswapped split writes the exact bytes 0.17.14 wrote, and only for a session whose P line exists — the layout describes the pair. Written after the P lines, before K, by the same convention as K. Downgrade: an older build ignores L (unknown line types are ignored) and restores the split in the default layout — the P line is untouched, so a downgrade loses the layout, not the split — and drops the line on its next save. On load each field is validated on its own: an axis other than the two words restores vertical, an order other than 0 / 1 restores 0, each named in the log; an L for an owner with no P line is dropped and named.
G owner ratio P9 split slot 0's share, written with three decimals only when not 0.5, with the layout lines. Valid range 0.05..0.95; a bad value restores 0.5 with a warning. Requires a valid split. A swap preserves slot shares.
K i pane0 pane1 The captured commands of the session at S index i (parity batch P3, restore capture): pane0 is the slot of the session's own shell, pane1 the slot of its split shell (empty when there is no split, or nothing was captured there). An empty field is "none"; a slot is a plain string with no rules of its own, so unlike C one line carries both panes. One line per session with at least one slot. The fields are by ROLE, not by position on screen: pane0 is always the session's own shell and pane1 always the split shell, whatever slot each sits in — the L line carries the order, K does not repeat it. After a promotion (the session's own shell closed, the split shell became the session) the survivor's slot is pane0, because it is the session's own shell now. Written after the P lines by convention — K sits with the P lines it describes — not by requirement: the reader collects every line type in file order and applies them in its own fixed order, so a K above a P restores identically. A slot is a checkpoint a caller reads back (tree --json, capturedCommands); P10b may replay it only when restore-commands is enabled (default false). The writers are restore capture and, when restore-commands is enabled, the close itself: OnDestroy captures every real pane while its shell is still alive, before the final save, so the checkpoint describes the moment the window went away (agwinterm's SaveState(captureCommands: true)). Malformed owner indices are refused, never coerced to zero.
K2 i pane0 pane1 P10b lossless captured slots, with the same roles and owner guards as K. Written instead of K when either capture contains a tab, CR or LF; both fields then use the R/B JSON-content codec. Older builds ignore K2 and drop those captures on their next save; ordinary captures retain legacy K.
R i pane0 pane1 Explicit restore pins, by ROLE like K, using escaped command fields. Written after K when either shell has a pin. Tree reads them as restoreCommands, keyed by pane id.
B i agent0 agent1 Replay bindings, by ROLE like R; binding wins over pin. Escaped command fields preserve case and command bytes; not exposed in the tree.
A ws The active workspace. Clamped on load to the workspaces the file has.
O ws The focused workspace. Read when present; this build does not write it.

A file written by the current build, one workspace, two sessions, the first with a context, a split stacked top/bottom and swapped (its own shell in the bottom slot), and a captured command in each pane:

V1
W	main
S	0	build	pwsh.exe	C:\src\app
S	0		pwsh.exe	C:\src
F	0
D	a1b2c3d4-…	e5f6a7b8-…
C	0	reviewing the P3 diff
P	0	pwsh.exe	C:\src\app\tests
L	0	horizontal	1
K	0	npm test	ping -n 3 127.0.0.1
A	0

The same file with the split left/right and unswapped has no L line at all — byte for byte what the build before P4 wrote.

The rules the reader applies

P9 R/B fields follow K's positional guards: a malformed S set drops them; a missing rebuilt split drops its pane-1 field, not the owner's field. G follows L's split/owner guards. Older builds ignore R/B/G and restore without pins, bindings or custom ratios; their next save drops those lines. No migration changes older line types. Read-only and search state are not saved.

Only a fresh restored shell receives replay: one 2500 ms timer resolves each queued pane id to its current binding, otherwise its current pin, otherwise its captured K/K2 slot only when restore-commands is enabled, then writes outside the state lock. It is a delay, not a readiness guarantee. Adopted/exited/gone/read-only panes are skipped. Changes before the timer fires are honored. Captured replay defaults off; explicit pins and bindings do not depend on that setting.

  • Unknown line types are ignored, and the lines it knows are honoured. That is what lets an older build read a newer file (it restores the sessions and drops what it cannot carry — and writes the file back without those lines on its next save, so a downgrade loses contexts, splits, slots and layouts, not sessions; a build before P4 reads an L-carrying file and restores the split left/right, unswapped) and a newer build read an older one (a file from before P3 has no C or K, one from before P4 no L, and each restores exactly as it did). Nothing migrates in either direction.
  • A malformed S line still counts. C, P, L, K and D index sessions by position among the S lines, so a session line the reader could not parse shifts every index after it. Rather than hang a context, a split, a layout or a slot on the wrong session, each of those sets is refused wholesale when the number of S lines differs from the number that parsed, with one logWarn naming both counts (state: 2 session line(s) but 1 parsed - refusing 1 context line(s) rather than attaching them to the wrong sessions). Every parseable session still restores.
  • An L line needs its pair. The layout is applied onto the split the matching P line rebuilds; an L whose owner has no P line (state: layout line for session index 0 has no split (P) line to describe - dropped), or whose split failed to start (restore: layout for session 'x' dropped - its split was not restored), is dropped and named. A bad axis word or order digit loses that one field to its default (vertical / 0), named in the log, never the line.
  • An index past the S list (a hand-shortened file) drops that one line, named in the log; the sessions it does not name are untouched.
  • A loaded context goes through the verb's own rules — trimmed, not blank, no control character, at most 200 characters. One that fails is dropped on load with one logWarn naming the session and the refusal (restore: context for session 'x' dropped - session context: control character U+0001 at offset 3 …); the session restores without it and nothing is drawn.
  • A K slot lands on the session the restore creates: pane0 on the session, pane1 on the split the matching P line rebuilds. When that split did not come back — the split failed to start, the owner session itself failed to start (a dead entry gets no split, so its P line is never attempted), or the K line names a split the file has no P line for (a hand-edited or downgrade-written file) — the pane1 slot has no pane to belong to and is dropped with a logWarn naming the session. (A P set refused by the count guard takes the K set with it — the same comparison — so that case never reaches the per-session drop.) A session whose app failed to start keeps its pane0 slot the way it keeps its name, and re-saves both.
  • .tmp, then rename, one .bak. The save writes sessions.tsv.tmp, rotates the current file to sessions.tsv.bak and renames the temp over the target. A zero-session save over a populated file is refused (the log says so); the one legitimate empty is the user closing the last session. Any thread may save — a dedicated saver thread that every tree change wakes (it lets a burst settle for 200 ms and saves once), the UI thread at quit, the control-pipe thread after restore capture so the verb's reply describes a file that already exists — serialised by one lock around the write and the rename. A tree change never writes the file on the UI thread: one rename held by an endpoint-security filter used to hang the window, keystrokes included, for as long as the filter took. The verbs whose reply claims durability (session resize, session restore / bind, restore capture, workspace move, restore clear, claude adopt / restart) still save synchronously, some on the UI thread, so one of those calls can still wait on the filter — a click or a keystroke cannot. A save whose bytes equal the last published ones is skipped while that file is still on disk (the skip advances the stamp fence exactly as a write would), so the log's save ok lines record changes, not repaints; a missing primary is written again from the same bytes.
  • Restore order: sessions.tsv, then .bak when the primary is missing, empty or parses to no sessions, then a fresh window.

test/restore-matrix.ps1 seeds these files byte for byte and asserts the branches above (a control character in a C line, a C/P/L/K count mismatch, a stray index, a pre-P3 file, an L with a bad axis word or order, an L without its P, a default split writing no L, a truncated write, a .bak fallback, a future line type) and restarts a horizontal and a swapped split, gracefully and killed; test/control-honesty.ps1 reads the C and K lines back after each verb.