Documentation
¶
Overview ¶
Package adapter defines the per-Agent compatibility layer.
Everything that knows how a specific AI coding agent stores its sessions on disk lives behind this interface: where the files are, how to read them without disturbing a running agent, which parts of them are bound to absolute paths, and how to write them back atomically.
Nothing above this layer may assume any particular on-disk format. Adding support for a new agent means adding one implementation here and touching no sync logic (§8.2).
Agents change their internal structures without notice, so implementations must degrade rather than guess: read leniently, write conservatively (§9.9).
Index ¶
- Constants
- Variables
- func DefaultCodexHome() (string, error)
- func DefaultHome() (string, error)
- func EncodeProjectSlug(projectRoot string) string
- func EnvironmentFor(layout SessionLayout) environment.Provider
- func Localize(raw []byte, space PathSpace) ([]byte, error)
- type Adapter
- type AgentSessions
- type Canonicalizer
- type CodexLayout
- func (l CodexLayout) Detect(ctx context.Context) (Installation, error)
- func (l CodexLayout) DiscoverSessions(projectRoot string) ([]SessionRef, error)
- func (l CodexLayout) Environment() environment.Provider
- func (l CodexLayout) HookInstalled() (bool, error)
- func (l CodexLayout) HooksPath() string
- func (l CodexLayout) InstallHook(executable string, includeWorkspace ...bool) error
- func (l CodexLayout) Name() string
- func (l CodexLayout) ReadSession(ref SessionRef) (SessionData, error)
- func (l CodexLayout) RemoveHook() error
- func (l CodexLayout) ReplaceSession(projectRoot, sessionID string, records [][]byte) error
- func (l CodexLayout) SessionsDir() string
- func (l CodexLayout) TouchedFiles(records [][]byte, projectRoot string) []FileAccess
- func (l CodexLayout) WriteSession(projectRoot, sessionID string, records [][]byte) error
- type Compatibility
- type EnvironmentCapable
- type FileAccess
- type HookInstaller
- type Installation
- type Layout
- func (l Layout) Detect(ctx context.Context) (Installation, error)
- func (l Layout) DiscoverSessions(projectRoot string) ([]SessionRef, error)
- func (l Layout) Environment() environment.Provider
- func (l Layout) HookInstalled() (bool, error)
- func (l Layout) InstallHook(executable string, includeWorkspace ...bool) error
- func (l Layout) Name() string
- func (l Layout) ProjectsDir() string
- func (l Layout) ReadSession(ref SessionRef) (SessionData, error)
- func (l Layout) RemoveHook() error
- func (l Layout) ReplaceSession(projectRoot, sessionID string, records [][]byte) error
- func (l Layout) SessionDir(projectRoot string) string
- func (l Layout) SessionFile(projectRoot, sessionID string) string
- func (l Layout) SettingsPath() string
- func (l Layout) TouchedFiles(records [][]byte, projectRoot string) []FileAccess
- func (l Layout) WriteSession(projectRoot, sessionID string, records [][]byte) error
- type PathSpace
- type ProjectPaths
- type SessionData
- type SessionLayout
- type SessionRef
Constants ¶
const ( TokenProject = "${AS_PROJECT}" TokenAgentHome = "${AS_AGENT_HOME}" )
Tokens standing in for machine-specific path prefixes in canonical records.
The remote stores canonical records rather than device-local bytes. Session records embed absolute paths, so the same logical record has different bytes on every machine; storing local bytes would make the prefix comparison behind fast-forward vs fork always report divergence, and the version model would never work (§9.6, spec §3).
const ( // MaxSessionRecordBytes is the largest raw JSONL line accepted by an // adapter. The syncer has a 64 MiB shard envelope, so a larger record could // never be published as one immutable record anyway. MaxSessionRecordBytes = 64 << 20 // MaxSessionBytes is the largest complete session snapshot read into the // core. It leaves room for long-running sessions while keeping the // adapter/core slice representation bounded. MaxSessionBytes = 512 << 20 )
The session reader keeps one complete record and the accumulated snapshot bounded. These limits are deliberately much larger than normal Agent records, but prevent a malformed or unexpectedly huge JSONL file from turning a push/list operation into an unbounded allocation.
Variables ¶
var ( // ErrCorruptSession reports a record that is fully written but unparseable. // A truncated tail is not corruption - see ReadRecords. ErrCorruptSession = errors.New("adapter: session contains an unparseable record") // ErrSessionRecordTooLarge reports a JSONL line that cannot be safely // retained as one Agent record. ErrSessionRecordTooLarge = errors.New("adapter: session record exceeds size limit") // ErrSessionTooLarge reports a complete session snapshot that exceeds the // bounded adapter input size. ErrSessionTooLarge = errors.New("adapter: session exceeds size limit") )
var ErrInvalidRecord = errors.New("adapter: record is not a single line")
ErrInvalidRecord reports a record that would break the file's line structure.
var ErrInvalidSessionID = errors.New("adapter: session id is not a safe filename")
ErrInvalidSessionID reports an identifier that must not be joined onto a path.
var ErrNotInstalled = errors.New("adapter: agent not installed")
ErrNotInstalled is returned by Detect when the agent is absent from this machine. It is an expected outcome, not a failure: a missing agent must never produce an error message or affect any other agent (§9.2).
var ErrSessionExists = errors.New("adapter: session already exists")
ErrSessionExists reports that the target session is already present.
Whether an existing session may be replaced is a sync-layer decision - a fast-forward legitimately replaces it, a fork must not - so the adapter refuses by default and offers ReplaceSession for the case where the caller has established that replacing is correct (spec §5).
var ErrUnexpectedSettings = errors.New("adapter: agent settings have an unexpected shape")
ErrUnexpectedSettings reports settings that parse as JSON but are shaped differently from what this adapter models.
Functions ¶
func DefaultCodexHome ¶
DefaultCodexHome returns the Codex state directory for this machine.
func DefaultHome ¶
DefaultHome returns the agent's data directory for this machine.
CLAUDE_CONFIG_DIR relocates it; the agent honours that variable, so anything that ignored it would read and write the wrong place entirely. A relative value is resolved here, because the agent resolves it against its own working directory and ours is not the same - leaving it relative would point us at a different directory than the one actually in use.
func EncodeProjectSlug ¶
EncodeProjectSlug derives the directory name Claude Code uses for a project from its absolute path.
The rule is: replace every character that is not an ASCII letter or digit with `-`, and if the result exceeds slugMaxLen, keep the first slugMaxLen characters and append `-` plus a base-36 hash of the original path.
This must match the agent exactly. An encoder that is merely close produces a directory the agent never reads: discovery then finds nothing and silently backs up no sessions, and a restore reports success while `--resume` cannot see the session. Both failures are silent, which is why this is reproduced character for character rather than approximated.
Everything operates on UTF-16 code units because the agent's implementation is JavaScript: a character outside the basic multilingual plane is two code units there and therefore becomes two dashes, not one.
There is deliberately no decoder. The encoding is heavily lossy - `my_app`, `my-app` and `my.app` all produce the same slug - so reversing it would be guessing. Callers that need to know which project a session belongs to read `cwd` from the session itself, which is authoritative (spec §2).
func EnvironmentFor ¶
func EnvironmentFor(layout SessionLayout) environment.Provider
EnvironmentFor returns the environment provider owned by a layout. Layouts that do not expose one receive a fail-closed provider instead of causing the command layer to branch on an Agent name.
Types ¶
type Adapter ¶
type Adapter interface {
// Name returns a short, stable identifier such as "claude-code".
Name() string
// Detect locates the agent on this machine and classifies its version.
// It returns ErrNotInstalled if the agent is absent.
Detect(ctx context.Context) (Installation, error)
// DiscoverSessions lists the sessions the agent currently holds.
//
// If projectPath is non-empty, only sessions belonging to that project are
// returned.
DiscoverSessions(ctx context.Context, projectPath string) ([]SessionRef, error)
// ReadSession returns the full session content.
//
// The agent may be writing to the session concurrently, so implementations
// must return a complete, parseable state and must never return a truncated
// tail. Returning less data is always preferable to returning a partial
// record (§9.2).
ReadSession(ctx context.Context, ref SessionRef) ([]byte, error)
// Rewrite translates a session captured on another machine into this
// machine's path space.
//
// Cross-device restore is a structural transformation, not a file copy:
// sessions embed absolute paths in several places, and the directory that
// holds them may itself encode the project path. Implementations handle
// separator, case-sensitivity and encoding differences between platforms,
// and must leave paths outside the project root untouched rather than
// guessing (§9.3, BR-10).
//
// It must fail rather than emit a partially rewritten session.
Rewrite(ctx context.Context, session []byte, from, to ProjectPaths) ([]byte, error)
// WriteSession installs a session into the agent's data directory so the
// agent's own resume flow can find it.
//
// The write must be atomic: an interrupted call must leave either the
// previous state or the new one, never a half-written session (BR-11).
WriteSession(ctx context.Context, ref SessionRef, session []byte) error
// TouchedFiles extracts the set of project-relative file paths this session
// read or wrote.
//
// This set scopes the workspace consistency check. Comparing the whole
// working tree would flag unrelated edits and train users to ignore the
// warning, which would make the feature worthless; restricting the check to
// files the session actually touched is what keeps it credible (§9.5).
TouchedFiles(ctx context.Context, session []byte) ([]string, error)
}
Adapter is the contract every supported agent implementation satisfies.
Implementations must never lock, move or modify files the agent is using, and must leave the agent fully functional if CtxHop is removed (§4 P2, P5, BR-06, BR-13).
type AgentSessions ¶
type AgentSessions struct {
Layout SessionLayout
Installation Installation
Sessions []SessionRef
}
AgentSessions pairs an installed agent with the sessions it owns for one project. Sessions from different agents remain separate at this boundary; the remote summary carries the agent name so resume can select the same layout on another device.
func DiscoverInstalled ¶
func DiscoverInstalled(ctx context.Context, projectRoot string) ([]AgentSessions, error)
DiscoverInstalled returns every installed built-in agent and its sessions for projectRoot. An absent agent is normal and is skipped.
func FindInstalled ¶
func FindInstalled(ctx context.Context, name string) (AgentSessions, error)
FindInstalled locates one named built-in agent. It is used by resume when a remote summary records the source adapter explicitly.
type Canonicalizer ¶
type Canonicalizer struct {
// contains filtered or unexported fields
}
Canonicalizer converts a machine's session records into canonical form.
It is stateful only to accumulate diagnostics across the records of one session; the conversion itself is independent per record.
func NewCanonicalizer ¶
func NewCanonicalizer(space PathSpace) *Canonicalizer
NewCanonicalizer returns a Canonicalizer for records written on space.
func (*Canonicalizer) Record ¶
func (c *Canonicalizer) Record(raw []byte) ([]byte, error)
Record converts one raw JSONL record into its canonical form.
The result is byte-for-byte identical on every machine for the same logical record, which is what makes prefix comparison meaningful.
func (*Canonicalizer) UnknownPathFields ¶
func (c *Canonicalizer) UnknownPathFields() []string
UnknownPathFields returns safe field names for path-bearing object keys. Unknown leaf values use the structural fallback instead.
Unknown object keys remain conservative because their semantics are ambiguous, and changing arbitrary user content would be unsafe. The caller downgrades compatibility when an unresolved path key is present.
type CodexLayout ¶
type CodexLayout struct {
// Home is normally ~/.codex or the directory selected by CODEX_HOME.
Home string
// contains filtered or unexported fields
}
CodexLayout locates the JSONL sessions written by Codex CLI.
Codex stores sessions globally under CODEX_HOME/sessions/YYYY/MM/DD rather than in one directory per project. The session_meta.cwd and turn_context.workspace_roots fields are therefore the source of truth when a project is being discovered.
func (CodexLayout) Detect ¶
func (l CodexLayout) Detect(ctx context.Context) (Installation, error)
Detect locates Codex state without starting the Codex executable.
func (CodexLayout) DiscoverSessions ¶
func (l CodexLayout) DiscoverSessions(projectRoot string) ([]SessionRef, error)
DiscoverSessions lists Codex sessions belonging to projectRoot. It reads only one JSON object at a time, which is important because Codex can retain very large histories and discovery should not load every session into RAM.
func (CodexLayout) Environment ¶
func (l CodexLayout) Environment() environment.Provider
Environment returns the Codex-specific filtered environment capability. Core invokes it through adapter.EnvironmentFor and never selects it by comparing an Agent name.
func (CodexLayout) HookInstalled ¶
func (l CodexLayout) HookInstalled() (bool, error)
HookInstalled reports whether a CtxHop command is registered for Codex.
func (CodexLayout) HooksPath ¶
func (l CodexLayout) HooksPath() string
HooksPath is the user-level Codex hook configuration file. Project-local hooks are intentionally not used: CtxHop's project selection is stored in its own configuration and should not require adding files to every project.
func (CodexLayout) InstallHook ¶
func (l CodexLayout) InstallHook(executable string, includeWorkspace ...bool) error
InstallHook registers a Codex SessionEnd hook. The command starts an independent ctxhop push process because Codex gives SessionEnd handlers a short shutdown window; waiting for a remote push here would make the hook unreliable for normal S3 latency.
Existing Codex hooks and unrelated top-level settings are preserved. The operation is idempotent and updates the generated command if the executable moved.
func (CodexLayout) Name ¶
func (l CodexLayout) Name() string
Name identifies the Codex adapter in configuration and session metadata.
func (CodexLayout) ReadSession ¶
func (l CodexLayout) ReadSession(ref SessionRef) (SessionData, error)
ReadSession reads the complete Codex JSONL snapshot identified by ref.
func (CodexLayout) RemoveHook ¶
func (l CodexLayout) RemoveHook() error
RemoveHook removes only the CtxHop command from Codex hooks.json.
func (CodexLayout) ReplaceSession ¶
func (l CodexLayout) ReplaceSession(projectRoot, sessionID string, records [][]byte) error
ReplaceSession installs a Codex session over the existing native id.
func (CodexLayout) SessionsDir ¶
func (l CodexLayout) SessionsDir() string
SessionsDir is the root of Codex's dated JSONL session tree.
func (CodexLayout) TouchedFiles ¶
func (l CodexLayout) TouchedFiles(records [][]byte, projectRoot string) []FileAccess
TouchedFiles extracts file arguments from Codex function/tool records. Shell commands remain intentionally conservative: their effects are covered by the Git/workspace fingerprint rather than guessed from command text.
func (CodexLayout) WriteSession ¶
func (l CodexLayout) WriteSession(projectRoot, sessionID string, records [][]byte) error
WriteSession installs a new Codex session without replacing an existing id.
type Compatibility ¶
type Compatibility int
Compatibility expresses whether the adapter can safely handle the session structure it actually observed. Agent versions are diagnostic metadata only; a release is not limited merely because its version is new or unrecognised.
const ( // CompatUnknown means compatibility has not been evaluated yet. CompatUnknown Compatibility = iota // CompatFull means the observed session fields are understood. All // operations are allowed. CompatFull // CompatLimited means the adapter has only partial structural evidence. // Backup may continue, but restoring requires explicit user confirmation // because writing is the operation that can destroy data. CompatLimited // CompatStopped means sessions cannot be parsed or fail validation. The // adapter performs no reads or writes and existing remote data is left // untouched. CompatStopped )
func GradeSession ¶
func GradeSession(level Compatibility, findings []string) (Compatibility, string)
GradeSession classifies compatibility from the fields actually present in a session. A new Agent release remains fully compatible when the structural adapter can rewrite all path-bearing fields it encounters.
findings are the field names reported by a Canonicalizer, which are already redacted for diagnostics (BR-09).
type EnvironmentCapable ¶
type EnvironmentCapable interface {
Environment() environment.Provider
}
EnvironmentCapable is an optional Adapter capability. Core session, workspace, Git, and no-Git synchronization never requires it; it is only used for environment components whose on-disk format belongs to one Agent.
type FileAccess ¶
type FileAccess struct {
// Path is relative to the project root, slash-separated.
Path string
// Written reports whether a write tool touched it.
Written bool
}
FileAccess records how a session interacted with one project file.
func TouchedFiles ¶
func TouchedFiles(records [][]byte, projectRoot string) []FileAccess
TouchedFiles returns the project files a session read or wrote.
This set scopes the per-file half of the workspace consistency check. The whole working tree would be the wrong scope: unrelated edits on the target machine would be flagged, users would learn to dismiss the warning, and a check nobody reads protects nobody (§9.5).
It is deliberately not the only input to that check. PoC-2 measured that roughly 40% of tool calls are shell commands recording no path at all, and a shell command can rewrite anything, so the caller anchors on git state and uses this only to narrow the comparison. The gap is inherent to what the session records and cannot be closed here.
Records that do not parse are skipped: one odd record must not hide every file the rest of the session touched.
type HookInstaller ¶
type HookInstaller interface {
// InstallHook registers a hook that runs `ctxhop push` when a session
// ends. The optional workspace flag includes the project files and Git
// state in that automatic push. It must be idempotent and must not disturb
// hooks installed by anyone else.
InstallHook(executable string, includeWorkspace ...bool) error
// RemoveHook removes only the hook this tool installed.
RemoveHook() error
// HookInstalled reports whether our hook is currently registered.
HookInstalled() (bool, error)
}
HookInstaller is implemented by adapters whose agent offers a session lifecycle hook.
Where available, a hook is preferred over filesystem watching: it fires at a well-defined moment, needs no resident process, and the user can remove it to uninstall CtxHop completely (spec §8.5, §4 P5).
type Installation ¶
type Installation struct {
// Version is the agent version observed in its local session records, empty
// if no record exposed one. It is diagnostic metadata only; it never decides
// compatibility.
Version string
// VersionSource explains where Version came from. Adapters must not run an
// agent executable just to obtain a version, so the built-in adapters use
// "session-record" or "unavailable" here.
VersionSource string
// DataDir is the root directory holding the agent's local state.
DataDir string
// Compatibility is the level determined from the observed session fields.
Compatibility Compatibility
// CompatibilityReason explains the classification in user-facing terms and
// is surfaced by `ctxhop doctor`. It must never contain paths, project
// names or session content, so that users can paste diagnostics into public
// issues (§9.9, BR-09).
CompatibilityReason string
}
Installation describes a detected agent on the local machine.
type Layout ¶
type Layout struct {
// Home is the agent's data directory, normally ~/.claude.
Home string
// contains filtered or unexported fields
}
Layout locates Claude Code's data directory on this machine.
func (Layout) Detect ¶
func (l Layout) Detect(ctx context.Context) (Installation, error)
Detect locates Claude Code on this machine and reports a structural compatibility baseline. Individual sessions are classified after their fields have been canonicalized. It returns ErrNotInstalled when the agent is absent, which is an expected outcome and not a failure (§9.2).
The agent's own executable is deliberately never run - not even for `--version`. Starting it would hand our "no network traffic we did not ask for" guarantee to somebody else's startup path, which does things like check for updates (§4 P7). The version is read from what the agent wrote instead, and retained for diagnostics. It does not decide compatibility; the fields in the records we are about to parse do.
func (Layout) DiscoverSessions ¶
func (l Layout) DiscoverSessions(projectRoot string) ([]SessionRef, error)
DiscoverSessions lists the sessions Claude Code holds for one project.
A session that cannot be read at all is skipped rather than failing the scan: one damaged file must not hide every other session from the user.
func (Layout) Environment ¶
func (l Layout) Environment() environment.Provider
Environment returns Claude's filtered environment capability. Session, workspace, Git, and no-Git synchronization remain Core behavior; the provider only handles Claude-specific Skill, MCP, and allowlisted settings formats.
func (Layout) HookInstalled ¶
HookInstalled reports whether our entry is currently registered.
func (Layout) InstallHook ¶
InstallHook registers a SessionEnd hook that runs `ctxhop push`.
Idempotent: a second call updates the command if the executable moved and otherwise changes nothing. Hooks belonging to anyone else are preserved untouched - this is the user's file, and we are a guest in it (spec §4.9).
func (Layout) ProjectsDir ¶
ProjectsDir is where per-project session directories live.
func (Layout) ReadSession ¶
func (l Layout) ReadSession(ref SessionRef) (SessionData, error)
ReadSession reads the local file identified by ref.
func (Layout) RemoveHook ¶
RemoveHook deletes only the entry this tool installed, leaving the rest of the user's configuration - and any empty containers we created - as it found them. Removing the hook must leave the agent exactly as it was (BR-13).
func (Layout) ReplaceSession ¶
ReplaceSession installs a session over any existing one. The caller is asserting that replacing is correct, which for a session log means the new content extends the old rather than diverging from it (BR-03).
func (Layout) SessionDir ¶
SessionDir returns the directory holding the sessions of one project.
func (Layout) SessionFile ¶
SessionFile returns the path a session with the given native id occupies.
func (Layout) SettingsPath ¶
SettingsPath is the user-level settings file the hook is registered in.
User level rather than project level: whether a given project syncs is our configuration to decide, not something to encode by scattering hooks through the user's projects.
func (Layout) TouchedFiles ¶
func (l Layout) TouchedFiles(records [][]byte, projectRoot string) []FileAccess
TouchedFiles extracts Claude Code tool accesses from one session.
type PathSpace ¶
type PathSpace struct {
// ProjectRoot is the absolute path of the project root.
ProjectRoot string
// AgentHome is the absolute path of the agent's data directory.
AgentHome string
}
PathSpace describes one machine's view of the paths a session refers to.
type ProjectPaths ¶
type ProjectPaths struct {
// Root is the absolute path of the project root on that machine.
Root string
// AgentProjectKey is however the agent names this project internally, for
// example a directory name derived from the encoded absolute path.
AgentProjectKey string
}
ProjectPaths pairs a project's root directory with the identity the agent uses to refer to it, so Rewrite can map one machine's view onto another's.
type SessionData ¶
type SessionData struct {
// Records holds complete, parseable records in file order.
Records [][]byte
// DroppedTail reports that trailing bytes were left out because they were
// not yet a finished record. It is normal, not an error.
DroppedTail bool
// Skipped counts malformed records passed over. Only a lenient read
// produces a non-zero value; a strict read fails instead.
Skipped int
}
SessionData is the result of reading a session that the agent may still be appending to.
func ReadRecords ¶
func ReadRecords(r io.Reader) (SessionData, error)
ReadRecords reads the complete records of a JSONL session.
The agent writes to these files while we read them, so the last record may be half written. A record only counts once its terminating newline has landed: bytes without one may still be in flight, and shards are immutable once pushed, so an incomplete record would be a permanent mistake. Returning one record less is always recoverable; returning half a record is not (§9.2).
A malformed record that *is* terminated is different: it was fully written and is genuinely corrupt, so it fails loudly rather than being skipped.
func ReadRecordsLenient ¶
func ReadRecordsLenient(r io.Reader) (SessionData, error)
ReadRecordsLenient reads what it can, counting malformed records instead of failing on them.
Listing sessions uses this: a session whose middle is damaged - which is exactly what a kill during a write leaves behind, since the agent appends after the partial line - must still appear in the listing. Dropping it would make the session vanish from the user's view and never be backed up. Anything that will be pushed uses the strict read instead, because a shard is immutable once written (spec §4.2, §6.4).
func ReadSessionFile ¶
func ReadSessionFile(path string) (SessionData, error)
ReadSessionFile reads a session from disk without disturbing the agent.
The file is opened read-only and never locked, moved or modified: the agent owns it and must keep working whether or not we are running (§4 P2, BR-06).
type SessionLayout ¶
type SessionLayout interface {
Name() string
Detect(context.Context) (Installation, error)
DiscoverSessions(projectRoot string) ([]SessionRef, error)
ReadSession(SessionRef) (SessionData, error)
WriteSession(projectRoot, sessionID string, records [][]byte) error
ReplaceSession(projectRoot, sessionID string, records [][]byte) error
TouchedFiles(records [][]byte, projectRoot string) []FileAccess
}
SessionLayout is the command-facing contract for an installed coding agent.
The syncflow package only needs a small set of operations: discover local sessions, read one complete snapshot, and install a localised snapshot. Keeping this separate from Adapter preserves the richer compatibility contract above while allowing the CLI to support more than one agent without coupling it to a particular on-disk layout.
func DefaultLayouts ¶
func DefaultLayouts() ([]SessionLayout, error)
DefaultLayouts returns the built-in agent layouts in a stable order. Claude remains first for backward-compatible discovery; project sessions are still collected from every installed layout, so Codex and Claude can coexist.
type SessionRef ¶
type SessionRef struct {
// Agent is the stable adapter name that owns this session, for example
// "claude-code" or "codex". It is empty in legacy local/test values.
Agent string
// NativeID is the agent's own identifier for this session.
NativeID string
// ProjectPath is the absolute local path of the project this session
// belongs to, as recorded by the agent.
ProjectPath string
// Title is a human-readable label derived locally for display. Agents
// generally do not name sessions, so this is synthesised (§9.2.1). It is
// content-derived and therefore encrypted before it ever leaves the machine.
Title string
// CreatedAt and UpdatedAt come from the agent's own records where
// available, falling back to file timestamps.
CreatedAt time.Time
UpdatedAt time.Time
// Size is the on-disk size of the session in bytes.
Size int64
// contains filtered or unexported fields
}
SessionRef identifies a session discovered on disk, without its contents.
Discovery is deliberately cheap: listing sessions must not require reading or decrypting session bodies.