Documentation
¶
Overview ¶
Package session provides the core session model: identity, the linked history journal, the reducer that replays it, recovery analysis, and the persistence seam.
A session's durable form is the bundle described in docs/design/local-session-storage.md: metadata in meta.json, the conversation in an append-only history.jsonl, traces and artifacts alongside them. This package owns what those records mean and how a branch reduces to the state a resumed turn starts from. Making them durable belongs to internal/infra/sessionstore; deciding when state commits belongs to internal/agentapp.
Index ¶
- Constants
- Variables
- func CtxWithSessionID(ctx context.Context, id string) context.Context
- func Head(items []Item) (string, error)
- func NewID() string
- func RewindLanding(items []Item, head, messageID string) (string, error)
- func SessionIDFromContext(ctx context.Context) (string, bool)
- func Validate(items []Item) error
- type AbandonedEffect
- type AbandonedWork
- type AdditionalPromptSet
- type Compaction
- type ConversationStats
- type ForkedFrom
- type HeadSelected
- type Header
- type Item
- type ItemSummary
- type Kind
- type LoadMode
- type Loaded
- type MessageItem
- type Meta
- type MetaUpdate
- type NotesReplaced
- type Payload
- type Recovery
- type State
- type Store
- type TodosReplaced
- type ToolCallOutcome
- type ToolExecutionStarted
- type ToolResult
- type ToolStats
- type TurnFinished
- type TurnRecovered
- type TurnStarted
- type UnknownPayload
- type Validator
- type Writer
Constants ¶
const ( ItemTurnStarted = "turn_started" ItemMessage = "message" ItemToolExecutionStarted = "tool_execution_started" ItemToolResult = "tool_result" ItemCompaction = "compaction" ItemNotesReplaced = "notes_replaced" ItemTodosReplaced = "todos_replaced" ItemAdditionalPromptSet = "additional_prompt_set" ItemHeadSelected = "head_selected" ItemTurnFinished = "turn_finished" ItemTurnRecovered = "turn_recovered" )
Item types. The set is closed for the semantics defined here; a reader that meets a type outside it decides what to do from Item.Required, not from this list. See docs/design/local-session-storage.md §6.3 and §6.4.
const ( // The first three are aliases rather than fresh literals: the agent loop // classifies a finished call and this package writes that classification // down, so one definition keeps the file format and the loop from drifting. ToolStatusCompleted = agent.ToolStatusCompleted ToolStatusFailed = agent.ToolStatusFailed ToolStatusDenied = agent.ToolStatusDenied // ToolStatusUnknown has no counterpart in the loop because the loop never // produces it: it is written by recovery, for a call the loop did not live // long enough to classify. ToolStatusUnknown = "unknown" )
Tool outcome statuses carried by a ToolResult.
ToolStatusUnknown is not an outcome the tool reported. It is what BuildMax writes for a call that crossed the execution boundary without returning, whether the turn was cancelled, interrupted, or lost with its process: the call may already have changed the world, and saying so is the only honest answer available.
const ( TurnCompleted = "completed" TurnFailed = "failed" TurnCanceled = "canceled" TurnInterrupted = "interrupted" )
Terminal turn statuses. Canceled and interrupted are separate because they are separate events — a person stopping the turn against the process being shut down under it — and they must not read the same. The run layer draws the same line; see docs/design/graceful-shutdown.md.
const ( // OutcomeNotStarted: the model asked for the call and BuildMax never // recorded entering the tool, so nothing outside BuildMax happened. OutcomeNotStarted = "not_started" // OutcomeUnknown: BuildMax recorded entering the tool and never recorded a // result. The call may have changed the world. It is never retried // automatically. OutcomeUnknown = "outcome_unknown" // OutcomeKnown: a result was recorded, whatever it says. OutcomeKnown = "known" )
Tool outcome classifications for a call found on an interrupted branch. See docs/design/local-session-storage.md §7.3.
const HistoryVersion = 1
HistoryVersion is the journal format this build writes and is the only one it reads. See docs/design/local-session-storage.md §6.1.
const MetaVersion = 1
MetaVersion is the meta.json format this build writes and is the only one it reads. See docs/design/local-session-storage.md §5.
Variables ¶
var ( ErrHistoryVersion = errors.New("unsupported history version") ErrHistoryCorrupt = errors.New("history corrupt") ErrUnknownRequired = errors.New("history contains an unknown required record") ErrHeadNotFound = errors.New("history head not found") )
History errors. They are distinct because callers act on them differently: an unsupported version is a build that is too old, corruption is a file that cannot be trusted, and an unknown required type is a record this build would mis-reduce if it guessed.
var ErrAlreadyHead = errors.New("this point is already the head")
ErrAlreadyHead reports that a point is where the branch already ends, so nothing follows it. Rewinding there is a caller mistake; forking there is the ordinary "branch off from here", so a fork surface reads this as an empty span rather than a failure.
var ErrLocked = errors.New("session is open in another process")
ErrLocked reports that something else already holds a session's writer lock.
It lives here rather than in the file backend because Store.Open's contract names it: a caller deciding whether to report "busy" or to fail programs against the interface, and would otherwise have to import an implementation to ask a question the interface already answers.
var ErrNoLanding = errors.New("no record precedes this message")
ErrNoLanding reports that a message cannot be rewound because nothing on the branch precedes it. A picker filters those out; a surface handed one anyway needs to say why rather than fail as "not found".
var ErrSessionNotFound = errors.New("session not found")
ErrSessionNotFound is returned when a session does not exist.
Functions ¶
func CtxWithSessionID ¶
CtxWithSessionID returns a context that carries the given session ID.
func Head ¶
Head returns the id of the item the next append extends.
It is the last physical record, with no special case for rewind. Every record chains to its physical predecessor except HeadSelected, which chains to the item being returned to, so redirecting the branch is already expressed in the parent links and the head never has to be stored or searched for.
func RewindLanding ¶
RewindLanding returns the record a rewind removing messageID lands on.
Rewind is exclusive: the message a person picks is the one they want back in the input box, so it leaves the branch along with everything after it, and the head has to name the record before it rather than the message itself.
That record is the physical predecessor on the branch, not the previous message: notes, todos, a compaction, and the `turn_finished` of the turn before all sit between two messages, and they belong to work that is being kept. The one record stepped over is `turn_started`, because §7.1 opens a turn before the prompt that starts it — landing there would leave the branch inside the turn being dropped.
A message with nothing before it has no landing. That is the first prompt of a session, and rewinding it would ask for a branch with no records at all; starting a new session says the same thing honestly.
func SessionIDFromContext ¶
SessionIDFromContext returns the session ID from ctx, or ("", false) if not set.
Types ¶
type AbandonedEffect ¶
type AbandonedEffect struct {
ToolCallID string
ToolName string
// Returned reports whether the tool produced a result. A call that crossed
// the execution boundary without returning is listed too, and is the more
// dangerous of the two: it may have changed as much as one that finished,
// and nothing recorded what.
Returned bool
}
AbandonedEffect is one tool call the conversation is about to move past.
type AbandonedWork ¶
type AbandonedWork struct {
// Messages is how many model-visible messages leave the branch.
Messages int
// Effects are the tool calls that reached their tools, in the order they
// ran. Empty means the abandoned span was conversation only — the one case
// where a rewind really does undo everything it moved past.
Effects []AbandonedEffect
}
AbandonedWork is what a rewind moves the conversation past and does not undo.
It exists because rewind is honest about a hazard rather than hiding it (§8.1 of docs/design/local-session-storage.md). The model's history returns to an earlier point; files, processes and network calls do not. A surface that offers rewind without showing this leaves the user believing the opposite, and leaves the model reasoning from a workspace picture that is no longer true.
func Abandoned ¶
func Abandoned(items []Item, head, target string) (AbandonedWork, error)
Abandoned reports what rewinding from head to target would leave in place.
target must be on the branch ending at head, and must not be head itself: asking about where the branch already ends is answered with ErrAlreadyHead rather than an empty span, because a surface that computed the wrong target would otherwise report "nothing abandoned" and look correct.
func (AbandonedWork) Undoable ¶
func (a AbandonedWork) Undoable() bool
Undoable reports whether the rewind moves past nothing that touched the world, so a surface can say so plainly instead of warning about nothing.
type AdditionalPromptSet ¶
type AdditionalPromptSet struct {
Text string `json:"text"`
}
AdditionalPromptSet replaces the durable additional system prompt.
type Compaction ¶
type Compaction struct {
CoveredHeadID string `json:"covered_head_id"`
Summary string `json:"summary"`
}
Compaction replaces the model-visible prefix of this branch.
CoveredHeadID names the last item the summary accounts for, which is what keeps compaction branch-scoped: a summary produced after a fork point cannot be reused by a branch that does not contain the items it summarised.
type ConversationStats ¶
type ConversationStats struct {
// UserMessages counts messages the user actually wrote. A background
// event travels as a user-role message because no provider has a portable
// role for one, and counting those as things the user said would inflate
// the number that reads most like effort.
UserMessages int
// BackgroundMessages counts the user-role messages that carry a Source:
// command results, subagent results, monitor events.
BackgroundMessages int
// AssistantTurns counts assistant messages, including the ones whose whole
// content was a tool call.
AssistantTurns int
// ToolCalls counts calls the assistant issued. ToolResults counts the
// results that came back; the two differ when a run was cut short between
// the call and its result.
ToolCalls int
ToolResults int
// Tools is the per-tool breakdown, heaviest by result bytes first. A tool
// the assistant called but whose result never arrived still appears, with
// zero bytes.
Tools []ToolStats
// TextBytes is what the conversation's own text weighs — user prompts and
// assistant replies. ToolResultBytes is what came back from tools.
//
// Kept apart because they are spent differently: the first is the
// conversation, the second is what the run pulled into it, and on a long
// agent session the second is usually the larger by an order of magnitude.
TextBytes int
ToolResultBytes int
// CompactedMessages is how many messages sit before the compaction
// boundary — summarized away, still stored.
CompactedMessages int
// Notes and Todos are the durable state the session carries.
Notes int
Todos int
}
ConversationStats is the shape of a session's history: who said how much, which tools ran, and how many bytes each of them put back into the context.
It is derived from the stored messages alone and needs no run to be live, so it answers for a session whose traces have been removed. What it cannot answer is anything time-shaped — durations, run boundaries, which model ran — because the history carries no timestamps. That half comes from the traces.
func Stats ¶
func Stats(st State) ConversationStats
Stats folds a session's stored history into its shape.
A tool result names the call it answers rather than the tool it came from, so results are attributed by walking the assistant tool calls first and looking each result's call id up. A result whose call is not in the history — the assistant message was compacted out from under it — is counted in the totals under an empty name rather than dropped: the bytes are in the context either way.
type ForkedFrom ¶
type ForkedFrom struct {
SessionID string `json:"session_id"`
CheckpointID string `json:"checkpoint_id"`
HeadID string `json:"head_id"`
}
ForkedFrom is immutable provenance on a session created by forking another. It is written once at fork time and never updated afterward.
type HeadSelected ¶
type HeadSelected struct {
Reason string `json:"reason,omitempty"`
}
HeadSelected records a branch choice, which is why rewind is one append and not an append plus a metadata write that could disagree with it.
The item being returned to is ParentID. It is not repeated in the payload: every other record's parent is its physical predecessor, and this is the one record that deliberately points somewhere else, so the parent link already says everything a target field would. Storing it twice would only create a pair that could disagree.
type Header ¶
type Header struct {
Type string `json:"type"`
Version int `json:"version"`
SessionID string `json:"session_id"`
CreatedAt time.Time `json:"created_at"`
}
Header is the journal's immutable first record.
type Item ¶
type Item struct {
Seq uint64
ID string
ParentID string
TS time.Time
Required bool
TurnID string
Payload Payload
}
Item is one journal record. Seq is its physical position and ID/ParentID its logical one; the two are separate because rewind leaves abandoned branches in place rather than truncating them.
func Branch ¶
Branch returns the items from the root to head in logical order.
Items on abandoned branches are skipped, which is the whole point of keeping them: rewind leaves them in the file, and only the parent chain decides what the model sees.
func ForkPrefix ¶
ForkPrefix returns the branch ending at throughID as a journal of its own.
It is the branch, not the physical prefix: a parent that was rewound holds abandoned records too, and a child that copied those would carry history its own parent chain never reaches. Branch already answers this — every item it returns has its parent in the same slice — so the result is self-contained and needs no repair.
Item ids are preserved, because they are the stable identity §8.3 keeps, and preserving them is what lets a child's records be recognised as the same work the parent did. Sequence numbers are not: seq is a record's physical position in the journal holding it, and carrying a parent's numbering across would make it describe a file this journal is not. The child is renumbered from one, which is also why a gap in the parent leaves no trace here.
func NewItem ¶
NewItem builds an item with Required derived from the payload, which is the only place the two are allowed to be decided together.
func (Item) MarshalJSON ¶
MarshalJSON writes the record shape documented in §6.2. Required is always emitted, including when false, so an older reader meeting an unknown type always finds an explicit answer rather than having to assume one.
func (*Item) UnmarshalJSON ¶
UnmarshalJSON decodes a record, keeping an unrecognised type as an UnknownPayload instead of failing. Refusing here would make every forward extension a load error even when the record only adds information.
type ItemSummary ¶
type ItemSummary struct {
ID string `json:"id"`
// ProjectID is what the picker and --continue filter by, so it is in the
// projection: scoping a list to the current Project must not cost a read of
// every session's meta.json.
ProjectID string `json:"project_id,omitempty"`
Kind Kind `json:"kind"`
// CreatedAt is carried as well as UpdatedAt because the picker orders by
// it: a list that reordered itself every time a session was touched would
// move entries under the cursor.
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Title string `json:"title,omitempty"`
Workspace string `json:"workspace,omitempty"`
Pinned bool `json:"pinned,omitempty"`
ForkedFrom *ForkedFrom `json:"forked_from,omitempty"`
}
ItemSummary is one session's row in the picker projection (§12): enough to list and group forks without reading a session's journal.
type Kind ¶
type Kind string
Kind distinguishes an ordinary user session from a subagent's private one. See docs/design/local-session-storage.md §9.
type Loaded ¶
type Loaded struct {
Meta Meta
// Head is the current head's id, or "" for a session with no items yet.
Head string
// Items is the branch from the root to Head, in logical order. Populated
// only for LoadFull.
Items []Item
// State is Reduce(Items, Head). Populated only for LoadFull.
State State
// Recovery classifies an interrupted turn on this branch, per §7.3.
// Populated only for LoadFull.
Recovery Recovery
}
Loaded is what Store.Load or Store.Open returns.
type MessageItem ¶
MessageItem carries one complete portable message, not only its text: an assistant record keeps its tool calls and its opaque provider state, and a user record keeps background provenance and non-text parts.
type Meta ¶
type Meta struct {
Version int `json:"version"`
ID string `json:"id"`
Kind Kind `json:"kind"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// ProjectID is the local Project this session belongs to. It is the
// relationship key --continue, the picker, and project memory all select
// by, and it is immutable: a session may move among the Workspace roots of
// its Project, but it never silently moves to another Project. MetaUpdate
// has no field for it.
//
// It is optional at this boundary rather than required because task-run and
// other non-local sessions have no local Project, and giving them a
// fabricated one to satisfy the shape would put fake rows in the catalog.
// CLI and Desktop enforce the stronger local invariant where they create
// sessions. See docs/design/local-project-memory.md §6.3.
ProjectID string `json:"project_id,omitempty"`
Title string `json:"title,omitempty"`
Workspace string `json:"workspace,omitempty"`
Pinned bool `json:"pinned,omitempty"`
// SelectedModel is what the next turn should use. A completed or
// interrupted turn's own TurnStarted record is what it actually used;
// see §5.
SelectedModel string `json:"selected_model,omitempty"`
// Usage and cost are local aggregate reporting, not resume input, and are
// carried forward by MetaUpdate rather than recomputed from history: the
// rates that applied to an earlier turn are not necessarily the ones
// configured now.
PromptTokens int `json:"prompt_tokens,omitempty"`
CompletionTokens int `json:"completion_tokens,omitempty"`
CacheReadTokens int `json:"cache_read_tokens,omitempty"`
CacheWriteTokens int `json:"cache_write_tokens,omitempty"`
Cost *llm.Cost `json:"cost,omitempty"`
CostIncomplete bool `json:"cost_incomplete,omitempty"`
// Lineage fields are set only when Kind is KindSubagent, and are immutable
// once written: they describe how this session came to exist, not
// anything it is currently doing.
ParentSessionID string `json:"parent_session_id,omitempty"`
ParentRunID string `json:"parent_run_id,omitempty"`
ParentToolCallID string `json:"parent_tool_call_id,omitempty"`
AgentType string `json:"agent_type,omitempty"`
DelegationDepth int `json:"delegation_depth,omitempty"`
// Hidden excludes the session from the ordinary picker and --continue. It
// is set by kind, not toggled independently.
Hidden bool `json:"hidden,omitempty"`
// ForkedFrom is set only on a user session created by forking another.
ForkedFrom *ForkedFrom `json:"forked_from,omitempty"`
}
Meta is a session's current metadata record: presentation, the current selections a new turn would use, and running aggregates. It holds nothing history also determines — see §4 — so it carries no head and no sequence counter; both are derived from the journal by Head (§6.2).
func ApplyMetaUpdate ¶
func ApplyMetaUpdate(m Meta, update MetaUpdate, now time.Time) Meta
ApplyMetaUpdate returns m with update applied and UpdatedAt advanced to now. It does not mutate m.
func NewMeta ¶
NewMeta returns a fresh Meta for a new session. CreatedAt and UpdatedAt start equal, since nothing has changed since creation.
func (Meta) CacheReadShare ¶
CacheReadShare is the fraction of the session's prompt that was served from a provider's cache, and ok=false when there is nothing to divide or the provider reported no cache usage at all. A zero share and an unreported one are different facts: only the first says the cache missed.
type MetaUpdate ¶
type MetaUpdate struct {
Title *string
Workspace *string
Pinned *bool
SelectedModel *string
AddPromptTokens int
AddCompletionTokens int
AddCacheReadTokens int
AddCacheWriteTokens int
// AddCost is added to the running total. A currency mismatch against the
// existing total is not an error here: it marks the total incomplete
// instead, because BuildMax holds no exchange rate and inventing one would
// produce a figure that is wrong in both currencies.
AddCost *llm.Cost
MarkCostIncomplete bool
}
MetaUpdate describes a change to a session's current selections or running aggregates. It cannot express a change to Kind, lineage, or ForkedFrom — those are immutable — and it has no field for anything resumable, because a resumable change must go through history instead (§4). A nil field leaves that value unchanged; token and cost fields are deltas, added to what is already recorded, because usage accumulates across turns.
type NotesReplaced ¶
NotesReplaced carries the complete stamped list, not a delta. Durable state is small and rewritten wholesale, so a full list costs little and removes any question about how two partial writes combine.
type Payload ¶
type Payload interface {
// contains filtered or unexported methods
}
Payload is one item type's body. Implementations are the types below; an unrecognised type decodes to UnknownPayload rather than failing, so that Item.Required rather than the decoder decides whether the journal is usable.
type Recovery ¶
type Recovery struct {
// TurnID is the interrupted turn. Empty when nothing needs repair.
TurnID string
// Uncertain are calls that crossed the execution boundary without
// returning. Each needs a durable unknown result before the model sees the
// branch again, so it is told to verify rather than left to assume.
Uncertain []ToolCallOutcome
// NotStarted are calls the model requested that never reached a tool. They
// need no repair record; they are reported because a caller deciding what
// to say to the user wants the distinction.
NotStarted []ToolCallOutcome
}
Recovery is what an interrupted branch needs before it accepts new work.
func Analyze ¶
Analyze classifies the branch ending at head for interruption repair.
A branch whose last turn was closed needs nothing: a turn that ended as completed, failed, canceled, or interrupted was ended by a process that knew what it had done, and re-deriving that judgement would only risk contradicting it. Only a turn with no terminal record is repaired.
type State ¶
type State struct {
Messages []llm.Message
CompactionIdx int
CompactionSummary string
Notes []agent.Note
Todos []agent.Todo
AdditionalPrompt string
// LastTurn is the turn id of the most recent TurnStarted on this branch,
// and Open reports whether it lacks a matching TurnFinished. Together they
// are what tells a caller whether it is resuming or repairing.
LastTurn string
Open bool
}
State is what a branch of history reduces to: everything a resumed turn needs and nothing a turn produced only as evidence. Timing, usage, and sandbox details are the trace's business, not this.
func Reduce ¶
Reduce replays the branch ending at head and returns the state it produces.
It is deterministic and total over a validated journal: the same items and head always give the same State, which is what lets an incremental reducer be checked against a full replay.
func (State) HistoryMessages ¶
HistoryMessages returns the model-visible messages: everything after the compaction boundary, since what precedes it is represented by the summary.
type Store ¶
type Store interface {
// Create makes a new session directory with its metadata and an empty,
// headered journal. It fails if the session already exists.
Create(ctx context.Context, meta Meta) error
// Open acquires the writer lock and returns a Writer for id, along with
// what was already on this session's journal — repairing a torn tail and
// classifying an interrupted turn for recovery in the process. It fails
// with ErrLocked if another process already holds the lock.
Open(ctx context.Context, id string) (Writer, error)
// Load reads a session without acquiring the writer lock or repairing
// anything. A writer may be active concurrently; Load only ever sees a
// stable prefix.
Load(ctx context.Context, id string, mode LoadMode) (Loaded, error)
// UpdateMeta changes current selections or running aggregates. It cannot
// change anything resumable — MetaUpdate has no field for that — so it
// never touches the journal.
UpdateMeta(ctx context.Context, id string, update MetaUpdate) error
// List returns the picker projection. includeHidden controls whether
// subagent sessions (§9) are included; the ordinary picker passes false.
List(ctx context.Context, includeHidden bool) ([]ItemSummary, error)
}
Store is the persistence seam between AgentApp and physical storage. It expresses session semantics, not paths: core owns what these operations mean, infra owns making them durable. See docs/design/local-session-storage.md §14.
Store itself never appends to a journal. Appending requires the writer lock, which Open acquires and Writer.Close releases — a capability the interface makes explicit so a caller cannot append without holding it, and so the lock is held for as long as a caller is actively committing a turn rather than re-acquired call by call, which is what keeps two turns from interleaving into one open span. See §12.
type TodosReplaced ¶
TodosReplaced carries the complete stamped list. See NotesReplaced.
type ToolCallOutcome ¶
ToolCallOutcome is one call's classification after an interruption.
type ToolExecutionStarted ¶
type ToolExecutionStarted struct {
ToolCallID string `json:"tool_call_id"`
ToolName string `json:"tool_name"`
}
ToolExecutionStarted marks that an approved call is about to cross into the tool. It is the record that makes an interrupted call distinguishable from one that never ran, and it is worthless unless it reaches stable storage before the tool may change anything.
type ToolResult ¶
type ToolResult struct {
ToolCallID string `json:"tool_call_id"`
Status string `json:"status"`
Content string `json:"content,omitempty"`
Parts []llm.ContentPart `json:"parts,omitempty"`
}
ToolResult closes one call. It is stored as its own item rather than as a second generic message; the reducer projects it to the tool-role message that provider adapters require.
type ToolStats ¶
type ToolStats struct {
Name string
// Calls is how many times the assistant asked for it.
Calls int
// ResultBytes is what its results put back into the context. This is the
// number that answers which tool is filling the context window, and it is
// not derivable from the call count: one search can outweigh fifty reads.
ResultBytes int
// MaxResultBytes is the largest single result, so one outlier is not
// hidden inside an average.
MaxResultBytes int
}
ToolStats is one tool's share of a session.
type TurnFinished ¶
type TurnFinished struct {
Status string `json:"status"`
ErrorClass string `json:"error_class,omitempty"`
}
TurnFinished closes a turn. Its presence is what tells the next open that there is nothing to recover.
type TurnRecovered ¶
type TurnRecovered struct {
TurnID string `json:"turn_id"`
UncertainToolCallIDs []string `json:"uncertain_tool_call_ids,omitempty"`
}
TurnRecovered makes a cold recovery explicit before new work is accepted, so the repair appears in the journal once rather than being re-derived on every open.
type TurnStarted ¶
type TurnStarted struct {
RunID string `json:"run_id"`
Model string `json:"model,omitempty"`
WorkspaceRoot string `json:"workspace_root,omitempty"`
ContextWindow int `json:"context_window,omitempty"`
InputKind string `json:"input_kind,omitempty"`
}
TurnStarted opens a turn and fixes the runtime identity it ran under.
Model and WorkspaceRoot are what this turn actually used. The session's current selections live in metadata and can differ, which is the point: resuming after switching either must not restate earlier turns.
type UnknownPayload ¶
type UnknownPayload struct {
Kind string
Raw json.RawMessage
}
UnknownPayload is an item this build does not define. It keeps the bytes so a reader that only passes the journal through does not destroy them, and it carries no opinion of its own: whether the session is usable is decided by Item.Required.
type Validator ¶
type Validator struct {
// contains filtered or unexported fields
}
Validator checks a journal's items as a graph, one at a time in physical order.
It is incremental rather than whole-slice so a caller reading a damaged file knows exactly which record failed, and can therefore offer the prefix that was still good instead of only reporting that something somewhere is wrong.
func NewValidator ¶
func NewValidator() *Validator
NewValidator returns a Validator for one journal.
func (*Validator) Add ¶
Add checks one item against everything already accepted.
It rejects what would make a reduction wrong: a duplicate identity, a parent that has not appeared, more than one root, and a record this build cannot interpret but must. It does not check that seq numbers are contiguous — see docs/design/local-session-storage.md §7.2 for why a gap is not a corruption signal.
type Writer ¶
type Writer interface {
// Loaded is what Open found when the writer lock was acquired.
Loaded() Loaded
// Append writes items and returns only once they are durable. items must
// continue the branch this Writer opened: each item's ParentID must chain
// from Loaded().Head (or from a preceding item in the same call), and
// their Seq values must be contiguous starting after the highest Seq this
// Writer has appended so far. A caller that gets this wrong is a bug, not
// a race — the lock already rules out a second writer — so Append reports
// it as an ordinary error rather than needing an optimistic-concurrency
// retry.
Append(ctx context.Context, items ...Item) error
// Close releases the writer lock. It is always safe to call, including
// after Append has failed.
Close() error
}
Writer is one session held open for append. It owns the writer lock for its whole lifetime: nothing else may append to, rewind, or fork from this session until Close releases it.