codexlifecycle

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: 13 Imported by: 0

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

View Source
const (
	KindStarted  = "task_started"
	KindComplete = "task_complete"
	KindAborted  = "turn_aborted"
)

Event kinds, as they appear in a rollout's event_msg payload `type`.

View Source
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.

View Source
const (
	ZeroGuardRefused = "guard_refused"
	ZeroPreambleNoop = "preamble_noop"
	ZeroTalkOnly     = "talk_only"
	ZeroSilent       = "silent"
)

Zero-tool turn categories, from the turn's trailing agent message.

View Source
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.

View Source
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

func ClassifyZeroTool(msg string) string

ClassifyZeroTool returns the category of a zero-tool turn from its trailing agent message.

func ParseHealthRollout

func ParseHealthRollout(r io.Reader) (Meta, []HealthRow, error)

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

func ReadAnalyticsRollout(r io.Reader) (Meta, []ARecord, error)

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

func ReadRollout(r io.Reader) (Meta, []Event, error)

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 ChurnRow

type ChurnRow struct {
	File  string `json:"file"`
	Count int64  `json:"count"`
}

ChurnRow is one per-file patch-churn offender.

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

type Contributor struct {
	Category string `json:"category"`
	MS       int64  `json:"ms"`
}

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

func DecodeEnvelope(output string) Envelope

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

func ParseRollout(r io.Reader) ([]Event, error)

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

func (m Meta) ProviderVersion() string

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.

const (
	Complete     Outcome = "complete"
	Aborted      Outcome = "aborted"
	Superseded   Outcome = "superseded"
	ProcessDeath Outcome = "process_death"
	Live         Outcome = "live"
)

func (Outcome) Success

func (o Outcome) Success() bool

Success reports whether the outcome is a genuine completion. Superseded, ProcessDeath, and Live are NOT successes — a consumer must not count them as one.

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

func Fold(events []Event, fresh bool) Report

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

func (r Report) CountByOutcome() map[Outcome]int

CountByOutcome tallies reconciled tasks by outcome — the before/after shape a provider/version rollup reports.

func (Report) Unclassified

func (r Report) Unclassified() []Task

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 SigRow

type SigRow struct {
	Tool  string `json:"tool"`
	Sig   string `json:"sig"`
	Count int64  `json:"count"`
}

SigRow is one repeat-failure / success-loop offender, keyed by hashed signature.

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

func (c ToolClass) CountsAsFailure() bool

CountsAsFailure reports whether the class belongs in a failure ranking. Expected negatives, control exits, and open-tail unknowns do NOT.

Jump to

Keyboard shortcuts

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