xa¶
xa — tools for managing remote Claude Code sessions.
Functions
|
Encode a cwd into the |
|
Yield entries from |
|
Yield all live ephemeral session dicts. |
|
Yield slugs (directory names) under |
|
Yield every event from a transcript JSONL. |
|
Yield transcript JSONL paths, optionally restricted to one project. |
|
Decode a |
|
Read the per-process session file for |
|
Extract postmortem-relevant facts from the tail of a transcript. |
|
Summarise a transcript without holding it all in memory. |
|
Return the transcript path for |
|
Return |
|
Launch |
|
Create a detached tmux session running |
|
Find one session by full ID or unique prefix. |
|
Legacy local-only iterator. |
|
Kill a live |
|
Return sessions sorted by recency (live first, then newest-modified). |
|
Resume a transcript / archived session on its originating host. |
|
The event log at |
|
Per-session pane logs at |
|
|
|
|
|
Mark an archived session as hidden (or un-hide it). |
|
Set or clear a user-supplied display label for a session. |
|
|
|
Pick the most specific death reason from available signals. |
|
Fold |
|
Emit |
|
Return per-session summaries, newest-first (by creation time). |
The out-of-the-box registry: one local host, nothing else. |
|
|
|
|
Return |
|
Shortcut: just the host registry. |
Classes
|
One line from |
|
Postmortem facts extracted by walking a transcript from the end. |
|
Summary of a transcript JSONL, cheap enough to compute for listings. |
|
What |
|
Canonical session record — valid across hosts and states. |
|
Directory of files, accessed by key. |
|
Append-only JSONL file with an |
|
|
|
Client for a remote |
|
Duck-typed interface every transport satisfies. |
|
The machine |
|
Transcripts via rsync; actions via ssh exec. |
|
- class xa.ArchiveRecord(id, name, cwd, created, url, gone, gone_detected, gone_reason, pane_log_bytes, claude_session_id, forensics, label=None, hidden=False)[source]¶
Bases:
object
- class xa.FileStore(root, *, suffix='')[source]¶
Bases:
objectDirectory of files, accessed by key.
Values are
bytes. Keys must match[A-Za-z0-9_.-]+— this rejects path traversal attempts like../etc/passwd.
- class xa.HTTPHost(name, *, base_url, auth=None, username=None, password=None, token=None, timeout=30.0)[source]¶
Bases:
objectClient for a remote
xa serveinstance.
- class xa.HistoryEntry(cwd, project, display, pasted_contents)[source]¶
Bases:
objectOne line from
~/.claude/history.jsonl.
- class xa.Host(*args, **kwargs)[source]¶
Bases:
ProtocolDuck-typed interface every transport satisfies.
- class xa.JsonLinesStore(path)[source]¶
Bases:
objectAppend-only JSONL file with an
Iterablereader.>>> import tempfile, pathlib >>> with tempfile.TemporaryDirectory() as td: ... s = JsonLinesStore(pathlib.Path(td) / 'log.jsonl') ... s.append({'a': 1}) ... s.append({'b': 2}) ... list(s) == [{'a': 1}, {'b': 2}] True
- class xa.LocalHost(name='local', *, claude_home=PosixPath('/home/runner/.claude'), claude_bin='claude', tmux_bin='tmux', alive_predicate=None)[source]¶
Bases:
objectThe machine
xais running on.
- class xa.SSHHost(name, *, host, user=None, remote_claude_home='~/.claude', cache_dir=PosixPath('/home/runner/.cache/xa/remotes'), stale_threshold_sec=3600, claude_bin='claude', tmux_bin='tmux', ssh_bin='ssh', rsync_bin='rsync')[source]¶
Bases:
objectTranscripts via rsync; actions via ssh exec.
- Parameters:
name (
str) – Logical name used inSession.hostand elsewhere.host (
str) – SSH hostname / alias. Matches cc-sessions: either an~/.ssh/configalias (devbox) or a raw host (1.2.3.4).remote_claude_home (
str) – Remote path to~/.claude/. Defaults to~/.claudewhich resolves relative to the SSH user’s home on the remote side.cache_dir (
Path) – Where to stage the rsync’d tree locally.stale_threshold_sec (
int) – Re-sync when the cached copy is older than this.0means always sync.
- class xa.Session(id, claude_session_id, bridge_session_id, host, cwd, project_slug, state, live_pid, tmux_name, name, summary, first_user_message, turn_count, forked_from, created, modified, url, url_source, transcript_path, pre_first_turn=False, attention=None, attention_hint=None, tmux_pane=None)[source]¶
Bases:
objectCanonical session record — valid across hosts and states.
Fields that don’t apply to a given state are
None. Discovery functions return fresh instances; callers should treat it as immutable.
- class xa.Settings(cache_dir, stale_threshold_sec=3600, claude_bin='claude', tmux_bin='tmux')[source]¶
Bases:
object
- class xa.SpawnResult(name, cwd, claude_pid, claude_session_id, bridge_session_id, url, url_source, warning, attention=None)[source]¶
Bases:
objectWhat
spawn_session/resume_sessionreturn to the caller.
- class xa.TranscriptForensics(transcript_path, line_count, last_tool_name, last_tool_command, last_tool_exit_code, last_tool_result_tail, final_assistant_text, user_interrupted)[source]¶
Bases:
objectPostmortem facts extracted by walking a transcript from the end.
user_interruptedis the raw marker presence — it is ambiguous (also fires on phone-standby bridge drops), so do not derive user intent from it alone. Callers should corroborate against pane-log tails.
- class xa.TranscriptMeta(path, session_id, cwd, project_slug, summary, custom_title, first_user_message, turn_count, forked_from, created, modified, size_bytes)[source]¶
Bases:
objectSummary of a transcript JSONL, cheap enough to compute for listings.
Mark an archived session as hidden (or un-hide it).
- Return type:
- xa.append_label(events, *, id, label)[source]¶
Set or clear a user-supplied display label for a session.
idcan be an archive id, a tmux session name, or aclaude_session_id— whatever key the caller will use to look it up later. Empty-string orNonelabel clears any prior label.- Return type:
- xa.classify_death(pane_kind, *, replaced=False, forensics=None, oom_markers=())[source]¶
Pick the most specific death reason from available signals.
oom_markersis the tuple of OOM-shaped strings observed in the pane tail (see_OOM_PANE_MARKERS). When the last tool exited with 137 and at least one marker is present, we promote the verdict tooom_killed— that pair is the strongest single signal we can get without reading kernel logs (which would need root and is not portable).- Return type:
Literal['clean_exit','abrupt','interrupted','tool_crash','oom_killed','replaced','missing']
- xa.default_events_store(state_dir=PosixPath('/home/runner/.xa'))[source]¶
The event log at
<state_dir>/events.jsonl.- Return type:
- xa.default_pane_store(state_dir=PosixPath('/home/runner/.xa'))[source]¶
Per-session pane logs at
<state_dir>/panes/<id>.log.- Return type:
- xa.encode_project_slug(cwd)[source]¶
Encode a cwd into the
~/.claude/projects/slug form.- Return type:
>>> encode_project_slug('/root/py/proj/tt/glossa') '-root-py-proj-tt-glossa' >>> encode_project_slug('/') '-'
- xa.get_session(session_id, *, hosts=None, claude_home=None)[source]¶
Find one session by full ID or unique prefix.
Raises
LookupErrorif the prefix is ambiguous.
- xa.history_iter(*, claude_home=PosixPath('/home/runner/.claude'))[source]¶
Yield entries from
~/.claude/history.jsonlin file order (oldest first).Useful for cross-project full-text prompt search without loading every transcript.
- Return type:
- xa.iter_ephemeral_sessions(*, claude_home=PosixPath('/home/runner/.claude'))[source]¶
Yield all live ephemeral session dicts.
A session file disappears when claude exits, so this reflects momentary state only. NB: claude deletes the file on clean exit only — a crash / SIGKILL / reboot leaves a stale file behind, so callers who care about actual liveness must validate each dict with
ephemeral_session_alive()(or an equivalent predicate).
- xa.iter_local_sessions(*, claude_home=PosixPath('/home/runner/.claude'), project_slug=None, include_live=True, tmux_bin=None)[source]¶
Legacy local-only iterator.
Retained for backward compatibility and because some tests construct sessions directly from a fake
~/.claude/. Wraps a freshLocalHost.
- xa.iter_project_slugs(*, claude_home=PosixPath('/home/runner/.claude'))[source]¶
Yield slugs (directory names) under
~/.claude/projects/.
- xa.iter_transcript_events(path)[source]¶
Yield every event from a transcript JSONL.
Lines that fail to parse are silently skipped — transcripts are append-only on a live process, so the tail can be partial.
- xa.iter_transcript_files(*, claude_home=PosixPath('/home/runner/.claude'), project_slug=None)[source]¶
Yield transcript JSONL paths, optionally restricted to one project.
Only files whose stem is a valid UUID are yielded — this filters out the
memory/subfolder and other non-session artefacts Claude Code stores alongside transcripts.
- xa.kill_session(session, *, hosts=None)[source]¶
Kill a live
Session, scoped to what actually belongs to it.Kills the backing tmux session only when it is dedicated to this claude (single pane, claude verified in it) — a claude living in one window of a shared multi-window workspace must not take the whole workspace down, so there we SIGTERM the identity-verified pid instead. Remote hosts keep the name-kill contract (their server applies its own scoping). Returns a short description of what was killed (e.g.
"tmux:mysess"/"pid:1234").- Return type:
- xa.list_sessions(*, hosts=None, project=None, include_forks=True, include_live=True, state=None, limit=None, claude_home=None, tmux_bin=None)[source]¶
Return sessions sorted by recency (live first, then newest-modified).
When
hostsisNonewe default to a singleLocalHost. Ifclaude_homeortmux_binare given, they’re forwarded into that defaultLocalHost— handy for fixture-based tests.Filters:
project— substring match againstcwd(case-insensitive)include_forks=False— drop sessions with aforked_frominclude_live=False— skip live discovery (per host)state— one of"live"/"archived"/"transcript_only"
- xa.load(path=None)[source]¶
Return
(settings, hosts_registry)from a config file.Missing file →
(defaults, {"local": LocalHost()}).
- xa.overlays(events)[source]¶
Fold
labeled/hiddenevents into{id: {label, hidden}}.Later events win. Useful when rendering live sessions too — callers look up by whatever id they know (archive id, claude_session_id, tmux name) and apply the overlay if present.
- xa.parse_project_slug(slug)[source]¶
Decode a
~/.claude/projects/slug back to a cwd.- Return type:
>>> parse_project_slug('-root-py-proj-tt-glossa') '/root/py/proj/tt/glossa' >>> parse_project_slug('-Users-thorwhalen-tw-server') '/Users/thorwhalen/tw/server'
NB: This transform is lossy — a cwd with literal
-in segment names round-trips to a different slug. Claude Code itself has the same limitation, so we accept it.
- xa.read_ephemeral_session(pid, *, claude_home=PosixPath('/home/runner/.claude'))[source]¶
Read the per-process session file for
pidif present.
- xa.reconcile(events, panes, live_sessions, *, claude_home=PosixPath('/home/runner/.claude'))[source]¶
Emit
goneevents for archived sessions missing fromlive_sessions.Returns the list of freshly-emitted events (handy for tests). Idempotent: calling twice with the same live list produces no new events the second time.
- xa.records(events, panes)[source]¶
Return per-session summaries, newest-first (by creation time).
- Return type:
- xa.resolve_bridge_url(session_name, *, claude_home=PosixPath('/home/runner/.claude'), tmux_bin='tmux', scrape_lines=400)[source]¶
Return
(url, source)for a live tmux-hosted claude session.Primary path: find the claude descendant pid → read
~/.claude/sessions/<pid>.json→ takebridgeSessionId. Fallback: regex-scrapecapture-paneoutput for the full URL.
- xa.resume(session, *, cwd=None, name=None, hosts=None, **opts)[source]¶
Resume a transcript / archived session on its originating host.
- Return type:
- xa.resume_session(claude_session_id, *, cwd, name=None, claude_bin='claude', claude_home=PosixPath('/home/runner/.claude'), tmux_bin='tmux', url_timeout_sec=120.0, auto_remote_control=True, model=None, effort=None, pane_log_path=None, archive_store=None, pane_store=None, archive_id=None)[source]¶
Launch
claude --resume <id>in a new detached tmux session.A user-supplied
nameis also passed toclaude --name(renaming the resumed session’s display name); the auto-generated fallback name is tmux-only so a resume never clobbers the session’s existing name.- Return type:
- xa.spawn_session(name, *, cwd, claude_bin='claude', claude_home=PosixPath('/home/runner/.claude'), tmux_bin='tmux', url_timeout_sec=120.0, auto_remote_control=True, model=None, effort=None, pane_log_path=None, archive_store=None, pane_store=None, archive_id=None)[source]¶
Create a detached tmux session running
claudeincwd.Waits up to
url_timeout_secfor the bridge URL to appear, dismissing the “trust this folder” prompt and enabling remote control (via the--remote-controlflag on modern claude binaries, or the TUI handshake on older ones). The session name is also passed toclaude --nameso the Claude-side display name matches;modelandeffortbecome per-session--model/--effortflags.If
archive_store(and optionallypane_store) are given, emitscreated+url_acquiredevents to the archive and pipes the pane output to a file underpane_store.Services that want an immediate return should call
prepare_spawn()and runcomplete_spawn()in the background instead — this function is simply the composition of the two.- Return type:
- xa.transcript_forensics(path)[source]¶
Extract postmortem-relevant facts from the tail of a transcript.
Walks from the end of the file so we find the most recent
tool_use/tool_result/ assistanttextwithout reparsing the whole transcript.- Return type:
- xa.transcript_metadata(path)[source]¶
Summarise a transcript without holding it all in memory.
Walks forward, counting turns and capturing the first user message / summary / custom title / fork pointer / cwd / sessionId. Stops reading content fields once it has what it needs, but still counts turns to the end. For very large transcripts, callers who only need counts should use
iter_transcript_eventsdirectly.- Return type:
- xa.transcript_path(cwd, session_id, *, claude_home=PosixPath('/home/runner/.claude'))[source]¶
Return the transcript path for
(cwd, session_id)if it exists.>>> from pathlib import Path >>> import tempfile >>> with tempfile.TemporaryDirectory() as td: ... home = Path(td) ... (home / 'projects' / '-foo-bar').mkdir(parents=True) ... f = home / 'projects' / '-foo-bar' / 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee.jsonl' ... _ = f.write_text('') ... p = transcript_path('/foo/bar', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', claude_home=home) ... p == f True
Modules
Postmortem archive of Claude Code sessions spawned by |
|
Spawn, resume, and URL-resolve Claude Code sessions. |
|
Pure filesystem view of |
|
Command-line interface for |
|
TOML config loader for |
|
Host abstraction — local, SSH, HTTP backends. |
|
Detect — and reconnect — Claude Code panes whose Remote Control dropped. |
|
FastAPI service for |
|
The unified |
|
Minimal key-value and append-log storage. |
|
Pure tmux wrappers. |