SessionKit is a Go library for moving a stopped Codex conversation between workspaces. It captures one native rollout, installs identical bytes into an isolated Codex home, and reports how to resume its original UUID. It does not launch Codex, execute tools, move repository files, or copy a whole agent home.
The first profile is codex/paginated@0.160.0, pinned to
Codex 0.160.0.
Layout is detected from the records and path, then the writer version is checked.
Changing a version label does not make a legacy file supported.
| Term | Meaning |
|---|---|
| Cold handoff | Stop the source writer, capture saved conversation data, then launch a fresh destination client. |
| Rollout | Codex's JSONL conversation file, including native context and tool records. |
| Bundle | A rollout plus a manifest with its identity, inspection, size, and SHA-256. |
| Compaction | Native replacement history used to shorten model context. The original bytes remain intact. |
| Harness | The coding-agent program. It is distinct from its model provider. |
| Contract | Supported behavior |
|---|---|
| Source | sessions/YYYY/MM/DD/rollout-<timestamp>-<UUIDv7>.jsonl, with a single first session_meta, history_mode: paginated, and cli_version: 0.160.0. |
| Records | Every record has a string timestamp and an unsigned integer ordinal, in non-decreasing order. Ordinal gaps are reported. |
| Identity | Keep the source UUID. Refuse any matching filename under destination sessions/ or archived_sessions/. |
| Context | Preserve all bytes, opaque passive native records, function/custom tool calls/results, and complete compaction records. Reject types outside the pinned native enums, Other response items, request-only compaction triggers, incomplete compactions, and orphan tool calls or outputs in effective history, including replacement history. Provider file IDs and external media URLs are rejected; inline data URLs remain in the rollout. Local-shell and tool-search records are rejected until native validation is available. |
| Lineage | Reject non-null history_base, fork/parent/subagent fields, inherited/sender response metadata, internal and subagent source classifications, subagent activity/collaboration items, inter-agent communication records, additional metadata records, and _<rolloutId> filename suffixes. Ordinary local retained-source metadata stays byte-identical. |
| Relocation | No rewrites. The caller overrides cwd, runtime workspace roots, provider, and policy at resume. |
| Quiescence | Acquire Codex's coordination lock, then try the thread's writer lock. An active writer, unfinished turn, or exec_command without a terminal CommandExecution completion is a typed rejection. Hold the thread lock through copying and digest rechecks using the same home descriptor. |
| Installation | Private staging, file fsync, hard-link publication without replacement, directory fsync, and digest verification. Probe the destination thread's writer lock while holding Codex's coordination lock across publication. |
| Omitted metadata | Thread name, git metadata, and memory mode are reported as omitted. Their paginated metadata lives outside the selected rollout. Any historical metadata already in the file remains byte-identical. |
| Unsupported files | Legacy, compressed .jsonl.zst, archived sources, and multi-file lineage. |
| Host | Linux and macOS, with Go 1.26 or later and a filesystem supporting flock, hard links, and directory fsync. |
Use the pinned client for sessions intended to migrate. A state database must be
available when the source thread is created so Codex writes paginated history.
Ephemeral or database-less sessions can write legacy history and are rejected.
The destination starts in a fresh or isolated CODEX_HOME. Codex itself rebuilds
the state_5.sqlite row and thread_history_1.sqlite projection when loading the
installed file. SessionKit never opens those databases, session_index.jsonl,
or history.jsonl.
An isolated destination is a requirement, not something filename scans can prove.
A stale paginated database row can shadow the installed file without filename
fallback. Keep CODEX_SQLITE_HOME unset or directed at the destination's private
database directory. A return to the source machine needs another isolated home
beside the preserved original.
The public operations are Inspect, Capture, OpenBundle, PlanInstall,
Install, and Verify. For example, inside a caller that supplies ctx,
sourceHome, threadID, and prepared destination directories:
src := sessionkit.Source{
Harness: sessionkit.Codex,
Root: sourceHome,
ThreadID: threadID,
}
bundle, err := sessionkit.Capture(ctx, src, sessionkit.CaptureOptions{
BundleDir: bundleDir, // must not exist; its parent must exist
})
if err != nil {
return err
}
dst := sessionkit.Destination{
Harness: sessionkit.Codex,
CLIVersion: "0.160.0", // checked against the caller's destination binary
Root: destinationHome,
WorkingDir: destinationWorkspace,
JournalDir: journalDir, // existing directory outside destinationHome and bundleDir
}
plan, err := sessionkit.PlanInstall(ctx, bundle, dst)
if err != nil {
return err
}
// Review plan.ResumeHints and plan.Omitted. Persist json.Marshal(plan) privately
// if the caller needs retries across process restarts.
receipt, err := sessionkit.Install(ctx, plan)
if err != nil {
return err // inspect receipt.Outcome; preserve an Unknown receipt and plan
}
_, err = sessionkit.Verify(ctx, receipt, dst)
return errAll supplied directories must be absolute. Destination home, workspace, journal,
and the bundle's parent must already exist. Every path component is opened
without following symlinks. Resolve a platform alias such as macOS /tmp before
passing a caller-selected directory. Use caller-owned private roots; SessionKit
creates files with mode 0600 and directories with mode 0700, and leaves existing
directory modes alone. The destination home and bundle must not overlap.
Inspection changes only lock coordination files, never the rollout.
The caller supplies the verified destination binary version in CLIVersion.
Planning accepts only the tested 0.160.0 source/destination pair; SessionKit
does not launch a binary to discover its version.
Verification also holds Codex's writer coordination and rejects an
active writer. Planning performs no writes.
OpenBundle checks the manifest schema, rejects undeclared entries, re-inspects
the rollout, and verifies that every summary and component digest matches.
A bundle contains:
manifest.json
components/rollout.jsonl
Keep bundle files private and unchanged during handoff operations.
During capture, the caller must exclusively control the destination bundle name
and its parent directory. Concurrent renaming or replacement during directory
creation requires a separate coordination and publication contract.
Bundle.Manifest is a verification snapshot. PlanInstall revalidates the files
and Install checks the resulting plan's frozen digest before publication.
Concurrent edits to bundle files require caller-owned coordination.
Use the pinned client to produce native payloads. Rollout, response-item, and
event type tags are checked against the pinned profile. Hand-edited nested
payloads require separate native schema validation.
PlanInstall freezes the verified manifest digest and original relative path.
It does not recompute date directories from timestamps, because Codex's directory
and filename time zones can differ. JSON serialization retains the plan's bundle
and destination binding. Its digest detects accidental edits; it is not an
authorization token or a signature. The consumer must authorize the destination
and the data it accepts.
The caller launches a fresh client with ResumeHints.CodexHome, explicitly
resumes ThreadID, and supplies its own configuration. For app-server, start
with ResumeHints.AppServerParams, which contains threadId, cwd, and
runtimeWorkspaceRoots. Add destination-owned modelProvider, approvalPolicy,
approvalsReviewer, and sandbox settings and verify the effective values in
the thread/resume response before starting a turn. Provider definitions,
credentials, tools, MCP connections, and network policy also belong to the
destination. Saved cwd and policy occur in several native record types; replacing
one metadata field would not relocate the session safely.
ResumeHints.NativeCommand contains the base codex exec resume <UUID> command.
It is a hint, not a complete launch configuration. Supply explicit destination
settings and cwd through the pinned client's supported options. Picker visibility
and resume --last are not promised because they filter by provider ID. Vekil's
explicit UUID return command has this shape:
CODEX_HOME=<isolated-home> vekil launch codex -- resume <thread-id>The launcher owns fresh provider settings. Its per-launch provider IDs need
separate selection validation.
Orka's Codex/ACP pin update is
separate from the capture/restore and CLI integration.
This library does not make orka session migrate available.
Each install has JournalDir/<OperationID>.json and an operation lock file.
The journal advances through planned, staged, published, and verified
using atomic replacements. It is not a session catalog. RejectedBeforeMutation
means this attempt did not publish a rollout; private staging directories and
receipts may exist. Installed means the target bytes were verified. Unknown
means publication may have happened. Preserve the plan, bundle, journal, and
staging evidence and retry the same plan. Concurrent uses of that plan serialize.
A staged retry can prove publication when target and staged file are the same
inode. published is recorded before that staged witness is removed, so a
retry after any later interruption removes the leftover witness and verifies
the target without the bundle.
Verify compares a completed receipt to installed bytes without starting Codex.
After a native turn appends new data, that earlier digest is expected to differ;
capture a new bundle for the next handoff.
Errors support errors.As with RejectionError, ActiveWriterError,
CollisionError, BudgetError, IntegrityError, and UnknownOutcomeError.
Rejections identify components and ordinals without quoting conversation bodies.
Metadata reports and manifests still contain private paths and provider IDs.
Keep them private with the bundle.
One budget covers each operation's reads, parsed records/nodes, temporary output, and elapsed time. Zero fields choose these defaults; negative limits reject:
| Limit | Default |
|---|---|
| Total bytes read, including repeated verification reads | 1 GiB |
| One JSONL line, excluding LF | 64 MiB |
| Parsed records | 100,000 |
| JSON nesting depth | 128 |
| JSON tokens and traversed filesystem entries | 10,000,000 |
| Temporary bytes written/reserved | 1 GiB |
| Elapsed time | 5 minutes |
A canceled attempt that has not begun publication may reset its journal using an independent one-second cleanup budget with a 16 KiB temporary-write limit.
Inspect, Capture, and OpenBundle accept caller budgets. PlanInstall,
Install, and Verify use the fixed defaults above. Raising a capture budget
does not raise installation limits; large bundles captured with custom limits
can exceed installation's cumulative read budget. Configurable installation
budgets are a separate API follow-up.
Manifests and journals also have fixed 1 MiB and 16 KiB file-size caps. These
limits bound files and I/O, not heap memory. Bounded manifest serialization is a
separate follow-up. A capture reads the rollout three times: hashing while
copying, source rechecking, and inspection. Budget total reads accordingly.
Duplicate keys, invalid Unicode, malformed JSON,
and a missing final newline are rejected. JSON numbers never pass through
float64 in the library.
There is no automatic redaction. Conversations and tool outputs can contain
secrets even though authentication files are excluded. Inspection warns about
repository URL userinfo and workspace roots outside the recorded cwd. Non-null
encrypted_content is rejected until pinned native tests establish a safe reload
contract. Byte preservation does not prove that another provider will accept it.
Run the ordinary checks without a native client:
make test vet lint
go test -race ./...The separate native gate requires curl, tar, a SHA-256 utility, and sqlite3
for a read-only test assertion. Its installer downloads the exact native npm
payload into a scratch prefix and verifies both archive and binary SHA-256:
export SESSIONKIT_CODEX_BIN="$(./scripts/install-codex.sh)"
make native-gateThe gate fails if the variable is absent or the binary digest differs. It passed on Linux amd64 and macOS arm64. Linux arm64 and macOS amd64 digests are recorded, but those host gates have not been run. CI runs ordinary checks and native gates on Linux and macOS.
The native tests use fresh homes, a fake API key, and a local scripted Responses
provider. They verify original roles, ordering, multiplicity, tool arguments and
nonce output, explicit effective destination settings, native SQLite backfill,
new appended replies and ordinals, restart, compaction replacement history,
return into a third home, source preservation, collisions, and active writers.
A real turn/interrupt produces a matching turn_aborted record; after native
unload and process exit, capture/install/resume must retain the interrupted user
input and original tool history in the actual provider request. A client killed
with SIGKILL after persisting a blocked turn must instead fail capture with
in_flight_turn, preserve the source, and publish no bundle. These tests do not
repair unfinished turns or expand accepted formats or abort reasons. See the
native gate documentation for the focused command.
Source UUID resolution uses read-only thread/read so validation cannot append
to the original. App-server verifies explicit resume settings; native exec
exercises restart and return. Sandbox/approval bypass settings are confined to
these isolated fixtures and prove format compatibility only. Consumer permission
and live-provider behavior require their own validation.
Fixture origins and regeneration are recorded in
harness/codex/testdata/README.md.
After intentionally regenerating them, update inspection and normalized plan
goldens with UPDATE_GOLDEN=1 go test . -run TestGoldenInspectionAndPlan and rerun
both test layers.
Follow-up scope is tracked in lineage bundles, identity and metadata extensions, and compressed sources and conversion. Legacy compatibility, shared-home identities, subagents, and cross-harness conversion need separate profiles and native evidence before support expands.
Licensed under MIT.