pub struct Session {
pub meta: SessionMeta,
pub messages: Vec<ChatMessage>,
pub subagents: Vec<Session>,
pub raw: Vec<String>,
pub raw_trailing_newline: bool,
pub imported_message_count: Option<usize>,
pub raw_is_verbatim: bool,
pub parse_error_lines: usize,
pub load_residue: Vec<String>,
}Expand description
A normalized, replayable conversation loaded from a tool’s session log.
Fields§
§meta: SessionMetaRecovered metadata.
messages: Vec<ChatMessage>The conversation, normalized to the OpenAI chat-completions shape.
subagents: Vec<Session>Subagent (Task) sub-conversations. Claude Code stores these as separate
<session>/subagents/agent-*.jsonl files; loading a session by path now
discovers and attaches them here (each is a full Session whose
meta.agent_id / meta.parent_tool_use_id link it back to its spawn).
raw: Vec<String>Every original JSONL line of the source log, STRICT-VERBATIM (IX-1):
captured via split_lines_verbatim, not the blank-skipping/trimming
non_empty_lines parse view, so a blank line, a CRLF (\r\n)
terminator, or trailing whitespace on a line all survive bit-for-bit
rather than being dropped/normalized away. Normalization into
messages is still lossy by design (it targets the OpenAI replay
shape), but these raw lines retain everything — including records
with no canonical representation (e.g. Claude file-history-snapshot)
— so a round-trip through the supercode-native format
(Session::to_native_jsonl) is byte-lossless for the line-oriented
formats (Claude Code/Codex/Pi), for ANY input (see
Self::raw_trailing_newline for the one piece of information a line
list alone can’t carry).
raw_trailing_newline: boolWhether the source text raw was captured from ended with a trailing
\n. raw’s line list alone can’t distinguish a source ending with a
trailing newline from one that doesn’t (both split into the same
lines) — this flag carries that fact out-of-band so
Self::to_native_jsonl/Self::from_native_str can reproduce the
original source bytes exactly, including the presence/absence of a
final newline. true for a Session whose raw isn’t captured
verbatim from real source text (e.g. OpenCode’s re-synthesized
export-document raw, or a Session assembled programmatically) —
matching the historical always-terminated-by-newline behavior for
those cases.
imported_message_count: Option<usize>How many of messages (and, symmetrically, of raw — see below) came
from parsing the imported log, as opposed to being appended after
import. Set once, at the end of Self::from_claude_code_str /
Self::from_codex_str, to messages.len() at that moment — i.e.
before Self::from_native_str’s subsequent loop reattaches any
appended crate::sidecar::NativeTurn records onto messages/raw.
That loop pushes exactly one raw line and one message per appended
turn, so the two lists grow in lockstep from here on: the raw-prefix
boundary A12’s Self::to_jsonl_spliced needs is always recoverable
as raw.len() - (messages.len() - imported_message_count), without a
second counter. None only when a Session is constructed some other
way than through those two loaders — splicing then has no boundary to
honor and treats every message as imported (equivalent to
Some(messages.len())).
A bounded display-history projection uses this field for the total
number of normalized messages observed before its in-memory window was
applied. Such a semantic view is never a continuation source, and all
splice callers clamp the value to messages.len().
raw_is_verbatim: boolWhether raw was captured strict-verbatim from real source text
(true) or re-synthesized by this crate (false) — the fact
Self::raw_verbatim’s callers need to know before claiming a
same-format convert is byte-identical (PARITY-AUDIT.md P006/P007).
true for every line-oriented loader (from_claude_code_str,
from_codex_str, from_pi_str) and OpenCode’s own ENVELOPE read
surface (from_opencode_str’s per-line loop) — each of those splits
raw directly out of the source text via split_lines_verbatim, so
replaying it reproduces the original bytes exactly. false for
OpenCode’s EXPORT-DOCUMENT read surface
(Session::from_opencode_export_doc): a pretty-printed
{info, messages:[...]} document has no per-line envelope structure
of its own, so raw there is one envelope line RE-SYNTHESIZED per
record — faithful in value, but not the original document’s bytes.
A Session assembled programmatically (not through a from_*_str
loader) also defaults to false — no real source text was captured
at all.
parse_error_lines: usizePARITY-15: how many non-empty lines of the source text FAILED to
deserialize at all (a genuinely malformed/truncated JSON line — not
a well-formed-but-unmodeled record type, which is a normal,
intentional “skip”, tracked separately by crate::audit). Every
line-oriented loader tolerates a stray corrupt line rather than
hard-failing the whole load (a single bad line must not make an
otherwise-healthy multi-thousand-line session unloadable) — but that
tolerance used to be completely invisible: Session::load returned
Ok either way, with no signal that anything was skipped. This
count is what lets a caller (the CLI, inspect/convert) surface
that loss loudly instead of silently. 0 for a cleanly-parsed file,
and for a Session assembled programmatically.
load_residue: Vec<String>Named degradations a Fidelity::Semantic load accepted instead of
failing — the same “say exactly what was given up” residue list
harness.v1.sessions.export already reports for artifacts.
ALWAYS empty for a lossless load: every stricter fidelity refuses a
transcript it cannot reconstruct exactly, which is what keeps
continuation/transfer/export guarantees intact. A non-empty list means
this session is a read-only VIEW (Session::load_with_fidelity with
Fidelity::Semantic) and must not be used as a continuation source.
Implementations§
Source§impl Session
impl Session
Sourcepub fn load_fidelity(&self) -> Fidelity
pub fn load_fidelity(&self) -> Fidelity
The fidelity this reconstruction actually achieved.
Same rule the export path applies to an artifact: named residue means
Fidelity::Semantic; otherwise a verbatim source capture is
Fidelity::ByteLossless and a re-synthesized one is
Fidelity::ValueLossless. A subagent’s residue counts as this
session’s: the whole reconstruction is only as faithful as its least
faithful part, and each child still reports its own residue where it
was measured.
Sourcepub fn from_native_messages(messages: Vec<ChatMessage>) -> Session
pub fn from_native_messages(messages: Vec<ChatMessage>) -> Session
Assemble a session from supercode’s own flat store transcript (one
ChatMessage per JSONL line). These files are the native working
format written by Supercode’s native session store, not a foreign
harness log, so routing them through format auto-detection would
misclassify them as an empty Claude Code session.
Sourcepub fn load(path: impl AsRef<Path>) -> Result<Session>
pub fn load(path: impl AsRef<Path>) -> Result<Session>
Load a session, auto-detecting whether it’s a Claude Code or Codex log
— or, when path looks like a SQLite database, a real OpenCode
opencode*.db store (PARITY-3/PARITY-16): that check runs BEFORE any
UTF-8 text read, so a binary .db file is routed to
Self::from_opencode_sqlite instead of failing on a raw “stream
did not contain valid UTF-8” error (the confirmed footgun these items
close — see looks_like_sqlite and the UTF-8 diagnostic reader).
A DIRECTORY is also accepted directly: path is probed with
detect_opencode_storage_surface BEFORE the SQLite/UTF-8 file
checks below (both of which assume a file and would otherwise surface
a cryptic “Is a directory” io::Error — the confirmed footgun this
closes). This lets inspect/convert/resume accept an OpenCode
DATA-ROOT directly (e.g. ~/.local/share/opencode), matching what
audit --format opencode already does. A resolved Sqlite surface
loads exactly like pointing load at that opencode*.db file
directly (most-recently-updated top-level session). The legacy
JsonTreeA/JsonTreeB surfaces are classifier-only (see
OpenCodeStorageSurface) — there’s no direct-JSON-tree loader, so
that case returns a clear error naming the .db file / audit as the
way in, rather than silently doing nothing or crashing.
Sourcepub fn load_with_fidelity(
path: impl AsRef<Path>,
fidelity: Fidelity,
) -> Result<Session>
pub fn load_with_fidelity( path: impl AsRef<Path>, fidelity: Fidelity, ) -> Result<Session>
Load a session at a declared Fidelity.
Fidelity::Semantic is the READ-ONLY VIEW mode: a transcript whose
record graph cannot be reconstructed exactly (the everyday case for a
Claude Code session that has been compacted or resumed across files,
where a live record’s parentUuid names a record that was pruned)
still loads, stitched best-effort in transcript order, and names what
it gave up in Session::load_residue. Every stricter level keeps
the historical behavior — refuse loudly — because a continuation,
transfer or export built on a guessed graph is exactly the loss
supercode exists to prevent. Callers that go on to RESUME a session
must therefore use Session::load.
Sourcepub fn from_claude_code(path: impl AsRef<Path>) -> Result<Session>
pub fn from_claude_code(path: impl AsRef<Path>) -> Result<Session>
Load a Claude Code transcript from a file, attaching any subagent
(Task) sub-conversations stored alongside it.
Sourcepub fn from_claude_code_with_fidelity(
path: impl AsRef<Path>,
fidelity: Fidelity,
) -> Result<Session>
pub fn from_claude_code_with_fidelity( path: impl AsRef<Path>, fidelity: Fidelity, ) -> Result<Session>
Session::from_claude_code at a declared Fidelity — see
Session::load_with_fidelity for what Fidelity::Semantic buys.
Sourcepub fn from_claude_code_str(jsonl: &str) -> Result<Session>
pub fn from_claude_code_str(jsonl: &str) -> Result<Session>
Parse a Claude Code transcript from an in-memory JSONL string.
Sourcepub fn from_claude_code_str_with_fidelity(
jsonl: &str,
fidelity: Fidelity,
) -> Result<Session>
pub fn from_claude_code_str_with_fidelity( jsonl: &str, fidelity: Fidelity, ) -> Result<Session>
Session::from_claude_code_str at a declared Fidelity — see
Session::load_with_fidelity for what Fidelity::Semantic buys.
Sourcepub fn reconstruct_tree(sessions: Vec<Session>) -> Vec<Session>
pub fn reconstruct_tree(sessions: Vec<Session>) -> Vec<Session>
Reconstruct multi-file subagent trees from a flat set of loaded sessions.
Codex stores subagents as separate rollout files linked to their parent
by lineage["parent_thread_id"] (→ the parent’s session_id). Given a
collection of sessions, this nests each child into its parent’s
Session::subagents and returns only the roots. Children whose parent
isn’t in the set are returned as roots themselves (best effort).
Sourcepub fn load_str(jsonl: &str, format: SessionFormat) -> Result<Session>
pub fn load_str(jsonl: &str, format: SessionFormat) -> Result<Session>
Parse a session of a known format from an in-memory JSONL string.
Sourcepub fn to_jsonl(&self, format: SessionFormat) -> Result<String>
pub fn to_jsonl(&self, format: SessionFormat) -> Result<String>
Serialize this session to JSONL in the given format.
The conversation is synthesized from the canonical messages, so this works for sessions loaded from either tool as well as ones supercode built itself. Converting between formats (e.g. Codex → Claude Code) is an “export”: format-specific framing that has no slot in the target may be dropped, but the user/assistant/tool conversation is preserved.
Sourcepub fn to_jsonl_spliced(
&self,
format: SessionFormat,
session_id: Option<&str>,
) -> Result<String>
pub fn to_jsonl_spliced( &self, format: SessionFormat, session_id: Option<&str>, ) -> Result<String>
Export back to format, replaying the imported raw prefix
verbatim — original uuids/ids, real timestamps, and
loader-skipped records (e.g. Claude Code file-history-snapshot) that
Self::to_jsonl’s full synthesis discards or fakes — when format
is the session’s own origin (format.source() == self.meta.source,
see SessionFormat::source) and there is a raw prefix to replay.
Only messages appended after import (tracked by
Self::imported_message_count) are synthesized, chained onto the
last original record found in the raw prefix.
session_id of Some(new) rewrites the session id on every emitted
line, raw and synthesized alike (sessionId for Claude Code,
session_meta.payload.id for Codex); None leaves ids as recorded.
Cross-format export (no verbatim prefix exists in the target dialect,
by definition) and a session with no raw lines both fall back
unchanged to Self::to_jsonl — full synthesis, same output as
today. A12 (SPEC.md §6): this turns “export back to origin” from
semantic to near-byte fidelity for the dominant hop-back case;
cross-format stays at the documented semantic tier.
Sourcepub fn save(&self, path: impl AsRef<Path>, format: SessionFormat) -> Result<()>
pub fn save(&self, path: impl AsRef<Path>, format: SessionFormat) -> Result<()>
Write this session to path in the given format.
Sourcepub fn raw_verbatim(&self) -> String
pub fn raw_verbatim(&self) -> String
Reconstruct the exact source bytes this Session was loaded from,
out of Self::raw + Self::raw_trailing_newline (the exact
inverse of the strict-verbatim capture those two fields record — see
join_lines_verbatim).
For a genuinely line-oriented source (Claude Code, Codex, Pi, and an
OpenCode envelope-form JSONL), raw is captured verbatim from the
original text, so this reproduces the original file byte-for-byte —
the P008/P009 diagonal-convert fix (convert <file> --to <same-format> is byte-identical to <file>) is built on exactly
this. The one documented exception is an OpenCode export-document
source (a single pretty-printed JSON value, not JSONL): raw there
is RE-SYNTHESIZED as one envelope line per record (see
from_opencode_export_doc’s contract), so this returns a
verbatim reproduction of THAT captured representation rather than the
original pretty-printed document — a known, narrow residue, not a
silent loss (the same records are all still present).
Sourcepub fn to_native_jsonl(&self) -> String
pub fn to_native_jsonl(&self) -> String
Serialize to the supercode-native lossless format: a header line
recording the original source, followed by every original JSONL line
verbatim. Unlike Self::to_jsonl (which targets a foreign tool’s
schema and is necessarily lossy), this preserves everything — including
records with no canonical representation — so Self::from_native_str
reconstructs the session with full fidelity.
Sourcepub fn to_native_jsonl_v2(&self, appended: &[ChatMessage]) -> String
pub fn to_native_jsonl_v2(&self, appended: &[ChatMessage]) -> String
Serialize to the supercode-native v2 format: the same imported-body
mechanism as Self::to_native_jsonl (a versioned header line
followed by every Session.raw line verbatim), plus one
crate::sidecar::NativeTurn record per message in appended — turns
produced after import, which have no backing raw line of their own.
NativeTurn carries metadata in full (unlike ChatMessage’s wire
serde), so nothing the live agent loop records is lost to disk.
appended is caller-supplied rather than inferred from
self.messages: A1 doesn’t track which of self.messages came from
import vs. the live loop — that bookkeeping belongs to the live writer
built on top of this (A2/A3).
Sourcepub fn from_native_str(jsonl: &str) -> Result<Session>
pub fn from_native_str(jsonl: &str) -> Result<Session>
Parse the supercode-native format produced by Self::to_native_jsonl
/ Self::to_native_jsonl_v2. A v1 file (no appended turns) parses
exactly as before. A v2 file’s appended NativeTurn records —
discriminated by the supercode_turn key, which never appears in a v1
body — are split out before the imported body is handed to the
per-source loader, then reattached in file order: to messages (via
crate::sidecar::NativeTurn::into_message) and to raw (verbatim),
so a v2 file round-trips byte-for-byte through
Self::to_native_jsonl_v2 again.
Sourcepub fn from_sidecar_str(s: &str) -> Result<Session>
pub fn from_sidecar_str(s: &str) -> Result<Session>
The full-fidelity Session a sidecar denotes.
The sidecar (native-v2 format, D1) is the imported body plus every
appended crate::sidecar::NativeTurn. Unlike the deliberately
tolerant lower-level native parser, this persisted-store entry point
validates its framing header before loading anything: a missing,
malformed, or unsupported header must never become a zero-message
session that callers could continue as if it were complete.
Sourcepub fn from_codex_str(jsonl: &str) -> Result<Session>
pub fn from_codex_str(jsonl: &str) -> Result<Session>
Parse a Codex rollout from an in-memory JSONL string.
Sourcepub fn from_pi_str(jsonl: &str) -> Result<Session>
pub fn from_pi_str(jsonl: &str) -> Result<Session>
Parse a Pi session (docs/interop/opencode-pi-spec.md §1.1,
docs/interop/research/pi-fields.md) from an in-memory JSONL string.
Line 1 is the session header; every other line is one SessionEntry
in a tree keyed by id/parentId — file order is append order, not
tree order. raw captures every line verbatim (byte-lossless T1,
exactly like Claude Code/Codex). messages is the active path
only: pi’s own leaf rule is “the last entry in file order”
(pi-fields.md sm:897), so this walks parentId from there back to
the root and linearizes root→leaf. Non-active branches, labels, and
state records (thinking_level_change/model_change/custom/
session_info) are never visited by that walk — they survive in
raw only, pi’s defining residue (§1.1).
message.role is an OPEN union upstream (§1.1 S6): a role outside the
five modeled here (user/assistant/toolResult/bashExecution/
custom) produces no canonical message — raw-only survival, never a
panic — and the Pi corpus audit turns that into a
visible coverage failure rather than a silent drop.
Same fail-loud discipline applies to ImageContent blocks
(user/toolResult/custom* content, see pi_image_shape): the
assumed {mimeType, data} shape is UNVERIFIED against real pi output
(pi-fields.md doesn’t enumerate ImageContent’s own fields, only
cites the containing union) — a follow-up TR tracks confirming it
against a real corpus. Until then, an image block that doesn’t match
that shape never gets silently synthesized as an empty/corrupt
image_url part; the containing message survives in raw only and
trips crate::audit::Corpus::Pi’s message/UnknownImageShape bucket.
Sourcepub fn from_grok(path: impl AsRef<Path>) -> Result<Session>
pub fn from_grok(path: impl AsRef<Path>) -> Result<Session>
Load Grok’s resumable chat_history.jsonl transcript.
The surrounding session directory carries the session id, workspace,
and summary.json; Self::from_grok_str handles the transcript
itself while this path-aware entry point overlays that directory
metadata.
Sourcepub fn from_grok_str(jsonl: &str) -> Result<Session>
pub fn from_grok_str(jsonl: &str) -> Result<Session>
Parse Grok’s line-oriented chat_history.jsonl format.
Conversational records are user, assistant, and tool_result.
system is the regenerated base prompt and is retained in
SessionMeta::system_prompt; encrypted reasoning and backend-only
state remain byte-exact in Session::raw but are intentionally not
replayed as chat turns.
Sourcepub fn from_gemini(path: impl AsRef<Path>) -> Result<Session>
pub fn from_gemini(path: impl AsRef<Path>) -> Result<Session>
Load a Gemini CLI transcript from disk.
Sourcepub fn from_gemini_str(jsonl: &str) -> Result<Session>
pub fn from_gemini_str(jsonl: &str) -> Result<Session>
Parse Gemini CLI’s line-oriented session format.
Gemini stores a header without a type, followed by user and
gemini records. Function calls are embedded in assistant content
parts and function responses in user content parts. Unknown records
remain byte-exact in Session::raw instead of silently entering the
replay conversation.
Sourcepub fn from_goose(path: impl AsRef<Path>) -> Result<Session>
pub fn from_goose(path: impl AsRef<Path>) -> Result<Session>
Load a Goose session-export JSON document from disk.
Sourcepub fn from_goose_str(json: &str) -> Result<Session>
pub fn from_goose_str(json: &str) -> Result<Session>
Parse Goose’s official native import/export document.
Goose’s durable store is SQLite, but its own
_goose/unstable/session/export and /session/import boundary is one
JSON object containing a conversation array. Unknown native content
blocks are retained on the first canonical message in a namespaced
portability envelope; unchanged same-format exports replay the exact
source bytes.
Sourcepub fn from_goose_sqlite(db_path: &Path, session_id: &str) -> Result<Session>
pub fn from_goose_sqlite(db_path: &Path, session_id: &str) -> Result<Session>
Load one Goose session directly from its native SQLite store.
The selector is Goose’s stable sessions.id. The reconstructed JSON
uses Goose’s own public export shape, so the ordinary Goose codec is
the single normalization boundary for both files and the live store.
Sourcepub fn from_opencode(path: impl AsRef<Path>) -> Result<Session>
pub fn from_opencode(path: impl AsRef<Path>) -> Result<Session>
Load an OpenCode session from a file — either read surface, see
Self::from_opencode_str.
Sourcepub fn from_opencode_sqlite(
db_path: &Path,
session_id: Option<&str>,
) -> Result<Session>
pub fn from_opencode_sqlite( db_path: &Path, session_id: Option<&str>, ) -> Result<Session>
Load an OpenCode session from a real SQLite store (PARITY-3): opens
db_path, resolves session_id (explicit, or — when None — the
most-recently-updated top-level session, see
opencode_sqlite_primary_session_id) and reconstructs the SAME
envelope form Self::from_opencode_str already parses for the
JSON-tree surfaces — so canonicalization (§2.1 mapping, compaction
discipline, S1 tool-output masking, …) is shared code, not
reimplemented here. See docs/interop/opencode-pi-spec.md §1.2/S9c
for the envelope-construction rules this follows (all-columns rule,
raw revert column carried verbatim).
Sourcepub fn from_opencode_str(text: &str) -> Result<Session>
pub fn from_opencode_str(text: &str) -> Result<Session>
Parse an OpenCode session from either of its two frozen read
surfaces (docs/interop/opencode-pi-spec.md §1.2, S9a,
opencode-fields.md):
- the envelope form: each line is
{"key":[<storage key path>],"value":<record>}, minified — the synthesized raw-capture unit for the JSON-tree/SQLite storage generations; - the export-document form: a single pretty-printed JSON document
{info: SessionInfo, messages:[{info: Message, parts:[Part]}, …]}— theopencode export/importinterchange shape, and EXACTLY what the OpenCode writer emits.
Both forms are parsed into the same (session_info, side_records, Vec<OcMsg>) shape and funnel through the SAME shared canonicalizer,
opencode_session_from_records — so the same underlying records
produce identical messages regardless of which surface carried
them in. This is what makes load(to_opencode_jsonl(S)) round-trip
(§4.1’s OpenCode diagonal, the exact circuit the fidelity matrix
exercises): previously this function parsed the envelope form only
and silently returned an empty-but-Ok Session for an export
document — the confirmed footgun this now closes.
Record classification (envelope form) is driven by the envelope
key’s first component ("session" / "message" / "part" /
"session_diff" / "todo") — the frozen key scheme for all three
storage generations plus SQLite rows (§1.2 S9c: a SQLite row’s
envelope synthesizes key:["<table>","<ses>",...ids] and MUST carry
every column, data and non-data alike — e.g. the session row’s
revert column under the V2 Revert.State schema, whose extra
files field the CLI’s own row→V1 reconstruction drops; the
envelope’s raw capture keeps that raw column value regardless of
what this loader’s canonicalization understands).
Mapping to canonical messages (§2.1, shared by both forms via
push_opencode_user/push_opencode_assistant): User/Assistant
text parts → content; a User file part whose mime is an image
and whose url is a data: URI → an image_url content_parts
entry; an Assistant tool part’s callID + state.*.input → a
ToolCall, and the SAME part’s state.completed.output /
state.error.error → a paired Tool message split by callID
(opencode keeps call+result on one record; this loader splits it
into the two OpenAI-shape messages the other loaders already
produce).
S1 (time.compacted): when a tool part’s
state.completed.time.compacted is set, the emitted Tool
message’s content is the placeholder
OPENCODE_COMPACTED_TOOL_PLACEHOLDER — mirroring what opencode’s
own toModelMessage replays — while the REAL output survives in
raw (always) and in metadata["oc_tool_output_compacted"] (full
text) + metadata["oc_tool_time_compacted"] (the mask timestamp), so
it is reversible, never actually lost.
Compaction boundary: a compaction part’s tail_start_id marks
every message strictly before that message id
metadata["compacted_out"]="true" (honored uniformly by
is_replay_excluded) — except a summary:true Assistant
message, which opencode itself hoists in FRONT of the retained tail
on replay (message-v2.ts:521-572) and so must never be excluded
regardless of its position, mirroring pi’s identical exemption for
its own compaction/branch-summary entries.
Unknown part type or unknown tool.state.status: never
canonicalized — raw-only survival, exactly like an unmodeled Pi
message.role (S6-style fail-loud). The OpenCode corpus audit
is what turns that into a visible coverage failure rather than a
silent drop.
Export-document raw: an export document is a single
pretty-printed JSON value with no per-line envelope structure of its
own to capture verbatim, so raw here is RE-SYNTHESIZED — one
envelope line per session/message/part record found in the
document, in the exact {"key":[...],"value":...} shape the native
envelope form uses — so every native/T1-value-tier path
(to_native_jsonl, opencode_records_from_raw, the
splice/direct-write writers) stays consistent regardless of which
read surface produced this Session.
Malformed input: input that reaches this function non-empty but
yields zero session/message/part records under EITHER form returns a
clear Err rather than a silently-empty Ok(Session) — the
confirmed footgun (supercode resume/convert/inspect on such
input must not silently succeed with an empty session). A
legitimately-empty session — a real session record with zero
messages, or a valid export document with an empty messages array
— is not an error.
Sourcepub fn to_session_tree(&self, created_at_ms: i64) -> SessionTree
pub fn to_session_tree(&self, created_at_ms: i64) -> SessionTree
P5-5 (design §2 module 21 session.tree, §2.1 D-6 “session.tree →
core.session(tree-addressable transcript)”): materialize this
session’s linear Self::messages into a native in-place
crate::session_tree::SessionTree — the bridge a caller uses the
FIRST time it wants to run a tree operation (rewind/branch/label)
against an otherwise-linear Session. created_at_ms stamps every
synthesized node (see
crate::session_tree::SessionTree::from_linear’s doc comment for
why a single timestamp is used: the source linear messages carry no
per-turn timestamp of their own here).
This does not mutate self or persist anything — see
the composition layer’s session-store tree writer for persistence, and
Self::apply_session_tree for the inverse bridge.
Sourcepub fn apply_session_tree(&mut self, tree: &SessionTree) -> Result<()>
pub fn apply_session_tree(&mut self, tree: &SessionTree) -> Result<()>
P5-5: the inverse of Self::to_session_tree — overwrite
Self::messages with tree’s ACTIVE branch’s linear projection
(C7’s “tree-with-linear-projection”: this is exactly what keeps every
existing linear consumer — the agent loop, exporters — working
unchanged after a tree operation runs). Nothing else on self
(meta, raw, …) is touched.
Fail-closed. Propagates crate::session_tree::SessionTree::linear_projection’s
Err rather than applying anything — a structurally-corrupt tree
(a cycle, a dangling leaf, an active branch pointing at nothing) must
error, not silently overwrite Self::messages with an empty Vec.
self is left untouched on Err (the assignment only happens after
the projection has already succeeded).
Source§impl Session
impl Session
Sourcepub fn to_opencode_direct_write(&self, data_root: &Path) -> Result<PathBuf>
pub fn to_opencode_direct_write(&self, data_root: &Path) -> Result<PathBuf>
The required direct-write fallback (S5): write the imported
OpenCode records verbatim — excess/unknown keys, part-row
timestamps, and session_diff/todo side-records intact — to a
generation-B JSON-file storage tree
(docs/interop/opencode-pi-spec.md §1.2), the fidelity path
opencode import cannot provide (S5: import re-decodes through a
strict schema and STRIPS excess keys; inserts part rows without
time_created/time_updated, so those reset to Date.now(); and
has no ingestion path for session_diff/todo at all).
Writes the JSON-FILE layout rather than a live SQLite write
specifically to avoid a new rusqlite-class dependency on this
build’s memory-constrained box (see the build report); session_diff
itself is still JSON-written by upstream even on SQLite installs
(§1.3), so this is a real fidelity path, not a fictional one.
Returns the storage/session/<projectID>/ directory written to.