session

package
v0.44.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 18, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

Documentation

Overview

Rung H6 (issue #1899): the session-side wiring of the relay driver's Recontinue seam. internal/relay's driver (driver.go) deliberately never imports this package — its LegConfig.Recontinue hook is "wired in by a later floor", and this file is that floor. A relay leg IS a session generation, so a rotation reuses the table's existing budget-reset lineage verb (Recontinue: ContinuationID / Generation / ParentTrace) instead of minting a parallel lineage type. The baton stays an opaque type parameter so this package never imports internal/relay either — the seam is cycle-proof in BOTH directions, matching how armtriggers.go takes its axis numbers as bare scalars.

Package session is the per-session DRIVE state — the first-class, queryable, live-mutable control state of a served agent session: its run-state, planner budget, scheduling priority, and per-turn pace. It is the structural twin of internal/ifc's Ledger (TraceID-keyed, bounded-LRU, RWMutex), widened from the single taint high-water mark Ledger carries to a small drive-state struct.

THE GAP IT CLOSES. A served session's drive changes while it runs — an operator drops its budget mid-flight, lowers its priority so an urgent one passes, pauses it, or stops it. Today none of that has a home: the turn loop's cap is frozen at entry, the matmul budget is resolved once at init, and "is this session still going, and how hard?" is RECONSTRUCTED after the fact from git commits + a process scan + a 0-byte-log heuristic (docs/dispatch-loop.md). Reconstruction is lossy, racy, and read-only — you can observe a guessed state, never SET it. This package makes the drive a value: written live, read each turn, so the current state is a lookup, never a re-derivation.

THE SEAM IT GENERALIZES. internal/ifc.Ledger is already a TraceID-keyed, bounded-LRU, concurrent, live-mutable per-session store with a GET /v1/fak/trace/{id} read and a POST /v1/fak/trace/reset write — carrying exactly ONE value (the taint mark). Table is that exact mechanism with a wider value; the gateway session routes (GET/POST /v1/fak/session/...) are the trace routes with a wider payload.

WRITE side (the control verbs): Transition / SetBudget / SetPace / SetPriority, each bumping a monotonic Rev so a stale operator write can be rejected (CAS). READ side: Get (one session), Snapshot (every live session, the SCHEDULER's data structure), and Decide (the one call the turn loop makes per boundary — it debits the turn and returns whether to proceed and, if not, why).

SCHEDULING POSTURE. The table HOLDS Priority and exposes Snapshot; it never PICKS a winner. A multi-session scheduler reads the snapshot and decides who yields — keeping policy out of the table is what keeps the table a value. Budget exhaustion and pause/stop are scheduling EVENTS (a slot frees); a supervisor observes them through the table instead of re-deriving from a process scan.

This package is a foundation leaf: stdlib-only (container/list + sync) plus the shared internal/lifecycle vocabulary leaf and the internal/dormancy clock (both tier-1 foundation leaves), off the request path, registers nothing. The zero Table is not usable — construct with NewTable / NewTableWithLimit.

Index

Constants

View Source
const (
	// CompactCeilingLateFraction — a fire whose pre-fire resident had already reached
	// this fraction of the model context window fired LATE: it ran to the ceiling.
	CompactCeilingLateFraction = 0.95
	// CompactCeilingApproachFraction — a session whose peak resident reached this
	// fraction of the window was under real pressure, so zero fires is an anomaly
	// rather than simply a short session.
	CompactCeilingApproachFraction = 0.90
	// CompactOversizedResidualRatio — post/pre above this means the fire shed little:
	// it technically fired but left most of the window resident.
	CompactOversizedResidualRatio = 0.50
	// CompactFastReboundTurns — refilling to CompactReboundFraction of the pre-fire
	// resident within this many turns means the shed bought almost no headroom.
	CompactFastReboundTurns = 3
	// CompactReboundFraction — the share of pre-fire resident that counts as rebounded.
	CompactReboundFraction = 0.90
	// CompactPairWindow — two fire events within this window are the same fire (the
	// compacted/context_compacted pair), not two fires.
	CompactPairWindow = 2 * time.Second
	// CompactAdjacencyTurns — a pre/post witness further than this many turns from the
	// fire is too far to bind confidently; the fire is classified low-confidence rather
	// than scored as fact.
	CompactAdjacencyTurns = 2
)

Fire-classification thresholds. They are named constants (not magic numbers at the comparison site) because an operator reading an anomaly needs to know the bar it failed against.

View Source
const (
	AnomalyNoFireAboveCeiling = "NO_FIRE_ABOVE_CEILING"
	AnomalyLateFire           = "LATE_FIRE"
	AnomalyIneffectiveFire    = "INEFFECTIVE_FIRE"
	AnomalyOversizedResidual  = "OVERSIZED_RESIDUAL"
	AnomalyFastRebound        = "FAST_REBOUND"
	AnomalyDuplicateFireEvent = "DUPLICATE_FIRE_EVENT"
	AnomalyMissingPreWitness  = "MISSING_PRE_WITNESS"
	AnomalyMissingPostWitness = "MISSING_POST_WITNESS"
	// AnomalyWedgedAtCeiling — the session FIRED repeatedly yet resident context never came
	// down off the ceiling: every measured fire left post-fire resident still above the
	// late-fire fraction of the window. This is the signature an oversized single item produces
	// (a large image or paste the shedder cannot drop): compaction keeps firing but the window
	// walk can never seat a kept window under budget, so the session sails at the top firing
	// uselessly. It is distinct from INEFFECTIVE_FIRE (one fire that did not reduce resident) and
	// from NO_FIRE_ABOVE_CEILING (never fired at all): here the fires HAPPEN and still do not help.
	AnomalyWedgedAtCeiling = "WEDGED_AT_CEILING"
)

Typed anomaly tokens. A closed vocabulary: an operator ranks sessions by these, so they are stable identifiers, not prose.

View Source
const (
	CompactConfidenceHigh = "high"
	CompactConfidenceLow  = "low"
	CompactConfidenceNone = "none"

	CompactReasonOK                 = "OK"
	CompactReasonAdjacencyAmbiguous = "ADJACENCY_AMBIGUOUS"
	CompactReasonTelemetryMissing   = "TELEMETRY_MISSING"
)

Typed confidence + reason for a fire's pre/post binding.

View Source
const (
	VerdictFiredAndHeld     = "FIRED_AND_HELD"
	VerdictFiredWithAnomaly = "FIRED_WITH_ANOMALIES"
	VerdictNoFireBounded    = "NO_FIRE_BOUNDED"
	VerdictNoFireAtCeiling  = "NO_FIRE_ABOVE_CEILING"
	VerdictTelemetryMissing = "NO_TELEMETRY"
	// VerdictWedgedAtCeiling — fired repeatedly and still never came off the ceiling (the
	// oversized-item wedge). Ranked as the worst FIRED outcome: unlike FIRED_WITH_ANOMALIES it is
	// not a one-off artifact but a session that is structurally stuck.
	VerdictWedgedAtCeiling = "WEDGED_AT_CEILING"
)

Session verdicts — the headline the human report leads with, so a large append-only file is never read as a failure.

View Source
const (
	CompactRankFires           = "fires"
	CompactRankPeakResident    = "peak-resident"
	CompactRankCumulativeInput = "cumulative-input"
)
View Source
const (
	// RegrowthReboundTokens — the resident count that counts as "the window came back".
	// Matches the issue's 200k rebound bar.
	RegrowthReboundTokens = 200000
	// RegrowthFastReboundSeconds — the fast/slow cohort split (30 minutes; the issue's
	// audit saw 508 of 699 rebounds inside it).
	RegrowthFastReboundSeconds = 1800
	// RegrowthWithin15MinSeconds — the issue's tighter headline bucket.
	RegrowthWithin15MinSeconds = 900
	// RegrowthOversizedRowBytes — one transcript row at/above this is an oversized
	// single event (a paste, image, or giant tool result the shedder will fight).
	RegrowthOversizedRowBytes = 256 << 10
	// RegrowthDupMinBytes — a row must be at least this large for a repeat of it to
	// count as duplication; identical tiny rows ("ok") are noise, not reinjection.
	RegrowthDupMinBytes = 2048
	// RegrowthDupToolMinRows — repeated tool output needs at least this many duplicate
	// result rows in one window; a single re-read is routine.
	RegrowthDupToolMinRows = 2
	// RegrowthSuffixWindowSamples / RegrowthSuffixBurstTokens — regaining this many
	// tokens within the first few post-fire samples is the #3071 suffix-recreation
	// signature: the window jumps right back before any real work happened.
	RegrowthSuffixWindowSamples = 2
	RegrowthSuffixBurstTokens   = 40000
)

Regrowth thresholds and classification bars. Named so an operator reading a flag knows the bar it tripped.

View Source
const (
	// AnomalyDuplicateSetup counts ROLLOUT-level restatement, not resident waste. The
	// dedup table is session-scoped across fires, so a post-fire reinjection of setup
	// the cut already discarded reads as a duplicate even though it is the window's
	// only resident copy. The #5255 attribution refuted the dedupe reading: within a
	// post-fire window every instruction payload occurs exactly once, the fires that
	// append setup are precisely those whose replacement_history omits it, and the
	// emitter is the upstream CLI's post-compaction rebuild, not this repo — so
	// removing the reinjection would strip the agent's instructions, not save context.
	// Kept deliberately as an honest restatement counter; decision witness:
	// testdata/compactaudit/setup-reinjection-decision-2026-08-09.md.
	AnomalyDuplicateSetup     = "DUPLICATE_SETUP_REINJECTION"
	AnomalyRepeatedToolResult = "REPEATED_TOOL_RESULT"
	AnomalyOversizedEvent     = "OVERSIZED_EVENT"
	AnomalySuffixRecreation   = "SUFFIX_RECREATION"
	AnomalyTimestampSuspect   = "TIMESTAMP_SUSPECT"
)

Regrowth anomaly tokens — a closed vocabulary, same contract as the #4763 set.

View Source
const (
	RegrowthCensorNextFire   = "NEXT_FIRE"
	RegrowthCensorRolloutEnd = "ROLLOUT_END"
)

Censor tokens: why a trajectory ended without reaching the rebound bar. A censored window is an observation limit, not a verdict.

View Source
const (
	RegrowClassInstructions   = "instructions"
	RegrowClassUserMessage    = "message/user"
	RegrowClassSystemMessage  = "message/system"
	RegrowClassDeveloperMsg   = "message/developer"
	RegrowClassAssistantMsg   = "message/assistant"
	RegrowClassReasoning      = "reasoning"
	RegrowClassCompactSummary = "compaction_summary"
	RegrowClassToolCallPrefix = "tool_call/"
	RegrowClassToolResPrefix  = "tool_result/"
	RegrowClassUnknown        = "unknown"
)

Content classes. Tool traffic is keyed per tool ("tool_call/shell", "tool_result/shell") so the attribution table can name the dominant tool.

View Source
const (
	// ReasonControlSessionTerminal refuses any control write against a terminal
	// (Stopped) session: you cannot cancel, pause, resume, throttle, re-budget,
	// re-pace, or re-prioritize a stopped session — you start a new one
	// (Recontinue), you do not un-stop one.
	ReasonControlSessionTerminal = "CONTROL_SESSION_TERMINAL"
	// ReasonControlRevStale refuses an optimistic-concurrency (--if-rev) control
	// write whose expected revision no longer matches: a newer transition landed
	// between the caller's read and its write. Re-read and retry.
	ReasonControlRevStale = "CONTROL_REV_STALE"
)

Control-op refusal tokens — the closed vocabulary for an ILLEGAL-FOR-STATE drive-state control write. Two tokens cover every refusal the Table's write verbs can produce today; ControlRefusalTokens enumerates them for completeness tests and vocabulary sync checks.

View Source
const (
	// DefaultSpikeMinRatio is the suddenness floor: the latest turn's context must be
	// at least this multiple of the previous turn's. 1.5 means "grew by half or more
	// in one turn".
	DefaultSpikeMinRatio = 1.5
	// DefaultSpikeMinDeltaTokens is the materiality floor: the one-turn context growth
	// in tokens. 16384 (~16k) is a large tool result or an unwindowed file read —
	// exactly the ingests the nudge exists to make deliberate.
	DefaultSpikeMinDeltaTokens = 16384
)

Default spike thresholds. Conservative on purpose: both must hold, so the nudge fires on "a third of a typical window arrived in one turn" and stays silent on ordinary growth. Not tuned constants — seeds for the operator-set policy rung.

View Source
const (
	ReasonBudgetTurns     = "BUDGET_TURNS_EXHAUSTED"     // TurnsLeft hit zero
	ReasonBudgetTokens    = "BUDGET_TOKENS_EXHAUSTED"    // TokensLeft hit zero
	ReasonBudgetContext   = "BUDGET_CONTEXT_EXHAUSTED"   // ContextTokensLeft hit zero
	ReasonBudgetQueries   = "BUDGET_QUERIES_EXHAUSTED"   // ClarificationQueriesLeft hit zero
	ReasonBudgetToolCalls = "BUDGET_TOOLCALLS_EXHAUSTED" // ToolCallsLeft hit zero — the runaway floor (#2887), debited per dispatched tool call, not per turn
	ReasonBudgetSpend     = "BUDGET_SPEND_EXHAUSTED"     // SpendMicroCentsLeft hit zero (priced dollar ceiling); never auto-reset — a spent cap is terminal, not a fresh-window continuation
	ReasonPaused          = "PAUSED"                     // operator hold; not terminal, the loop waits
	ReasonDrained         = "DRAINING"                   // operator stop, taken at this boundary
	ReasonTerminated      = "TERMINATED"                 // operator FORCEFUL stop (#2758): in-flight work cancelled at the next safe point, no new work — the deliberate counterpart of DRAINING (which lets the turn finish)
	ReasonStopped         = "STOPPED"                    // already terminal
	ReasonBudgetReset     = "BUDGET_RESET"               // budget-drained, then re-armed on a fresh window (Recontinue)
	ReasonResumeCancelled = "RESUME_CANCELLED"           // a WaitResume parked on a Paused session ended because its context was cancelled (#916)
	ReasonInterrupted     = "INTERRUPTED"                // operator MID-FLIGHT interrupt (#5158), stamped by the loop (not Decide): the arm stops at its next clean turn boundary — softer than DRAINING (no drive-state transition) and never mid-tool, unlike TERMINATED
)

Stop reason tokens — the closed vocabulary Decide stamps, so "why did this turn not run" is a checkable field, never free text. They mirror the refusal-reason discipline the kernel uses elsewhere.

View Source
const (
	EvRewindRefused  = "refused"  // the arbiter refused the change set; zero files modified
	EvRewindForced   = "forced"   // an operator force cleared the non-exclusive conflicts and the restore proceeded
	EvRewindAdmitted = "admitted" // the change set was disjoint (or the caller's own lease); the restore proceeded
)

Closed-vocabulary event kinds recorded on the rewind ledger.

View Source
const (
	EvScratchMinted         = "minted"          // a lease was minted at session start; Dir recorded
	EvScratchGC             = "gc"              // session-end GC reclaimed the tree; BytesReclaimed / FilesDropped set
	EvScratchForked         = "forked"          // a copy-on-write fork produced a child lease over its own dir
	EvScratchCheckpoint     = "checkpointed"    // a checkpoint archived the tree and recorded its Digest
	EvScratchRestored       = "restored"        // restore-with-scratch reproduced the checkpoint tree; Digest set
	EvScratchRestoreSkipped = "restore_skipped" // restore-without-scratch left the current tree untouched
)

Closed-vocabulary event kinds recorded on the scratchpad journal. A scratchpad lifecycle never invents a free-text kind (the same discipline as EvRewind*).

View Source
const (
	// CacheAffinityPreserve means a continuation should keep using the same opaque
	// routing/cache-affinity key as its parent lineage.
	CacheAffinityPreserve = "preserve"
)
View Source
const CompactWedgeMinFires = 2

CompactWedgeMinFires — the minimum number of measured fires before a session's persistent high-ceiling residency is read as a WEDGE rather than one late fire. Two consecutive fires that both leave resident above the ceiling is the smallest pattern that distinguishes "stuck" from a single ill-timed cut.

View Source
const CompactWitnessSchemaVersion = 1

CompactWitnessSchemaVersion stamps every durable witness row so a later reader can tell which shape wrote it. It moves only when CompactSessionReport itself moves in a way an old reader cannot absorb.

View Source
const CostRingSize = 8

CostRingSize is the number of recent turns the per-session cost ring retains. It is small on purpose: `fak ps` renders the latest cost and a short trailing window, and the ring is hot-path-adjacent (pushed under the table lock on every debited turn), so a fixed handful of entries keeps the State copy Snapshot makes cheap. Eight turns is enough to SEE a spike (the last few "normal" turns next to the runaway one) without carrying a transcript.

View Source
const DefaultDescriptorTTL = 30 * time.Minute

DefaultDescriptorTTL is the staleness window a Descriptor is GC'd after when none is configured: a descriptor whose LastSeen is older than this (relative to the sweep's now) is reaped at read time. It is generous — a live session re-stamps LastSeen on every drive change (each Decide, each control verb), so only a session whose process is genuinely gone for this long ages out. Per-descriptor TTL overrides it (TTL > 0).

View Source
const DefaultReservationGrace = time.Second

DefaultReservationGrace is the reclaim window after a known-coming turn's predicted arrival. The reservation is advisory and lower-class than real work: if the matching request does not promote before this grace closes, the slot is reclaimed.

View Source
const DefaultTableLimit = 8192

DefaultTableLimit bounds the process-local per-session drive records. Gateways mint a non-empty TraceID per served session, so a long-running process must not retain every historical session forever — the same rationale as ifc's DefaultLedgerLimit, and the same value, so the two per-session tables age in lockstep.

View Source
const DenyAllDefaultThreshold = 3

DenyAllDefaultThreshold is the default consecutive-stuck turn count at which the breaker stops auto-continuing. It is deliberately SMALL: a model that re-proposes the same refused call three turns running is not going to route around it on the fourth, and every extra continuation is pure waste (the audited loop burned 211k tokens this way). It mirrors the first escalation rung of the Claude Stop-hook ladder (guardStopHookDefaultWarn) so the two paths bound at the same depth.

View Source
const EnvelopeUnbounded = "unbounded"

EnvelopeUnbounded is the sentinel a user types to explicitly request "no limit" on an axis (mirrors Budget.Unbounded/TimeBudget.TimeUnbounded's -1 convention, surfaced here as the one string token both the token/turn axes and the CLI flag parser accept).

View Source
const GuardWitnessDirName = "fak-guarded-sessions"

GuardWitnessDirName is the ledger directory, relative to the Codex home.

View Source
const GuardWitnessSchema = "fak.codex_guard_witness.v1"

GuardWitnessSchema is the schema tag `fak guard` stamps on each per-session witness.

View Source
const KindKVSpan = "kv_span"

WarmKV is one paused session's offloaded attention state, parked for a warm resume. The Cache is the kernel-owned KVCache the session held when it was preempted; ColdTier is the tier it was offloaded to while paused (the tier MoveTo promotes it FROM). A session offloads its KV here at pause and reclaims it on resume; if it is evicted while paused, the entry is dropped and the resume degrades to cold.

View Source
const MicroCentsPerCent int64 = 1_000_000

MicroCentsPerCent converts a user-stated cent ceiling (SpendEnvelope.MaxCents) into the micro-cent unit the spend axis debits: 1 cent = 1_000_000 micro-cents.

View Source
const MinPlannerBudgetDivisor = 8

MinPlannerBudgetDivisor floors a composed planner budget at base/MinPlannerBudgetDivisor: no matter how hard a session is throttled, its resident-context window keeps at least this fraction of its baseline, so the structural pins (system / first+last user / the active goal) and a minimal recency tail still fit. A proportional floor (rather than a machine-wide magic constant) scales with the configured window: a 4096-token base floors at 512, a 1024 base at 128. The throttle drives the window DOWN, never to a unusable size.

View Source
const ReasonDenyAllLoop = "DENY_ALL_LOOP_BOUNDED"

ReasonDenyAllLoop is the closed stop token the breaker stamps when it bounds a deny-all auto-continue loop — so "why did this session stop re-prompting" is a checkable field, never free text, mirroring the Reason* discipline Decide uses.

View Source
const ReasonRelayArmed = "RELAY_ARMED"
View Source
const ReasonRepeatedToolRejection = "REPEATED_TOOL_REJECTION"

ReasonRepeatedToolRejection is the session-stop reason a declared policy uses when a run of bad tool-call outcomes reaches its stop threshold. It is deliberately not a tool refusal token such as MALFORMED.

View Source
const ReasonThroughputFloor = "THROUGHPUT_BELOW_FLOOR"

ReasonThroughputFloor marks a session Draining because its sustained observed throughput stayed below the operator's configured minimum rate — the throughput-axis peer of ReasonBudgetSpend/ReasonTimeBudgetExhausted in the closed stop-reason vocabulary (decide.go).

View Source
const (
	// ReasonTimeBudgetExhausted marks a session Draining because its wall-clock
	// envelope elapsed. Distinct from ReasonBudgetTokens/Turns/Context so an operator
	// or a supervisor can tell a time-out from a token exhaustion at a glance.
	ReasonTimeBudgetExhausted = "TIME_BUDGET_EXHAUSTED"
)

Wall-clock stop/observe reasons — closed tokens in the same family as decide.go's Reason* constants, so "why did the time budget stop this run" is a checkable field.

View Source
const ResetTransactionSchema = "fak.session.reset_transaction.v1"

ResetTransactionSchema is the stable schema token for a budget-reset audit row.

View Source
const SurfaceRestore = "restore"

SurfaceRestore is the execution-surface name a workspace restore identifies as when it asks the lane arbiter for admission (the restore/time-travel surface, alongside laneadmit's SurfaceDispatch / SurfaceLoop / SurfaceManual).

View Source
const TimeUnbounded = -1

TimeUnbounded is the sentinel for "no wall-clock limit configured" — the time-axis analogue of Unbounded. A TimeBudget at its zero value is unbounded (LimitNanos <= 0 means off), matching Budget's "unconfigured axis is permissive" default.

View Source
const Unbounded = -1

Unbounded is the sentinel for a budget axis with no limit (the v0.1 default — a session runs until it ends on its own). A non-negative TurnsLeft/TokensLeft is a real remaining allotment that Decide/Debit debits toward zero; ContextTokensLeft uses 0 as "not configured" and a positive value as the long-window reset budget.

Variables

View Source
var RegrowthThresholds = []int{50000, 100000, 150000, RegrowthReboundTokens}

RegrowthThresholds are the resident-token milestones each trajectory times.

Functions

func AppendCompactWitnesses added in v0.42.0

func AppendCompactWitnesses(path string, reports []CompactSessionReport, at time.Time) error

AppendCompactWitnesses appends one durable witness row per report to the JSONL ledger at path, creating it if absent. Append-only by construction (O_APPEND): a later sweep adds rows, it never rewrites history — the same discipline as every other durable ledger in the tree.

func ContinuationEpoch added in v0.35.0

func ContinuationEpoch(id string) (uint64, bool)

ContinuationEpoch decodes a continuation id to its uint64 epoch — its point in the shared lineage id space (abi.SpeculationContext.Epoch). The bool is false for any string that is not a well-formed continuation id (notably an ORIGINAL trace that never came from a re-continuation), which a caller reads as "generation 0 / epoch 0" — never a guessed non-zero epoch.

func ContinuationID added in v0.35.0

func ContinuationID(trace string, rev uint64) string

ContinuationID is the exported form of the fresh-window handoff id a budget- exhausted session hands its next generation — the same value the internal mint writes to State.ContinuationID. Exported so the lineage bridge can derive the epoch a continuation from (trace, rev) would carry.

func ContinuationIDForEpoch added in v0.35.0

func ContinuationIDForEpoch(epoch uint64) string

ContinuationIDForEpoch is the inverse of ContinuationEpoch over the id's hex tail: it rebuilds the continuation id a given epoch encodes. It round-trips every id ContinuationID produces — ContinuationIDForEpoch(epoch) == the original id — so the id and the epoch are two faces of one lineage value, not two id spaces.

func ControlRefusalTokens added in v0.38.0

func ControlRefusalTokens() []string

ControlRefusalTokens returns the closed control-refusal vocabulary — the value space a completeness test or a dos.toml sync check enumerates.

func Eligible added in v0.35.0

func Eligible(st State) bool

Eligible reports whether a session may be PICKED to run next: its run-state advances (Running or Throttled — Paused/Draining/Stopped are held or ending, never eligible) AND it has not exhausted a configured budget axis. It is the helper both policies share, exported so a host can apply the same eligibility test (e.g. to render which sessions are in contention) without re-deriving the rule.

func IsCorruptDescriptorFile added in v0.41.0

func IsCorruptDescriptorFile(err error) bool

IsCorruptDescriptorFile reports whether err means the descriptor index was readable but its contents could not be trusted.

func IsIncompatibleSchema added in v0.42.0

func IsIncompatibleSchema(err error) bool

IsIncompatibleSchema reports whether err is a refuse-to-start incompatible schema jump on the durable session ledger. It is deliberately distinct from IsCorruptDescriptorFile: an incompatible jump must NOT be recovered by quarantine (which would drop live sessions), it must halt the upgrade loudly.

func LoadGuardWitnessIDs added in v0.44.0

func LoadGuardWitnessIDs(dir string) (map[string]struct{}, error)

LoadGuardWitnessIDs reads the guard ledger directory and returns the set of session ids fak is witnessed to have routed. Junk files are skipped rather than fataled — the directory is live and a half-written witness must not sink the sweep — but a directory that yields NO ids is an error, because silently returning an empty set turns a guarded-only sweep into a zero-row pass.

func QuarantineCorruptRegistry added in v0.42.0

func QuarantineCorruptRegistry(path string, now time.Time) (string, error)

QuarantineCorruptRegistry renames the corrupt index at path to a timestamped `.corrupt-` evidence sibling and returns the evidence path. Stamp collisions get a numeric suffix. If path no longer exists the rename reports os.ErrNotExist: a concurrent recoverer already quarantined it.

func QuarantineEvidenceCount added in v0.42.0

func QuarantineEvidenceCount(activePath string) (int, error)

QuarantineEvidenceCount reports how many quarantine evidence files currently sit beside activePath, for diagnostic surfaces.

func ReapQuarantine added in v0.42.0

func ReapQuarantine(activePath string, policy QuarantineRetention, now time.Time) (removed []string, err error)

ReapQuarantine applies the retention policy to the `.corrupt-*` evidence siblings of activePath. It never touches the active file itself, always preserves the newest evidence file even when that file alone exceeds a bound, and removes files one atomic os.Remove at a time, continuing past individual failures. The joined error is advisory: callers must treat cleanup failure as a warning, never a startup blocker.

func RecoveryLedgerPath added in v0.42.0

func RecoveryLedgerPath(activePath string) string

RecoveryLedgerPath returns where recovery stats for activePath live.

func RelayRecontinueHook added in v0.42.0

func RelayRecontinueHook[B any](tbl *Table, fresh Budget, mint func(b B) (parent, child string)) func(B) (string, error)

RelayRecontinueHook binds tbl to relay's LegConfig.Recontinue seam: instantiated with B = relay.Baton it returns exactly `func(relay.Baton) (successorTrace string, err error)` — the driver's hook type — so it drops straight into LegConfig. When a rotation fires, the closure asks mint for the lineage pair — the closing leg's trace (the baton's parent_trace) and the fresh child trace to re-arm under (canonically the ContinuationID the context drain already minted) — then re-arms the child via tbl.Recontinue with the fresh budget: Generation = parent+1, ParentTrace = the closing leg, and the parent's terminal record left exactly as the drain wrote it (Recontinue never revives a Stopped parent). B is opaque here on purpose (no internal/relay import); the wiring site, which holds the concrete baton type, supplies the two-line mint.

func RenderCompactAudit added in v0.42.0

func RenderCompactAudit(w io.Writer, res CompactAuditResult, topN int)

RenderCompactAudit writes the human report. It deliberately prints append-only bytes and cumulative tokens NEXT TO peak resident context, labelled, because the whole point is that the first two do not answer the compaction question and the third does.

func ScanCompactRolloutReplay added in v0.44.0

func ScanCompactRolloutReplay(r io.Reader, path string, size int64, opt RegrowthReplayOptions) (CompactSessionReport, RegrowthReplayStat, error)

ScanCompactRolloutReplay is ScanCompactRollout with the #5254 counterfactual dedup replay armed: opt.Fold is run over the tool-result bodies of every post-fire window and the returned RegrowthReplayStat scores what that mechanism would have collapsed. A zero-value opt (nil Fold) is exactly ScanCompactRollout. Bodies live in memory for one window and are never persisted.

func WriteCompactAuditJSON added in v0.42.0

func WriteCompactAuditJSON(w io.Writer, res CompactAuditResult) error

WriteCompactAuditJSON emits the machine form.

func WriteCompactTrajectoryRanking added in v0.44.0

func WriteCompactTrajectoryRanking(w io.Writer, reports []CompactSessionReport, topN int, rank string) error

WriteCompactTrajectoryRanking writes the selected token-trajectory view.

Types

type Assumption added in v0.37.0

type Assumption struct {
	Key        string  `json:"key"`
	Statement  string  `json:"statement,omitempty"`
	Source     string  `json:"source"` // user_stated, inferred, queried, witnessed, stale, unknown
	Confidence float64 `json:"confidence,omitempty"`
	Expiry     string  `json:"expiry,omitempty"`
	SourceRef  string  `json:"source_ref,omitempty"`
}

Assumption is one active fact-like row the session is relying on. It is data-only: the planner/self-query leaves decide whether a row is safe to use; the session table keeps the visible ledger so operator surfaces can show what is being assumed without reading hidden transcript text.

type AttachOptions added in v0.35.0

type AttachOptions struct {
	// WarnFraction is forwarded verbatim to Table.WatchBudget, so a host that wants the
	// #743 pre-exhaustion warning still receives it through its pass-through observer.
	// The scheduler itself ignores BudgetWarn — a warning does not free a slot; only
	// BudgetExhausted does. A value outside (0,1) disables the warning (the table's
	// documented behavior), leaving only the exhaustion event firing.
	WarnFraction float64
	// Budget, if non-nil, is the host's pass-through budget observer (e.g. an operator
	// webhook). It is invoked for EVERY BudgetEvent before the scheduler interprets the
	// event. nil means the scheduler owns the budget seam alone.
	Budget BudgetObserver
	// Transitions, if non-nil, is the host's pass-through transition observer, invoked
	// for every TransitionEvent before the scheduler maps it to a slot-freed cause.
	Transitions TransitionObserver
}

AttachOptions carries the optional pass-through observers and warn fraction Attach installs alongside the scheduler's own handlers. WatchBudget / WatchTransitions each hold exactly ONE observer, so the scheduler composes: it installs a fan-out handler that first calls the host's pass-through (if any) and then interprets the event for its own slot-freed accounting. A zero AttachOptions means the scheduler takes sole ownership of both seams (no pass-through, warning disabled).

type Budget

type Budget struct {
	TurnsLeft                int `json:"turns_left"`                           // remaining model round-trips; Unbounded = no cap
	TokensLeft               int `json:"tokens_left"`                          // remaining output tokens; Unbounded = no cap
	ContextTokensLeft        int `json:"context_tokens_left,omitempty"`        // remaining prompt/context tokens; 0 = not configured
	ContextTokensCap         int `json:"context_tokens_cap,omitempty"`         // the configured context-budget size; the denominator the pre-exhaustion warning measures consumed-share against (0 = no context budget)
	ClarificationQueriesLeft int `json:"clarification_queries_left,omitempty"` // remaining clarification/self-query asks; 0 with no cap = not configured
	ClarificationQueriesCap  int `json:"clarification_queries_cap,omitempty"`  // configured clarification-query budget; positive cap with 0 left = exhausted
	// SpendMicroCentsLeft is the remaining PRICED-spend allotment in micro-cents
	// (1e-6 cent = 1e-8 USD — fine enough that every per-token price in the
	// published Anthropic tables, including the 0.1x cache-read multiplier, debits
	// as an exact integer). 0 = no spend budget configured, matching the context
	// axis's convention. The caller prices each turn (the table is deliberately
	// price-blind — see Usage.CostMicroCents); DebitUsage debits toward zero and
	// drains the session with ReasonBudgetSpend when the ceiling is crossed.
	SpendMicroCentsLeft int64 `json:"spend_micro_cents_left,omitempty"`
	// SpendMicroCentsCap is the configured spend-budget size (the denominator a
	// consumed-share display measures against); 0 = no spend budget.
	SpendMicroCentsCap int64 `json:"spend_micro_cents_cap,omitempty"`
	// ToolCallsLeft is the remaining DISPATCHED-tool-call allotment — the runaway
	// floor a scheduled/dispatched agent cannot extend (#2887, Hermes cron hardening).
	// Unlike TurnsLeft it is debited per tool call, not per model round-trip, so a
	// single turn that emits a long tool-call loop is cut at the budget too.
	// DebitToolCall spends one unit per call and drives the session to Draining with
	// ReasonBudgetToolCalls when the ceiling is crossed. It follows the spend/query
	// axis's 0=off convention (not the turns/tokens Unbounded=-1 sentinel), so a
	// Budget literal that never sets it stays permissive — the zero value is no cap.
	ToolCallsLeft int `json:"tool_calls_left,omitempty"`
	// ToolCallsCap is the configured tool-call-budget size, stamped from ToolCallsLeft
	// at set-time so "0 left with a positive cap = exhausted" is distinguishable from
	// "0 = unconfigured" once the remaining decrements to zero (the same disambiguation
	// ClarificationQueriesCap/SpendMicroCentsCap carry).
	ToolCallsCap int `json:"tool_calls_cap,omitempty"`
}

Budget is a session's remaining work allotment. Decide debits TurnsLeft by one each turn and TokensLeft/ContextTokensLeft by the turn's reported usage; hitting a configured axis drives the session to Draining (the budget-exhausted stop). An operator RE-SETS any axis live — raising it (speed up / extend) or cutting it (slow down / the priority-queue "let an urgent one pass" move). Unbounded (-1) means no limit for the turn/output axes; context 0 means off.

type BudgetEnvelope added in v0.37.0

type BudgetEnvelope struct {
	Budget              Budget             `json:"budget"`
	WallClockLimitNanos int64              `json:"wall_clock_limit_nanos,omitempty"`
	Pace                Pace               `json:"pace,omitempty,omitzero"`
	Spend               SpendEnvelope      `json:"spend,omitempty,omitzero"`
	Throughput          ThroughputEnvelope `json:"throughput,omitempty,omitzero"`
}

BudgetEnvelope is the canonical parsed form of a managed-context budget envelope.

func NewBudgetEnvelope added in v0.37.0

func NewBudgetEnvelope() BudgetEnvelope

NewBudgetEnvelope returns the permissive default: unbounded turn/output-token budgets, no context/time/spend/throughput envelope, and no pace opinion.

func ParseBudgetEnvelope added in v0.37.0

func ParseBudgetEnvelope(spec string) (BudgetEnvelope, error)

ParseBudgetEnvelope parses the compact CLI syntax into the deterministic envelope.

func (BudgetEnvelope) ExpectedThroughput added in v0.37.0

func (e BudgetEnvelope) ExpectedThroughput() Throughput

ExpectedThroughput returns the runtime throughput expectation carried by the envelope.

func (BudgetEnvelope) SessionBudget added in v0.37.0

func (e BudgetEnvelope) SessionBudget() Budget

SessionBudget projects the envelope onto the session budget axes. The spend ceiling is projected from cents into the micro-cent axis DebitUsage debits, so a stated `spend=$25` becomes a live runtime budget the moment the host prices turns (Usage.CostMicroCents) — not just an inspectable contract field.

func (BudgetEnvelope) SessionPace added in v0.37.0

func (e BudgetEnvelope) SessionPace() Pace

SessionPace projects the envelope onto the per-turn pace axes.

func (BudgetEnvelope) ThroughputBudget added in v0.40.0

func (e BudgetEnvelope) ThroughputBudget() ThroughputBudget

ThroughputBudget projects the envelope's throughput axis onto the live drive state axis (#2762): the expected rate as the soft pace-shaping reference and the min rate as the enforced sustained-rate floor (see throughput.go). The observation window starts empty — it accumulates from real reported turns.

func (BudgetEnvelope) TimeBudget added in v0.37.0

func (e BudgetEnvelope) TimeBudget() TimeBudget

TimeBudget returns the unstarted wall-clock budget for this envelope.

func (BudgetEnvelope) WallClockLimit added in v0.37.0

func (e BudgetEnvelope) WallClockLimit() time.Duration

WallClockLimit returns the configured wall-clock limit.

type BudgetEvent

type BudgetEvent struct {
	Kind                    BudgetEventKind       `json:"kind"`
	TraceID                 string                `json:"trace_id"`
	ContinuationID          string                `json:"continuation_id,omitempty"` // set on Exhausted: the fresh-window handoff id
	Reason                  string                `json:"reason,omitempty"`          // the closed budget reason token at this event
	CacheAffinity           CacheAffinityDecision `json:"cache_affinity,omitempty,omitzero"`
	Rev                     uint64                `json:"rev"`
	ContextTokensLeft       int                   `json:"context_tokens_left"`
	ContextTokensCap        int                   `json:"context_tokens_cap,omitempty"`
	ResidentContextTokens   int                   `json:"resident_context_tokens,omitempty"` // this debit's resident prompt/context tokens
	ResidentContextCap      int                   `json:"resident_context_cap,omitempty"`    // the leg ceiling used for ResidentContextFraction
	ResidentContextFraction float64               `json:"resident_context_fraction"`         // 0..1, resident context divided by the leg ceiling
	FractionConsumed        float64               `json:"fraction_consumed"`                 // 0..1, the share of the context budget spent at this event
}

BudgetEvent is the immutable snapshot a BudgetObserver receives. It is built under the table lock and delivered AFTER the lock is released, so an observer may do slow work (a webhook POST) without stalling the debit hot path or any other session.

type BudgetEventKind

type BudgetEventKind uint8

BudgetEventKind classifies a budget-lifecycle event a BudgetObserver is told about.

const (
	// BudgetWarn fires once, when a session's context budget first crosses the
	// pre-exhaustion warning threshold (the configured consumed share, e.g. 80%) —
	// early enough that a supervisor can extend the budget or wind the session down
	// before it drains.
	BudgetWarn BudgetEventKind = iota
	// BudgetExhausted fires when the context budget hits zero — the reset trigger.
	// ContinuationID names the fresh window the session continues under.
	BudgetExhausted
)

func (BudgetEventKind) String

func (k BudgetEventKind) String() string

String renders the event kind as its lowercase wire token ("warn"/"exhausted"); an out-of-range value renders "unknown" rather than panicking.

type BudgetObserver

type BudgetObserver func(BudgetEvent)

BudgetObserver is the threshold-and-reset callback seam. The table invokes it from DebitUsage AFTER releasing its lock, so the callback is free to block (a webhook POST) without holding up other sessions. The host owns fan-out and failure policy — cmd/fak fires the webhook fire-and-forget, fail-open; the table only delivers the typed event.

type CacheAffinityDecision added in v0.37.0

type CacheAffinityDecision struct {
	Action      string `json:"action,omitempty"`
	AffinityKey string `json:"affinity_key,omitempty"`
	FromTraceID string `json:"from_trace_id,omitempty"`
	ToTraceID   string `json:"to_trace_id,omitempty"`
	Reason      string `json:"reason,omitempty"`
}

CacheAffinityDecision is the auditable cache-affinity handoff stamped when a context-budget reset mints a continuation id. It is deliberately provider-neutral and ADVISORY: session owns the lineage decision that says a hidden reset kept the same opaque key, and the decision is serialized onto the reset directive for the HOST to act on. Nothing in fak's own outbound wire path consumes AffinityKey — the live provider routing hint (responsesPromptCacheKey in internal/agent) is derived independently from the request head and never consults this value.

func (CacheAffinityDecision) IsZero added in v0.37.0

func (d CacheAffinityDecision) IsZero() bool

IsZero reports whether the decision is absent, for json omitzero.

type CompactAggregate added in v0.42.0

type CompactAggregate struct {
	Sessions              int               `json:"sessions"`
	Bytes                 int64             `json:"rollout_bytes"`
	Fires                 int               `json:"fires"`
	MeasuredFires         int               `json:"measured_fires"` // fires with both witnesses
	CompactedSessions     int               `json:"compacted_sessions"`
	Sampled               int               `json:"telemetry_sessions"`
	CumulativeInputTokens int64             `json:"cumulative_input_tokens"`
	ResidentTokensShed    int64             `json:"resident_tokens_shed"`
	Daily                 []DailyTokenStats `json:"daily,omitempty"`

	MedianPreTokens     int     `json:"median_pre_tokens"`
	MedianPostTokens    int     `json:"median_post_tokens"`
	MedianShedTokens    int     `json:"median_shed_tokens"`
	MedianResidualRatio float64 `json:"median_residual_ratio"`

	AnomalyCounts map[string]int `json:"anomaly_counts"`
	VerdictCounts map[string]int `json:"verdict_counts"`

	// Regrowth is the corpus-wide rebound/attribution roll-up (#4768); nil when no
	// fire carried post-fire telemetry.
	Regrowth *CompactRegrowthRollup `json:"regrowth,omitempty"`
}

func AggregateCompactReports added in v0.42.0

func AggregateCompactReports(reports []CompactSessionReport) CompactAggregate

AggregateCompactReports rolls per-session reports up to the fleet answer. Medians (not means) are the headline: fire sizes are heavy-tailed, so a mean is dragged by a handful of enormous sessions.

type CompactAuditOptions added in v0.42.0

type CompactAuditOptions struct {
	// Roots merges multiple rollout corpora into one aggregate. Root remains the
	// compatibility form used when Roots is empty.
	Roots []string
	Root  string
	// Since drops rollouts not modified at/after this instant. Zero = no filter.
	Since time.Time
	// Cwd, when set, keeps only rollouts whose session_meta cwd contains this string —
	// the "just my repo's sessions" filter.
	Cwd string
	// Limit caps the number of rollouts scanned (0 = unbounded), so an operator can
	// smoke the sweep on a big corpus.
	Limit int
	// GuardedOnly keeps only sessions present in the `fak guard` witness ledger — the
	// fak-routed cohort. Without it a sweep over ~/.codex/sessions measures every Codex
	// session on the box, most of which never crossed fak's wire, which makes any
	// gateway-side before/after unfalsifiable (#5254). See compactaudit_provenance.go.
	GuardedOnly bool
	// GuardWitnessDir is the ledger directory GuardedOnly reads (the caller resolves the
	// Codex home; this package does not guess it).
	GuardWitnessDir string
}

CompactAuditOptions configures a corpus sweep.

type CompactAuditResult added in v0.42.0

type CompactAuditResult struct {
	Root      string `json:"root,omitempty"`
	Generated string `json:"generated,omitempty"`
	// Provenance says which corpus subset this sweep measured (#5254). It survives
	// --scrub: it carries no paths, and without it a guarded-only aggregate is
	// indistinguishable from a whole-corpus one.
	Provenance CompactProvenance      `json:"provenance"`
	Aggregate  CompactAggregate       `json:"aggregate"`
	Sessions   []CompactSessionReport `json:"sessions,omitempty"`
}

CompactAuditResult is a whole sweep: the per-session reports plus the roll-up.

func AuditCompactCorpus added in v0.42.0

func AuditCompactCorpus(opts CompactAuditOptions) (CompactAuditResult, error)

AuditCompactCorpus streams every rollout under opts.Root and reports compaction health. Files are scanned one at a time and each is streamed head-bounded, so corpus size drives wall time, not memory.

func DecodeCompactAudit added in v0.42.0

func DecodeCompactAudit(r io.Reader) (CompactAuditResult, error)

DecodeCompactAudit parses the machine form `fak session compact-audit --json` emits (the exact document WriteCompactAuditJSON writes). It is the one parser a consumer — #3187's live-session dogfood in particular — should use, so the audit's schema stays the single source instead of each consumer growing its own rollout reader.

func ScrubCompactResult added in v0.42.0

func ScrubCompactResult(res CompactAuditResult) CompactAuditResult

ScrubCompactResult strips everything that cannot be checked into a public repo: filesystem paths and the session cwd. Session ids (opaque UUIDs) and the numeric witnesses survive, which is what reproduces the headline counts. Prompt and tool-output bodies never enter a report in the first place — the scanner drops them at read time — so there is nothing to scrub there.

type CompactCeilingSession added in v0.42.0

type CompactCeilingSession struct {
	SessionID          string `json:"session_id"`
	PeakResidentTokens int    `json:"peak_resident_tokens"`
	ContextWindow      int    `json:"context_window"`
	FireCount          int    `json:"fire_count"`
	Verdict            string `json:"verdict"`
}

CompactCeilingSession is one session scored against a resident-token ceiling — the per-session slice of the #3187 dogfood view, carrying just the fields the ceiling question needs, each copied from the shared report (never re-derived from rollouts).

type CompactFire added in v0.42.0

type CompactFire struct {
	Index int       `json:"index"`
	At    time.Time `json:"at"`
	Turn  int       `json:"turn"`

	// PreTokens/PostTokens are RESIDENT context (last_token_usage.input_tokens) at the
	// nearest non-zero sample either side of the fire; 0 means "no witness", which is
	// reported as an anomaly rather than as a real zero.
	PreTokens  int `json:"pre_tokens"`
	PostTokens int `json:"post_tokens"`
	Shed       int `json:"shed_tokens"`

	// ResidualRatio is post/pre — how much of the window survived the fire.
	// CeilingRatio is pre/window — how close to the ceiling it fired.
	ResidualRatio float64 `json:"residual_ratio"`
	CeilingRatio  float64 `json:"ceiling_ratio"`
	ContextWindow int     `json:"context_window"`

	// ReboundTurns/ReboundSeconds measure how fast resident context returned to
	// CompactReboundFraction of pre-fire. 0 = never rebounded within the session.
	ReboundTurns   int     `json:"rebound_turns"`
	ReboundSeconds float64 `json:"rebound_seconds"`

	Confidence string   `json:"confidence"`
	Reason     string   `json:"reason"`
	Anomalies  []string `json:"anomalies,omitempty"`

	// Regrowth is this fire's post-fire trajectory and content-class attribution
	// (#4768) — how fast the window refilled, out of what, and how the observation
	// ended. See compactregrowth.go.
	Regrowth *CompactRegrowth `json:"regrowth,omitempty"`
}

CompactFire is one compaction event joined to its resident-context witnesses.

type CompactProvenance added in v0.44.0

type CompactProvenance struct {
	// GuardedOnly is true when the sweep kept only ledger-present sessions.
	GuardedOnly bool `json:"guarded_only"`
	// LedgerSessions is how many guarded session ids the ledger held. The remaining
	// fields describe the filter's effect and are omitted when it is off.
	LedgerSessions int `json:"ledger_sessions,omitempty"`
	// Guarded/Unguarded split the rollouts that survived the other filters, so the
	// cohort's share of its own corpus slice is visible rather than inferred.
	Guarded   int `json:"guarded_sessions,omitempty"`
	Unguarded int `json:"unguarded_sessions,omitempty"`
}

CompactProvenance records WHICH subset of the corpus a sweep measured. It rides in the result so a checked-in witness JSON says on its face whether it is a fak-routed cohort or the whole mixed-provenance box — two documents that are otherwise identical in shape and wildly different in what they license you to claim.

type CompactRegrowth added in v0.42.0

type CompactRegrowth struct {
	// PostFloorTokens is the lowest resident sample observed after the fire — the
	// point regrowth is measured from.
	PostFloorTokens    int `json:"post_floor_tokens"`
	LastResidentTokens int `json:"last_resident_tokens"`
	GrowthTokens       int `json:"growth_tokens"`

	Samples   int     `json:"samples"`
	Turns     int     `json:"turns"`
	ToolCalls int     `json:"tool_calls"`
	Seconds   float64 `json:"seconds"`

	TokensPerSample float64 `json:"tokens_per_sample,omitempty"`
	TokensPerTurn   float64 `json:"tokens_per_turn,omitempty"`
	// TokensPerSecond is only computed on a clean clock; a TIMESTAMP_SUSPECT window
	// never reports a wall-clock velocity.
	TokensPerSecond float64 `json:"tokens_per_second,omitempty"`

	Crossings []RegrowthCrossing `json:"crossings,omitempty"`

	// Rebounded — resident reached RegrowthReboundTokens within this window.
	// Censored names why observation stopped short when it did not.
	Rebounded       bool    `json:"rebounded"`
	Censored        string  `json:"censored,omitempty"`
	NextFireSeconds float64 `json:"next_fire_seconds,omitempty"`

	// Cache join: summed provider input/cache-read tokens across the window's
	// samples. CacheReadFraction = cache reads / total input, i.e. how much of the
	// regrowth pricing was reuse. -1 when the window carried no samples.
	WindowInputTokens int     `json:"window_input_tokens"`
	CacheReadTokens   int     `json:"cache_read_tokens"`
	CacheReadFraction float64 `json:"cache_read_fraction"`

	// Classes attribute the transcript bytes appended during the window.
	Classes map[string]*RegrowthClassStat `json:"classes,omitempty"`

	Anomalies  []string `json:"anomalies,omitempty"`
	Confidence string   `json:"confidence"`
	Reason     string   `json:"reason"`
}

CompactRegrowth is one fire's regrowth trajectory: how fast the window refilled, out of what, and how the observation ended.

type CompactRegrowthRollup added in v0.42.0

type CompactRegrowthRollup struct {
	// FiresWithTelemetry — fires with at least one subsequent resident sample (the
	// issue's 1,044 denominator).
	FiresWithTelemetry int `json:"fires_with_telemetry"`
	// Rebounds — windows that reached RegrowthReboundTokens (the issue's 699).
	Rebounds int `json:"rebounds"`
	Censored int `json:"censored"`
	// TimestampSuspect windows still count as rebounds but are excluded from every
	// wall-clock statistic below.
	TimestampSuspect int `json:"timestamp_suspect"`

	MedianSecondsToRebound float64 `json:"median_seconds_to_rebound"`
	P90SecondsToRebound    float64 `json:"p90_seconds_to_rebound"`
	MedianSamplesToRebound int     `json:"median_samples_to_rebound"`
	ReboundsWithin15Min    int     `json:"rebounds_within_15min"`
	ReboundsWithin30Min    int     `json:"rebounds_within_30min"`
	MedianNextFireSeconds  float64 `json:"median_next_fire_seconds"`

	MedianCacheReadFraction float64 `json:"median_cache_read_fraction"`

	ClassTotals   map[string]*RegrowthClassStat `json:"class_totals,omitempty"`
	AnomalyCounts map[string]int                `json:"anomaly_counts,omitempty"`

	Fast RegrowthCohort `json:"fast_cohort"`
	Slow RegrowthCohort `json:"slow_cohort"`
}

CompactRegrowthRollup is the corpus roll-up: the issue's headline counts plus the ranked attribution table.

type CompactSessionReport added in v0.42.0

type CompactSessionReport struct {
	SessionID string    `json:"session_id"`
	Path      string    `json:"path"`
	Model     string    `json:"model"`
	Cwd       string    `json:"cwd"`
	StartedAt time.Time `json:"started_at,omitempty"`
	EndedAt   time.Time `json:"ended_at,omitempty"`

	// The three quantities the report exists to keep apart.
	Bytes                 int64 `json:"rollout_bytes"`           // append-only: grows forever
	CumulativeInputTokens int   `json:"cumulative_input_tokens"` // monotonic: grows forever
	PeakResidentTokens    int   `json:"peak_resident_tokens"`    // the real occupancy signal
	FinalResidentTokens   int   `json:"final_resident_tokens"`

	ContextWindow int `json:"context_window"`
	Turns         int `json:"turns"`
	ToolCalls     int `json:"tool_calls"`
	TokenSamples  int `json:"token_samples"`

	Fires          []CompactFire `json:"fires"`
	FireCount      int           `json:"fire_count"`
	PairedEvents   int           `json:"paired_events"`   // deduped compacted/context_compacted halves
	DuplicateFires int           `json:"duplicate_fires"` // genuine same-kind repeats

	Verdict   string   `json:"verdict"`
	Anomalies []string `json:"anomalies,omitempty"`
}

CompactSessionReport is one rollout file's compaction health.

func ScanCompactRollout added in v0.42.0

func ScanCompactRollout(r io.Reader, path string, size int64) (CompactSessionReport, error)

ScanCompactRollout streams one Codex rollout and reports its compaction health. size is the rollout's byte length, recorded so the report can show append-only bytes beside resident context and refuse the "big file = broken" read.

type CompactWitnessRow added in v0.42.0

type CompactWitnessRow struct {
	SchemaVersion int    `json:"schema_version"`
	RecordedAt    string `json:"recorded_at"` // RFC3339 UTC — when the row was witnessed, not when the session ran
	CompactSessionReport
}

CompactWitnessRow is the durable per-session compaction-health row #3152 asks for. It EMBEDS CompactSessionReport rather than re-declaring any field: the miner's schema is the single source, and the row adds only the durable-ledger envelope. A row is self-contained — session id, fires, verdict, and every resident-context witness are readable from the JSONL alone, with no live gateway process.

func NewCompactWitnessRow added in v0.42.0

func NewCompactWitnessRow(rep CompactSessionReport, at time.Time) CompactWitnessRow

NewCompactWitnessRow wraps one mined report in the durable-row envelope.

func ReadCompactWitnesses added in v0.42.0

func ReadCompactWitnesses(path string) ([]CompactWitnessRow, error)

ReadCompactWitnesses reads every durable witness row back from the JSONL ledger at path, in append order. Blank lines are skipped; a malformed row is an error, not a silent drop — a durable witness that cannot be re-read is the failure mode the ledger exists to rule out.

type ComposedBudgets added in v0.35.0

type ComposedBudgets struct {
	PlannerBudget  int     `json:"planner_budget"`
	WorkerFraction float64 `json:"worker_fraction"`
	Ratio          float64 `json:"ratio"`
}

ComposedBudgets is the two derived per-session budgets a single throttle produces from one knob (Pace.MaxTokensPerTurn). PlannerBudget is the resident-context window to set on agent.SessionPlanner.Budget; WorkerFraction is the matmul FAK_BUDGET fraction in (0,1] to feed model.SetWorkerBudget (single-session-sound only — see the file header fence). Ratio is the throttle that produced them (1.0 == unthrottled), carried so a consumer can log or gate on "by how much" without recomputing.

type ControlRefusal added in v0.38.0

type ControlRefusal struct {
	// Op names the refused control verb ("cancel", "pause", "budget", ...) —
	// observability only; the closed decision surface is Reason.
	Op string `json:"op"`
	// Reason is the closed refusal token (ControlRefusalTokens).
	Reason string `json:"reason"`
	// Detail is human-facing context (the live run-state, the stale rev).
	Detail string `json:"detail,omitempty"`
}

ControlRefusal is the structured refusal of one drive-state control op. It implements error so plumbing can thread it, but callers should switch on Reason (a closed token), never parse Detail.

func ControlRefusalFor added in v0.38.0

func ControlRefusalFor(op string, st State, ok bool) *ControlRefusal

ControlRefusalFor maps the (State, ok) pair every Table control verb returns onto the structured refusal: nil for an applied write (ok=true), the closed terminal-session token when the refusing record is terminal, and the closed stale-revision token otherwise (the only other refusal the write verbs produce: a CompareAndSet that lost its race against a live session). The mapping is total over the verbs' actual refusal behavior, so a caller can wrap any existing verb without that verb changing shape.

func (*ControlRefusal) Error added in v0.38.0

func (r *ControlRefusal) Error() string

type CorruptDescriptorFileError added in v0.41.0

type CorruptDescriptorFileError struct {
	Cause RecoveryCause
	Err   error
}

CorruptDescriptorFileError reports malformed or unsupported descriptor-index content. Callers may recover by quarantining the index: descriptors are a rebuildable projection of live session state, not the session transcript. Cause carries the normalized, privacy-safe corruption class so recovery observability never has to echo descriptor contents (#4658).

func (*CorruptDescriptorFileError) Error added in v0.41.0

func (*CorruptDescriptorFileError) Unwrap added in v0.41.0

func (e *CorruptDescriptorFileError) Unwrap() error

type CostRing added in v0.35.0

type CostRing struct {
	Turns [CostRingSize]TurnCost `json:"turns"`
	Head  int                    `json:"head"`  // index of the NEXT write (the oldest entry once full)
	Count int                    `json:"count"` // live entries, capped at CostRingSize
}

CostRing is the bounded per-session record of the last CostRingSize turns' cost. It is a fixed array plus a head cursor and a live count, so it never allocates and never grows: the (head+CostRingSize-1) slot is the most recent push, older entries trail backward, and once count reaches CostRingSize the oldest is overwritten in place. Carried inline on State (omitzero), it rides Snapshot with no side table. The zero CostRing is a valid empty ring.

func (CostRing) CostSummary added in v0.35.0

func (r CostRing) CostSummary() CostSummary

CostSummary folds the ring into the render-ready summary. SpikeRatio divides the latest turn's total by the LARGEST total among the prior entries (not the immediately-previous one) so a single normal turn wedged between two runaway turns cannot mask the spike; it is 0 when no prior turn exists (a session's first debit cannot be a spike). The fold is pure and reads only the ring, so it is reproducible and table-testable.

func (CostRing) IsZero added in v0.35.0

func (r CostRing) IsZero() bool

IsZero reports whether the ring holds no recorded turns — the safe default a renderer reads as "no cost history yet". It drives the `omitzero` JSON tag so a session that has never been debited marshals byte-identically to a pre-ring State.

func (CostRing) LatestContextTokens added in v0.38.0

func (r CostRing) LatestContextTokens() int

LatestContextTokens returns the most recent debited turn's resident context/prompt tokens — the full window the model read that turn — or 0 for a ring that has never been debited. It is the single scalar the outbound-compaction burst gate needs to reason about a context-budgeted session's remaining horizon: paired with the session's ContextTokensLeft it bounds how many more turns the session can run before its context budget drains. Pure and reads only the ring (like CostSummary), so it is reproducible and table-testable; the zero ring reads 0, the safe "no history yet" default that leaves any consumer's horizon UNSET rather than guessed.

func (CostRing) SpikeAdvisory added in v0.37.0

func (r CostRing) SpikeAdvisory(p SpikePolicy) SpikeAdvisory

SpikeAdvisory folds the ring's two most recent turns into the context-growth advisory under the given policy. It needs a real baseline: fewer than two recorded turns, or a previous turn with no context accounting (ContextTokens 0 — an output-only debit), folds to the zero advisory — a session's first big window cannot be SUDDEN, and inventing a baseline would fabricate a ratio. Pure: same (ring, policy) => identical advisory.

type CostSummary added in v0.35.0

type CostSummary struct {
	Latest     int     `json:"latest"`      // most recent turn's total token cost
	Previous   int     `json:"previous"`    // the turn before it (0 if only one recorded)
	Delta      int     `json:"delta"`       // Latest - Previous
	SpikeRatio float64 `json:"spike_ratio"` // Latest / max(prior window); 0 when there is no prior turn
	Count      int     `json:"count"`       // live entries in the ring
}

CostSummary is the render-ready fold of a session's cost ring — the numbers `fak ps` shows in a cost-per-iteration column. Latest/Previous are the two most recent turns' combined cost; Delta is Latest-Previous (a positive jump is a climbing cost); SpikeRatio is Latest over the MAX of the prior window (the runaway tell: a 200x loop reads ~200.0, a steady session reads ~1.0). Count is how many turns the ring actually holds. All zero for an empty ring, so a fresh session renders blank rather than a divide-by-zero.

type CumulativeEnvelope added in v0.42.0

type CumulativeEnvelope struct {
	// contains filtered or unexported fields
}

CumulativeEnvelope folds attributed deltas for exactly one session. A trip is latched: subsequent Observe calls return the same checkpoint receipt instead of repeating warnings or silently moving the recovery pointer after intervention.

func NewCumulativeEnvelope added in v0.42.0

func NewCumulativeEnvelope(policy CumulativeEnvelopePolicy) *CumulativeEnvelope

NewCumulativeEnvelope returns an empty per-session fold governed by policy.

func (*CumulativeEnvelope) Observe added in v0.42.0

Observe folds one session-attributed delta and returns either CONTINUE or one latched CHECKPOINT_RECOVERY receipt. It never mutates State or Outcome, so the existing denial and run-state control planes remain at least as restrictive as they were before this accounting signal was attached.

type CumulativeEnvelopeAction added in v0.42.0

type CumulativeEnvelopeAction uint8

CumulativeEnvelopeAction is the closed intervention vocabulary returned by CumulativeEnvelope.Observe.

const (
	EnvelopeActionContinue CumulativeEnvelopeAction = iota
	EnvelopeActionCheckpointRecovery
)

func (CumulativeEnvelopeAction) String added in v0.42.0

func (a CumulativeEnvelopeAction) String() string

type CumulativeEnvelopeDecision added in v0.42.0

type CumulativeEnvelopeDecision struct {
	Action             CumulativeEnvelopeAction
	Reason             SessionEnvelopeReason
	Totals             CumulativeEnvelopeTotals
	EquivalentRefusals int
	RefusalDensity     float64
	Outcome            ToolCallOutcome
	Recovery           SessionRecoveryCheckpoint
}

CumulativeEnvelopeDecision is the fold's current verdict. Outcome is copied verbatim from the latest sample: a CHECKPOINT_RECOVERY action therefore cannot accidentally weaken or rewrite the tool denial that contributed to it.

type CumulativeEnvelopePolicy added in v0.42.0

type CumulativeEnvelopePolicy struct {
	MaxUncachedInputTokens int64   `json:"max_uncached_input_tokens,omitempty"`
	MaxWallTimeNanos       int64   `json:"max_wall_time_nanos,omitempty"`
	MaxEquivalentRefusals  int     `json:"max_equivalent_refusals,omitempty"`
	MinRefusalDensity      float64 `json:"min_refusal_density,omitempty"`
}

CumulativeEnvelopePolicy is the resolved policy for one session. Non-positive maxima are unlimited/disabled, matching this package's existing unbounded envelope convention. MinRefusalDensity is the fraction of observed tool calls that must be refused before MaxEquivalentRefusals can trip; zero disables the density qualifier while retaining the equivalent-refusal bound.

type CumulativeEnvelopeSample added in v0.42.0

type CumulativeEnvelopeSample struct {
	InputTokens         int64
	CachedInputTokens   int64
	OutputTokens        int64
	ModelCalls          int
	ToolCalls           int
	ManualContinuations int
	WallTimeNanos       int64
	Outcome             ToolCallOutcome
}

CumulativeEnvelopeSample is one independently attributed delta. InputTokens includes CachedInputTokens; the fold charges their non-negative difference to UncachedInputTokens. Outcome is the latest tool outcome, if any. ToolCalls must include Outcome when set; a refused Outcome with ToolCalls==0 is conservatively counted as one call so refusal density cannot divide by zero.

type CumulativeEnvelopeTotals added in v0.42.0

type CumulativeEnvelopeTotals struct {
	InputTokens         int64 `json:"input_tokens"`
	CachedInputTokens   int64 `json:"cached_input_tokens"`
	UncachedInputTokens int64 `json:"uncached_input_tokens"`
	OutputTokens        int64 `json:"output_tokens"`
	ModelCalls          int   `json:"model_calls"`
	ToolCalls           int   `json:"tool_calls"`
	ManualContinuations int   `json:"manual_continuations"`
	Refusals            int   `json:"refusals"`
	WallTimeNanos       int64 `json:"wall_time_nanos"`
}

CumulativeEnvelopeTotals is the cumulative, never-reset accounting folded from session-attributed deltas. Cached input remains explicit and non-zero-cost; only the difference is compared with the uncached envelope.

type DailyTokenStats added in v0.44.0

type DailyTokenStats struct {
	Date                  string `json:"date"`
	Sessions              int    `json:"sessions"`
	Sampled               int    `json:"telemetry_sessions"`
	CumulativeInputTokens int64  `json:"cumulative_input_tokens"`
	Fires                 int    `json:"fires"`
	ResidentTokensShed    int64  `json:"resident_tokens_shed"`
}

CompactAggregate is the fleet-wide roll-up across many rollouts. DailyTokenStats attributes session-level work to the rollout start day and fire-level effects to the fire timestamp. It makes multi-day audits comparable without pretending cumulative provider input is resident context or compaction savings.

type DenyAllBreaker added in v0.38.0

type DenyAllBreaker struct {
	// Threshold is the consecutive-stuck turn count at which Observe returns a
	// bounded stop. <=0 falls back to DenyAllDefaultThreshold.
	Threshold int
	// FloorSource names the capability-floor origin the diagnostic points at for
	// recovery (the embedded guard policy path). Empty falls back to the canonical
	// embedded floor path so the diagnostic always names a concrete source.
	FloorSource string
	// contains filtered or unexported fields
}

DenyAllBreaker is the per-session bounded deny-all loop breaker. The zero value is usable: a zero Threshold falls back to DenyAllDefaultThreshold (so an unconfigured breaker bounds at the default rather than never firing). It is not safe for concurrent use without external serialization — like the rest of a session's per-turn fold, it is driven from one turn boundary at a time.

func (*DenyAllBreaker) Observe added in v0.38.0

Observe folds one served turn's deny-all shape and returns the verdict. It is the ONE call a loop driver makes per turn (the deny-all twin of Table.Decide). The decision is pure over (breaker state, observation); the diagnostic is built only on a stop so the hot path pays nothing for string formatting.

Stuck test (all four, mirroring the issue): Tool is non-empty (a call was refused), Progress is false (no useful work this turn), Tool equals the prior stuck turn's Tool, and Reason equals the prior Reason. A stuck turn that changes EITHER Tool or Reason re-seeds the run at 1 (a flap is not the same spin). A clean or progressing turn resets the run to 0.

func (*DenyAllBreaker) Reset added in v0.38.0

func (b *DenyAllBreaker) Reset()

Reset clears any in-progress stuck run. A loop driver calls this at an objective boundary (a new /goal, a session resume, a manual retry) to drop a stale streak without synthesizing a fake clean observation — so a breaker carried across objectives cannot false-stop on a fresh goal because of a run the previous goal accrued. It is idempotent and a no-op on an already-clear breaker.

type DenyAllDisposition added in v0.38.0

type DenyAllDisposition int

DenyAllDisposition classifies the refused tool so the diagnostic's recovery line matches the failure class. The SAME bounded-stop fires either way (fail-closed is structural); only the advice differs — a coverage fix for plumbing, a "do not allow-list" warning for an effectful tool.

const (
	// DenyAllHostPlumbing marks an orchestration / read-only host tool
	// (update_plan, tool_search, MCP list/read, planning/state helpers). A
	// DEFAULT_DENY here is a harness-dialect COVERAGE problem — the tool's schema
	// is plan-state / read-only, so the recovery is to admit it on the floor, not
	// to weaken the guard.
	DenyAllHostPlumbing DenyAllDisposition = iota
	// DenyAllEffectful marks a write / shell / mutation tool. The floor is CORRECT
	// to deny it; the recovery must NOT auto-allow it without mirroring the
	// dangerous-command argument rules the named shell aliases carry.
	DenyAllEffectful
)

func (DenyAllDisposition) String added in v0.38.0

func (d DenyAllDisposition) String() string

String renders the disposition as the lowercase noun the diagnostic embeds.

type DenyAllObservation added in v0.38.0

type DenyAllObservation struct {
	// Tool is the name of the only/dominant tool call the floor refused this turn
	// (e.g. "update_plan"). Empty means the turn had NO refused call — a clean or
	// pure-text turn that resets the run.
	Tool string
	// Reason is the refusal reason token (DEFAULT_DENY in the observed case). It is
	// part of the "unchanged" test: a run only counts while the reason is identical
	// across turns, so a flap between DEFAULT_DENY and a policy deny re-seeds.
	Reason string
	// Progress is true if the turn made useful tool progress — at least one
	// surviving call, or meaningful non-tool work. A turn with Progress resets the
	// run (the loop is NOT stuck if it is getting somewhere).
	Progress bool
	// Disposition classifies Tool for the diagnostic's recovery line. It does not
	// change the decision — the bounded stop fires either way.
	Disposition DenyAllDisposition
}

DenyAllObservation is one served turn's deny-all shape, fed to the breaker. It is the minimal information needed to run the issue's four-criterion stuck test.

type DenyAllVerdict added in v0.38.0

type DenyAllVerdict struct {
	// Continue is true when the loop may keep auto-continuing (under threshold, or
	// the turn reset the run). False once the bounded stop fires.
	Continue bool
	// Stopped is true exactly when the consecutive-stuck threshold was reached this
	// turn — the loop must stop re-prompting. Mutually exclusive with Continue.
	Stopped bool
	// Consecutive is the current run depth AFTER this observation (0 on a reset, 1
	// for the first stuck turn, up to Threshold on the stop turn).
	Consecutive int
	// Threshold is the effective bound the verdict was measured against (the
	// breaker's configured value or the default), surfaced so a caller rendering
	// the diagnostic does not need to re-derive it.
	Threshold int
	// Reason is the closed stop token (ReasonDenyAllLoop) when Stopped, else "".
	Reason string
	// Diagnostic is the surfaced explanation when Stopped, else "". It names the
	// refused tool, the reason, the disposition, the floor source, and the
	// disposition-specific recovery — never a recommendation to auto-allow.
	Diagnostic string
}

DenyAllVerdict is the result of folding one observation. Continue is the common case; Stopped ends auto-continuation with a closed reason and a diagnostic.

type Descriptor added in v0.35.0

type Descriptor struct {
	// ID is the stable, addressable key — the guard --session-id (defaulted to a
	// content/host-derived id when unset). Re-registering the same ID is idempotent.
	ID string `json:"id"`
	// Host names where the session runs, so an index spanning hosts stays addressable.
	Host string `json:"host,omitempty"`
	// PID is the hosting fak process id. The wrapped child may be relaunched under
	// the same descriptor; the durable owner is the guard/serve process maintaining
	// the session table.
	PID int `json:"pid,omitempty"`
	// Argv is the wrapped command vector the host is driving. It is copied on write
	// so a caller cannot mutate a stored descriptor by retaining the input slice.
	Argv []string `json:"argv,omitempty"`
	// StartSHA is the git HEAD the host observed at registration time, when one was
	// available. It is a pointer for operators, not a trust decision.
	StartSHA string `json:"start_sha,omitempty"`
	// PCBState is the human/index form of Run: RUNNING/THROTTLED/PAUSED/DRAINING/
	// STOPPED. Run remains the typed field Table.Restore consumes.
	PCBState string `json:"pcb_state,omitempty"`
	// CacheKey is the stable prompt/cache lineage key the host derived for this
	// session. It is opaque to the registry.
	CacheKey string `json:"cache_key,omitempty"`
	// Trace is the live Table key (State.TraceID) the descriptor mirrors. It MAY differ
	// from ID (a re-homed session keeps its ID but takes a new trace), which is why both
	// are carried — the restart re-attaches the persisted State under this Trace.
	Trace string `json:"trace"`
	// ParentID is the id of the session this one was FORKED from (issue #1200). It is set
	// only on a branch descriptor and links the fork to its parent in the registry, so the
	// lineage a `fak session branch` minted is an addressable fact here as well as in the
	// branch image's migration log. Empty for a normal (non-branched) session.
	ParentID string `json:"parent_id,omitempty"`
	// Run is the persisted PCB position. A restart re-attaches at THIS state, not the
	// Running default — a Stopped descriptor restores Stopped, never silently resurrected.
	Run RunState `json:"run"`
	// Budget / Priority / Pace / Generation mirror the live State fields so a restart
	// resumes at the real allotment / rank / throttle / re-continuation depth, not at
	// defaults.
	Budget     Budget `json:"budget"`
	Priority   int    `json:"priority"`
	Pace       Pace   `json:"pace"`
	Generation int    `json:"generation,omitempty"`
	// Reason is the closed token on a Throttled/Stopped descriptor ("" otherwise), carried
	// so a restart of a terminal session still reports WHY it stopped.
	Reason string `json:"reason,omitempty"`
	// CacheAffinity mirrors State.CacheAffinity so a process restart does not erase
	// whether a continuation preserved or changed provider/engine cache affinity.
	CacheAffinity CacheAffinityDecision `json:"cache_affinity,omitempty,omitzero"`
	// ResetTransaction mirrors State.ResetTransaction so a child trace restored after a
	// process restart still carries the replayable reset row that minted it.
	ResetTransaction ResetTransaction `json:"reset_transaction,omitempty,omitzero"`
	// ObjectivePin mirrors State.ObjectivePin (issue #1589) so a session migrated to a
	// new process — a hidden restart, a re-home to another host, or a sessionimage
	// dump/restore — still reports the same pinned objective (PinID + content Digest)
	// it held before migration, instead of silently dropping the managed-context
	// continuity contract #1583 established for in-process resets.
	ObjectivePin ctxplan.ObjectivePin `json:"objective_pin,omitempty,omitzero"`
	// PendingTurn mirrors State.PendingTurn (issue #1363) — the write-ahead
	// checkpoint of an in-flight turn's retry progress — so a restart re-attaches
	// knowing how far a lost turn had gotten, not with that progress silently
	// dropped. The zero value means no turn was in flight.
	PendingTurn PendingTurn `json:"pending_turn,omitempty,omitzero"`
	// Time mirrors the live State's wall-clock budget (issue #1584): the persisted
	// LimitNanos/ElapsedNanos/StartedAtUnixNano so a process restart re-attaches the
	// accumulated elapsed time, not a zeroed clock. descriptorFromState copies whatever
	// TimeBudget the caller's State carries verbatim — Register/Update do not themselves
	// call Pause before persisting, so a descriptor snapshotted mid-tick (StartedAtUnixNano
	// set) is possible if the process dies before an explicit shutdown-time Pause. That is
	// fine by construction: RestoredState below always loads Time back through
	// TimeBudget.restoredPaused, which discards a live StartedAtUnixNano rather than
	// trusting a wall-clock instant from a (possibly now-dead) process, so the durably-
	// stored clock is never resumed ticking from a stale instant regardless of when the
	// snapshot was taken. A caller that DOES pause cleanly before a graceful shutdown
	// (Table.PauseTimeBudget) simply gets a descriptor whose ElapsedNanos is already
	// exact and whose StartedAtUnixNano is already 0 — restoredPaused is then a no-op.
	Time TimeBudget `json:"time,omitempty,omitzero"`
	// LastActive mirrors State.LastActive (issue #1179, the dormancy-clock epic #1178) so a
	// session re-attached after a process restart keeps its durable LastActiveAt stamp — and
	// therefore its derivable dormancy band (dormancy.Stamp.HorizonAt) — instead of presenting
	// as never-active. That distinction is load-bearing even in Phase 1: a zero stamp buckets
	// to Ancient (HorizonAt returns the most-stale band on an unknown gap), so DROPPING the
	// stamp across a restart would silently claim maximal dormancy and, once the rehydration
	// rungs (#1181-#1186) read it, force needless full revalidation. Advisory/no-behavior-
	// change like the State field: carried, never gated; omitzero keeps a pre-clock descriptor
	// wire-identical.
	LastActive dormancy.Stamp `json:"last_active,omitempty,omitzero"`
	// Rev is the live State's monotonic revision at the last stamp — the optimistic-
	// concurrency cursor, preserved so an operator UI that held an If-Rev across the
	// restart still composes (the same Rev-preservation discipline as Table.Restore).
	Rev uint64 `json:"rev"`
	// CreatedAt is set once on register; UpdatedAt / LastSeen are re-stamped on every
	// drive change. LastSeen drives the TTL sweep — a descriptor older than its TTL is
	// stale and GC'd. TTL <= 0 means "use DefaultDescriptorTTL".
	CreatedAt time.Time     `json:"created_at"`
	UpdatedAt time.Time     `json:"updated_at"`
	LastSeen  time.Time     `json:"last_seen"`
	TTL       time.Duration `json:"ttl,omitempty"`
}

Descriptor is the small durable index record for one live session — the persisted projection of its drive State plus the pointers a restart needs to re-attach it. It is deliberately a PROJECTION (it reuses State's RunState/Budget/Priority/Rev/ Generation, adding no drive field) so the live Table stays the single source of the drive and the Descriptor never drifts into a second, competing copy of policy.

The TRANSCRIPT is NOT here (and never will be): the conversation lives in the provider's / Claude Code's own store and sessionimage deliberately excludes it for privacy. The Descriptor carries DRIVE STATE + POINTERS only.

func (Descriptor) RestoredState added in v0.35.0

func (d Descriptor) RestoredState() State

RestoredState rebuilds the drive State a restart re-attaches into the live Table from this descriptor — the load-time inverse of descriptorFromState. It carries the persisted Run/Budget/Priority/Generation/Reason/Rev under the descriptor's Trace, so Table.Restore(d.Trace, d.RestoredState()) re-attaches the session at its REAL state, not DefaultState's Running/unbounded default. The Rev is preserved (Restore does not bump it), so a Snapshot -> Descriptor -> RestoredState -> Restore round-trip is the identity on the drive fields.

type DescriptorMeta added in v0.35.0

type DescriptorMeta struct {
	PID      int
	Argv     []string
	StartSHA string
	CacheKey string
	ParentID string // set on a branch (#1200): links the forked descriptor to its parent
}

DescriptorMeta is the host-owned pointer metadata stamped into a Descriptor at register/update time. It deliberately carries no drive state; descriptorFromState remains the only source for the live PCB projection.

type DescriptorStore added in v0.35.0

type DescriptorStore interface {
	Put(d Descriptor) error
	Delete(id string) error
	List() ([]Descriptor, error)
}

DescriptorStore is the pluggable persistence seam the Registry writes through — the only boundary between the in-memory index and durable storage. A production host wires a sessionimage-backed store (composing the session.json writer); a test wires MemStore. The Registry never imports a filesystem, so the package stays a foundation leaf and the persistence backend is swapped without touching the register / update / GC core.

Put writes (or overwrites — idempotent by ID) one descriptor. Delete removes one by ID (the GC reap). List returns every persisted descriptor (unordered; the Registry sorts). All three may return an error the Registry surfaces to its caller; none is called under the Registry lock for an unbounded duration (the store owns its own I/O concurrency).

type Envelope added in v0.37.0

type Envelope struct {
	// Tokens caps total output tokens across the run. <0 (EnvelopeUnbounded) means
	// no cap; 0 means "not stated" (parses to Budget's Unbounded default); >0 is a
	// real ceiling.
	Tokens int `json:"tokens,omitempty"`
	// WallClock caps real elapsed time across the run's whole lineage. <=0 means
	// "not stated" (unbounded); this is the one axis with no "unbounded" string
	// form, since a zero/absent duration already means unbounded.
	WallClock time.Duration `json:"wall_clock,omitempty"`
	// Turns caps the number of model round-trips. Same tri-state convention as
	// Tokens: <0 explicit-unbounded, 0 not-stated, >0 a real ceiling.
	Turns int `json:"turns,omitempty"`
	// SpendCapCents caps the run's rough dollar cost, in integer cents (avoiding a
	// float money type). 0 means not stated. Advisory only today — see file header;
	// no runtime path debits it yet, but it round-trips through Parse/inspect so a
	// user's stated ceiling is never silently dropped.
	SpendCapCents int64 `json:"spend_cap_cents,omitempty"`
	// ThroughputFloor is the minimum tokens/sec the user expects this run to
	// sustain — the user-facing twin of Throughput.ExpectedTokensPerSec. 0 means
	// not stated (no expectation configured, exactly Throughput's zero-value
	// convention).
	ThroughputFloor float64 `json:"throughput_floor,omitempty"`
}

Envelope is the user-stated budget goal: the plain, product-facing contract for "how much may this managed run cost, on every axis it might cost something." Every field is independently optional (zero = "the user expressed no opinion on this axis"), so a user may state just one axis (e.g. only WallClock) and get a deterministic parse that leaves every other axis unbounded — never a surprise cap on an axis nobody mentioned.

func ParseEnvelopeFlags added in v0.37.0

func ParseEnvelopeFlags(tokens, wallClock, turns, spend, throughputFloor string) (Envelope, error)

ParseEnvelopeFlags parses the CLI's flat string-flag form into an Envelope — the one place the "unbounded" string token, a duration string ("10m"), and a dollar string ("$5.00" or "500c") are interpreted. Every argument is optional: an empty string means "not stated" for that axis, so a caller only sets the flags it read from the command line. A non-empty, unparsable value is a hard error (fail closed on a malformed user envelope rather than silently ignoring the axis).

func (Envelope) IsZero added in v0.37.0

func (e Envelope) IsZero() bool

IsZero reports whether the envelope states no opinion on any axis — the safe default a caller reads as "no envelope was requested" before doing any parsing work or attaching a budget to a run.

func (Envelope) Parse added in v0.37.0

func (e Envelope) Parse() ParsedEnvelope

Parse folds the projections into the one deterministic artifact a caller inspects. It performs no I/O and no clock read (WallClock is a duration, not a start time — ToTimeBudget's Start(now) is the caller's job at the moment the run actually begins), so the same Envelope always parses to a byte-identical ParsedEnvelope.

func (Envelope) ToBudget added in v0.37.0

func (e Envelope) ToBudget() Budget

ToBudget projects the token/turn axes onto a Budget, in the exact Unbounded (-1) / not-configured (0 context) shape Decide/DebitUsage already consume. Context and clarification-query axes are left at their DefaultState zero (this envelope layer speaks the product-facing axes the issue names; a caller wanting a context cap too still sets Budget.ContextTokensLeft directly, unchanged).

func (Envelope) ToPace added in v0.37.0

func (e Envelope) ToPace() Pace

ToPace projects the throughput floor onto a Pace's expectation half. Pace itself carries no throughput field (see compose.go's file header: Throughput is deliberately a standalone type, not fields on Pace), so ToPace returns the zero Pace (no per-turn MaxTokensPerTurn/MinTurnGapMs opinion) — callers wanting the throughput floor read ToThroughput instead. Kept as a named method (rather than omitted) so ParsedEnvelope's shape is self-documenting: an envelope's per-turn pace opinion is always the runtime default unless a caller composes one separately.

func (Envelope) ToThroughput added in v0.37.0

func (e Envelope) ToThroughput() Throughput

ToThroughput projects the throughput floor onto a Throughput's expected-rate axis, leaving ObservedTokensPerSec at its zero ("no observation yet") — the runtime measures the observed rate; the envelope only states the floor it is judged against.

func (Envelope) ToTimeBudget added in v0.37.0

func (e Envelope) ToTimeBudget() TimeBudget

ToTimeBudget projects the wall-clock axis onto a TimeBudget with WithLimit — a non-positive/absent WallClock yields the unbounded zero value, exactly TimeBudget's own "not configured" convention, so an envelope that never mentions wall-clock produces a TimeBudget byte-identical to NewTimeBudget().

type FileStore added in v0.35.0

type FileStore struct {
	// contains filtered or unexported fields
}

FileStore persists Descriptor rows into one JSON file. It is the production DescriptorStore for the live session registry: Put/Delete rewrite the small descriptor index, while List reads the current file back. The file is an index of drive state only, not a transcript.

func NewFileStore added in v0.35.0

func NewFileStore(path string) *FileStore

NewFileStore returns a DescriptorStore backed by path.

func (*FileStore) Delete added in v0.35.0

func (s *FileStore) Delete(id string) error

Delete removes id from the file. Deleting a missing id is a no-op.

func (*FileStore) List added in v0.35.0

func (s *FileStore) List() ([]Descriptor, error)

List returns every descriptor currently persisted in the file.

func (*FileStore) Put added in v0.35.0

func (s *FileStore) Put(d Descriptor) error

Put writes one descriptor keyed by ID, replacing any prior row for that ID. The cross-process lock orders writers: for the same ID, the last Put that acquires the lock wins, regardless of the descriptor's embedded Rev value.

type Goal added in v0.35.0

type Goal struct {
	// ID is the opaque goal/root identifier — a digest or the /goal id, structural only.
	// "" means no goal is set (the zero value). NEVER the goal text or a transcript.
	ID string `json:"id,omitempty"`
	// Priority is the OPTIONAL scheduling rank this goal lends its session (lower yields
	// first, matching State.Priority's convention). 0 = no opinion; the scheduler falls
	// back to State.Priority.
	Priority int `json:"priority,omitempty"`
	// Budget is the OPTIONAL token budget the goal is granted. 0 = no opinion.
	Budget int `json:"budget,omitempty"`
}

Goal is the structural root descriptor carried on State (issue #849). It names the session's active goal so a scheduler reading Table.Snapshot can rank by it — the cross-session counterpart of the in-window goal pin (#845) that today lives only in SessionPlanner.pins(). It is deliberately data-only: an opaque ID (a digest or /goal id, never the goal text or a transcript), an optional scheduling Priority, and an optional token Budget. Every field defaults to the safe "no opinion" zero value.

FENCE: advisory, never trust. A goal root affects RETENTION/ranking, never the answer — a scheduler MAY order a session by it but MUST behave identically when it is absent. The ranking reader is still pending — no scheduler orders by Goal today — but the field is NOT consumer-free: as of b588466054 cumulative_envelope.go copies State.Goal into SessionRecoveryCheckpoint, so a tripped envelope checkpoints the active root instead of losing it. Re-check both claims against the tree before trusting them.

func (Goal) IsZero added in v0.35.0

func (g Goal) IsZero() bool

IsZero reports whether the goal carries no root — the safe default a scheduler reads as "this session has no active goal to rank by". A consumer checks this before acting on any field, so an unset goal is never mistaken for a positive root. It also drives the `omitzero` JSON tag so a goal-less State marshals byte-identically to today.

type IncompatibleSchemaError added in v0.42.0

type IncompatibleSchemaError struct {
	FileVersion string
	Supported   string
}

IncompatibleSchemaError reports that the durable session ledger carries a well-formed schema header (descriptorFileMagic) at a version this fak build does not support — an incompatible schema jump across a live-deployment upgrade seam (#3424). Unlike CorruptDescriptorFileError it is NOT quarantined: the records are intact but unreadable by this version, so the runtime refuses to start against them rather than partially migrating or silently dropping live sessions. Recover by rolling back to a fak build that supports FileVersion, or by running a forward migration for Supported.

func (*IncompatibleSchemaError) Error added in v0.42.0

func (e *IncompatibleSchemaError) Error() string

type KVSpanPointer added in v0.38.0

type KVSpanPointer struct {
	Kind string `json:"kind,omitempty"`
	Ref  string `json:"ref,omitempty"`
}

func (KVSpanPointer) IsZero added in v0.38.0

func (p KVSpanPointer) IsZero() bool

type MemStore added in v0.35.0

type MemStore struct {
	// contains filtered or unexported fields
}

MemStore is the in-memory DescriptorStore — the test backend and the byte-identical reference implementation the durable store must behave like. It is concurrency-safe (its own mutex) and keeps the latest descriptor per ID, so a re-Put of the same ID overwrites rather than duplicates (the idempotence the Registry relies on). The zero MemStore is not usable; construct with NewMemStore.

func NewMemStore added in v0.35.0

func NewMemStore() *MemStore

NewMemStore returns an empty in-memory DescriptorStore.

func (*MemStore) Delete added in v0.35.0

func (s *MemStore) Delete(id string) error

Delete removes the descriptor for id. Deleting a missing id is a no-op (the GC reap is idempotent — a descriptor swept twice is not an error).

func (*MemStore) List added in v0.35.0

func (s *MemStore) List() ([]Descriptor, error)

List returns a copy of every persisted descriptor (unordered). The slice is freshly allocated, so a caller may sort/mutate it without racing the store.

func (*MemStore) Put added in v0.35.0

func (s *MemStore) Put(d Descriptor) error

Put writes one descriptor keyed by its ID, overwriting any prior record for that ID (idempotent). A blank ID is rejected so a malformed descriptor cannot occupy the empty key.

type Pace

type Pace struct {
	MaxTokensPerTurn int `json:"max_tokens_per_turn"` // 0 = planner default
	MinTurnGapMs     int `json:"min_turn_gap_ms"`     // 0 = no inter-turn delay
}

Pace is the per-turn throttle — how to slow a session WITHOUT pausing it. It is admission control's cooperative twin: lowering MaxTokensPerTurn gives a shared GPU/CPU budget to an urgent session while the slow one keeps making progress. MaxTokensPerTurn caps THIS turn's output (lowered into the planner via agent.WithMaxTokens); MinTurnGapMs spaces turns apart. Zero on either axis means "no opinion" — the planner's own default stands, byte-identical to the pre-table path.

func (Pace) Compose added in v0.35.0

func (p Pace) Compose(basePlannerBudget, baselineOutput int) ComposedBudgets

Compose folds both derived budgets (and the ratio that produced them) into one record — the single call a consumer makes to turn the Pace knob into the two real budgets. It is a pure projection of ComposePlannerBudget + ComposeWorkerFraction over the same baseline.

func (Pace) ComposePace added in v0.37.0

func (p Pace) ComposePace(t Throughput, basePlannerBudget, baselineOutput int) int

ComposePace folds BOTH pace signals — the configured MaxTokensPerTurn cap (p) and the observed runtime Throughput (t) — into a single resident-context window: whichever signal is more constraining (the smaller of the two composed budgets) wins, so a session that is both throttled by configuration AND running behind its expected throughput gets the harder of the two shrinks, never the milder one silently overriding the other. This is the one-call entry point a caller composing BOTH #628's configured pace and #1585's observed throughput into one planner Budget should use, in place of calling ComposePlannerBudget and ComposePlannerBudgetForThroughput separately and having to reconcile them by hand.

func (Pace) ComposePlannerBudget added in v0.35.0

func (p Pace) ComposePlannerBudget(basePlannerBudget, baselineOutput int) int

ComposePlannerBudget scales a base resident-context window down by this Pace's throttle ratio, floored at base/MinPlannerBudgetDivisor so a hard throttle never starves the window below a usable size. A non-positive base is returned unchanged (nothing to scale); an unthrottled Pace (ratio 1.0) returns the base verbatim, so an un-paced session's planner budget is byte-for-byte what it was before this composition existed. The result is the value to assign to agent.SessionPlanner.Budget.

func (Pace) ComposeWorkerFraction added in v0.35.0

func (p Pace) ComposeWorkerFraction(baselineOutput int) float64

ComposeWorkerFraction is the matmul FAK_BUDGET fraction in (0,1] this Pace asks the cores to run at — the throttle ratio directly: a session paced to half its baseline output runs its forward pass on (about) half the machine. The value is shaped to feed model.SetWorker- Budget, which floors any positive fraction to at least one worker, so a deep throttle slows a session without ever zeroing its compute. SOUND ONLY in a single-session process (the file header fence): the matmul pool is process-global.

func (Pace) ThrottleRatio added in v0.35.0

func (p Pace) ThrottleRatio(baselineOutput int) float64

ThrottleRatio is the fraction in (0,1] by which this Pace throttles a session's per-turn work, relative to baselineOutput — the session's unthrottled per-turn output target. It is 1.0 (no throttle) when this Pace voices no opinion (MaxTokensPerTurn <= 0), when there is no baseline to scale against (baselineOutput <= 0), or when the cap sits at or above the baseline (a cap that does not actually lower the turn is not a throttle). Otherwise it is the quotient MaxTokensPerTurn/baselineOutput, a value in (0,1). It is the shared primitive both ComposePlannerBudget and ComposeWorkerFraction round identically against.

type ParsedEnvelope added in v0.37.0

type ParsedEnvelope struct {
	Envelope   Envelope   `json:"envelope"`
	Budget     Budget     `json:"budget"`
	TimeBudget TimeBudget `json:"time_budget"`
	Pace       Pace       `json:"pace"`
}

ParsedEnvelope is the deterministic output a user (or a test) inspects: the Envelope they stated, plus the three runtime primitives it produces. It is the one artifact `fak session envelope` prints and the one shape a witness test compares for equality — never a narrated "budget looks right".

type PendingTurn added in v0.38.0

type PendingTurn struct {
	// Attempt is the retry attempt number in progress (1 = first attempt). 0 means
	// no attempt has been checkpointed yet.
	Attempt int `json:"attempt,omitempty"`
	// LastStatus is the last HTTP status the retry loop observed (e.g. 429, 503).
	// 0 means none observed yet.
	LastStatus int `json:"last_status,omitempty"`
	// StartedAtUnixNano is the wall-clock instant (unix nanoseconds) this turn
	// began, so a restart can tell how long the lost turn had been running. It is a
	// timestamp, not a monotonic clock reading (the same TimeBudget.StartedAtUnixNano
	// discipline: a value that must survive a JSON round-trip and a process restart
	// cannot depend on a monotonic reading that dies with the process). Zero means
	// no turn is checkpointed.
	StartedAtUnixNano int64 `json:"started_at_unix_nano,omitempty"`
}

PendingTurn is one in-flight turn's write-ahead checkpoint (issue #1363): how many retry attempts it has made, the last HTTP status observed, and when the turn started. It is deliberately narrow — just enough for a restart to tell "a turn was mid-retry, this far along" — not a replay log of the turn's messages (those stay in the provider's / Claude Code's own transcript store, matching the Descriptor's existing drive-state-not-transcript fence).

func (PendingTurn) IsZero added in v0.38.0

func (p PendingTurn) IsZero() bool

IsZero reports whether no turn is currently checkpointed — the safe default a restart reads as "nothing was in flight". A consumer checks this before treating the fields as a real in-progress attempt.

type Policy added in v0.35.0

type Policy uint8

Policy selects how Pick breaks contention among the live, eligible sessions that share one gateway. The zero value is StrictPriority — the safe, trivially-correct default that simply honors the snapshot's existing sort.

const (
	// StrictPriority returns the FIRST eligible session in Snapshot order. Snapshot is
	// already sorted (Priority ascending, then Rev descending, then TraceID), so this
	// is deterministic and correct by construction: the lowest Priority value that is
	// eligible wins, and a budget cut / priority raise re-sorts the snapshot the next
	// time Pick reads it.
	StrictPriority Policy = iota
	// WeightedFair returns a deterministic weighted round-robin winner, giving a lower
	// Priority value a proportionally larger share of the picks while still letting
	// every eligible session make progress. See pickWeightedFairLocked for the exact
	// algorithm (smooth weighted round-robin — no clock, no randomness).
	WeightedFair
)

func (Policy) String added in v0.35.0

func (p Policy) String() string

String renders a Policy as its lowercase token; an out-of-range value renders "unknown" rather than panicking, matching the rest of the package's enums.

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

pool.go — the OPTIONAL fleet-wide budget pool (#744). Each session's Budget is otherwise INDEPENDENT: N served sessions each carry their own TokensLeft, and a budget-reset (Recontinue) re-arms a session from a fresh PER-SESSION allotment with no awareness of its siblings — so N sessions resetting under "150k each" silently consume up to N×150k. A Pool turns that into a SHARED ceiling: one token allotment that many sessions DRAW from, so a whole fan-out can be held under a single cap (e.g. a 150k pool across N sessions) instead of N independent caps that sum without bound.

The pool is deliberately a SEPARATE value from Budget, not a field on it. A Budget is a single session's remaining allotment, debited per turn by Decide/DebitUsage; a Pool is the cross-session ceiling a RESET draws a fresh allotment OUT OF. Keeping them distinct is what lets the pool stay OPTIONAL — a nil *Pool means "no shared cap," and every per-session path is byte-identical to today. The pool never touches the per-turn hot path (Decide/DebitUsage stay pool-free, lock-free of the pool); it is consulted only at a reset boundary, where a fresh window is armed, and read by the Snapshot surface, which reports the fleet ceiling alongside the per-session State rows.

Concurrency: a Pool is safe for concurrent use by many sessions' reset boundaries. It guards a single remaining counter under a Mutex — the draws are short critical sections far off the per-turn path, so a plain Mutex is right (no need for the Table's RWMutex read/write split). A nil *Pool is a valid UNBOUNDED pool: every method is nil-safe so a host that wired no pool calls the same API and gets the no-shared-cap behavior.

func NewPool

func NewPool(total int) *Pool

NewPool returns a fleet-wide budget pool seeded with total tokens. A total <= 0 builds an UNBOUNDED pool: Draw always grants the full request and Remaining reports Unbounded, so constructing a pool is never, by itself, a tightening — only a positive ceiling bounds. The pool starts full (remaining == total).

func (*Pool) Cap

func (p *Pool) Cap() int

Cap reports the configured fleet-wide ceiling (0 = unbounded). It never changes after construction, so it is read without the lock.

func (*Pool) Draw

func (p *Pool) Draw(want int) (granted int, ok bool)

Draw takes up to want tokens from the pool for a fresh session window, returning how many were GRANTED (0..want) and whether the full request was met. An unbounded pool (or a nil receiver) grants want in full — a missing pool never tightens. A bounded pool grants min(want, remaining) and debits exactly that, so the sum of all live draws can never exceed the ceiling: when the pool runs dry a reset gets granted==0/ok==false (and a partial grant likewise returns ok==false), so a host that wants a hard fleet stop can decline the continuation instead of minting an over-budget window. A non-positive want grants 0/true — nothing requested, nothing refused.

func (*Pool) Remaining

func (p *Pool) Remaining() int

Remaining reports the tokens still available to draw. An unbounded (or nil) pool reports Unbounded (-1), distinguishing "no shared cap" from a bounded pool that is exactly dry (0).

func (*Pool) Report

func (p *Pool) Report() PoolReport

Report renders the pool's current fleet-wide state for the Snapshot surface. It is a consistent read — Cap, Drawn, and Remaining are captured under one lock. A nil/unbounded pool reports Bounded=false with Remaining=Unbounded and Drawn=0, so a host that wired no pool still emits a well-formed (no-ceiling) row.

func (*Pool) Return

func (p *Pool) Return(n int)

Return puts n tokens back into a bounded pool — the inverse of Draw, for when a drawn allotment is released (a session ends without spending it, or a reset is rolled back). It never raises remaining above the ceiling (a buggy double-Return cannot inflate the cap) and is a no-op on an unbounded/nil pool or a non-positive n.

type PoolReport

type PoolReport struct {
	Cap       int  `json:"cap"`       // configured fleet-wide ceiling; 0 = unbounded
	Drawn     int  `json:"drawn"`     // tokens handed out to sessions so far
	Remaining int  `json:"remaining"` // tokens still available; Unbounded(-1) = no shared cap
	Bounded   bool `json:"bounded"`   // whether a shared cap is enforced at all
}

PoolReport is the fleet-wide budget snapshot the scheduler/Snapshot surface emits alongside the per-session State rows: how big the shared ceiling is, how much N sessions have already drawn, and how much headroom a further reset has. Drawn is Cap-Remaining (0 for an unbounded pool); Remaining is Unbounded(-1) when there is no shared cap. Bounded distinguishes "a ceiling is enforced" from "no pool wired," so a consumer never mistakes an unbounded pool's zero Drawn for a fully-spent one.

type QualityEnvelope added in v0.38.0

type QualityEnvelope struct {
	// Budget is the budget envelope the session opened under (the same axes
	// budget_envelope.go parses), recorded so the origin record is self-contained.
	Budget BudgetEnvelope `json:"budget"`
	// WitnessPolicy names the evidence class the session's claims must satisfy — e.g.
	// "proof-by-default" (a captured artifact) or "dos-verify" (a plan/phase referee).
	WitnessPolicy string `json:"witness_policy,omitempty"`
	// DogfoodProbes are the at-origin scorecard/check probes expected to run for this
	// session (the QA-dogfood spine's "run the score where the work is created").
	DogfoodProbes []string `json:"dogfood_probes,omitempty"`
	// ScorecardCards are the control-pane scorecards this session is a member of (their
	// stable card keys, e.g. "code_quality", "milestone_scorecard").
	ScorecardCards []string `json:"scorecard_cards,omitempty"`
}

QualityEnvelope is the origin record of the QA controls that govern a session: the budget axes it opened under, the witness policy that gates its claims, the dogfood probes expected to run at origin, and the control-pane scorecards it is a member of. It is deterministic data only — a value a session snapshot carries, not runtime state. Canonical makes its byte form stable so it rides the image's sha256 integrity index like every other part.

func (QualityEnvelope) Canonical added in v0.38.0

func (e QualityEnvelope) Canonical() QualityEnvelope

Canonical returns a copy with the probe and scorecard lists deduplicated and sorted, so a fixed set of controls serializes to byte-identical bytes regardless of the order they were declared in — the determinism the image's integrity index and .faksession archive rely on. It never mutates the receiver's slices.

func (QualityEnvelope) IsZero added in v0.38.0

func (e QualityEnvelope) IsZero() bool

IsZero reports the permissive default: no witness policy, no probes, no scorecard membership, and a zero budget envelope. Supports treating an absent envelope as "no QA controls declared" without a nil pointer.

type QuarantineRetention added in v0.42.0

type QuarantineRetention struct {
	MaxCount int           // keep at most this many evidence files
	MaxAge   time.Duration // remove evidence older than this
	MaxBytes int64         // keep at most this many total evidence bytes
	Off      bool          // disable cleanup entirely
}

QuarantineRetention bounds how much quarantine evidence may accumulate beside one active registry. Zero-valued dimensions are unbounded; Off disables cleanup entirely (evidence is then kept forever, the pre-#4658 behavior).

func DefaultQuarantineRetention added in v0.42.0

func DefaultQuarantineRetention() QuarantineRetention

DefaultQuarantineRetention is deliberately conservative: it keeps plenty of evidence for diagnosis while guaranteeing repeated corruption cannot grow the user profile unbounded.

func ParseQuarantineRetention added in v0.42.0

func ParseQuarantineRetention(s string) (QuarantineRetention, error)

ParseQuarantineRetention parses an operator retention override: "" means the default policy, "off" disables cleanup, and a comma list of count=N, age=DURATION, bytes=N overrides individual dimensions (unset dimensions keep their defaults; 0 makes a dimension unbounded). On a parse error the default policy is returned so a typo can never disable retention or block startup.

type QueryBudgetVerdict added in v0.37.0

type QueryBudgetVerdict struct {
	Proceed   bool
	Stop      bool
	Reason    string
	Remaining int
	State     State
}

QueryBudgetVerdict is the clarification/self-query budget gate. It is separate from Verdict because a query-budget miss should degrade the clarification path, not stop the main session.

type RecoveryCause added in v0.42.0

type RecoveryCause string

RecoveryCause is the normalized, privacy-safe class of descriptor-index corruption. It names why the index could not be trusted without echoing any of its contents.

const (
	// RecoveryCauseDecode means the index was not valid JSON.
	RecoveryCauseDecode RecoveryCause = "decode"
	// RecoveryCauseVersion means the index carried an unsupported version tag.
	RecoveryCauseVersion RecoveryCause = "version"
	// RecoveryCauseBlankID means a descriptor row had no ID.
	RecoveryCauseBlankID RecoveryCause = "blank-id"
	// RecoveryCauseUnknown covers corrupt errors with no recorded class.
	RecoveryCauseUnknown RecoveryCause = "unknown"
)

func ClassifyRecoveryCause added in v0.42.0

func ClassifyRecoveryCause(err error) RecoveryCause

ClassifyRecoveryCause maps a restore error to its normalized recovery cause. Non-corrupt errors and untagged corrupt errors classify as unknown.

type RecoveryEvent added in v0.42.0

type RecoveryEvent struct {
	At              time.Time     `json:"at"`
	Cause           RecoveryCause `json:"cause"`
	Bytes           int64         `json:"bytes"`
	ActivePath      string        `json:"active_path"`
	Quarantined     bool          `json:"quarantined"`
	QuarantinePath  string        `json:"quarantine_path,omitempty"`
	QuarantineError string        `json:"quarantine_error,omitempty"`
}

RecoveryEvent is one privacy-safe corrupt-registry recovery observation. It records outcome, cause class, sizes and paths only — never descriptor contents.

type RecoveryStats added in v0.42.0

type RecoveryStats struct {
	Version            string         `json:"version"`
	Total              int            `json:"total"`
	QuarantineFailures int            `json:"quarantine_failures"`
	Causes             map[string]int `json:"causes,omitempty"`
	LastAt             time.Time      `json:"last_at"`
	LastCause          RecoveryCause  `json:"last_cause,omitempty"`
	LastBytes          int64          `json:"last_bytes"`
}

RecoveryStats is the cumulative, privacy-safe recovery ledger persisted beside the active registry. Missing or corrupt ledgers reset to zero: the ledger is itself rebuildable observability, never load-bearing state.

func ReadRecoveryStats added in v0.42.0

func ReadRecoveryStats(activePath string) (RecoveryStats, bool, error)

ReadRecoveryStats reads the recovery ledger beside activePath without taking any lock or creating any file: diagnostic surfaces must stay strictly read-only. A missing ledger returns ok=false with zero stats; a corrupt ledger returns ok=false so callers report "none recorded" rather than trusting garbage.

type Registry added in v0.35.0

type Registry struct {
	// contains filtered or unexported fields
}

Registry is the in-session DURABLE index of live descriptors (issue #1197). It owns the three moves — Register (on start), Update (on transition), and GC (TTL sweep at read time) — over a pluggable DescriptorStore. It is a thin, pure coordinator: it holds NO drive state of its own (the live Table is the source) and never reaches a filesystem (the store does). Construct with NewRegistry; a nil receiver is a no-op- permissive shell so a host with no registry wired behaves byte-identically.

func NewRegistry added in v0.35.0

func NewRegistry(store DescriptorStore) *Registry

NewRegistry builds a Registry over store. A nil store is replaced with a fresh MemStore so a registry is always usable (the caller does not need a nil check); a host that wants persistence wires the durable store explicitly.

func (*Registry) GC added in v0.35.0

func (r *Registry) GC(now time.Time) (int, error)

GC sweeps stale descriptors as of now and returns how many were reaped — the explicit form of the read-time sweep List performs, for a host that wants to age the index on a timer without listing. A nil receiver reaps nothing.

func (*Registry) Get added in v0.35.0

func (r *Registry) Get(id string) (Descriptor, bool, error)

Get returns the persisted descriptor for id and whether it is present. It does NOT sweep (a pure read of one row); a stale row is still returned so a caller can inspect it. A nil receiver reports absent.

func (*Registry) List added in v0.35.0

func (r *Registry) List(now time.Time) ([]Descriptor, error)

List returns every NON-stale descriptor as of now, sorted by ID for determinism, AND sweeps the stale ones — the read-time TTL-GC. A descriptor whose LastSeen is older than its TTL is Deleted from the store and excluded from the result; a fresh one (re-stamped within its TTL) is never reaped, so the sweep never kills a live lease. The sweep is best-effort: a Delete error does not abort the listing (the row is simply omitted and retried on the next sweep). A nil receiver returns no descriptors.

func (*Registry) Register added in v0.35.0

func (r *Registry) Register(id, host string, st State, ttl time.Duration, now time.Time) (Descriptor, error)

Register records a descriptor for a session at start, keyed by id, mirroring the live drive st, as of now. It is IDEMPOTENT: re-registering the same id UPDATES the existing descriptor (re-stamping UpdatedAt / LastSeen and the drive projection) rather than duplicating it, and PRESERVES the original CreatedAt — a relaunch of the same session is the same row, aged from its first start. host is recorded once (kept if a relaunch passes a blank). ttl <= 0 uses DefaultDescriptorTTL. It returns the stored descriptor.

A nil receiver returns the projected descriptor without persisting, so a loop with no registry wired behaves byte-identically to the pre-registry path.

func (*Registry) RegisterWithMeta added in v0.35.0

func (r *Registry) RegisterWithMeta(id, host string, st State, ttl time.Duration, now time.Time, meta DescriptorMeta) (Descriptor, error)

RegisterWithMeta is Register plus the host-owned pointer metadata (pid/argv/ start_sha/cache_key) needed by a live guard-session descriptor.

func (*Registry) Update added in v0.35.0

func (r *Registry) Update(id string, st State, now time.Time) (Descriptor, error)

Update re-stamps the descriptor for id from the live drive st as of now — the update-on-transition move. It re-projects the drive fields (Run/Budget/Priority/ Generation/Reason/Rev) and bumps UpdatedAt / LastSeen, so the persisted pcb_state tracks the live Table on every control verb / Decide. The durable id / host / CreatedAt / TTL are preserved. An Update for an id that was never Registered is treated as a register (idempotent create), so a transition observed before an explicit register still persists. A nil receiver is a no-op.

func (*Registry) UpdateWithMeta added in v0.35.0

func (r *Registry) UpdateWithMeta(id string, st State, now time.Time, meta DescriptorMeta) (Descriptor, error)

UpdateWithMeta is Update plus host-owned pointer metadata. Existing descriptors preserve their original metadata unless a non-zero field is supplied, so a normal drive transition cannot erase pid/argv/start_sha/cache_key.

type RegistryRecovery added in v0.42.0

type RegistryRecovery struct {
	Event  RecoveryEvent
	Stats  RecoveryStats
	Reaped []string
	// AlreadyRecovered reports that a concurrent recoverer quarantined the
	// active file first; the caller should simply retry its restore.
	AlreadyRecovered bool
	// LedgerErr and ReapErr are advisory measurement/cleanup failures. They
	// must never prevent startup; callers may warn about them.
	LedgerErr error
	ReapErr   error
}

RegistryRecovery is the result of one corrupt-registry recovery pass.

func RecoverCorruptRegistry added in v0.42.0

func RecoverCorruptRegistry(path string, cause error, policy QuarantineRetention, now time.Time) (RegistryRecovery, error)

RecoverCorruptRegistry quarantines the corrupt descriptor index at path, records a privacy-safe recovery event in the sidecar ledger, and applies the retention policy to accumulated evidence. Only a quarantine failure is returned as a hard error (evidence preservation stays load-bearing, as in #4647); ledger and cleanup failures ride along as advisory fields.

The whole pass serializes under the sidecar-ledger lock: a bare rename race is NOT enough to elect one winner, because Windows renames a source that a concurrent recoverer already renamed onward from its new location (rename works by open handle), so every unserialized recoverer can report success. Under the lock, losers observe the active file already gone and defer.

type RegrowthClassStat added in v0.42.0

type RegrowthClassStat struct {
	Rows     int   `json:"rows"`
	Bytes    int64 `json:"bytes"`
	DupRows  int   `json:"dup_rows,omitempty"`
	DupBytes int64 `json:"dup_bytes,omitempty"`
}

RegrowthClassStat is one content class's share of a window (or of the corpus). Lengths and duplicate counts only — never bodies.

type RegrowthCohort added in v0.42.0

type RegrowthCohort struct {
	Windows                 int     `json:"windows"`
	MedianToolCalls         int     `json:"median_tool_calls"`
	MedianTurns             int     `json:"median_turns"`
	MedianGrowthTokens      int     `json:"median_growth_tokens"`
	MedianCacheReadFraction float64 `json:"median_cache_read_fraction"`
}

RegrowthCohort summarizes one side of the fast/slow comparison, so "optimize the rebound away" can be checked against what the fast sessions were actually doing.

type RegrowthCrossing added in v0.42.0

type RegrowthCrossing struct {
	Threshold int     `json:"threshold"`
	Seconds   float64 `json:"seconds"`
	Samples   int     `json:"samples"`
	Turns     int     `json:"turns"`
	ToolCalls int     `json:"tool_calls"`
}

RegrowthCrossing times one resident-token milestone after a fire.

type RegrowthReplayFolder added in v0.44.0

type RegrowthReplayFolder func(bodies []string) []string

RegrowthReplayFolder is the candidate dedup mechanism under counterfactual test. It receives the decoded tool-result bodies of ONE post-fire window in wire order and must return a slice of the SAME length, where element i is the (possibly folded) rendering of body i. Returning a slice of any other length is treated as a replay error and the window is skipped, because a mechanism that drops a span outright cannot be scored on the same axis as one that shortens it.

type RegrowthReplayOptions added in v0.44.0

type RegrowthReplayOptions struct {
	// Fold is the mechanism under test.
	Fold RegrowthReplayFolder
	// WindowByteBudget caps the tool-result bytes retained for one window; <= 0 uses the default.
	WindowByteBudget int64
	// MinDupLines is the candidate's own minimum-duplicate-run floor, used ONLY for the reach
	// diagnostic DupRowsUnderLineFloor (a body with fewer lines than the floor can never fold, no
	// matter how often it repeats). 0 disables that diagnostic. The replay never enforces it.
	MinDupLines int
}

RegrowthReplayOptions arms the replay. A nil Fold leaves the whole pass off and costs nothing.

type RegrowthReplayStat added in v0.44.0

type RegrowthReplayStat struct {
	// Denominator.
	Rollouts        int   `json:"rollouts"`
	Windows         int   `json:"windows"`           // post-fire windows carrying >= 1 tool-result row
	ToolResultRows  int   `json:"tool_result_rows"`  // rows scored
	ToolResultBytes int64 `json:"tool_result_bytes"` // their rollout row bytes

	// BEFORE — the audit's own session-wide duplicate verdict, re-totalled per window so the
	// anomaly rule (RegrowthDupToolMinRows / RegrowthDupMinBytes) can be re-applied after the fold.
	AnomalyWindowsBefore int   `json:"anomaly_windows_before"`
	DupRowsBefore        int   `json:"dup_rows_before"`
	DupBytesBefore       int64 `json:"dup_bytes_before"`

	// REACH — how much of that duplication a WITHIN-WIRE fold can even see.
	InWindowDupRows   int   `json:"in_window_dup_rows"` // earliest occurrence is inside this window: foldable
	InWindowDupBytes  int64 `json:"in_window_dup_bytes"`
	CrossFireDupRows  int   `json:"cross_fire_dup_rows"` // earliest occurrence precedes the fire: NOT foldable
	CrossFireDupBytes int64 `json:"cross_fire_dup_bytes"`
	// DupRowsUnderLineFloor counts in-window duplicate rows whose body has fewer lines than
	// MinDupLines — foldable in principle, unreachable by a line-run matcher.
	DupRowsUnderLineFloor int `json:"dup_rows_under_line_floor,omitempty"`

	// AFTER — the same accounting recomputed on the folded bodies.
	AnomalyWindowsAfter int   `json:"anomaly_windows_after"`
	DupRowsAfter        int   `json:"dup_rows_after"`
	DupBytesAfter       int64 `json:"dup_bytes_after"`
	WindowsCollapsed    int   `json:"windows_collapsed"` // fired before, does not fire after
	FoldedRows          int   `json:"folded_rows"`
	ShedBytes           int64 `json:"shed_bytes"`

	// LOSS — the counterfactual's correctness side, measured at SPAN granularity because that is
	// the granularity the mechanism works at. A body-level check ("a body that is not a whole-body
	// duplicate must survive byte-identical") is the wrong test and reads as a catastrophic
	// false-positive rate: two genuinely different tool outputs routinely share a long identical
	// line run (two `git status` runs, two builds of the same target), and folding that shared run
	// is the mechanism working, not failing.
	//
	// The real property is lossless-by-RELOCATION: every line the fold removes must still be
	// reachable, verbatim, in an EARLIER body of the same window. RemovedLinesLost counts lines
	// removed that appear nowhere earlier — content actually destroyed — and is the number that
	// must be zero.
	RemovedLinesRelocated int64 `json:"removed_lines_relocated"`
	RemovedLinesLost      int64 `json:"removed_lines_lost"`
	// Diagnostics, not defects: how much of the fold's work is invisible to the audit's body-level
	// duplicate accounting.
	WholeBodyDupRows int `json:"whole_body_dup_rows"` // byte-identical to an earlier body
	PartialFoldRows  int `json:"partial_fold_rows"`   // folded, but NOT a whole-body duplicate

	// Fidelity caveats, reported so the bound above is read with them.
	TruncatedRows        int `json:"truncated_rows"`         // clipped by the scanner's 128 KB head bound
	BudgetSkippedWindows int `json:"budget_skipped_windows"` // over WindowByteBudget, excluded entirely
	ShapeErrorWindows    int `json:"shape_error_windows"`    // folder returned a wrong-length slice
}

RegrowthReplayStat is the scored result of a replay. Every field is a count or a byte total; no body ever reaches this struct.

func (*RegrowthReplayStat) Add added in v0.44.0

Add folds another rollout's replay stat into r, so a corpus sweep can total them.

type RelayShadowEvent added in v0.37.0

type RelayShadowEvent struct {
	TraceID                 string  `json:"trace_id"`
	Reason                  string  `json:"reason"`
	Rev                     uint64  `json:"rev"`
	SoftMark                float64 `json:"soft_mark"`
	ContextTokensLeft       int     `json:"context_tokens_left"`
	ContextTokensCap        int     `json:"context_tokens_cap,omitempty"`
	ResidentContextTokens   int     `json:"resident_context_tokens,omitempty"`
	ResidentContextCap      int     `json:"resident_context_cap,omitempty"`
	ResidentContextFraction float64 `json:"resident_context_fraction"`
	FractionConsumed        float64 `json:"fraction_consumed"`
}

RelayShadowEvent is the advisory, behavior-free relay soft-mark signal. It uses the closed RELAY_ARMED reason token from the relay vocabulary but does not change State.Run: a relay driver may rotate at a later safe point, while non-relay sessions ignore it.

type RelayShadowObserver added in v0.37.0

type RelayShadowObserver func(RelayShadowEvent)

RelayShadowObserver receives the RELAY_ARMED would-fire event when a session's resident-context meter first crosses the configured soft mark. The callback runs after the table lock is released, matching BudgetObserver's slow-sink discipline.

type RepeatedBadToolCallPolicy added in v0.38.0

type RepeatedBadToolCallPolicy struct {
	Name         string
	EndTurnAfter int
	StopAfter    int
	StopReason   string
}

RepeatedBadToolCallPolicy declares how a streak of bad tool outcomes escalates to session control. Zero thresholds mean "do not escalate"; that keeps a missing policy permissive for loop progress instead of recreating the accidental "four JSON errors stopped the session" failure mode.

type RepeatedBadToolCallTracker added in v0.38.0

type RepeatedBadToolCallTracker struct {
	Policy RepeatedBadToolCallPolicy
	// contains filtered or unexported fields
}

RepeatedBadToolCallTracker is the per-session state for the declared policy. It counts only consecutive identical bad outcomes: same tool, same per-tool reason, same disposition, and no intervening progress.

func (*RepeatedBadToolCallTracker) Observe added in v0.38.0

Observe folds one per-call outcome into the declared policy and returns the session-control decision. Bad outcomes below threshold still return CONTINUE.

func (*RepeatedBadToolCallTracker) Reset added in v0.38.0

func (t *RepeatedBadToolCallTracker) Reset()

Reset clears the streak, for objective boundaries or manual operator retries.

type ResetBudgetRearm added in v0.37.0

type ResetBudgetRearm struct {
	TurnsLeft         int `json:"turns_left"`
	TokensLeft        int `json:"tokens_left"`
	ContextTokensLeft int `json:"context_tokens_left,omitempty"`
	ContextTokensCap  int `json:"context_tokens_cap,omitempty"`
}

ResetBudgetRearm records the fresh budget a reset armed on the child trace.

type ResetOmittedSpan added in v0.37.0

type ResetOmittedSpan struct {
	Index  int    `json:"index"`
	Role   string `json:"role,omitempty"`
	Digest string `json:"digest"`
	Reason string `json:"reason,omitempty"`
}

ResetOmittedSpan is a payload-free pointer to transcript bytes that did NOT land verbatim in the carryover seed. Digest is the replay handle; it is over role+text, not a model-authored claim.

type ResetTransaction added in v0.37.0

type ResetTransaction struct {
	Schema           string             `json:"schema,omitempty"`
	OldTrace         string             `json:"old_trace,omitempty"`
	NewTrace         string             `json:"new_trace,omitempty"`
	SeedDigest       string             `json:"seed_digest,omitempty"`
	Contributors     []string           `json:"contributors,omitempty"`
	OmittedSpans     []ResetOmittedSpan `json:"omitted_spans,omitempty"`
	BudgetRearm      ResetBudgetRearm   `json:"budget_rearm,omitempty,omitzero"`
	WarmPrefixDigest string             `json:"warm_prefix_digest,omitempty"`
}

ResetTransaction is the replayable row for one context-budget reset. The table owns the old/new trace and budget re-arm; sessionreset fills SeedDigest, Contributors, OmittedSpans, and WarmPrefixDigest when a carryover seed exists.

func NewResetTransaction added in v0.37.0

func NewResetTransaction(parent, child string, fresh Budget) ResetTransaction

NewResetTransaction records the table-owned half of a reset transaction.

func (ResetTransaction) IsZero added in v0.37.0

func (tx ResetTransaction) IsZero() bool

IsZero reports whether no transaction was recorded.

type ResetTransactionLog added in v0.37.0

type ResetTransactionLog struct {
	// contains filtered or unexported fields
}

ResetTransactionLog is the append-only, replayable ledger of budget-triggered reset transactions — the runtime-continuity audit trail issue #1582 asks for, one level above the single ResetTransaction row a child State carries. A State only ever holds the transaction that minted IT (the latest reset in its own lineage); a long-lived goal that survives many hidden restarts needs the FULL chain, so a caller (a host's reset boundary, e.g. cmd/fak's resetServedSessionOnBudget) appends every transaction it produces here in occurrence order. The zero value is a usable empty log. Mirrors ctxplan.ObjectiveLog / ctxplan.PageFaultLog in shape (Entries/Latest/Replay/Summary), but — unlike those single-goroutine-owned siblings — this log is wired into a served gateway's reset hook where concurrent traces can reset at the same time, so it guards its own state with a mutex rather than leaving that to the caller.

func (*ResetTransactionLog) Append added in v0.37.0

Append records tx as the next entry and returns it unchanged — the one call sites need to both persist the row and keep using the value they just built. A zero tx (IsZero()) is still recorded: the log is a raw event history, not a filtered one; a caller that wants to skip no-op resets should check IsZero() itself before calling.

func (*ResetTransactionLog) Entries added in v0.37.0

func (l *ResetTransactionLog) Entries() []ResetTransaction

Entries returns a defensive copy of the logged transactions in occurrence order.

func (*ResetTransactionLog) Explain added in v0.37.0

func (l *ResetTransactionLog) Explain() string

Explain renders the log as an operator-readable report, in the ObjectiveLog.Explain / PageFaultLog.Explain style: one line per transaction plus the count footer.

func (*ResetTransactionLog) Latest added in v0.37.0

func (l *ResetTransactionLog) Latest() (ResetTransaction, bool)

Latest returns the most recently appended transaction and whether the log has any entries at all. A caller chaining resets across an arbitrarily long-lived goal uses this to find "what did the last reset arm", without needing to track it separately.

func (*ResetTransactionLog) Replay added in v0.37.0

func (l *ResetTransactionLog) Replay() (verdicts []ResetTransactionReplayVerdict, allMatch bool)

Replay recomputes the well-formedness and chain-linkage of every logged entry and reports any DIVERGED entry, so a caller can tell "is this audit trail coherent" from the stored rows themselves instead of trusting that the log was assembled correctly. A row is well-formed when it carries the schema token and both trace ids; the chain link check only applies from the second entry onward and only when consecutive entries share the same lineage (entry N's OldTrace == entry N-1's NewTrace) — a log that interleaves independent lineages is expected to show ChainLinked=false for the first row of each new lineage, which is not itself a divergence.

func (*ResetTransactionLog) Summary added in v0.37.0

Summary computes the aggregate counts over every logged entry.

type ResetTransactionReplayVerdict added in v0.37.0

type ResetTransactionReplayVerdict struct {
	Index       int    `json:"index"`
	OldTrace    string `json:"old_trace,omitempty"`
	NewTrace    string `json:"new_trace,omitempty"`
	WellFormed  bool   `json:"well_formed"`
	ChainLinked bool   `json:"chain_linked"`
	Diverged    bool   `json:"diverged"`
	DivergeNote string `json:"diverge_note,omitempty"`
}

ResetTransactionReplayVerdict is the outcome of replaying one logged entry: whether the row is internally well-formed and, when it is not the first entry in the chain, whether its OldTrace connects to the prior entry's NewTrace. A ResetTransaction is deliberately payload-free (SeedDigest/OmittedSpans carry digests, never transcript text), so there is no original content left to re-hash and compare — replay here checks the SHAPE of the audit trail is coherent, the same class of non-forgeable evidence a git-diff witness gives a commit claim, not a re-derivation of dropped bytes the schema exists specifically to avoid persisting.

type ResetTransactionSummary added in v0.37.0

type ResetTransactionSummary struct {
	Total          int `json:"total"`
	WithSeedDigest int `json:"with_seed_digest"`
	WithWarmPrefix int `json:"with_warm_prefix"`
	OmittedSpans   int `json:"omitted_spans"`
}

ResetTransactionSummary folds the log into counts — the O(1) health signal a debug surface prints instead of walking every entry (mirrors ObjectiveSummary/PageFaultSummary).

type ResidentCeilingWitness added in v0.42.0

type ResidentCeilingWitness struct {
	Ceiling     int                     `json:"ceiling"`
	Sessions    int                     `json:"sessions"`
	OverCeiling []CompactCeilingSession `json:"over_ceiling,omitempty"`
}

ResidentCeilingWitness is the resident-token-ceiling answer #3187's dogfood scores against: of the audited sessions, which ones carried a peak RESIDENT window over the ceiling, and did compaction fire for them. Derived entirely from a decoded CompactAuditResult, so the dogfood's witness and the miner's report can never drift.

func AuditResidentCeiling added in v0.42.0

func AuditResidentCeiling(res CompactAuditResult, ceiling int) ResidentCeilingWitness

AuditResidentCeiling folds a decoded compact-audit sweep into the resident-ceiling witness. ceiling is the resident-token bar (compactcohere's FAK_CTX_YIELD_CEILING in the #3187 dogfood); a non-positive ceiling yields an empty over-ceiling set, matching compactcohere's "non-positive ceiling disables every resident-token term".

type ResumeMode added in v0.35.0

type ResumeMode uint8

ResumeMode names how a Paused->Running resume re-admits a session. Warm means the wired splicer reported the session's KV was reattached (KVCache.Clone / MoveTo(KVRestore) on the host) so the resumed turn reuses it; Cold means no warm KV was available and the caller re-prefills as it does today. The zero value is Cold — the safe default, so a resume with no splicer wired (or a splicer that declines) is always the correct, if slower, cold path.

const (
	// ResumeCold is the fallback: warm KV was unavailable (no splicer, the splicer declined,
	// or the session was not actually held), so the caller cold re-prefills. The zero value.
	ResumeCold ResumeMode = iota
	// ResumeWarm means the wired WarmKVSplicer reattached the session's KV, so the resumed
	// turn reuses it instead of re-prefilling.
	ResumeWarm
)

func (ResumeMode) String added in v0.35.0

func (m ResumeMode) String() string

String renders a ResumeMode as its lowercase wire token; an out-of-range value renders "unknown" rather than panicking.

type ResumeVerdict added in v0.35.0

type ResumeVerdict struct {
	Resumed     bool
	Mode        ResumeMode
	State       State
	Reason      string
	SpanPointer KVSpanPointer
}

ResumeVerdict is what WaitResume returns once a Paused session leaves the hold. Resumed is true when the session was transitioned back to a live (Running/Throttled) state and should re-admit at the next boundary; it is false when the wait ended because the session was STOPPED or the context was cancelled (the caller ends the loop instead of re-admitting). Mode is warm/cold (meaningful only when Resumed); State is the drive record observed at the resume edge. Reason carries why a non-resume wait ended (a closed token).

type RevisionObserver added in v0.35.0

type RevisionObserver func(State)

RevisionObserver is the EVERY-revision callback seam (#630): unlike a TransitionObserver (which fires only on the few notable run-state moves) it is invoked on every monotonic Rev bump — a budget cut, a pace change, a priority re-rank, an intent/goal update, a debit, a transition — so a host can stream the drive table as a live "what is every session doing right now" tail and key each event on State.Rev. It is delivered SYNCHRONOUSLY under the table lock in strict Rev order, so the sink MUST be fast and MUST NOT call back into the table (it would deadlock on the held lock): the one production consumer is the in-process gateway change-ring append, which only takes its own cheap mutex. The lock-held, in-order delivery is deliberate — a cursor feed needs revisions in Rev order, not the reordering an after-unlock fan-out (TransitionObserver/BudgetObserver) allows.

type RewindApplier added in v0.38.0

type RewindApplier interface {
	Apply() error
}

RewindApplier is the bulk-write step — the actual re-application of the checkpoint tree onto the workspace. Rewind calls Apply ONLY after the arbiter admits the change set (or an operator force clears it), so a refused restore modifies zero files. A nil Applier makes Rewind a pure admission decision.

type RewindEvent added in v0.38.0

type RewindEvent struct {
	Kind   string    `json:"kind"`             // EvRewindRefused / EvRewindForced / EvRewindAdmitted
	Holder string    `json:"holder,omitempty"` // the operator/agent that requested the restore
	Tree   []string  `json:"tree,omitempty"`   // the change set the decision was made against
	Reason string    `json:"reason,omitempty"` // the closed lane-conflict reason on a refusal
	At     time.Time `json:"at"`               // when the decision was made
}

RewindEvent is one journaled rewind record.

type RewindInput added in v0.38.0

type RewindInput struct {
	Holder   string             // the operator/agent requesting the restore
	Lane     string             // named dos.toml lane the restore acts on ("" = a tree-only request)
	LeaseID  string             // the restore's own lease id; a live lease with this id is the caller's own and never conflicts
	Tree     []string           // the change set: repo-relative globs the restore would touch
	Leases   []laneadmit.Lease  // the live leases (projected from refs/fak/locks/* via leaseref)
	Taxonomy laneadmit.Taxonomy // the dos.toml lane taxonomy
	Force    bool               // operator force: clears geometric/same-lane conflicts, still refuses over a live EXCLUSIVE lane
	Applier  RewindApplier      // the bulk-write step (nil => admission decision only, no apply)
	Journal  RewindJournal      // the ledger (nil => no journaling)
	Now      time.Time          // injected clock for deterministic journal stamps (zero => time.Now)
}

RewindInput configures a workspace restore.

type RewindJournal added in v0.38.0

type RewindJournal interface {
	Record(e RewindEvent) error
}

RewindJournal is the ledger a refusal / force / admission is recorded on. It is the same shape every other fak execution surface journals through (an append-only event log); Rewind never invents a free-text kind.

type RewindVerdict added in v0.38.0

type RewindVerdict struct {
	Admit     bool                 `json:"admit"`
	Reason    string               `json:"reason,omitempty"` // the arbiter's closed lane-conflict reason (COLLISION_RISK) on a refusal
	Detail    string               `json:"detail,omitempty"`
	Tree      []string             `json:"tree,omitempty"` // the change set the decision was made against (after taxonomy fallback)
	Conflicts []laneadmit.Conflict `json:"conflicts,omitempty"`
	Forced    bool                 `json:"forced,omitempty"` // true when an operator force path cleared the non-exclusive conflicts
}

RewindVerdict is the workspace-restore decision. A refusal carries the arbiter's closed lane-conflict reason (COLLISION_RISK) plus the conflicting live leases as evidence — each naming its holder, the surface a refused operator reads (#2297).

func Rewind added in v0.38.0

func Rewind(in RewindInput) (*RewindVerdict, error)

Rewind is the workspace-restore handler. It consults the DOS lane arbiter over the change set BEFORE any tree mutation:

  • a live intersecting lease refuses the restore with the arbiter's closed lane-conflict reason (COLLISION_RISK), naming the holder, and zero files are modified (the Applier is not called);
  • a lease over a disjoint tree does not block, and the Applier runs;
  • an operator force (Force=true) clears the geometric / same-lane conflicts but still refuses over a live EXCLUSIVE lane.

The returned verdict is the decision; a nil error means the gate ran. An Apply error is returned verbatim and only after the arbiter admitted the change set (so an Apply error is proof the restore was permitted to start).

type RunState

type RunState uint8

RunState is a served session's lifecycle position — a small, total state machine. The transitions are the control verbs the design names: throttle/pause/resume (reversible drive changes) and drain/stop (terminal). The zero value is Running, so an unseen trace is a live session at its defaults — never a phantom Stopped.

const (
	// Running is the default: the session advances each turn at its budget/pace.
	Running RunState = iota
	// Throttled means the session still advances but under a tightened pace
	// (lower MaxTokensPerTurn / a turn gap). It carries a Reason token for "why".
	Throttled
	// Paused holds the session at the next turn boundary without ending it; a
	// resume (Paused -> Running) is a state flip, not a cold re-attach.
	Paused
	// Draining means a stop was requested; the loop takes it at the NEXT turn
	// boundary (never mid-decode, so a stop never tears a half-emitted tool call).
	Draining
	// Stopped is terminal; it carries a closed Reason token so "why did it stop"
	// is a field, not an inference from an exit code.
	Stopped
	// Terminating means a FORCEFUL stop was requested (#2758): the loop takes it at
	// the next SAFE POINT — the in-flight model call's context is cancelled and no
	// further tool call is dispatched — unlike Draining, which lets the current turn
	// run to completion. Appended after Stopped so persisted numeric values of the
	// prior states never shift. Like Draining it advances exactly one more step (a
	// Decide finalizes it to Stopped with the TERMINATED reason), so it is not
	// terminal itself.
	Terminating
)

func ParseRunState

func ParseRunState(s string) (RunState, bool)

ParseRunState maps a wire token back to a RunState. The bool is false for an unrecognized token, so a caller fails closed (the route returns 400) rather than defaulting an unknown verb to Running. The four shared tokens go through lifecycle.Parse — the single definition both layers share.

func RunStateFromPhase added in v0.35.0

func RunStateFromPhase(p lifecycle.Phase) (RunState, bool)

RunStateFromPhase lifts a shared lifecycle Phase into a RunState. It is total over the four Phases (every shared state has a RunState peer); an out-of-range Phase yields (0, false).

func (RunState) Phase added in v0.35.0

func (s RunState) Phase() (lifecycle.Phase, bool)

Phase projects a RunState onto the shared lifecycle skeleton. The bool is false for Throttled (a session-only pace modifier with no shared peer), for Terminating (the session-only forceful-stop staging state, #2758) and for any out-of-range value — the projection is explicit about the extras, never a silent default. This is the served-session half of the #912 "one machine" converter; internal/lifebridge composes it with the supervisor half.

func (RunState) String

func (s RunState) String() string

String renders a RunState as its lowercase wire token (the form the /v1/fak/session routes emit and accept). The four shared-lifecycle tokens are SOURCED from internal/lifecycle (not re-spelled here) so the served session and the loop supervisor cannot drift apart; Throttled is the one session-only token. An out-of-range value renders "unknown" rather than panicking — a wire value is never trusted to be in range.

type Scheduler added in v0.35.0

type Scheduler struct {
	// contains filtered or unexported fields
}

Scheduler reads a *Table via Snapshot and picks the live session that should run next under a chosen Policy, and consumes budget-exhaustion / pause / stop as slot-freed scheduling events. It is the policy layer that keeps the table policy- free. The zero value is not usable — construct with NewScheduler. A nil *Scheduler is a sane no-op (Pick returns no winner; OnSlotFreed / Attach do nothing), so a host with no scheduler wired behaves like the pre-scheduler path.

func NewScheduler added in v0.35.0

func NewScheduler(policy Policy) *Scheduler

NewScheduler builds an unattached scheduler under the given Policy. Bind it to a table with Attach before calling Pick; an unattached scheduler's Pick returns no winner.

func (*Scheduler) Attach added in v0.35.0

func (s *Scheduler) Attach(t *Table, opts AttachOptions)

Attach binds the scheduler to a table and installs the internal slot-freed handlers on the table's existing WatchBudget / WatchTransitions seams, composing with any pass-through observers in opts. After Attach, Pick reads this table's live Snapshot and slot-freed events flow to the OnSlotFreed callback. Calling Attach again re-binds (the last Attach wins — it overwrites the table's single observer slots, by design). A nil receiver or nil table is a no-op.

The composed handlers are safe against deadlock: the table fires observers AFTER it releases its own lock, and the scheduler's handler only takes the scheduler's lock (a distinct lock the table never holds) before invoking the host callback.

func (*Scheduler) ExpireReservations added in v0.35.0

func (s *Scheduler) ExpireReservations(now time.Time) []SlotReservation

ExpireReservations reclaims stale advisory reservations and returns the slots it dropped. It never mutates session state and never reports a live request as refused; expiry only means the warm hold is gone and a later request must take the normal path.

func (*Scheduler) OnSlotFreed added in v0.35.0

func (s *Scheduler) OnSlotFreed(fn func(SlotEvent))

OnSlotFreed registers the host callback invoked once per slot-freed event. Passing nil clears it. Safe to call any time, including before Attach; a nil receiver is a no-op. The callback runs on the table's observer goroutine (after the table lock is released), so it may block on slow work without stalling the table — the host owns fan-out and failure policy, exactly like the table's own observer seams.

func (*Scheduler) Pick added in v0.35.0

func (s *Scheduler) Pick() (State, bool)

Pick names the live session that should run next, reading the attached table's LIVE Snapshot so an operator's budget/priority change is reflected on the very next call. ok is false when there is no eligible session — an empty table, an all-ineligible table (everyone paused/draining/stopped or budget-exhausted), or an unattached / nil scheduler. On ok==false the returned State is the zero value; a caller checks ok and idles the gateway rather than running a phantom session.

func (*Scheduler) PromoteReservation added in v0.35.0

func (s *Scheduler) PromoteReservation(trace, prefix string, now time.Time) (SlotPromotion, bool)

PromoteReservation adopts an active reservation into the real request slot when the arriving request proves it is the same session and prefix. A prefix mismatch or an expired/missing reservation returns ok=false and leaves any non-matching reservation intact, so the caller falls back to the normal cold path rather than fabricating reuse.

func (*Scheduler) Reservations added in v0.35.0

func (s *Scheduler) Reservations(now time.Time) []SlotReservation

Reservations returns the currently-active advisory reservations after first applying expiry at now. The returned slice is a copy sorted in deterministic map iteration replacement order by the table-free key fields.

func (*Scheduler) ReserveKnownComing added in v0.35.0

func (s *Scheduler) ReserveKnownComing(now time.Time) []SlotReservation

ReserveKnownComing scans the live snapshot for #811 forward-looking TurnIntent hints and records advisory reservations for those known-coming turns. The returned reservations are the holds a host may use to pin matching KV residency and keep a best-effort slot warm. They are strictly lower class than real requests: Pick ignores them, and ExpireReservations/PromoteReservation reclaim them without touching table state. now is supplied by the caller so the policy remains deterministic in tests.

type ScratchCheckpoint added in v0.38.0

type ScratchCheckpoint struct {
	TraceID string `json:"trace_id"`
	Digest  string `json:"digest"`
	Archive string `json:"archive"`
}

ScratchCheckpoint is the third optional axis a session checkpoint records for scratch state (alongside the drive/context axes the checkpoint epic already carries). It is a Digest — the checkable identity of the scratch tree at checkpoint time — plus an Archive path holding the copied bytes, so a rewind can REPRODUCE the tree (restore-with-scratch) rather than merely name it. The zero value means "no scratch axis recorded" (Digest == "").

func (ScratchCheckpoint) IsZero added in v0.38.0

func (c ScratchCheckpoint) IsZero() bool

IsZero reports whether the checkpoint carries no scratch axis — the safe default a rewind reads as "this checkpoint has no scratch to restore or skip".

func (ScratchCheckpoint) Restore added in v0.38.0

func (c ScratchCheckpoint) Restore(target ScratchLease, includeScratch bool, j ScratchJournal, now time.Time) error

Restore applies (or deliberately skips) a checkpoint's scratch axis onto target.

  • includeScratch=true — RESTORE-WITH-SCRATCH: the target tree is cleared and the checkpoint's archived tree is copied back in, so target.Digest() reproduces the checkpoint Digest. Journals EvScratchRestored.
  • includeScratch=false — RESTORE-WITHOUT-SCRATCH: the current scratch is left UNTOUCHED (a rewind that deliberately keeps live scratch state). Journals EvScratchRestoreSkipped and makes zero tree mutations.

A zero checkpoint (no scratch axis) is a no-op in either mode. This is the rewind consumer of the checkpoint axis — the checkpoint/rewind VERBS themselves (#2425 / #2426) stay out of scope; this only supplies the axis they read.

type ScratchEvent added in v0.38.0

type ScratchEvent struct {
	Kind           string    `json:"kind"`                      // one of the EvScratch* tokens
	TraceID        string    `json:"trace_id,omitempty"`        // the session trace the lease is bound to
	Dir            string    `json:"dir,omitempty"`             // the scratch directory the event concerns
	BytesReclaimed int64     `json:"bytes_reclaimed,omitempty"` // GC only: total bytes freed
	FilesDropped   int       `json:"files_dropped,omitempty"`   // GC only: regular files removed
	Digest         string    `json:"digest,omitempty"`          // checkpoint/restore only: the content digest of the tree
	At             time.Time `json:"at"`                        // when the event was recorded
}

ScratchEvent is one journaled scratchpad record — the union of the fields the five lifecycle events carry. A minted/forked event fills TraceID + Dir; a GC event adds BytesReclaimed + FilesDropped; a checkpoint/restore event adds Digest. It is data-only and self-describing, so a ledger row needs no out-of-band context.

type ScratchJournal added in v0.38.0

type ScratchJournal interface {
	Record(e ScratchEvent) error
}

ScratchJournal is the append-only ledger a scratchpad lifecycle records onto — the same seam shape rewind.go uses. It is nil-permissive at every call site, so a host with no ledger wired gets the identical lifecycle with the journaling elided.

type ScratchLease added in v0.38.0

type ScratchLease struct {
	TraceID   string    `json:"trace_id"`
	Dir       string    `json:"dir"`
	CreatedAt time.Time `json:"created_at"`
}

ScratchLease binds a session-scoped scratch directory to a trace. It is the value the lifecycle passes around: minted by MintScratch, reclaimed by GC, branched by Fork, and snapshotted by Checkpoint. The zero lease is not usable — a lease is always the return of a mint/fork, so Dir is a real directory.

func MintScratch added in v0.38.0

func MintScratch(base, traceID string, j ScratchJournal, now time.Time) (ScratchLease, error)

MintScratch mints a fresh scratch directory for traceID under base (os.TempDir() when base is ""), records it on the journal (EvScratchMinted), and returns the lease — the BIRTH of the lifecycle. The directory is unique per mint (a session re-home mints a new one), so two live sessions never share a tree. now stamps the lease and the journal event (zero => time.Now), the injected-clock posture the rest of the package takes.

func (ScratchLease) Checkpoint added in v0.38.0

func (l ScratchLease) Checkpoint(archiveBase string, j ScratchJournal, now time.Time) (ScratchCheckpoint, error)

Checkpoint records the scratch axis for a session checkpoint: it archives a copy of the live tree under archiveBase (os.TempDir() when ""), computes its Digest, journals EvScratchCheckpoint, and returns the ScratchCheckpoint. It NEVER removes the live tree — a checkpoint is not a GC (the #2420 fence) — so the session keeps writing scratch after a checkpoint is taken.

func (ScratchLease) Digest added in v0.38.0

func (l ScratchLease) Digest() (string, error)

Digest is the content identity of the scratch tree: a sha256 over every regular file's repo-relative path and bytes, in sorted path order (so the digest is a pure function of content, not of walk order or wall-clock). An empty or missing tree digests to a stable sentinel. It is the checkable equality "the scratch was preserved" the checkpoint axis records.

func (ScratchLease) Fork added in v0.38.0

func (l ScratchLease) Fork(base, childTrace string, j ScratchJournal, now time.Time) (ScratchLease, error)

Fork gives a child trace its OWN copy-on-write scratch dir under base, seeded with a copy of this lease's current contents, and journals EvScratchForked. Because each fork owns a distinct directory, two forks that write the SAME filename cannot collide — the isolation #2420 requires. The copy is eager (a real byte copy at fork time); "copy-on-write" is the SEMANTIC guarantee (neither fork sees the other's later writes), not a filesystem reflink dependency.

func (ScratchLease) GC added in v0.38.0

GC reclaims the whole scratch tree at session end (the DEATH of the lifecycle), journaling an EvScratchGC event with the bytes reclaimed and regular files dropped BEFORE the removal, so the ledger records what the reap freed even though the tree is then gone. GC is idempotent — reclaiming an already-removed lease reports zero bytes/files and does not error. It is SESSION-END only; a checkpoint never calls it.

type SessionControl added in v0.38.0

type SessionControl struct {
	Decision    SessionControlDecision
	Policy      string
	Reason      string
	Consecutive int
	Threshold   int
	Tool        string
	ToolCallID  string
	ToolReason  abi.ReasonCode
	Disposition ToolCallDisposition
}

SessionControl is the session loop's typed decision plus the evidence that produced it. ToolReason stays a per-call field; Reason names the control-plane reason only when Decision ends or stops the turn/session.

func (SessionControl) Continue added in v0.38.0

func (c SessionControl) Continue() bool

func (SessionControl) SessionStopReason added in v0.38.0

func (c SessionControl) SessionStopReason() (string, bool)

SessionStopReason returns the declared session-control reason and true only for a real session stop. It never reads ToolReason.

func (SessionControl) StopsSession added in v0.38.0

func (c SessionControl) StopsSession() bool

func (SessionControl) ToolReasonToken added in v0.38.0

func (c SessionControl) ToolReasonToken() string

type SessionControlDecision added in v0.38.0

type SessionControlDecision uint8

SessionControlDecision is the closed session-control vocabulary at this seam. It answers what the loop does, never why one tool call was refused.

const (
	SessionControlContinue SessionControlDecision = iota
	SessionControlEndTurn
	SessionControlPause
	SessionControlStop
)

func (SessionControlDecision) EndsTurn added in v0.38.0

func (d SessionControlDecision) EndsTurn() bool

func (SessionControlDecision) StopsSession added in v0.38.0

func (d SessionControlDecision) StopsSession() bool

func (SessionControlDecision) String added in v0.38.0

func (d SessionControlDecision) String() string

type SessionEnvelopeReason added in v0.42.0

type SessionEnvelopeReason string

SessionEnvelopeReason is the typed reason a cumulative envelope requests a recovery checkpoint. It is intentionally separate from both State.Reason (the run-state control plane) and abi.ReasonCode (one tool denial): observing a deny never converts it to an allow or copies SELF_MODIFY into a session stop reason.

const (
	ReasonEnvelopeUncachedInput   SessionEnvelopeReason = "UNCACHED_INPUT_ENVELOPE"
	ReasonEnvelopeWallTime        SessionEnvelopeReason = "WALL_TIME_ENVELOPE"
	ReasonEnvelopeSemanticRefusal SessionEnvelopeReason = "SEMANTIC_REFUSAL_ENVELOPE"
)

type SessionRecoveryCheckpoint added in v0.42.0

type SessionRecoveryCheckpoint struct {
	Reason         SessionEnvelopeReason `json:"reason"`
	TraceID        string                `json:"trace_id"`
	Goal           Goal                  `json:"goal,omitempty,omitzero"`
	PendingTurn    PendingTurn           `json:"pending_turn,omitempty,omitzero"`
	ContinuationID string                `json:"continuation_id,omitempty"`
	Generation     int                   `json:"generation,omitempty"`
	StateRev       uint64                `json:"state_rev"`
}

SessionRecoveryCheckpoint is the compact drive-state pointer captured when the envelope trips. Goal and PendingTurn are copied from the latest State passed to Observe, so a caller can checkpoint/re-route without losing the active root or the write-ahead retry position. Reason states why recovery was requested.

type SlotCause added in v0.35.0

type SlotCause uint8

SlotCause is the closed reason a scheduling slot freed — the "why" on a SlotEvent. It is a small total enum so a host reacts on a checkable token, never free text.

const (
	// CauseBudgetExhausted: a session drained a configured budget axis (observed via the
	// table's BudgetExhausted event on the WatchBudget seam).
	CauseBudgetExhausted SlotCause = iota
	// CausePaused: an operator paused the session (a hold, not an end) — its slot is
	// free while it waits.
	CausePaused
	// CauseDraining: a stop was requested; the session takes it at the next boundary.
	CauseDraining
	// CauseStopped: the session reached its terminal state.
	CauseStopped
)

func (SlotCause) String added in v0.35.0

func (c SlotCause) String() string

String renders a SlotCause as its lowercase token; an out-of-range value renders "unknown" rather than panicking.

type SlotEvent added in v0.35.0

type SlotEvent struct {
	TraceID string    `json:"trace_id"`
	Cause   SlotCause `json:"cause"`
	Rev     uint64    `json:"rev"`
}

SlotEvent is the immutable "a slot freed" signal the Scheduler emits to its host when a session leaves the eligible set (budget exhaustion, pause, drain, or stop). It is the scheduling-EVENT framing the package design names: the supervisor learns a slot opened the instant it happens, rather than re-deriving liveness from a process scan. Rev is the table revision at the freeing write, so a host can order or de-duplicate events against a /v1/fak/changes cursor.

type SlotPromotion added in v0.35.0

type SlotPromotion struct {
	SlotReservation
	PromotedAtUnixNano int64 `json:"promoted_at_unix_nano"`
}

SlotPromotion is returned when a real request matches an active reservation. Promoted false is never returned; the bool return on PromoteReservation carries that bit. The struct exists so a host can record "warm prefix adopted" with the reservation facts.

type SlotReservation added in v0.35.0

type SlotReservation struct {
	TraceID            string `json:"trace_id"`
	Prefix             string `json:"prefix"`
	SourceRev          uint64 `json:"source_rev,omitempty"`
	ReservedAtUnixNano int64  `json:"reserved_at_unix_nano"`
	ArrivesAtUnixNano  int64  `json:"arrives_at_unix_nano"`
	ExpiresAtUnixNano  int64  `json:"expires_at_unix_nano"`
}

SlotReservation is the scheduler's advisory hold for a known-coming turn (#811). It carries the exact prefix identity a host should keep resident and the expiry boundary after which the hold must be reclaimed. It is deliberately not a table write: reservations are scheduler-local policy, lower-class than real requests.

type SpendEnvelope added in v0.37.0

type SpendEnvelope struct {
	MaxCents int64  `json:"max_cents,omitempty"`
	Currency string `json:"currency,omitempty"`
}

SpendEnvelope records the user's spend ceiling in minor units so parsing is exact.

func (SpendEnvelope) IsZero added in v0.37.0

func (s SpendEnvelope) IsZero() bool

IsZero supports json omitzero.

type SpikeAdvisory added in v0.37.0

type SpikeAdvisory struct {
	Spiked        bool    `json:"spiked"`
	PrevContext   int     `json:"prev_context"`   // previous turn's context tokens
	LatestContext int     `json:"latest_context"` // latest turn's context tokens
	DeltaTokens   int     `json:"delta_tokens"`   // LatestContext - PrevContext
	Ratio         float64 `json:"ratio"`          // LatestContext / PrevContext
}

SpikeAdvisory is the fold's result: the latest turn-over-turn context comparison and whether it cleared both spike floors. The observed numbers are carried even when Spiked is false so a renderer (`fak ps`, /metrics — the #2197 observability rung) can show the growth shape without re-deriving it. The zero value is the quiet "nothing to say" advisory an empty or single-turn ring folds to.

func (SpikeAdvisory) Nudge added in v0.37.0

func (a SpikeAdvisory) Nudge() string

Nudge renders the model-facing advisory line, or "" when there is nothing to say (no spike). Deterministic — same advisory, byte-identical string — so a transcript diff or a test can bind to it. The text leads with the observed numbers (the model should see the cost, not just an instruction), names the concrete hygiene moves, and asks the model to make the NEXT large ingest deliberate — the ask-first posture #2197's example names — while stating honestly that nothing was blocked.

type SpikePolicy added in v0.37.0

type SpikePolicy struct {
	MinRatio       float64 `json:"min_ratio"`        // latest/previous context floor; <=1 (or NaN via zero) => default
	MinDeltaTokens int     `json:"min_delta_tokens"` // one-turn context growth floor; <=0 => default
}

SpikePolicy is the two-axis threshold a context spike must clear. The zero value is valid and means the defaults — a caller opts into stricter or looser thresholds by setting either field; non-positive/garbage fields fall back to the defaults, the same fail-closed posture PaceBudget applies to a poisoned ratio.

type SpliceResult added in v0.35.0

type SpliceResult struct {
	Warm              bool
	Restored          *model.KVCache
	Direction         cachemeta.KVTransferDirection
	FromTier          cachemeta.ResidencyTier
	ToTier            cachemeta.ResidencyTier
	RestoredPositions int
	SpanPointer       KVSpanPointer
	Residency         abi.KVResidency
}

SpliceResult is the typed record of one warm-KV splice. Warm is true exactly when a parked cache was found and reattached; Restored is the cloned cache the resumed turn attends (nil on a cold miss); Direction is the cachemeta transfer the promote emitted (KVRestore on a warm splice); RestoredPositions is the reattached span length (KVCache.Len). It is the auditable witness that the splice ran — a test (and an observability sink) reads it to prove the resumed turn reused warm KV instead of re-prefilling.

type State

type State struct {
	TraceID        string   `json:"trace_id"`
	Run            RunState `json:"run"`
	Budget         Budget   `json:"budget"`
	Priority       int      `json:"priority"` // scheduling rank; lower yields first under contention
	Pace           Pace     `json:"pace"`
	Reason         string   `json:"reason,omitempty"`          // closed token on Throttled/Stopped; "" otherwise
	ContinuationID string   `json:"continuation_id,omitempty"` // fresh-window handoff id minted on context exhaustion
	ParentTrace    string   `json:"parent_trace,omitempty"`    // the trace this session was re-continued FROM (Recontinue lineage)
	Generation     int      `json:"generation,omitempty"`      // how many budget-reset re-continuations preceded this session (0 = original)
	// CacheAffinity is the provider/engine cache-affinity decision attached to a
	// continuation lineage (issue #1609). A context-budget reset must not silently
	// throw away a warm provider/engine cache route just because the visible trace
	// id changed, so the state carries an auditable decision: preserve the lineage's
	// opaque affinity key from parent trace to continuation trace. Advisory only:
	// correctness never depends on provider affinity landing.
	CacheAffinity CacheAffinityDecision `json:"cache_affinity,omitempty,omitzero"`
	// Intent is the ADVISORY, never-trust projection of what the kernel knows about
	// this session's next turn but the GPU cannot see (issue #807, the intent conduit
	// #805). A scheduler reading Snapshot MAY act on it to place KV / order prefill,
	// but MUST degrade to the GPU-visible decision when it is absent or stale — a hint
	// that gates correctness is a bug. The zero value is "no opinion".
	Intent TurnIntent `json:"intent,omitempty,omitzero"`
	// Goal is the session's active root descriptor (issue #849, the reachability-layer
	// epic #844). It is the cross-session bridge for the in-window goal pin
	// (internal/agent/ctxplan_session.go's goalPin, #845): a structural root a scheduler
	// reading Snapshot can rank a session by — an opaque id/digest plus an optional
	// Priority and Budget, NO transcript and NO model judgment. The zero value is "no
	// goal set", and a session with no goal behaves exactly as today. Advisory only: a
	// goal field that gated any decision would be a bug. Zero readers required — the
	// field is inert until a consumer (the scheduler, #627) acts on it.
	Goal Goal `json:"goal,omitempty,omitzero"`
	// Cost is the bounded per-session ring of the last CostRingSize turns' token cost
	// (issue #756, epic #748 Pillar 2), recorded by DebitUsage and carried out through
	// Snapshot so `fak ps` can render a true cost-PER-ITERATION column — the metric that
	// spikes ~200x on a runaway loop. Advisory/observability only, never trust: a renderer
	// reads it, no decision gates on it. The zero ring is the safe "no cost history yet"
	// default and, via omitzero, marshals byte-identically to a pre-ring State.
	Cost CostRing `json:"cost,omitempty,omitzero"`
	// LastActive is the durable dormancy clock (issue #1179, the random-time-horizons
	// epic #1178): a monotonic LastActiveAt stamp from which a session's dormancy band
	// (warm/cool/cold/frozen/ancient) is derivable without I/O via
	// LastActive.HorizonAt(now). It is the session's home for the "how long has this
	// been off?" measurement the rehydration rungs (#1181-#1186) will scale revalidation
	// to. ADVISORY / no-behavior-change in Phase 1: zero readers gate on it, and the zero
	// (never-stamped) Stamp marshals away via omitzero, so a pre-clock State is wire-
	// identical. A consumer that promotes it to a live field (resume's idle figure, the
	// scheduler's dormant-vs-stuck split #1180) lands in a later phase.
	LastActive dormancy.Stamp `json:"last_active,omitempty,omitzero"`
	// Time is the wall-clock budget tracker (issue #1584, epic #1570 "managed
	// context"): a persisted, timestamp-based allotment of REAL elapsed time,
	// independent of the token axes on Budget. It is carried forward across a
	// Recontinue re-arm exactly like Generation/ParentTrace (see (*Table) RecontinueAt in
	// table.go), so a hidden context reset does not zero the wall-clock accounting.
	// The zero value is unbounded/never-started — a State with no configured time
	// envelope behaves byte-identically to a pre-#1584 State (omitzero keeps the wire
	// shape unchanged when unused).
	Time TimeBudget `json:"time,omitempty,omitzero"`
	// Throughput is the throughput envelope axis as live drive state (issue #2762,
	// the out-of-band operator-control epic #2753): the soft expected pace-shaping
	// rate, the enforced minimum sustained-rate floor, and the accumulated
	// observation window DebitUsage judges the floor against (see throughput.go).
	// Carried forward across a Recontinue re-arm like Time/spend, so a hidden
	// context reset cannot launder a session running below its floor. The zero
	// value is unconfigured — a State with no throughput envelope behaves
	// byte-identically to a pre-#2762 State (omitzero keeps the wire shape
	// unchanged when unused).
	Throughput ThroughputBudget `json:"throughput,omitempty,omitzero"`
	// Assumptions is the live, visible ledger of facts the session is relying on.
	// It carries provenance, confidence, and expiry only; it never carries hidden
	// transcript bytes and it does not gate behavior by itself. Empty means the
	// session has no active assumptions to report.
	Assumptions []Assumption `json:"assumptions,omitempty"`
	// ResetTransaction is the latest context-budget reset row that minted this trace.
	// It binds old trace, new trace, seed digest, contributor list, omitted spans, and
	// fresh budget re-arm to the child state, so a continuation can be audited from
	// kernel data instead of a model self-report. The zero value means this state was
	// not produced by a reset.
	ResetTransaction ResetTransaction `json:"reset_transaction,omitempty,omitzero"`
	// ObjectivePin is the standing user objective's stable, addressable span (issue
	// #1583, the managed-context runtime-continuity epic #1570): a PinID that must
	// survive every replan/reset/migration unchanged, plus a content Digest that makes
	// "the objective was preserved" a checkable equality instead of a narrative claim.
	// It rides the drive record — and therefore this State's existing dump/restore and
	// sessionimage migration paths — so a session migrated to a new process (issue
	// #1589) reports the SAME pinned objective before continuing, not a silently reset
	// one. The zero value means no objective has been pinned yet; a State with no pin
	// behaves byte-identically to a pre-#1589 State (omitzero keeps the wire shape
	// unchanged when unused). session owns no pinning policy of its own — sessionreset's
	// PinObjective/RepinObjective/CarryObjective mint and reconcile the pin; this field
	// is only its durable home on the drive so a migration cannot drop it.
	ObjectivePin ctxplan.ObjectivePin `json:"objective_pin,omitempty,omitzero"`
	// Pins is the OPERATOR-declared keep-set for this session (issue #2211, the
	// out-of-band control-plane epic #2208): the span / fact ids an operator has asked
	// to stay resident through the planner's automatic shed. It is the cross-session
	// home of the in-window keep-set the planner applies via SessionPlanner
	// .SetOperatorPins (internal/agent) — a declared INTENT input, never a plan: the
	// table records the ids and nothing more, the planner forces them ahead of the
	// knapsack, and eviction stays automatic for everything else. An id naming no live
	// span is harmless (the planner skips it), so drive state may legitimately run
	// ahead of ingestion.
	//
	// The set is REPLACED wholesale by each write and cleared by a nil/empty one (see
	// Table.SetPins) — pin/unpin merge discipline lives with the CLI's
	// read-modify-write, so two operators racing cannot silently union their keep-sets.
	// SetPins copies the caller's slice in; readers must treat the slice a Get/Snapshot
	// hands back as read-only, exactly like every other reference field on this record.
	// The zero value is "no operator pins" and, via omitempty, marshals byte-identically
	// to a pre-#2211 State.
	Pins []string `json:"pins,omitempty"`
	// PendingTurn is the write-ahead checkpoint of an in-flight turn's retry/backoff
	// progress (issue #1363, epic #1352 Pillar 3 "durable turn"). The retry loop
	// (internal/agent's HTTPPlanner.Complete) tracks its attempt count and last
	// observed status purely in local Go variables — a kill -9 mid-retry loses that
	// progress even though the rest of this State survives via the Descriptor/
	// Registry (session_durable.go's persistServeSessionRevision already writes
	// through on every Decide/DebitUsage). Recording it here, through the SAME
	// already-wired persistence path, gives a restart a real pointer to how far the
	// lost turn had gotten instead of silently starting over with no memory of what
	// already failed. The zero value means "no turn in flight" (StartedAt is zero);
	// a caller clears it (SetPendingTurn with the zero value) once the turn
	// completes, so a session with no in-flight retry restores byte-identically to
	// a pre-#1363 State (omitzero keeps the wire shape unchanged when unused). This
	// field is the durable primitive only — wiring internal/agent's retry loop to
	// call SetPendingTurn is the follow-on that actually closes #1363.
	PendingTurn PendingTurn `json:"pending_turn,omitempty,omitzero"`
	// QualityEnvelope is the session-start QA origin record (issue #1964, QA-dogfood
	// spine #1961/QD-004): the single record saying which QA controls govern this
	// session — the budget axes it opened under, the witness policy that gates its
	// claims, the dogfood probes expected at origin, and the control-pane scorecards
	// it is a member of (see quality_envelope.go). It rides the drive record — and
	// therefore this State's existing dump/restore and sessionimage migration paths —
	// so a session dumped and restored (or migrated to a new process) exposes the SAME
	// QA envelope it started under, not a re-derived or silently reset one. The zero
	// value means no envelope was stamped; a State with none marshals byte-identically
	// to a pre-#1964 State (omitzero keeps the wire shape unchanged when unused).
	// Advisory/observability only: no decision gates on it — a renderer or inspect
	// surface reads it, nothing trusts it.
	QualityEnvelope QualityEnvelope `json:"quality_envelope,omitempty,omitzero"`
	Rev             uint64          `json:"rev"`
}

State is the full drive record for one session, keyed by TraceID. It carries its own TraceID so a Snapshot row is self-describing for a scheduler (which sorts a []State without re-keying). Rev is a monotonic revision bumped on every write — the optimistic-concurrency guard a stale operator UI is checked against, and the cursor a /v1/fak/changes stream of drive revisions would key on.

func DefaultState

func DefaultState(traceID string) State

DefaultState is the drive a fresh/unseen session reads: Running, unbounded budget, zero priority, no pace opinion. It is what Get returns for a trace the table has never seen and what an LRU-evicted trace reads on its next touch — the safe default (a live session at its defaults), never a phantom Stopped.

type Table

type Table struct {
	// contains filtered or unexported fields
}

Table holds the live drive state of every recent served session, keyed by TraceID, bounded LRU. It is the gateway-owned object the session routes read and write, and the data structure a scheduler reads via Snapshot. Construct with NewTable / NewTableWithLimit; the zero value is not usable.

func NewTable

func NewTable() *Table

NewTable returns a Table bounded by DefaultTableLimit sessions.

func NewTableWithLimit

func NewTableWithLimit(limit int) *Table

NewTableWithLimit builds a table with a bounded session record table. limit<=0 uses DefaultTableLimit. The most recently touched sessions are retained.

func (*Table) CompareAndSet

func (t *Table) CompareAndSet(trace string, expectRev uint64, want State) (State, bool)

CompareAndSet applies want only if the session's current Rev equals expectRev — the optimistic-concurrency guard a stale operator UI is checked against, so a newer transition is never silently clobbered. want's TraceID and Rev are ignored (the key is trace; the new Rev is assigned by putLocked). ok=false means the Rev did not match (the caller re-reads and retries) OR the session is terminal.

func (*Table) ContextNudge added in v0.37.0

func (t *Table) ContextNudge(trace string) string

ContextNudge is the turn-boundary read the agent loop makes right after Decide: the rendered spike nudge for this session's latest debited turn, "" when there is nothing to say. Read-only — no debit, no state transition, no LRU-order guarantee beyond Get's own — and nil-safe (a loop with no table wired behaves byte-identically to the historical path, the same posture Decide's nil receiver takes). Uses the default SpikePolicy; the operator-set per-session policy is a follow-on rung of #2197.

func (*Table) Debit

func (t *Table) Debit(trace string, tokensUsed int) State

Debit decrements a session's remaining token budget by the reported usage of a just-completed turn, after the planner returns. Decide debits turns (it knows a turn is starting); only the loop knows the actual token usage, so it reports it here. A terminal/paused session is left unchanged. It returns the new state. A nil receiver is a no-op. Crossing zero is observed by the NEXT Decide (which sees TokensLeft<=0 and drains) — Debit itself does not transition, keeping the "stop is taken at a boundary" invariant.

func (*Table) DebitClarificationQuery added in v0.37.0

func (t *Table) DebitClarificationQuery(trace string) QueryBudgetVerdict

DebitClarificationQuery spends one clarification/self-query ask from the session's query budget. Unconfigured budgets are permissive and do not create a session record. Exhaustion refuses only the clarification path: the main session remains live so the caller can continue with known context or ask the user through a different governed path.

func (*Table) DebitToolCall added in v0.42.0

func (t *Table) DebitToolCall(trace string) Verdict

DebitToolCall spends one DISPATCHED tool call from the session's runaway floor (#2887, the Hermes cron-hardening angle) and reports whether the loop may proceed with this call. It is the tool-call twin of the turn debit in Decide, but taken per tool call rather than per turn boundary, so a single turn that emits a long tool-call loop is cut at the budget too — the structural floor a scheduled agent's model cannot extend. An unbounded (unconfigured) axis proceeds without a record; a held/terminal session proceeds no new work; a live session with the ceiling already spent drives to Draining/Stopped with ReasonBudgetToolCalls, exactly like a turn or token exhaustion, so the stop is observable in the drive state and witnessed on exit. A nil receiver is a permissive no-op so a loop with no table behaves byte-identically to the pre-axis path.

func (*Table) DebitUsage

func (t *Table) DebitUsage(trace string, u Usage) State

DebitUsage records a completed turn's token usage against the session's live budgets. Output-token exhaustion is still observed by the next Decide, matching Debit's original boundary discipline. Context-token exhaustion is the long-window reset trigger: the session is moved to Draining immediately and a continuation id is minted so the next boundary can tell a supervisor which fresh window to start.

func (*Table) Decide

func (t *Table) Decide(trace string) Verdict

Decide is the per-turn boundary gate. Given a session's TraceID it:

  1. reads the current drive record (default Running/unbounded if unseen),
  2. resolves terminal/paused/drain run-states to a non-proceed verdict WITHOUT debiting (a held or stopped session must not burn budget),
  3. for a live (Running/Throttled) session, debits one turn and checks the remaining budget — a zero/negative remaining axis drives the session to Draining (a real write, Rev bumped) so the exhaustion is observable, and returns a non-proceed verdict carrying the exhaustion reason,
  4. otherwise returns Proceed=true with the per-turn pace cap (MaxTokensPerTurn, MinTurnGapMs) for the loop to apply.

It is the table's only read-modify-write on the hot path; it takes the lock once. A nil receiver is a valid no-op-permissive gate (Proceed=true, no cap) so a loop with no table wired behaves byte-identically to the pre-table path — the caller does not need a nil check.

func (*Table) DecideTimeBudget added in v0.37.0

func (t *Table) DecideTimeBudget(trace string, now time.Time) Verdict

DecideTimeBudget is the wall-clock twin of Decide's budget check: given trace's current TimeBudget as of now, it reports whether the run should STOP (the envelope is exceeded), and if so drives the session to Draining/Stopped exactly like a token-axis exhaustion (ReasonTimeBudgetExhausted), folding the elapsed live duration into ElapsedNanos so the stopped record's accounting is final and correct. A session that is Paused/Draining/Stopped, or has no bounded time envelope, is left untouched and reports Proceed=true (Decide's own run-state gate already covers those cases; this verb only adds the wall-clock axis on top of a live session). Call this once per turn boundary alongside Decide — it is deliberately separate so a caller not using wall- clock budgets pays zero cost and sees zero behavior change.

func (*Table) Get

func (t *Table) Get(trace string) State

Get returns trace's current drive record (its default if unseen). It is a pure read — it does NOT touch recency, so polling a session's state from an operator UI never keeps a finished session resident or evicts an active one.

func (*Table) Len

func (t *Table) Len() int

Len reports the number of retained session records.

func (*Table) Limit

func (t *Table) Limit() int

Limit reports the configured maximum retained session records.

func (*Table) PauseTimeBudget added in v0.37.0

func (t *Table) PauseTimeBudget(trace string, now time.Time) State

PauseTimeBudget folds trace's live wall-clock duration into its TimeBudget's ElapsedNanos and clears the running clock, as of now — the write a hidden restart (or a clean shutdown) makes BEFORE the process goes away, so the durable State (persisted via Descriptor/Registry exactly like Budget already is) carries the true elapsed total forward instead of losing the live run's duration. A terminal session still accepts this write (unlike most control verbs): a Stopped/Draining session's elapsed time must still be foldable so its final accounting is correct, mirroring how Restore may re-establish a terminal record faithfully. Pausing an already-paused (or time-unconfigured) session is a safe no-op.

func (*Table) QueryTimeBudget added in v0.37.0

func (t *Table) QueryTimeBudget(trace string, now time.Time) TimeQueryVerdict

QueryTimeBudget answers "how much wall-clock budget does trace have left as of now" without mutating anything (Table.Decide's per-turn read is the mutating half; this is the pure query a `fak session status`/supervisor check calls as often as it likes). An unseen trace reports the default (unbounded, not running) TimeBudget's verdict.

func (*Table) Recontinue

func (t *Table) Recontinue(parent, child string, fresh Budget) State

Recontinue re-arms a budget-drained session under a FRESH trace (its continuation id), carrying a clean budget — the "human-like reset" write. It is the one verb that may follow a terminal budget exhaustion: the parent session stays Stopped (its closed Reason preserved for audit), and a NEW live session is minted under child, linked back via ParentTrace with Generation incremented from the parent. This is deliberately NOT SetBudget (which refuses a terminal session — you do not un-stop one) and NOT Reset (which deletes the record): the parent's drained record is left intact so the budget-exhaustion event stays observable, and the fresh session is a new key, not a resurrection of the old one.

The returned State is the fresh child (Running, fresh budget, ParentTrace=parent, Generation=parent.Generation+1, Reason=ReasonBudgetReset, Rev 1). The parent trace is left exactly as the budget drain left it, EXCEPT its TimeBudget's live clock is paused (folded into ElapsedNanos) — see RecontinueAt, which this delegates to with now defaulted to time.Now at the call boundary only (never inside a decision path). A nil receiver mints a detached default child (no table to record into) so a loop with no table behaves sanely.

func (*Table) RecontinueAt added in v0.37.0

func (t *Table) RecontinueAt(parent, child string, fresh Budget, now time.Time) State

RecontinueAt is Recontinue with an explicit now, for deterministic testing of the wall-clock carry-forward (issue #1584): the fresh child's TimeBudget PRESERVES the parent lineage's total ElapsedNanos and LimitNanos (a hidden context reset must not zero the wall-clock envelope any more than it zeros Generation), pausing the parent's clock at now (folding its live duration in) and re-arming the SAME accumulated total on the child, started fresh at now. A parent with no time budget configured (Bounded()==false and never started) carries forward a zero TimeBudget, so a caller not using wall-clock budgets sees no behavior change.

func (*Table) RecontinueAtWithTransaction added in v0.37.0

func (t *Table) RecontinueAtWithTransaction(parent, child string, fresh Budget, now time.Time, tx ResetTransaction) State

RecontinueAtWithTransaction is RecontinueAt with an explicit reset transaction. The table normalizes old/new trace and budget re-arm from its actual write, so a stale caller cannot attach a row that disagrees with the child it minted.

func (*Table) RecontinuePooled

func (t *Table) RecontinuePooled(parent, child string, want Budget, pool *Pool) (State, bool)

RecontinuePooled is Recontinue against a shared Pool: it re-arms a budget-drained session on a fresh window whose token allotment is DRAWN from the fleet-wide pool, so N sessions resetting under one ceiling cannot collectively exceed it. want is the budget the child would get with no pool; its TokensLeft axis is clamped to what the pool grants, every other axis passes through untouched (the pool caps OUTPUT tokens — the axis a fleet-wide "N sessions share 150k" cap is about). The bool is the pool's ok: false means the pool could not fully fund the request (dry, or only partially funded). The child is still armed with whatever was granted (granted==0 ⇒ TokensLeft 0, which the next Decide drains immediately), so a host wanting a hard fleet stop checks ok and declines the continuation rather than relying on this to refuse.

An unbounded/nil pool grants want.TokensLeft in full, so RecontinuePooled with no pool is byte-identical to Recontinue. An Unbounded want.TokensLeft (no per-session token cap) is passed through UNDRAWN: "unbounded per session" opts that session out of the shared cap by construction — a fleet token cap is only meaningful for a session that carries a finite token budget — so it neither draws nor reports against the pool.

func (*Table) RecontinueWithTransaction added in v0.37.0

func (t *Table) RecontinueWithTransaction(parent, child string, fresh Budget, tx ResetTransaction) State

RecontinueWithTransaction is Recontinue with a caller-supplied reset audit row. Hosts that build a sessionreset.Seed pass the transaction sessionreset derived from that seed so the child carries seed digest / contributor / omitted-span evidence along with the table-owned lineage and budget re-arm fields.

func (*Table) Reset

func (t *Table) Reset(trace string)

Reset clears a session's record (a fresh session / test isolation). The next touch reads the default — Running, unbounded budget. Mirrors ifc.Ledger.Reset.

func (*Table) Restore

func (t *Table) Restore(trace string, st State) State

Restore loads a full drive record verbatim under trace, preserving its Rev, and returns the stored record. It is the durable-resume inverse of Snapshot: a Stopped image restores AS Stopped (Restore is the only write that re-establishes a terminal session), and Snapshot followed by Restore is the identity. The empty TraceID in st is replaced by trace.

func (*Table) ResumeTimeBudget added in v0.37.0

func (t *Table) ResumeTimeBudget(trace string, now time.Time) (State, bool)

ResumeTimeBudget re-arms trace's wall-clock clock at now after a hidden restart, preserving the ElapsedNanos a prior PauseTimeBudget (or a persisted Descriptor restore) carried forward — the read-side counterpart of PauseTimeBudget. A terminal session rejects the change (a stopped session's clock should not resume ticking).

func (*Table) SetBudget

func (t *Table) SetBudget(trace string, b Budget) (State, bool)

SetBudget re-sets a session's remaining allotment live — raise to extend/speed up, cut to slow down or to let an urgent session pass. A terminal session rejects the change. Pass Unbounded on an axis to clear its cap.

func (*Table) SetGoal added in v0.35.0

func (t *Table) SetGoal(trace string, goal Goal) (State, bool)

SetGoal records the session's active goal root (issue #849, the reachability-layer epic #844). A terminal session rejects the change. The table only RECORDS it — a scheduler reading Snapshot decides whether to rank by it, and behaves identically when the goal is zero. The goal never gates correctness; it is a retention/ranking root only. Setting it bumps Rev like any other write, so a /v1/fak/changes cursor sees the goal update and a concurrent reader observes a monotonic version.

func (*Table) SetPace

func (t *Table) SetPace(trace string, p Pace) (State, bool)

SetPace re-sets a session's per-turn throttle live. A terminal session rejects the change. A zero axis means "no opinion" (planner default).

func (*Table) SetPendingTurn added in v0.38.0

func (t *Table) SetPendingTurn(trace string, pt PendingTurn) (State, bool)

SetPendingTurn records (or, with the zero PendingTurn, clears) the write-ahead checkpoint of an in-flight turn's retry progress (issue #1363). A terminal session rejects the change, matching every other Set*. The table only RECORDS it; the retry loop is the writer and the durable Registry — persisted through the SAME Update path as every other drive field — is what makes it survive a kill -9. Setting it bumps Rev like any other write.

func (*Table) SetPins added in v0.44.0

func (t *Table) SetPins(trace string, pins []string) (State, bool)

SetPins records the operator-declared keep-set for a session (issue #2211, the out-of-band control-plane epic #2208): the span/fact ids an operator asked to stay resident through the planner's automatic shed. A terminal session rejects the change, matching every other Set*. The table only RECORDS the set — the planner (internal/agent's SessionPlanner.SetOperatorPins) forces the ids ahead of the knapsack, and eviction stays automatic for everything else.

The write REPLACES the whole set, and a nil/empty set clears it: pin/unpin merge discipline lives with the CLI's read-modify-write, never in the table, so two operators racing cannot silently union their keep-sets. The caller's slice is COPIED in, so a caller that retains and later mutates the argument cannot reach into the stored record. Setting it bumps Rev like any other write, so a /v1/fak/changes cursor sees the pin update.

func (*Table) SetPriority

func (t *Table) SetPriority(trace string, priority int) (State, bool)

SetPriority re-sets a session's scheduling rank live. A terminal session rejects the change. Lower yields first under contention; the table only records it — a scheduler reading Snapshot acts on it.

func (*Table) SetThroughputBudget added in v0.40.0

func (t *Table) SetThroughputBudget(trace string, b ThroughputBudget) (State, bool)

SetThroughputBudget re-sets a session's throughput envelope live (#2762): the soft expected pace-shaping rate and the enforced minimum sustained-rate floor. The accumulated observation window is preserved — re-stating the rates must not forget what has already been measured under a live floor. A terminal session rejects the change, matching SetBudget.

func (*Table) SetTimeBudget added in v0.37.0

func (t *Table) SetTimeBudget(trace string, b TimeBudget) (State, bool)

SetTimeBudget re-sets a session's wall-clock envelope live (issue #1584) — raise it to grant more real time, cut it to bound a runaway managed run. A terminal session rejects the change, matching SetBudget. Unlike SetBudget, this does NOT arm the clock: pass a TimeBudget built with WithLimit (Start/Running left false) to configure the envelope without starting it, or one already Started/Resumed to configure AND arm it in one write. StartTimeBudget is the common case (configure once, arm now).

func (*Table) SetTurnIntent added in v0.35.0

func (t *Table) SetTurnIntent(trace string, intent TurnIntent) (State, bool)

SetTurnIntent records the ADVISORY next-turn hint set for a session (issue #807). A terminal session rejects the change. The table only RECORDS it — a scheduler reading Snapshot decides whether to act on it, and MUST degrade to the GPU-visible decision when the intent is zero or stale. The hint never gates correctness; it is a cost/latency lever (vCache posture). Setting it bumps Rev like any other write, so a /v1/fak/changes cursor sees the intent update.

func (*Table) SetWallClockLimit added in v0.40.0

func (t *Table) SetWallClockLimit(trace string, limit time.Duration, now time.Time) (State, bool)

SetWallClockLimit re-sets a session's wall-clock LIMIT live through the control route (#2762) — the operator form of SetTimeBudget that PRESERVES the lineage's accumulated elapsed time (an operator adjusting the ceiling mid-run must not zero the clock) and arms the clock at now when it is not already ticking, so a limit set on a session that never started one begins enforcing immediately. limit<=0 clears the envelope (WithLimit's TimeUnbounded rule) while the clock keeps ticking for observability, matching StartTimeBudget's unbounded case. A terminal session rejects the change, matching SetBudget.

func (*Table) Snapshot

func (t *Table) Snapshot() []State

Snapshot returns a copy of every retained session's drive record, sorted into the order a scheduler consumes: by Priority ascending (lower yields first), ties broken by Rev descending (the more recently changed session first), then TraceID for total determinism. This is the SCHEDULER's read — the table is its data structure; the scheduler reads this snapshot and picks who yields. The returned slice is a fresh copy, safe to sort/mutate by the caller. A read-only operation: it does not touch recency.

func (*Table) StartTimeBudget added in v0.37.0

func (t *Table) StartTimeBudget(trace string, limit time.Duration, now time.Time) (State, bool)

StartTimeBudget configures trace's wall-clock envelope to limit and arms the clock at now in one write — the usual entry point for a managed run that wants "govern me to at most N wall-clock minutes starting now". limit<=0 configures an unbounded budget (TimeUnbounded) that is still started (Running() true), so Elapsed(now) still reports real elapsed time even with no cap — useful for observability-only wall-clock tracking. A terminal session rejects the change.

func (*Table) TerminateSignal added in v0.38.0

func (t *Table) TerminateSignal(trace string) <-chan struct{}

TerminateSignal returns the channel that is CLOSED the moment trace enters Terminating — the loop-side wake-up runArm selects on to cancel in-flight work at the next safe point. It is level-triggered, not one-shot: a trace already Terminating (or already finalized Stopped with the TERMINATED reason) gets an already-closed channel, so a late registration never blocks on a signal that fired before it arrived. Successive calls for a live trace return the SAME channel. A nil receiver returns nil — a nil channel blocks forever in a select, so a loop with no table wired behaves byte-identically to the pre-terminate path.

func (*Table) Transition

func (t *Table) Transition(trace string, to RunState, reason string) (State, bool)

Transition requests a run-state change. The legal moves enforce the small state machine: a terminal (Stopped) session rejects every change (ok=false) — you start a new session, you do not un-stop one. Setting Throttled/Stopped records the reason; clearing back to Running clears it. Every other live->live move is allowed (the operator is trusted; the kernel only forbids resurrecting a terminal session).

func (*Table) WaitResume added in v0.35.0

func (t *Table) WaitResume(ctx context.Context, trace string) ResumeVerdict

WaitResume blocks while trace is Paused and returns when it is transitioned back to a live state, stopped, or ctx is cancelled. It is the live-resume loop a served session runs at a turn boundary: instead of cold re-attaching, it parks on the Paused hold and is woken the instant an operator resumes it, then re-admits — warm if the wired splicer reattached KV, cold otherwise.

A session that is NOT Paused returns immediately with Resumed=true (Running/Throttled) or Resumed=false (terminal) — the wait is for the operator HOLD, never for a turn. A nil receiver returns an immediate cold-resume default so a loop with no table wired behaves byte-identically to the pre-resume path.

func (*Table) WatchBudget

func (t *Table) WatchBudget(warnFraction float64, obs BudgetObserver)

WatchBudget wires the pre-exhaustion warning + exhaustion observer. warnFraction is the consumed share (0..1) at which BudgetWarn fires — 0.8 warns at 80% of the context budget spent; a value <=0 or >=1 disables the warning (only BudgetExhausted then fires). obs==nil clears the seam (back to the no-op default). Safe to call on a live table; a nil receiver is a no-op.

func (*Table) WatchRelayShadow added in v0.37.0

func (t *Table) WatchRelayShadow(softMark float64, obs RelayShadowObserver)

WatchRelayShadow wires the relay soft-mark observer. softMark is the resident-context fraction (current debit's context tokens / leg cap) at which the advisory RELAY_ARMED signal fires. Values outside (0,1] disable the signal; obs==nil clears it.

func (*Table) WatchResumeSplice added in v0.35.0

func (t *Table) WatchResumeSplice(splicer WarmKVSplicer)

WatchResumeSplice wires the warm-KV splice seam. splicer==nil clears it (every resume is then Cold — the byte-identical pre-splice path). Safe to call on a live table; a nil receiver is a no-op.

func (*Table) WatchRevisions added in v0.35.0

func (t *Table) WatchRevisions(obs RevisionObserver)

WatchRevisions wires the every-revision observer (#630) — the source of the gateway's /v1/fak/session/changes drive-state stream. obs==nil clears the seam (back to the byte-identical no-op default; the write path is unchanged when nothing is watching). Safe to call on a live table; a nil receiver is a no-op.

func (*Table) WatchTransitions added in v0.34.0

func (t *Table) WatchTransitions(obs TransitionObserver)

WatchTransitions wires the run-state transition observer. obs==nil clears the seam. Safe to call on a live table; a nil receiver is a no-op.

type Throughput added in v0.37.0

type Throughput struct {
	// ObservedTokensPerSec is the measured recent rate. 0 means no observation yet.
	ObservedTokensPerSec float64 `json:"observed_tokens_per_sec,omitempty"`
	// ExpectedTokensPerSec is the reference rate ObservedTokensPerSec is judged against
	// (analogous to baselineOutput for MaxTokensPerTurn). 0 means no expectation is
	// configured (no opinion).
	ExpectedTokensPerSec float64 `json:"expected_tokens_per_sec,omitempty"`
}

Throughput is a session's MEASURED recent pace (#1585): how many tokens per wall-clock second it actually produced over some recent window the caller owns (e.g. a rolling turn-completion tracker), judged against the rate it was expected to sustain. It is the runtime-observed twin of Pace.MaxTokensPerTurn, kept as its own type rather than fields on Pace itself — see the file header. The zero value means "no observation yet" and composes to "no opinion" everywhere it is used.

func (Throughput) ComposePlannerBudgetForThroughput added in v0.37.0

func (t Throughput) ComposePlannerBudgetForThroughput(basePlannerBudget int) int

ComposePlannerBudgetForThroughput scales a base resident-context window down by this Throughput's observed ratio, floored at base/MinPlannerBudgetDivisor — the same floor discipline ComposePlannerBudget applies to the configured MaxTokensPerTurn cap, now driven by a measured runtime rate instead of a configured one. A session that is falling behind its expected throughput sees its resident-context window shrink proportionally (fewer spans to hold hot while it is producing tokens slowly), and a session keeping pace or running ahead is untouched — byte-for-byte the base, exactly as an un-observed Throughput was before this composition existed. A non-positive base is returned unchanged.

func (Throughput) ThroughputRatio added in v0.37.0

func (t Throughput) ThroughputRatio() float64

ThroughputRatio (#1585, epic #1570 "managed context") is the fraction in (0,1] of its expected rate this session is ACTUALLY achieving, judged from a runtime observation rather than a configured cap — the measured twin of ThrottleRatio. It is 1.0 ("no constraint") when there is no observation yet (ObservedTokensPerSec <= 0), no expectation configured (ExpectedTokensPerSec <= 0), or the session is keeping pace or running faster than expected (Observed >= Expected — running ahead is never a reason to shrink the window here, exactly as ThrottleRatio never widens on a cap above baseline). Otherwise it is the quotient Observed/Expected, a value in (0,1): a session running at half its expected throughput yields 0.5. NaN/Inf inputs (a corrupt observation) fail closed to 1.0, never past infinity or negative.

type ThroughputBudget added in v0.40.0

type ThroughputBudget struct {
	// ExpectedTokensPerSec is the soft pace-shaping reference rate (#1585). It
	// never drains a session by itself.
	ExpectedTokensPerSec float64 `json:"expected_tokens_per_sec,omitempty"`
	// MinTokensPerSec is the enforced floor: sustained observed throughput below
	// it (past the grace window) drains the session. 0 = no floor configured.
	MinTokensPerSec float64 `json:"min_tokens_per_sec,omitempty"`
	// ObservedOutputTokens / ObservedNanos are the accumulated observation window
	// DebitUsage debits: total output tokens over total reported turn duration
	// since the floor was configured. Their ratio is the sustained rate the floor
	// is judged against.
	ObservedOutputTokens int64 `json:"observed_output_tokens,omitempty"`
	ObservedNanos        int64 `json:"observed_nanos,omitempty"`
}

ThroughputBudget is a session's throughput envelope as live drive state: the configured rates plus the accumulated observation window they are judged against. The zero value is "axis not configured" — no observation accumulates and BelowFloor is always false, so a pre-#2762 State behaves byte-identically.

func (ThroughputBudget) BelowFloor added in v0.40.0

func (b ThroughputBudget) BelowFloor() bool

BelowFloor reports whether the enforced floor is breached: a configured floor, an observation window past the grace period, and a sustained rate under the minimum. An unconfigured floor (or a still-in-grace window) is never a breach.

func (ThroughputBudget) Bounded added in v0.40.0

func (b ThroughputBudget) Bounded() bool

Bounded reports whether this axis carries an enforced floor, mirroring TimeBudget.Bounded / Budget.spendBounded. An expected-only envelope is NOT bounded — the expected rate shapes pace, it never stops a run.

func (ThroughputBudget) IsZero added in v0.40.0

func (b ThroughputBudget) IsZero() bool

IsZero supports json omitzero, so a session with no throughput envelope keeps the pre-#2762 wire shape byte-for-byte.

func (ThroughputBudget) ObservedTokensPerSec added in v0.40.0

func (b ThroughputBudget) ObservedTokensPerSec() float64

ObservedTokensPerSec is the sustained observed rate over the accumulated window; 0 when nothing has been observed yet.

type ThroughputEnvelope added in v0.37.0

type ThroughputEnvelope struct {
	ExpectedTokensPerSec float64 `json:"expected_tokens_per_sec,omitempty"`
	MinTokensPerSec      float64 `json:"min_tokens_per_sec,omitempty"`
}

ThroughputEnvelope records the expected/minimum throughput rates named by the user.

func (ThroughputEnvelope) IsZero added in v0.37.0

func (t ThroughputEnvelope) IsZero() bool

IsZero supports json omitzero.

type TimeBudget added in v0.37.0

type TimeBudget struct {
	// LimitNanos is the total wall-clock envelope in nanoseconds across the whole
	// lineage. <= 0 means TimeUnbounded (no limit) — the query/decide paths treat a
	// zero-value TimeBudget as fully permissive, matching Budget's Unbounded convention.
	LimitNanos int64 `json:"limit_nanos,omitempty"`
	// ElapsedNanos is the accumulated wall-clock time already consumed by every run in
	// this lineage BEFORE the current one started — the carry-forward a hidden restart
	// must not drop. It is only advanced by Pause (which folds the just-ended run's
	// duration in); Elapsed(now) adds the current run's live duration on top without
	// mutating this field, so repeated queries are idempotent.
	ElapsedNanos int64 `json:"elapsed_nanos,omitempty"`
	// StartedAtUnixNano is the wall-clock instant (unix nanoseconds) the CURRENT run
	// began ticking — set by Start on a fresh TimeBudget and re-armed by Resume after a
	// restart. Zero means the current run has not been started/resumed (no live tick;
	// Elapsed(now) then reports ElapsedNanos alone).
	StartedAtUnixNano int64 `json:"started_at_unix_nano,omitempty"`
}

TimeBudget is a session's wall-clock allotment: how much real elapsed time it may run across its whole lineage (the original trace plus every Recontinue'd child), tracked independently of token usage. The zero value is unbounded (no envelope configured) — a State with no TimeBudget behaves exactly as it did before this field existed.

Accounting is timestamp-based, not counter-based: StartedAtUnixNano marks the instant the CURRENT run (since the last resume/restart) began ticking, and ElapsedNanos is the carried-forward total from every PRIOR run in this lineage. Elapsed(now) sums the two, so "how much time has this managed run actually consumed" survives however many hidden restarts happened in between — each restart calls Pause (which folds the just-ended run's duration into ElapsedNanos) and the next boundary calls Resume (which re-arms StartedAtUnixNano at the fresh now). A TimeBudget that is never paused/resumed (a single continuous run) still works: Elapsed(now) is just now - StartedAtUnixNano.

func NewTimeBudget added in v0.37.0

func NewTimeBudget() TimeBudget

NewTimeBudget builds an unbounded TimeBudget (no envelope, not yet started). Use WithLimit to set the envelope and Start to arm the clock.

func (TimeBudget) Bounded added in v0.37.0

func (b TimeBudget) Bounded() bool

Bounded reports whether this axis carries a real envelope. Mirrors Budget's contextBounded/tokensUnbounded naming.

func (TimeBudget) Elapsed added in v0.37.0

func (b TimeBudget) Elapsed(now time.Time) time.Duration

Elapsed returns the total wall-clock time this lineage has consumed as of now: the carried-forward ElapsedNanos plus the current run's live duration (zero if not running). It never mutates the receiver, so polling it repeatedly (a `fak session status` call, a per-turn query) is side-effect-free — only Pause advances ElapsedNanos. A now earlier than StartedAtUnixNano clamps the live component to zero, so a backwards wall-clock never reports negative or inflated elapsed time.

func (TimeBudget) Exceeded added in v0.37.0

func (b TimeBudget) Exceeded(now time.Time) bool

Exceeded reports whether the wall-clock envelope has been reached or passed as of now. An unbounded budget is never exceeded.

func (TimeBudget) Pause added in v0.37.0

func (b TimeBudget) Pause(now time.Time) TimeBudget

Pause folds the current run's live duration into ElapsedNanos and clears StartedAtUnixNano — the "hidden restart is about to happen" write. This is the exact moment a hidden context reset (Recontinue) or a process shutdown must call, mirroring how a real pause/resume cycle works: the elapsed time BEFORE the pause is durably carried, so a subsequent Resume (even in a freshly restarted process, reading this value back from persisted State/Descriptor JSON) resumes accounting from the true total, not from zero. Pausing an already-paused (or never-started) budget is a safe no-op: it neither double-counts nor loses time. now earlier than StartedAtUnixNano (a backwards wall-clock) clamps the folded delta to zero, the same conservative rule dormancy.Stamp.GapAt applies.

func (TimeBudget) Query added in v0.37.0

func (b TimeBudget) Query(now time.Time) TimeQueryVerdict

Query answers "how much wall-clock budget is left as of now, and is the envelope exceeded" without mutating anything — the pure read half of the wall-clock gate, safe to call from `fak session status`, a per-turn check, or a supervisor deciding whether to re-admit a session after a hidden restart.

func (TimeBudget) Remaining added in v0.37.0

func (b TimeBudget) Remaining(now time.Time) (remaining time.Duration, ok bool)

Remaining returns how much wall-clock budget is left as of now: LimitNanos - Elapsed(now), floored at zero. An unbounded budget (Bounded()==false) returns (0, false) — mirroring Budget.contextBounded's "not configured" signal — so a caller never mistakes an absent envelope for a zero remaining allotment.

func (TimeBudget) Resume added in v0.37.0

func (b TimeBudget) Resume(now time.Time) TimeBudget

Resume re-arms the clock at now after a hidden restart, WITHOUT losing the ElapsedNanos carried across the gap — the read side of the same contract Pause writes. It is exactly Start when the budget is already paused (the common case: a restarted process rehydrates a TimeBudget from persisted state, where StartedAtUnixNano is necessarily 0, and Resume arms it at the current wall-clock instant). Calling Resume on an already-running budget is a no-op, matching Start.

func (TimeBudget) Running added in v0.37.0

func (b TimeBudget) Running() bool

Running reports whether the current run is live-ticking (StartedAtUnixNano set). A TimeBudget that has been Paused (or never Started) is not running: Elapsed(now) then reports only the carried-forward total, never accruing more from a stale now.

func (TimeBudget) Start added in v0.37.0

func (b TimeBudget) Start(now time.Time) TimeBudget

Start arms the clock at now for a fresh (or previously-paused) TimeBudget, marking the current run's beginning. Starting an already-running budget is a no-op (it does not reset StartedAtUnixNano out from under a live run) — call Pause first to fold the live duration into ElapsedNanos before re-Starting, or use Resume, which does both.

func (TimeBudget) WithLimit added in v0.37.0

func (b TimeBudget) WithLimit(limit time.Duration) TimeBudget

WithLimit returns a copy with the wall-clock envelope set to limit. A non-positive limit clears the envelope (TimeUnbounded) rather than being stored as a negative number, so a caller passing TimeUnbounded or any other <=0 value gets the same unbounded behavior.

type TimeQueryVerdict added in v0.37.0

type TimeQueryVerdict struct {
	// Bounded reports whether a wall-clock envelope is configured at all. False means
	// every other field is a zero/permissive default (Exceeded=false, Remaining=0,
	// Unbounded reads as "no opinion" rather than "zero time left").
	Bounded bool `json:"bounded"`
	// Exceeded is true when Elapsed >= the configured Limit — the caller should treat
	// this exactly like a token-budget exhaustion (stop at the next boundary).
	Exceeded bool `json:"exceeded"`
	// Elapsed is the total wall-clock time consumed so far across this lineage.
	Elapsed time.Duration `json:"elapsed"`
	// Remaining is Limit-Elapsed, floored at zero; meaningless (reported as 0) when
	// Bounded is false.
	Remaining time.Duration `json:"remaining"`
	// Limit echoes the configured envelope (0 when unbounded).
	Limit time.Duration `json:"limit"`
}

TimeQueryVerdict is the read-only "how much wall-clock budget is left, and should this run stop" answer — the time-axis analogue of QueryBudgetVerdict. It is deliberately a pure query (unlike Decide, it takes no lock and mutates nothing): a caller may ask it as often as it likes — at a turn boundary, from an operator CLI, or from a supervisor loop deciding whether to even re-admit a session after a restart — without perturbing the accounted time. Stopping the run (folding the elapsed time and transitioning the session) is a separate, explicit act via Table.DecideTimeBudget.

type ToolCallDisposition added in v0.38.0

type ToolCallDisposition string

ToolCallDisposition is the actionable guidance attached to a refusal. It is feedback to the model/operator, not a session-control decision.

const (
	ToolDispositionNone      ToolCallDisposition = ""
	ToolDispositionRetryable ToolCallDisposition = "RETRYABLE"
	ToolDispositionWait      ToolCallDisposition = "WAIT"
	ToolDispositionEscalate  ToolCallDisposition = "ESCALATE"
	ToolDispositionTerminal  ToolCallDisposition = "TERMINAL"
)

type ToolCallOutcome added in v0.38.0

type ToolCallOutcome struct {
	Tool        string
	ToolCallID  string
	Kind        ToolCallOutcomeKind
	Reason      abi.ReasonCode
	Disposition ToolCallDisposition
	Progress    bool
	// Target and IntendedEffect are the optional semantic identity of a refused
	// call. They deliberately exclude command bytes: retries that mutate shell
	// syntax while still reaching for the same guarded target/effect remain the
	// same refusal for session-envelope accounting. Empty fields preserve the
	// historical tool/reason/disposition identity used by callers that do not yet
	// report semantic coordinates.
	Target         string
	IntendedEffect string
}

ToolCallOutcome is one call's adjudication result as consumed by the session loop. Reason is the closed per-tool refusal vocabulary; it must never be copied into the session-stop reason slot. If the surrounding turn made useful progress despite this call, set Progress=true so repeated-bad-call policies reset.

func (ToolCallOutcome) BadForSessionControl added in v0.38.0

func (o ToolCallOutcome) BadForSessionControl() bool

BadForSessionControl reports whether this per-call outcome is eligible input to a declared repeated-bad-call policy. It does not make a control decision.

func (ToolCallOutcome) DefaultControl added in v0.38.0

func (o ToolCallOutcome) DefaultControl() SessionControl

DefaultControl is the load-bearing invariant: a tool-call outcome by itself keeps the turn/session going. Escalation requires a separate policy.

func (ToolCallOutcome) ReasonToken added in v0.38.0

func (o ToolCallOutcome) ReasonToken() string

ReasonToken renders the per-tool refusal reason. An allowed/repaired call with no refusal returns "" so it cannot masquerade as a stop reason.

type ToolCallOutcomeKind added in v0.38.0

type ToolCallOutcomeKind uint8

ToolCallOutcomeKind is the session-visible kind of one proposed tool call. It is not a turn/session decision; it is per-call feedback.

const (
	// ToolCallOutcomeAllowed means the call survived adjudication and can make progress.
	ToolCallOutcomeAllowed ToolCallOutcomeKind = iota
	// ToolCallOutcomeRejected means the call was denied as a value with a closed reason.
	ToolCallOutcomeRejected
	// ToolCallOutcomeRepaired means the call was transformed into an admitted shape.
	ToolCallOutcomeRepaired
	// ToolCallOutcomeQuarantined means the call/result was held out of context.
	ToolCallOutcomeQuarantined
)

func (ToolCallOutcomeKind) String added in v0.38.0

func (k ToolCallOutcomeKind) String() string

type TransitionEvent added in v0.34.0

type TransitionEvent struct {
	TraceID        string   `json:"trace_id"`
	From           RunState `json:"from"`
	To             RunState `json:"to"`
	Reason         string   `json:"reason,omitempty"`
	ContinuationID string   `json:"continuation_id,omitempty"`
	Rev            uint64   `json:"rev"`
}

TransitionEvent is the immutable snapshot a TransitionObserver receives when an operator run-state change lands. It is built under the table lock and delivered after release, mirroring BudgetEvent.

type TransitionObserver added in v0.34.0

type TransitionObserver func(TransitionEvent)

TransitionObserver is the run-state boundary callback seam. The host owns fan-out and failure policy; the table only delivers typed transition values.

type TurnCost added in v0.35.0

type TurnCost struct {
	OutputTokens  int `json:"output_tokens"`
	ContextTokens int `json:"context_tokens"`
}

TurnCost is one debited turn's token cost — the output tokens it emitted and the context/prompt tokens it had to read. These are exactly the two axes DebitUsage already receives (Usage), recorded so a renderer can show cost-per-iteration and a supervisor can see the per-turn shape of a runaway. Both are the turn's reported usage, not a running total.

type TurnIntent added in v0.35.0

type TurnIntent struct {
	// EndsSoon: the agent is at a settle point — drain this turn, don't admit new
	// prefill behind it.
	EndsSoon bool `json:"ends_soon,omitempty"`
	// IsSpeculative: this turn is a branch that may be thrown away — prefer-not-to-prefill.
	IsSpeculative bool `json:"is_speculative,omitempty"`
	// WillDiscard: this turn's result is already known to be discarded — the strongest
	// prefer-not-to-prefill signal (ties to discard-aware admission, #808).
	WillDiscard bool `json:"will_discard,omitempty"`
	// SharesPrefixWith names another live session (by TraceID) this turn shares a
	// verbatim prompt prefix with — co-batch / pin the shared KV. "" means no known overlap.
	SharesPrefixWith string `json:"shares_prefix_with,omitempty"`
	// ArrivingInMillis is the deterministic forward-looking signal for a known-coming
	// follow-up turn (issue #811): a tool has been dispatched and the kernel expects this
	// session to re-enter after roughly this many milliseconds. It is advisory and
	// expires in the scheduler; <=0 means no forward reservation request.
	ArrivingInMillis int64 `json:"arriving_in,omitempty"`
	// Prefix is the known reusable prefix identity for that follow-up turn. It is an
	// opaque digest/key, never transcript text. A scheduler may pin matching KV residency
	// and promote the reservation when the real request arrives with the same prefix.
	Prefix string `json:"prefix,omitempty"`
	// ResultAlreadyKnown: the call's output is determined — route to the avoid-the-
	// forward-pass path (ties to vToolcall / vCache, #794/#795).
	ResultAlreadyKnown bool `json:"result_already_known,omitempty"`
}

TurnIntent is the read-only, advisory hint set the adjudicator/session layer emits for a session's NEXT turn, folded into State so a scheduler reading Table.Snapshot can act on what the kernel already knows — the continuous-batching guesses it would otherwise have to reconstruct from sequence length, KV occupancy, and arrival order alone (issue #807). Every field defaults to the safe "no opinion" zero value.

FENCE: advisory, never trust. A hint can be wrong (a turn expected to end keeps going); every consumer degrades to the GPU-visible decision when a hint is absent or stale. This is a cost/latency lever only — a hint must NEVER gate correctness. It is a pure projection over Table.Snapshot and adds nothing to the frozen ABI beyond this struct. The snapshot-reading scheduler HAS landed: as of 7ad164d0fa (#811) Scheduler.ReserveKnownComing walks Table.Snapshot and reservationFromState consumes Intent.ArrivingInMillis/Prefix/WillDiscard to mint advisory slot reservations. What is still absent is a live DRIVER — no non-test caller invokes ReserveKnownComing — so the hint changes no serving behavior yet. Re-check both halves before trusting them.

func (TurnIntent) IsZero added in v0.35.0

func (ti TurnIntent) IsZero() bool

IsZero reports whether the intent carries no opinion — the safe default a scheduler reads as "fall back to the GPU-visible decision". A consumer checks this before acting on any field, so an unset (or never-emitted) intent is never mistaken for a positive hint.

type Usage

type Usage struct {
	OutputTokens   int
	ContextTokens  int
	CostMicroCents int64
	DurationNanos  int64
}

Usage is the per-turn token accounting the model boundary reports after a successful turn. OutputTokens debits the historical output-token budget. ContextTokens debits the long-context guardrail: the prompt/context window the model had to read for this turn, normalized by the caller from provider usage. CostMicroCents debits the spend ceiling (Budget.SpendMicroCentsLeft): the PRICED cost of this turn in micro-cents (1e-8 USD), computed by the caller — the table stays price-blind so the per-MTok price table lives in exactly one place (the host, which knows the provider). 0 = unpriced turn, no spend debit; a dollar-blind host therefore leaves a configured spend budget honestly untouched rather than debiting a guessed cost. DurationNanos is the turn's real wall-clock duration as reported by the caller (the table reads no clock of its own, matching TimeBudget's discipline); it feeds the throughput axis's sustained-rate observation (#2762). 0 = duration unknown, no throughput observation for this turn.

type Verdict

type Verdict struct {
	Proceed   bool
	MaxTokens int
	MinGapMs  int
	State     State
	Stop      bool
	Reason    string
}

Verdict is what Decide returns to the turn loop. Proceed gates the loop: false ends the session this boundary. MaxTokens is the per-turn output cap to lower into the planner (0 = planner default). State is the (possibly just-debited) drive record. Stop is true exactly when the session has reached a terminal boundary this turn (Stopped, or Draining taken now); Reason names which closed cause, so the loop and a supervisor agree on why the slot freed.

type WarmKV added in v0.35.0

type WarmKV struct {
	Cache      *model.KVCache
	ColdTier   cachemeta.ResidencyTier
	SpanDigest string
	Residency  abi.KVResidency
}

type WarmKVSplicer added in v0.35.0

type WarmKVSplicer func(State) SpliceResult

WarmKVSplicer is the host-wired seam that performs the actual warm-KV reattach on a Paused->Running resume. The session package never imports the KV mover (internal/model / internal/cachemeta); the host implements this to call KVCache.Clone / MoveTo(KVRestore) and returns true iff warm KV was available AND spliced, so the resumed turn may reuse it. It returns false to decline (no warm KV held, eviction happened while paused, or any error) — the loop then degrades to cold re-prefill. It is given the resume-edge State so it can key the splice on the trace / continuation lineage. A nil splicer always resumes Cold.

type WarmKVStore added in v0.35.0

type WarmKVStore struct {
	HotTier cachemeta.ResidencyTier
	// contains filtered or unexported fields
}

WarmKVStore parks the offloaded KV of paused sessions and performs the concrete warm splice on resume. It is the host-side object the gateway constructs once and wires into a Table via Splicer(): Park(trace, kv) at pause, and the returned WarmKVSplicer reattaches it on the Paused->Running edge. Safe for concurrent use (a gateway pauses/resumes many sessions).

HotTier is the tier a resume promotes warm KV back TO (default TierHBM — device memory, the hottest tier a served decode attends from). profiles is the tier characteristics map MoveTo consults to land the restored span Resident (default cachemeta.DefaultTierProfiles).

func NewWarmKVStore added in v0.35.0

func NewWarmKVStore() *WarmKVStore

NewWarmKVStore builds an empty store promoting to TierHBM with the default tier profiles.

func NewWarmKVStoreWithBackend added in v0.38.0

func NewWarmKVStoreWithBackend(backend abi.KVBackend) *WarmKVStore

NewWarmKVStoreWithBackend injects the residency backend used across relaunch.

func (*WarmKVStore) CarrySpan added in v0.38.0

func (s *WarmKVStore) CarrySpan(trace string, pointer KVSpanPointer)

CarrySpan installs a durable pointer in a fresh store after process relaunch.

func (*WarmKVStore) Evict added in v0.35.0

func (s *WarmKVStore) Evict(trace string)

Evict drops a trace's parked warm KV — the "evicted while paused" path. After an Evict the trace's next resume finds no warm cache and degrades to cold. A no-op for an unknown trace.

func (*WarmKVStore) LastSplice added in v0.35.0

func (s *WarmKVStore) LastSplice(trace string) (SpliceResult, bool)

LastSplice returns the most recent SpliceResult recorded for a trace and whether one exists. It is the observability read a supervisor / test uses to confirm a resume reused warm KV (Warm && Direction == cachemeta.KVRestore) rather than re-prefilling cold.

func (*WarmKVStore) Park added in v0.35.0

func (s *WarmKVStore) Park(trace string, cache *model.KVCache, coldTier cachemeta.ResidencyTier)

Park records a paused session's offloaded KV under its trace, to be reattached warm on resume. coldTier is the tier the KV was offloaded to while held (the promote source). A nil cache or a nil store is a no-op (the resume then degrades to cold). Calling Park again for a trace replaces the prior parked cache.

func (*WarmKVStore) Splice added in v0.35.0

func (s *WarmKVStore) Splice(trace string) SpliceResult

Splice reattaches a trace's parked warm KV: it CLONES the cache (an exact deep copy, so the resumed turn is bit-identical to a re-prefill) and drives the cachemeta lifecycle promote (MoveTo(HotTier)), which emits KVRestore because the hot tier outranks the cold tier the KV was parked at. It returns a SpliceResult witnessing the move. A trace with no parked cache returns a cold (Warm=false) result and reattaches nothing — the resume loop then falls back to cold re-prefill. The parked entry is consumed on a warm splice (a resume reclaims it once); a host that wants to keep it re-Parks at the next pause.

func (*WarmKVStore) Splicer added in v0.35.0

func (s *WarmKVStore) Splicer() WarmKVSplicer

Splicer adapts the store into the WarmKVSplicer the Table consults on a Paused->Running edge: it splices the resuming session's trace and reports true iff warm KV was reattached (so resume.go returns ResumeWarm). A store with no parked cache for the trace reports false and the resume degrades to cold. Wire it with table.WatchResumeSplice(store.Splicer()).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL