Skip to content

Latest commit

 

History

History
93 lines (79 loc) · 5.69 KB

File metadata and controls

93 lines (79 loc) · 5.69 KB

fx

fx is Vercel's terminal coding agent — the fx binary (fx.sh), a native Rust CLI that runs against Vercel's AI Gateway. txcript's mapping was reverse-engineered from real sessions written by fx 0.0.5 on this machine; fx is not open source and publishes no format spec, so the bytes on disk are the authority.

~/.fx/sessions/                            (or $FX_HOME/sessions)
├── index.json, latest/, *.lock            (cross-session state; not carried)
└── <session-id>/                          one dir per session
    ├── events.jsonl        ── the append-only event log  (the conversation)
    ├── session.json        ── derived header + byte offsets
    ├── authority.json      ── session identity
    ├── commit.<gen>.json   ┐ the commit boundary
    ├── commit.lock         ┘ (empty marker)
    ├── display.json        ── title/preview for `fx sessions`
    ├── usage-v2.json, checkpoint.json, images/*.bin   (carried when present)
    └── subagent/, artifacts/, logs/, *.lock           (ephemeral; not carried)

On disk

A session is a directory of plain files, not a single transcript file. The root is ~/.fx/sessions, overridden by $FX_HOME (sessions then live at $FX_HOME/sessions). Unlike most harnesses there is no per-workspace subdir: the session directory sits directly under the root, named by the fx session id (<ms>-<ns>-<16 hex>). txcript's discovery walks that one level and sniffs a session by an events.jsonl whose first record is a session_started event — the sibling index.json, latest/, and relationship-migration-index.json are skipped. Resume is by exact id (fx --resume <id> / fx ask --resume <id>); it reads the session directory directly and does not need the global index.json.

Dissection of a transcript

The conversation lives entirely in events.jsonl, an append-only log of typed events. Every event shares one envelope — schema_version, log_generation, seq, event_id, timestamp_ms, kind, payload — and fx's loader is strict: an unknown event kind, an unknown key at any level, a non-string tool output, or a null committed assistant fails the whole session load.

Their name What it is Maps to
events.jsonl event, kind: "session_started" The required header: id, workspace, preferences (model/effort/provider), a zeroed usage snapshot Meta; regenerated on write
kind: "history_turn_committed" One committed turn (see below) The conversation
kind: "recovery_checkpoint_set", "usage_checkpointed" Mid-turn recovery and token bookkeeping Skipped (kept in the native body)
turn user Prompt text plus images (each an images/*.bin snapshot with media_type + snapshot_sha256) Role::User message: Text + Image blocks (bytes read from images/)
turn execution.tool_steps[] Each step: an intermediate assistant text, its tool_calls (arguments_json is JSON-in-a-string), and tool_results (status success/failure) An assistant message (Text + ToolUse) and the paired ToolResult user message; status: "failure" sets is_error
turn assistant The concluding reply text The turn's final assistant Text, stamped StopReason::EndTurn
kind: "interrupted" turn A cancelled turn: a single in-flight tool_call, no result, terminal_reason Assistant message with the pending ToolUse, stamped StopReason::Aborted
session.json Derived header: id, workspace, model, and the event_log_bytes / generation_base_bytes byte offsets into the log Meta; regenerated with recomputed offsets
display.json Title/preview Meta.title

Tools

fx's file/shell tools map onto the Claude convention: read_fileRead, write_fileWrite, edit_fileEdit, terminalBash (its path keys become file_path, cwd becomes workdir, and the fx-only action/profile are dropped). Everything else — grep_files, glob_files, vision, semantic_search, mcp_*, … — passes through as Tool::Raw under its native name. from_common inverts the mapping, synthesizing the action/profile/ cwd fx expects.

Caveats

  • Strict loader, regenerated bytes. from_common regenerates the full events.jsonl (header + turns) plus session.json, authority.json, commit.<gen>.json, and an empty commit.lock, recomputing the byte offsets the header and commit record pin. Missing any of these makes fx report the session corrupt or its authority unavailable.
  • Reasoning has no native slot. fx's committed-turn schema has no field for model reasoning and rejects unknown keys, so Thinking blocks are preserved in a private txcript-meta.json sidecar (keyed by turn and assistant-message index) that fx ignores. Same-harness round trips keep reasoning; fx resume renders the conversation without it.
  • Representational losses through Common. Per-turn token usage, per-message model (fx stores one session model), replace_all on edits (fx's edit_file has no such flag), terminal action/profile and other non-Bash tool argument shapes when a session leaves fx, and the recovery-checkpoint state. execution.files (fx's changed-file review panel) is regenerated empty; the conversation itself is intact.
  • Not carried. subagent/, artifacts/, logs/, and *.lock scratch are ephemeral tool-output/state and are not part of the session round trip.

References

  • Observed from fx 0.0.5 sessions under ~/.fx/sessions on macOS.
  • Docs: https://fx.sh/docs (behavioral, not a format spec).
  • Parser: src/harness/fx.rs; tests: tests/integration/fx.rs.

Last verified: 2026-08-24 (fx 0.0.5).