Documentation
¶
Overview ¶
analytics.go — the #4767 half of this leaf: native Codex critical-path and TYPED tool-outcome analytics over the same rollout store the #4785 lifecycle fold reads.
WHY TYPED, NOT REGEX-ONLY. A naive output parser over the observed corpus counted 18,408 "non-zero/error-shaped" tool results — but 675 of them are expected `git rev-parse -q --verify MERGE_HEAD` negatives and 283 are `wait` exit-1 control outcomes. Counting those as failures poisons every failure ranking. This file decodes the STRUCTURED tool envelope first, maps the outcome into a CLOSED vocabulary (the non-guard tool-failure classes #2129 defined), and classifies registered expected-negative probes and control outcomes SEPARATELY from genuine failures. Missing results are typed off the #4785 reconciled task boundary — an unmatched call at an interruption boundary stays a typed interrupted outcome with inferred confidence, never a success and never a generic error.
PRIVACY CONTRACT (bodies are never retained). Ingestion streams the JSONL and keeps only ids, timestamps, tool names, decoded envelope numbers, apply_patch target paths, and a BOUNDED command head used exclusively for probe/loop classification. Prompts, result bodies, and agent messages are dropped at read time; corpus reports export classes, reasons, and hashed signatures — no raw commands.
analyticscorpus.go — the corpus half of #4767: fold a whole rollout store through AnalyzeRollout and report duration/TTFT percentiles, typed outcome totals, ranked outliers, and actionable repeated-cause findings.
SCRUBBING CONTRACT. Everything this report exports is stable-and-scrubbed: rollout/turn UUIDs, tool names, closed class/reason tokens, hashed loop signatures, and apply_patch file paths. Raw commands and result bodies are never exported — they were already dropped at ingestion (see analytics.go).
FINDINGS, NOT REMEDIATION. Repeated causes are emitted as Finding rows with stable reason tokens sized for `dos unstick` / issue filing. The report itself fixes nothing; its job is to hand the next actor a checkable cause.
Package codexlifecycle folds a native Codex rollout transcript (~/.codex/sessions/**/*.jsonl) into an EXACTLY-ONCE task lifecycle keyed by the exact turn_id (#4785).
THE DEFECT IT FENCES. A rollout can emit task_started(turn_id), do substantial work, and then emit ANOTHER task_started without either task_complete or turn_aborted for the earlier turn. A fold that tracks "am I in a turn?" as a boolean (tools/codex_turn_health.py's in_turn flag) silently resets on the second start, so the abandoned turn's duration, tokens, and tool outcomes leak into whichever turn happens to be open next. That is producer/data-integrity debt: a budget governor or critical-path analysis cannot know where the abandoned task ended. The 2026-07-15 corpus audit found 37 such mid-session gaps that are real task bodies (one spans 8,357 records incl. 2,253 function calls), not empty bookkeeping.
THE CONTRACT. For every observed task_started(turn_id) this fold persists exactly one TYPED terminal state, and never fabricates a success:
- Complete — an observed task_complete for that exact turn_id.
- Aborted — an observed turn_aborted for that exact turn_id.
- Superseded — SYNTHESIZED: a later task started while this turn was still active. The boundary is evidence-backed (the succeeding start's own timestamp), and the outcome is explicitly non-success.
- ProcessDeath — SYNTHESIZED: the rollout's final start never terminated and the rollout is STALE, so the writer died or the file is truncated.
- Live — SYNTHESIZED: the final start never terminated but the rollout is FRESH, so the task may genuinely still be running. EndedAt stays empty — a live turn has not ended, and inventing an end is the fabrication this fences.
Provenance separates what was READ from what was INFERRED, so a consumer can refuse to attribute post-boundary token/tool deltas to a turn whose end fak synthesized. Raw events are never rewritten; the fold is a projection beside them.
Tier: foundation (1) — see internal/architest. Pure: events in, reconciled lifecycle out. Stdlib-only, imports nothing internal, off the hot path. Freshness is an injected caller decision (a bool), never a clock read, so the fold stays deterministic and testable.
corpus.go — the READ-BACK half of #4785: fold a whole rollout STORE (~/.codex/sessions/**/*.jsonl) through the exactly-once reconciler and report the before/after integrity counts by provider/version.
WHY IT IS THE WITNESS. #4785's evidence is a corpus claim, so the fix needs a corpus proof: "zero UNCLASSIFIED mid-session starts after reconciliation". The audit's 144 unmatched starts decompose exactly as this fold types them —
superseded = a later task started while this one was open (audit: 37 mid-session) process_death + live = the rollout's final start, never terminated (audit: 107 open-at-end)
— so UnmatchedBefore is the population the naive boolean fold leaves dangling, and UnclassifiedAfter is what survives reconciliation. The contract is that the second number is ZERO while the first stays whatever the corpus really contains: this package classifies the gaps, it does not make them disappear.
FRESHNESS IS READ FROM THE FILE, NOT GUESSED. A rollout whose mtime is inside freshWithin may still be running, so its open final start is Live; an older one is ProcessDeath. That is the archive/read-back distinction #4785 requires, and it is the only place this package touches a clock — Fold itself stays pure.
health.go — the Go port of tools/codex_turn_health.py (#5063): the Codex turn/compaction health rollup, folded on the EXACT-turn_id lifecycle contract this package already enforces instead of the script's boolean in_turn flag.
THE DEFECT THE PORT REMOVES. The Python fold tracked "am I in a turn?" as a boolean: a second task_started silently RESET the per-turn tool counter, so an abandoned turn's tool/token work leaked into whichever turn was open next, and turn_aborted was not handled at all, so an explicitly aborted turn was never closed. Here every turn is keyed by its exact turn_id, a superseding start closes the abandoned turn with a typed NON-success outcome at an evidence-backed boundary (no post-boundary tool delta is attributable to it), and turn_aborted is a first-class terminal — the same repair Fold makes, applied to the health stats.
Like the script it replaces, this fold copies NO prompts, tool arguments, tool results, diffs, or model text into its report: only structural counts, rates, opaque session ids, and the coarse CATEGORY of a zero-tool turn's trailing message survive.
retention.go — the #4765 bound: a retention contract and byte/age cap for the native Codex rollout archive (~/.codex/sessions), planned WITHOUT losing witnesses.
THE PROBLEM IT FENCES. The 2026-07-14 audit found 2,125 sessions / 1.92 GB of append-only rollouts with no retention policy. Deleting blindly destroys the compaction/cost/task witnesses #2822/#3152/#2810 consume; keeping everything raw grows an unmanaged evidence store forever. This file is the PURE planning half: metadata in, a typed plan out. It never opens a rollout, never reads a body, and never touches the filesystem or the clock — Now is injected and a clockless policy is refused, so a plan is reproducible evidence.
THE RETENTION CONTRACT (#4765's four classes):
- active — a live writer or a recently-modified rollout. Never selected: an open session may still be written.
- warm_evidence — inside the warm window, OR protected by an active goal, guard witness, unresolved issue, or explicit pin. Protection is absolute: a protected session is NEVER selected for expiry, even when that leaves the byte cap unmet. The plan reports the unmet bound honestly (CapSatisfied=false + ProtectedOverCapBytes) instead of sacrificing a witness to satisfy arithmetic.
- compacted — already reduced to a scrubbed aggregate (bodies gone, token/tool-shape/compaction analytics kept). Retained as-is.
- expired — unprotected, not active, and either past the warm window (reason age_past_warm_window) or selected oldest-first to bring retained raw bytes under RawBytesCap (reason raw_bytes_over_cap).
Expiry here means "eligible for compaction into an aggregate", not deletion: the destructive step lives behind the dry-run-first maintenance verb and its quarantine/grace store, which BoundQuarantine bounds by age and bytes with the same oldest-first, receipt-emitting discipline.
taskpath.go — per-task critical-path decomposition and the #2365 behavioral detectors ported to native Codex event shape (#4767).
THE DECOMPOSITION. Every reconciled task's wall time is split into four typed buckets that sum to the observable timeline:
tool — spans from a function_call to its own function_call_output
wait — the same spans when the command is a registered blocking wait
model — inter-event gaps that END at a model-emitted record, up to the stall
threshold ("model-active time where observable" — the model was the
only thing that could have been running)
idle — the remainder of any gap beyond the stall threshold: nobody was
demonstrably working (harness stall, human/peer wait, scheduler pause)
The split is deliberately conservative: it never claims model activity beyond the stall threshold, and it attributes a gap ending at an output to the tool that produced it. Top critical-path contributors are ranked from these buckets, so "an 8.9-hour task" decomposes into typed causes instead of one opaque number.
Index ¶
- Constants
- func ClassifyOutcome(commandHead string, env Envelope) (ToolClass, string, Confidence)
- func ClassifyZeroTool(msg string) string
- func ParseHealthRollout(r io.Reader) (Meta, []HealthRow, error)
- func ReadAnalyticsRollout(r io.Reader) (Meta, []ARecord, error)
- func ReadRollout(r io.Reader) (Meta, []Event, error)
- type ARecord
- type AnalyticsCorpus
- type CallOutcome
- type ChurnRow
- type CodexBehavior
- type CompactionStats
- type Confidence
- type Contributor
- type CorpusReport
- type Counts
- type Decision
- type Envelope
- type EnvelopeKind
- type Event
- type Finding
- type HealthReport
- type HealthRow
- type HealthTotals
- type Inflation
- type Meta
- type Outcome
- type Percentiles
- type ProtectReason
- type Provenance
- type QuarantineBound
- type QuarantineItem
- type QuarantineReceipt
- type ReasonRow
- type RefusalLoop
- type Report
- type ResumeCohort
- type RetentionClass
- type RetentionContract
- type RetentionManifest
- type RolloutAnalytics
- type ScanOptions
- type SessionRecord
- type SessionStats
- type SigRow
- type Task
- type TaskAnalytics
- type TaskOutlier
- type ToolAgg
- type ToolClass
Constants ¶
const ( KindStarted = "task_started" KindComplete = "task_complete" KindAborted = "turn_aborted" )
Event kinds, as they appear in a rollout's event_msg payload `type`.
const ( // CompactBudget is codex model_auto_compact_token_limit for guarded launches // (cmd/fak/codex_launcher.go). A compaction near this value is the intended // #4253 budget, NOT premature. CompactBudget = 96_000 // PrematureFill: a compaction below this occupancy is genuinely premature — // almost always a stuck no-op loop re-accreting tiny context. PrematureFill = 40_000 // RefusalLoopMin: a session is a "guard-refusal loop" once this many of its // turns end with every proposed tool call refused. RefusalLoopMin = 3 // InflationMinTurns / InflationZeroRatio flag a session for turn inflation: // enough turns, and most call no tool. InflationMinTurns = 5 InflationZeroRatio = 0.5 )
Classification thresholds (named, not magic) — carried over from the script.
const ( ZeroGuardRefused = "guard_refused" ZeroPreambleNoop = "preamble_noop" ZeroTalkOnly = "talk_only" ZeroSilent = "silent" )
Zero-tool turn categories, from the turn's trailing agent message.
const ( ReasonAgePastWarmWindow = "age_past_warm_window" ReasonRawBytesOverCap = "raw_bytes_over_cap" ReasonQuarantineAge = "quarantine_past_grace" ReasonQuarantineBytes = "quarantine_bytes_over_cap" )
Expiry reason tokens, machine-readable in the manifest.
const HealthSchema = "fak-codex-turn-health/2"
HealthSchema identifies the report shape. /2 (not the script's /1): the fold is keyed by turn_id and reports aborted/superseded/unterminated turns the boolean fold could not see.
Variables ¶
This section is empty.
Functions ¶
func ClassifyOutcome ¶
func ClassifyOutcome(commandHead string, env Envelope) (ToolClass, string, Confidence)
ClassifyOutcome maps one decoded call/result pair into the closed vocabulary. commandHead is the bounded head retained at ingestion (empty for non-shell tools).
func ClassifyZeroTool ¶
ClassifyZeroTool returns the category of a zero-tool turn from its trailing agent message.
func ParseHealthRollout ¶
ParseHealthRollout reads a Codex rollout JSONL stream into health rows in file order, plus the rollout's Meta. Same durability contract as ReadRollout: a torn or non-JSON line is skipped, never fatal. The top-level `compacted` record is the real compaction marker; the paired event_msg/context_compacted is deliberately ignored to avoid double counting.
func ReadAnalyticsRollout ¶
ReadAnalyticsRollout streams one rollout and returns its Meta plus body-free analytics records in file order. Torn/non-JSON lines are skipped — the truncated tail is process-death evidence, not a read error.
func ReadRollout ¶
ReadRollout reads a rollout stream and returns both its identifying Meta and its lifecycle events. Only the FIRST session_meta record identifies the file: a subagent rollout carries the PARENT's metadata in its inherited context further down, so a last-wins read would relabel every child as its parent.
Types ¶
type ARecord ¶
type ARecord struct {
Kind string // task_started | task_complete | turn_aborted | function_call | function_call_output | token_count | compacted
TS time.Time
TurnID string
CallID string
Tool string
Head string // bounded command head (classification only; corpus reports never export it)
Targets []string // apply_patch file targets, paths only
Env Envelope
Reason string
DurationMS int64
GoalContinuation bool // structured harness envelope; no prompt body retained
}
ARecord is one retained, body-free analytics record from a rollout.
type AnalyticsCorpus ¶
type AnalyticsCorpus struct {
Root string `json:"root"`
Sessions int `json:"sessions"`
Unreadable int `json:"unreadable,omitempty"`
Tasks int `json:"tasks"`
Completed int `json:"completed"`
ToolCalls int `json:"tool_calls"`
Duration Percentiles `json:"duration"` // seconds, over completed tasks' recorded durations
TTFT Percentiles `json:"ttft"` // seconds, over tasks with an observable first token
Outcomes map[Outcome]int `json:"outcomes"`
Classes map[ToolClass]int `json:"classes"`
Reasons []ReasonRow `json:"reasons,omitempty"`
ByTool map[string]*ToolAgg `json:"by_tool,omitempty"`
TopTasks []TaskOutlier `json:"top_tasks,omitempty"`
TimeoutKills int64 `json:"timeout_kills"`
SleepPolls int64 `json:"sleep_polls"`
StallGaps int64 `json:"stall_gaps"`
Findings []Finding `json:"findings,omitempty"`
FreshHeadlessResume ResumeCohort `json:"fresh_headless_resume"`
}
AnalyticsCorpus is the whole-store #4767 report.
func ScanAnalyticsCorpus ¶
func ScanAnalyticsCorpus(root string, opt ScanOptions, topN int) (AnalyticsCorpus, error)
ScanAnalyticsCorpus folds every rollout under root. Unreadable files are counted, never fatal. topN bounds the ranked outlier table.
func (AnalyticsCorpus) HardFailureCount ¶
func (c AnalyticsCorpus) HardFailureCount() int
HardFailureCount counts calls in classes that belong in a failure ranking. Expected negatives and control exits are visible in Classes but excluded here — that exclusion is the whole point of the typed vocabulary.
type CallOutcome ¶
type CallOutcome struct {
TurnID string `json:"turn_id,omitempty"`
Tool string `json:"tool"`
Class ToolClass `json:"class"`
Reason string `json:"reason"`
Confidence Confidence `json:"confidence"`
SpanMS int64 `json:"span_ms,omitempty"`
Sig string `json:"sig,omitempty"` // hashed tool+head signature
}
CallOutcome is one typed, scrubbed call row: class + reason + span, no command.
type CodexBehavior ¶
type CodexBehavior struct {
TimeoutKills int64 `json:"timeout_kills"`
SleepPolls int64 `json:"sleep_polls"`
StallGaps int64 `json:"stall_gaps"`
MaxGapS float64 `json:"max_gap_s"`
RepeatFailures []SigRow `json:"repeat_failures,omitempty"`
MaxRepeatFailure int64 `json:"max_repeat_failure"`
SuccessLoops []SigRow `json:"success_loops,omitempty"`
MaxSuccessLoop int64 `json:"max_success_loop"`
EditChurn []ChurnRow `json:"edit_churn,omitempty"`
MaxEditChurn int64 `json:"max_edit_churn"`
}
CodexBehavior is the #2365 behavioral lens ported to Codex event shape. All rows are scrubbed: signatures are hashes, churn rows carry file paths only.
type CompactionStats ¶
type CompactionStats struct {
Events int `json:"events"`
Budget int `json:"budget"`
OccupancyP50 int `json:"occupancy_p50"`
OccupancyP90 int `json:"occupancy_p90"`
NearBudget96K int `json:"near_budget_96k"`
NearWindow200KPlus int `json:"near_window_200k_plus"`
PrematureLT40K int `json:"premature_lt40k"`
}
CompactionStats buckets compaction occupancies against the guarded budget.
type Confidence ¶
type Confidence string
Confidence says how the classifier knows. Observed = read from a decoded envelope; Assumed = no envelope semantics exist for the tool, absence of an error signal is being trusted; Inferred = synthesized from a reconciled task boundary.
const ( ConfidenceObserved Confidence = "observed" ConfidenceAssumed Confidence = "assumed" ConfidenceInferred Confidence = "inferred" )
type Contributor ¶
Contributor is one ranked critical-path bucket ("model", "idle", "wait", "tool:<name>") with its attributed milliseconds.
type CorpusReport ¶
type CorpusReport struct {
Root string `json:"root"`
Scanned int `json:"scanned"`
Unreadable int `json:"unreadable,omitempty"`
Totals Counts `json:"totals"`
ByProvider map[string]*Counts `json:"by_provider_version"`
// AllStartsTyped is the acceptance criterion: every start carries a typed
// terminal after reconciliation (i.e. UnclassifiedAfter == 0).
AllStartsTyped bool `json:"all_starts_typed"`
}
CorpusReport is the whole-store report.
func ScanCorpus ¶
func ScanCorpus(root string, opt ScanOptions) (CorpusReport, error)
ScanCorpus folds every rollout under root through the reconciler. A rollout that cannot be read or parsed is counted (Unreadable) rather than failing the scan: the store is append-only and a torn tail is normal, and refusing to report the other 2,900 sessions because one file is mangled would defeat the witness.
func (CorpusReport) ProviderVersions ¶
func (w CorpusReport) ProviderVersions() []string
ProviderVersions returns the table's row keys in a stable order.
type Counts ¶
type Counts struct {
Rollouts int `json:"rollouts"`
Starts int `json:"starts"`
// UnmatchedBefore is starts with NO observed terminal — what a naive fold leaves
// dangling (audit's "unmatched starts" column).
UnmatchedBefore int `json:"unmatched_before"`
// RolloutsWithGap is rollouts carrying at least one mid-rollout gap.
RolloutsWithGap int `json:"rollouts_with_gap"`
// After reconciliation, each formerly-unmatched start carries one typed terminal:
Superseded int `json:"superseded"` // mid-session gap (audit: 37)
ProcessDeath int `json:"process_death"` // final start, stale rollout
Live int `json:"live"` // final start, fresh rollout
Complete int `json:"complete"`
Aborted int `json:"aborted"`
// UnclassifiedAfter MUST be zero — the acceptance criterion.
UnclassifiedAfter int `json:"unclassified_after"`
Orphans int `json:"orphans,omitempty"`
Reused int `json:"reused,omitempty"`
MultiplyTerminated int `json:"multiply_terminated,omitempty"`
}
Counts is one provider/version row: the before/after integrity numbers.
type Decision ¶
type Decision struct {
ID string `json:"id"`
Class RetentionClass `json:"class"`
Bytes int64 `json:"bytes"`
Protected []ProtectReason `json:"protected,omitempty"`
Expire bool `json:"expire,omitempty"`
Reason string `json:"reason,omitempty"`
}
Decision is one session's classification and (non-destructive) fate.
type Envelope ¶
type Envelope struct {
Kind EnvelopeKind `json:"kind"`
HasExit bool `json:"has_exit,omitempty"`
ExitCode int `json:"exit_code,omitempty"`
WallS float64 `json:"wall_s,omitempty"`
TimedOut bool `json:"timed_out,omitempty"`
}
Envelope is the decoded, body-free summary of one function_call_output.
func DecodeEnvelope ¶
DecodeEnvelope decodes one output body into envelope numbers, structured form first, and drops the body. Only the FIRST body line is consulted for the harness timeout marker so result content mentioning "timed out" cannot fake a timeout.
type EnvelopeKind ¶
type EnvelopeKind string
EnvelopeKind is the decode path that produced an Envelope.
const ( EnvelopeStructured EnvelopeKind = "structured" // JSON object output (metadata.exit_code form or tool-native JSON) EnvelopeText EnvelopeKind = "text" // "Exit code: N\nWall time: X seconds\nOutput:\n…" harness form EnvelopeOpaque EnvelopeKind = "opaque" // free text with no envelope semantics (e.g. update_plan acks) EnvelopeMalformed EnvelopeKind = "malformed" // looks like an envelope but cannot be decoded )
type Event ¶
type Event struct {
Kind string `json:"kind"`
TurnID string `json:"turn_id,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
Reason string `json:"reason,omitempty"`
DurationMS int64 `json:"duration_ms,omitempty"`
}
Event is one lifecycle-relevant record read from a rollout, flattened from {"timestamp":…,"type":"event_msg","payload":{"type":…,"turn_id":…}}.
func ParseRollout ¶
ParseRollout reads a Codex rollout JSONL stream and returns its lifecycle events in file order. Non-lifecycle records are skipped. Rollouts are append-only and can carry a torn final line from a crashed writer or a stray non-JSON line, so a malformed line is skipped rather than failing the whole read — the truncated tail is exactly the process-death evidence this package must survive to classify.
type Finding ¶
type Finding struct {
Reason string `json:"reason"` // stable token, e.g. repeated_failure:exit_1
Count int `json:"count"`
Action string `json:"action"`
}
Finding is one actionable repeated cause, shaped for `dos unstick` / an issue.
type HealthReport ¶
type HealthReport struct {
Schema string `json:"schema"`
SessionsWithTurns int `json:"sessions_with_turns"`
Totals HealthTotals `json:"totals"`
ZeroToolBreakdown map[string]int `json:"zero_tool_breakdown"`
Compaction CompactionStats `json:"compaction"`
GuardRefusalLoops []RefusalLoop `json:"guard_refusal_loops"`
TurnInflation []Inflation `json:"turn_inflation"`
Flags []string `json:"flags"`
Scanned int `json:"scanned_files,omitempty"`
Unreadable int `json:"unreadable,omitempty"`
}
HealthReport is the corpus health report — the script's roll_up, typed.
func RollUp ¶
func RollUp(stats []SessionStats, top int) HealthReport
RollUp aggregates per-session stats into the health report. Sessions with no turns are dropped (a rollout that never started a task carries no health signal). Pure: no IO, deterministic ordering.
func ScanHealth ¶
func ScanHealth(root string, opt ScanOptions, top int) (HealthReport, error)
ScanHealth folds every rollout under root into the health report. Same durability as ScanCorpus: an unreadable rollout is counted, never fatal. opt.CWD scopes to one repository's sessions; opt.Limit caps files scanned, newest first.
type HealthRow ¶
type HealthRow struct {
Kind string
TurnID string
Message string // agent_message text; categorised by FoldHealth, never reported
Model string
InputTokens int
}
HealthRow is one health-relevant record read from a rollout: a lifecycle event (task_started / task_complete / turn_aborted, keyed by turn_id) or a structural delta (tool call, trailing message, token occupancy, compaction, model).
type HealthTotals ¶
type HealthTotals struct {
Turns int `json:"turns"`
ToolCalls int `json:"tool_calls"`
ToolCallsPerTurn float64 `json:"tool_calls_per_turn"`
ZeroToolTurns int `json:"zero_tool_turns"`
ZeroToolRate float64 `json:"zero_tool_rate"`
Aborted int `json:"aborted,omitempty"`
Superseded int `json:"superseded,omitempty"`
Unterminated int `json:"unterminated,omitempty"`
}
HealthTotals is the corpus-wide structural rollup.
type Inflation ¶
type Inflation struct {
Session string `json:"session"`
Model string `json:"model,omitempty"`
Turns int `json:"turns"`
ToolCalls int `json:"tool_calls"`
ZeroToolTurns int `json:"zero_tool_turns"`
}
Inflation is one worst-offender session whose turns mostly call no tool.
type Meta ¶
type Meta struct {
RolloutID string `json:"rollout_id,omitempty"`
Provider string `json:"provider,omitempty"` // model_provider, e.g. "fak" / "openai"
CLIVersion string `json:"cli_version,omitempty"` // e.g. "0.144.4"
CWD string `json:"cwd,omitempty"`
Originator string `json:"originator,omitempty"`
Source string `json:"source,omitempty"`
ThreadSource string `json:"thread_source,omitempty"`
}
Meta identifies one rollout, read from its FIRST session_meta record only. A subagent rollout starts with its own metadata and then carries the PARENT session metadata in the inherited context, so letting a later record win would make every child look like the same parent session (the same trap cmd/fak's applyCodexLoopSessionMeta guards).
func (Meta) ProviderVersion ¶
ProviderVersion is the table axis #4785 reports against ("fak 0.144.1").
type Outcome ¶
type Outcome string
Outcome is a task's typed terminal state. Every reconciled task carries exactly one; there is no "unknown" member, because an unclassified start is precisely the defect this package exists to remove.
type Percentiles ¶
type Percentiles struct {
N int `json:"n"`
P50 float64 `json:"p50"`
P90 float64 `json:"p90"`
P95 float64 `json:"p95"`
P99 float64 `json:"p99"`
Max float64 `json:"max"`
}
Percentiles is a nearest-rank summary over one observed distribution (seconds).
type ProtectReason ¶
type ProtectReason string
ProtectReason says WHY a session may not expire. A session carrying any reason is warm evidence regardless of age or size.
const ( ProtectActiveGoal ProtectReason = "active_goal" ProtectRefereeEvidence ProtectReason = "referee_evidence" ProtectUnresolvedIssue ProtectReason = "unresolved_issue" ProtectPin ProtectReason = "pin" )
type Provenance ¶
type Provenance string
Provenance records whether a terminal state was observed in the rollout or synthesized by this reconciler.
const ( Observed Provenance = "observed" Synthesized Provenance = "synthesized" )
type QuarantineBound ¶
type QuarantineBound struct {
Now time.Time `json:"now"`
MaxAge time.Duration `json:"max_age"` // grace period; older entries purge
MaxBytes int64 `json:"max_bytes"` // 0 = no byte bound
}
QuarantineBound bounds the grace store by age and bytes.
type QuarantineItem ¶
type QuarantineItem struct {
ID string `json:"id"`
Bytes int64 `json:"bytes"`
QuarantinedAt time.Time `json:"quarantined_at"`
}
QuarantineItem is one raw payload parked in the grace store by the maintenance verb, awaiting either restore or final purge.
type QuarantineReceipt ¶
type QuarantineReceipt struct {
Keep []QuarantineItem `json:"keep"`
Purge []Decision `json:"purge,omitempty"`
BeforeBytes int64 `json:"before_bytes"`
ReclaimedBytes int64 `json:"reclaimed_bytes"`
AfterBytes int64 `json:"after_bytes"`
}
QuarantineReceipt reports what the bound keeps and purges, with the same before/after/reclaimed arithmetic as a retention plan.
func BoundQuarantine ¶
func BoundQuarantine(items []QuarantineItem, b QuarantineBound) (QuarantineReceipt, error)
BoundQuarantine applies the age bound, then the byte bound oldest-first, and returns the receipt. Like DecideRetention it is pure and clock-refusing.
type ReasonRow ¶
type ReasonRow struct {
Class ToolClass `json:"class"`
Reason string `json:"reason"`
Count int `json:"count"`
}
ReasonRow is one ranked class/reason bucket.
type RefusalLoop ¶
type RefusalLoop struct {
Session string `json:"session"`
Model string `json:"model,omitempty"`
RefusedTurns int `json:"refused_turns"`
Turns int `json:"turns"`
}
RefusalLoop is one worst-offender session re-proposing refused tool calls.
type Report ¶
type Report struct {
Tasks []Task `json:"tasks"`
// Orphans are terminals whose turn_id was never started in this rollout — a
// truncated head, or a terminal for a turn that began in an earlier file.
Orphans []Event `json:"orphans,omitempty"`
// Reused are turn_ids started more than once. The exact-id contract makes this
// detectable; a boolean in_turn fold cannot see it at all.
Reused []string `json:"reused,omitempty"`
// MultiplyTerminated are turn_ids that received a second terminal after an
// already-OBSERVED one.
MultiplyTerminated []string `json:"multiply_terminated,omitempty"`
}
Report is the reconciled lifecycle of ONE rollout. The three integrity classes are reported SEPARATELY (not merged into one "bad" count) because they have different causes and different fixes.
func Fold ¶
Fold reconciles events into an exactly-once lifecycle. fresh says whether the rollout is still warm (recent mtime / an live session); it decides ONLY how the final unterminated start is typed — Live when fresh, ProcessDeath when stale — so process death is never confused with a running task.
Events must be in rollout order (append-only files already are).
func (Report) CountByOutcome ¶
CountByOutcome tallies reconciled tasks by outcome — the before/after shape a provider/version rollup reports.
func (Report) Unclassified ¶
Unclassified returns tasks the fold left without a typed terminal state. It is the corpus witness's assertion target and must always be empty: the fold assigns an outcome to every start, so a non-empty result is a bug in this package, not data.
type ResumeCohort ¶ added in v0.44.0
type ResumeCohort struct {
Started int `json:"started"`
UsefulWorkReached int `json:"useful_work_reached"`
Completed int `json:"completed"`
Crashed int `json:"crashed"`
Superseded int `json:"superseded"`
FailureReasons map[string]int `json:"failure_reasons,omitempty"`
}
ResumeCohort measures metadata-grounded fresh headless goal continuations.
type RetentionClass ¶
type RetentionClass string
RetentionClass is the closed retention vocabulary for one rollout session.
const ( ClassActive RetentionClass = "active" ClassWarmEvidence RetentionClass = "warm_evidence" ClassCompacted RetentionClass = "compacted" ClassExpired RetentionClass = "expired" )
type RetentionContract ¶
type RetentionContract struct {
Now time.Time `json:"now"`
ActiveWithin time.Duration `json:"active_within"` // mtime inside → active
WarmWithin time.Duration `json:"warm_within"` // mtime inside → warm evidence
// RawBytesCap bounds retained RAW bytes (active + warm, i.e. everything
// not yet compacted). 0 means no byte cap: age expiry still applies.
RawBytesCap int64 `json:"raw_bytes_cap"`
}
RetentionContract bounds the archive. Now is REQUIRED: reading the clock here would make a plan unreproducible, so a zero Now is refused.
type RetentionManifest ¶
type RetentionManifest struct {
Decisions []Decision `json:"decisions"`
BeforeBytes int64 `json:"before_bytes"` // raw (non-compacted) bytes going in
ReclaimedBytes int64 `json:"reclaimed_bytes"` // raw bytes scheduled for compaction
AfterBytes int64 `json:"after_bytes"` // raw bytes retained if applied
RawBytesCap int64 `json:"raw_bytes_cap"`
// CapSatisfied is false ONLY when protected/active evidence alone keeps
// AfterBytes over the cap — the honest "cannot bound without destroying
// witnesses" signal, with the overage quantified.
CapSatisfied bool `json:"cap_satisfied"`
ProtectedOverCapBytes int64 `json:"protected_over_cap_bytes,omitempty"`
}
RetentionManifest is the machine-readable manifest and receipt: every session's decision plus the before/after/reclaimed arithmetic, in input order.
func DecideRetention ¶
func DecideRetention(sessions []SessionRecord, pol RetentionContract) (RetentionManifest, error)
DecideRetention classifies every session under the contract above and selects expiries — age first, then oldest-first by bytes until the cap holds. It is deterministic: same records + same policy ⇒ byte-identical plan.
type RolloutAnalytics ¶
type RolloutAnalytics struct {
Meta Meta `json:"meta"`
Tasks []TaskAnalytics `json:"tasks"`
Calls int `json:"calls"`
Outcomes []CallOutcome `json:"outcomes,omitempty"`
Behavior CodexBehavior `json:"behavior"`
}
RolloutAnalytics is the full per-rollout report.
func AnalyzeRollout ¶
func AnalyzeRollout(meta Meta, records []ARecord, fresh bool) RolloutAnalytics
AnalyzeRollout joins calls/results/tasks by their ids, types every outcome, and decomposes each reconciled task's wall time. fresh has the same meaning as in Fold: it decides live vs process-death for the final open start, which in turn decides live_tail vs interrupted for calls missing their result at the tail.
type ScanOptions ¶
type ScanOptions struct {
// CWD, when set, keeps only rollouts whose session_meta.cwd matches it — the
// repository scoping #4785's evidence used.
CWD string
// FreshWithin decides Live vs ProcessDeath for an open final start, measured
// against Now and the file's mtime.
FreshWithin time.Duration
// Now is injected so a scan is reproducible; zero means time.Now().
Now time.Time
// Limit caps files scanned (0 = all), newest first when set.
Limit int
}
ScanOptions bounds a corpus scan.
type SessionRecord ¶
type SessionRecord struct {
ID string `json:"id"`
Bytes int64 `json:"bytes"`
ModTime time.Time `json:"mod_time"`
Live bool `json:"live,omitempty"` // open writer (#4785 Live outcome)
Compacted bool `json:"compacted,omitempty"` // already a scrubbed aggregate
Protected []ProtectReason `json:"protected,omitempty"`
}
SessionRecord is the metadata a plan consumes — never prompt or tool-output bodies, which keeps the whole planning path privacy-safe by construction.
type SessionStats ¶
type SessionStats struct {
Session string `json:"session"`
Model string `json:"model,omitempty"`
Turns int `json:"turns"`
ToolCalls int `json:"tool_calls"`
ZeroTool map[string]int `json:"zero_tool"` // category -> zero-tool COMPLETED turns
// Aborted / Superseded / Unterminated are the turns the boolean fold miscounted:
// none of them is a success, and none is zero-tool-classified as if it completed.
Aborted int `json:"aborted,omitempty"`
Superseded int `json:"superseded,omitempty"`
Unterminated int `json:"unterminated,omitempty"`
Compactions []int `json:"compactions,omitempty"` // occupancy at each real compaction
}
SessionStats is the pure per-session fold: structural counts only.
func FoldHealth ¶
func FoldHealth(rows []HealthRow) SessionStats
FoldHealth folds one session's health rows into structural stats, keyed by exact turn_id with the same reconciliation rules as Fold:
- a new task_started while an older turn is open closes the old turn as SUPERSEDED at that boundary — its tool count freezes, and every later tool call or message is attributed to the new turn, never the abandoned one;
- turn_aborted is a terminal (keyed by turn_id; an unkeyed legacy abort binds to the open turn) — the turn is Aborted, never a zero-tool "completion";
- a late OBSERVED task_complete for a turn we had synthesized closed repairs the row (observed evidence outranks the inference), still with the frozen, pre-boundary tool count;
- only a turn closed by task_complete is zero-tool classified.
Rows must be in rollout order (append-only files already are). Pure: no IO.
func (SessionStats) ZeroToolTotal ¶
func (s SessionStats) ZeroToolTotal() int
ZeroToolTotal is the count of completed turns that called no tool.
type Task ¶
type Task struct {
TurnID string `json:"turn_id"`
StartedAt string `json:"started_at,omitempty"`
EndedAt string `json:"ended_at,omitempty"` // empty for Live — a running turn has no end
Outcome Outcome `json:"outcome"`
Provenance Provenance `json:"provenance"`
Reason string `json:"reason,omitempty"`
DurationMS int64 `json:"duration_ms,omitempty"`
}
Task is one reconciled lifecycle: a start bound to exactly one typed terminal.
type TaskAnalytics ¶
type TaskAnalytics struct {
TurnID string `json:"turn_id"`
Outcome Outcome `json:"outcome"`
Provenance Provenance `json:"provenance"`
WallMS int64 `json:"wall_ms,omitempty"` // start → typed end (0 for live)
RecordedMS int64 `json:"recorded_ms,omitempty"` // producer-recorded duration_ms when observed
TTFTMS int64 `json:"ttft_ms"` // start → first model-emitted record; -1 unobserved
ToolMS int64 `json:"tool_ms"`
WaitMS int64 `json:"wait_ms"`
ModelMS int64 `json:"model_ms"`
IdleMS int64 `json:"idle_ms"`
ToolCalls int `json:"tool_calls"`
Compactions int `json:"compactions,omitempty"`
IdleGaps int `json:"idle_gaps,omitempty"`
Classes map[ToolClass]int `json:"classes,omitempty"`
Critical []Contributor `json:"critical,omitempty"`
}
TaskAnalytics is one reconciled task with its critical-path decomposition.
type TaskOutlier ¶
type TaskOutlier struct {
Session string `json:"session"` // rollout UUID (already opaque)
TurnID string `json:"turn_id"`
Outcome Outcome `json:"outcome"`
DurationS float64 `json:"duration_s"`
TTFTS float64 `json:"ttft_s"`
IdleS float64 `json:"idle_s"`
Top []Contributor `json:"top,omitempty"`
}
TaskOutlier is one ranked long task with its typed critical-path attribution.
type ToolAgg ¶
type ToolAgg struct {
Calls int `json:"calls"`
Failures int `json:"failures"`
Timeouts int `json:"timeouts"`
MS int64 `json:"ms"`
}
ToolAgg is one per-tool rollup.
type ToolClass ¶
type ToolClass string
ToolClass is the CLOSED tool-outcome vocabulary: every joined call/result pair maps to exactly one member. Expected negatives and control exits are first-class members precisely so they can never be lumped into failure counts again.
const ( ToolOK ToolClass = "ok" ToolExpectedNegative ToolClass = "expected_negative" // a registered probe whose non-zero exit IS the answer ToolControlExit ToolClass = "control_exit" // flow-control exit (e.g. `wait` propagating a child's status) ToolFailure ToolClass = "failure" // genuine command failure ToolTimeout ToolClass = "timeout" // killed at the harness deadline, partial state possible ToolMissingResult ToolClass = "missing_result" // call with no output despite later task evidence ToolMalformedEnvelope ToolClass = "malformed_envelope" // output present but its envelope is undecodable ToolInterrupted ToolClass = "interrupted" // call open at an aborted/superseded/dead task boundary ToolLiveTail ToolClass = "live_tail" // call open at the fresh tail — may genuinely still run )
func (ToolClass) CountsAsFailure ¶
CountsAsFailure reports whether the class belongs in a failure ranking. Expected negatives, control exits, and open-tail unknowns do NOT.