feat(session): make Emacs restart non-lossy for parallel agent sessions - #485
Draft
tninja wants to merge 3 commits into
Draft
feat(session): make Emacs restart non-lossy for parallel agent sessions#485tninja wants to merge 3 commits into
tninja wants to merge 3 commits into
Conversation
added 3 commits
August 17, 2026 20:11
At 2-4 parallel worktrees, an Emacs restart used to lose the mapping from task to agent session: the task file asked the user to paste a session id by hand, and the dashboard had no way to tell a finished session from one it simply had no signal for. Three new modules split the three separable responsibilities: - ai-code-session-state.el derive terminal-neutral state - ai-code-session-store.el persist state in the task org file - ai-code-session-restore.el discover backend session ids and resume Emacs now discovers the backend session id from the backend's own session store and records it in the task file, so `J` / dashboard `R` can resume through the backend's native --resume. Reading a session store is strictly read-only: nothing under ~/.claude or ~/.codex is created, modified, or deleted. State is honest tri-state: OSC 133 signals report as `osc133`, output timing reports as `idle?`, and no signal renders as `unknown` / `-` rather than being passed off as idle. Existing session APIs, dashboard keys, column order, and task header order are unchanged; new columns and the `R` key are additive. No new package dependency.
…k file Session state recording shipped on by default and assumed one task file described one backend. Both were wrong. Recording writes to a task file, scans ~/.claude and ~/.codex, and publishes terminal output timing. Those are side effects outside the buffer the user is editing, so they are now gated behind `ai-code-session-tracking-enabled', which defaults to nil. The gate covers automatic writes only: the restore commands (`C-c a J' and the dashboard's `R') keep working, and the dashboard keeps its State and Signal columns, which already report `unknown' honestly when nothing is observed. Reading a backend store stays read-only either way. A task file is per task, not per backend, so claude and codex can now be driven from one file in one worktree. Before a session id exists an entry is identified by worktree *and* backend, so the second launch adds a record instead of overwriting the first, and discovering one backend's id upgrades only that entry. Restore prompts with `completing-read' when a file records more than one session and resumes directly when it records exactly one; the dashboard's `R' passes the row's backend so it never needs to ask.
…y worktree Two defects that code review of the opt-in change found. Turning tracking off froze the last output-timing observation instead of clearing it, because metadata merging only overwrites and never clears. The heuristic then aged that leftover timestamp past its delay and the dashboard reported `idle' -- a state no terminal ever observed. A signal nobody refreshes is not evidence, so the timing observation is now dropped while the flag is off and the dashboard says `unknown'. The Ghostel signal is deliberately kept: it reports observed command boundaries from hooks that stay live, so suppressing it would hide a true state rather than an inferred one. The state module stays flag-free; the policy lives with the caller that owns the flag. Requiring backend equality to match a worktree-only entry was too strict and regressed entries recorded before backends were written: an entry with no `:BACKEND:' could never be upgraded in place, so discovering its session id appended a duplicate record. A backend named on only one side now agrees with the unnamed side, while two named backends still must be equal, which is what keeps claude and codex in one worktree separate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When I run 2-4 agent sessions across worktrees, restarting Emacs used to lose the
task ↔ session mapping. The task file's
#+SESSION_ID:line literally asked me togo find the id by hand (
<Usually you can get the session id with /status ...>),and the dashboard couldn't distinguish "this session finished" from "I have no
signal from this terminal" — both looked idle, which made the dashboard actively
misleading at exactly the moment I needed it.
Approach
Three new modules, split along the three separable responsibilities rather than
growing
ai-code-session.el:ai-code-session-state.el(100 lines) — derive terminal-neutral stateai-code-session-store.el(306) — persist state in the task org fileai-code-session-restore.el(316) — discover backend session ids and resumeEmacs now reads the backend's own session store (Claude's
~/.claude/projects/<slug>/<uuid>.jsonl, Codex'srollout-*.jsonlsession_meta)to discover the session id and records it into the task file, so
Jfrom the menuor
Rin the dashboard resumes through the backend's native--resume. Persistencelives in the task file (
#+SESSION_ID:mirror + an* AI SessionsPROPERTIESsection) — no new sidecar state file. Reading a session store is strictly
read-only: nothing under
~/.claudeor~/.codexis created, modified, or deleted.State is honest tri-state instead of an optimistic guess: OSC 133 / OSC 9;4 signals
report provenance as
osc133, vterm/eat output timing reports asidle?, and nosignal at all renders as
unknown/-. "No signal" is never dressed up asidle.Note this stays an observation layer — no agent loop, compaction, context reset,
or subagent orchestration moves into Emacs. That harness belongs inside the agent.
Verification
One entry point reruns everything:
bash test/run_ai_code_session_state_gauntlet.sh(focused tests → full suite → byte compile → checkdoc → whitespace → real execution
→ mutation; non-zero exit on the first broken layer).
HEAD failure set, verified by building a real baseline in a detached
git worktreeand diffing the sorted namessleepprocess, real clock, realgit worktree list, a real Org file, and the live~/.claude/~/.codexstores. Thislayer earned its keep: it caught two defects all 30 synthetic scenarios missed —
backends are lazily
required, so resume hituser-error ("Backend claude cannot be resumed automatically")in a freshly started Emacs (the exact situation restoreexists for); and a failed id lookup rescanned the store on every chunk of
streaming output (measured 0.076 s per miss). Both fixed in product code via
RED→GREEN, each pinned by a mutant.
(whole-package count identical to HEAD's 1, same location)
advicetripwire on 10 mutating primitives.To avoid a guard that can never fire counting as evidence, I deliberately made it
fire once with a probe file, confirmed it was caught, then removed it — no residue.
Existing session APIs, dashboard keys, column meaning/order, and task-header order
are unchanged; the new columns and
Rare additive. No new package dependency.Full evidence report, including the honest baseline correction (32 → 8 under this
entry point) and remaining risks, is in
docs/plans/2026-08-17-session-state-evidence.md.Reviewer notes / test plan
package-lintwas not run locally (not installed here) — relying on themelpazoid CI workflow for that layer
but not fixed here either
(unknown backend / malformed file →
nil), so an upstream format change fallsback to the portable agent handoff rather than breaking
ai-code-backends-infra-session-uuid-retry-intervaldefaults to 10s — worth asanity check that this feels right in practice