gate.ledger: Session ledger#
Agent session ledger adapter: lifecycle parsing and session-spend correlation.
Distills Claude Code and Factory Droid JSONL session transcripts into
SessionRecord dataclasses, correlates them with LiteLLM gateway spend rows,
and aggregates them into a SessionSummary for CLI reporting.
The module is deliberately content-free: it never retains or exposes prompts, responses, reasoning, or tool arguments from the parsed transcripts.
Session parsing#
- gate.ledger.parse_session_file(path: Path, *, idle: timedelta = datetime.timedelta(seconds=900)) SessionRecord | None#
Parse a single agent session transcript file into a
SessionRecord.Dispatches by harness detection: Droid sessions live under
~/.factory/sessions/and havesession_startevents; Claude Code sessions live under~/.claude/projects/and haveassistantevents withstop_reason. When the harness is ambiguous, the file is probed for asession_startevent type (Droid) or falls back to Claude Code.- Parameters:
path – The session transcript
.jsonlfile.idle – Gaps longer than this count as suspended time.
- Returns:
The aggregated record, or
Noneif no timed events exist.
- gate.ledger.discover_sessions(*, since: datetime, until: datetime, harness_dir: Path | None = None) list[Path]#
Find session transcript files in a time window.
Scans the built-in surface roots (Claude Code and Droid) by default, or a single custom
harness_dirwhen provided. Files are filtered by modification time falling within the[since, until]window.- Parameters:
since – Window lower bound (inclusive).
until – Window upper bound (inclusive).
harness_dir – Override the scan root. When None, scans all built-in surface roots.
- Returns:
Sorted list of session transcript paths.
- gate.ledger.parse_window(since: datetime, until: datetime, *, harness_dir: Path | None = None, idle: timedelta = datetime.timedelta(seconds=900)) list[SessionRecord]#
Discover and parse all session transcripts in a time window.
- Parameters:
since – Window lower bound (inclusive).
until – Window upper bound (inclusive).
harness_dir – Override the scan root (default: scan all built-in surfaces).
idle – Gaps longer than this count as suspended time.
- Returns:
List of parsed session records, sorted by end time.
Data models#
- class gate.ledger.SessionRecord(session_id: str = '', project: str = '', harness: str = 'claude', models: list[str] = <factory>, model_request_ids: list[str] = <factory>, tokens_in: int = 0, tokens_out: int = 0, cache_creation: int = 0, cache_read: int = 0, reasoning: int = 0, started_at: datetime | None = None, ended_at: datetime | None = None, duration_s: float = 0.0, tool_calls: int = 0, cost_status: str = 'unavailable', cost_microusd: int | None = None, cost_source: str = 'none')#
One agent session’s lifecycle, distilled from its transcript.
This is a stdlib dataclass (not pydantic) for standalone use in myGate. Fields capture only structural metadata – never prompt, response, or tool content.
- class gate.ledger.SessionSummary(total_sessions: int = 0, total_tokens_in: int = 0, total_tokens_out: int = 0, total_cost_microusd: int = 0, by_model: dict[str, int]=<factory>, by_project: dict[str, int]=<factory>, sessions: list[SessionRecord] = <factory>)#
Aggregated view of multiple session records.
- sessions#
The underlying session records.
- Type:
Cost correlation#
- gate.ledger.correlate_session_cost(record: SessionRecord, spend_rows: list[dict]) SessionRecord#
Correlate a session’s request IDs with LiteLLM spend rows.
Adapts corpus
model_cost.correlate_session. Matches the session’smodel_request_idsagainstspend_rows(each a dict withrequest_idandspendfields). When every observed request has an exact spend row, the total is summed and set on the record. Partial matches are discarded (no partial numeric totals).- Parameters:
record – The session record to correlate (modified in place and returned).
spend_rows – LiteLLM spend rows, each with
request_idandspend.
- Returns:
The updated record with
cost_status,cost_microusd, andcost_sourcefields set.
- gate.ledger.approximate_session_cost(record: SessionRecord, spend_window: list[dict], *, buffer_minutes: int = 5, model_min_score: float = 0.3) SessionRecord#
Time-window approximate cost join when exact request ID matching isn’t available.
Adapts corpus
model_cost.approximate_session_cost. Matches every spend row whosestart_time(orstartTime) falls within the session’s time span (padded bybuffer_minutes) and whose model label has at leastmodel_min_scoresimilarity to one of the session’s recorded models.- Parameters:
record – The session record to correlate (modified in place and returned).
spend_window – Gateway spend rows, each with
start_time/startTime,model, andspendfields.buffer_minutes – Padding (in minutes) around the session time span.
model_min_score – Minimum model similarity score for a match.
- Returns:
The updated record with estimated cost fields set.
- gate.ledger.baseline_cost(record: SessionRecord) SessionRecord#
Set baseline cost (local-zero-marginal for local models, unavailable otherwise).
Adapts corpus
model_cost.baseline_session_cost. Does not perform any network access; simply classifies the session’s cost status based on whether the models are local.- Parameters:
record – The session record to update (modified in place and returned).
- Returns:
The updated record with baseline cost fields set.
Aggregation#
- gate.ledger.summarize_sessions(records: list[SessionRecord]) SessionSummary#
Aggregate session records into a summary with per-model and per-project breakdowns.
- Parameters:
records – Session records to aggregate.
- Returns:
A
SessionSummarywith totals and per-dimension counts.