History & Session Guide¶
Long-term observability for your little-loops project: what ran, what changed, what was corrected, and why.
Table of Contents¶
- When to Use This Guide
- Querying Recipes
- What Is history.db?
- What Gets Recorded
- Getting Started: Backfill
- Querying Sessions
- Issue ↔ Session Cross-References
- Planning Skill Injection
- History Analytics
- Quality Metric Definitions
- Session Log Tooling (ll-logs)
- Advanced: LCM Compaction
- Retention & Pruning
- Configuration Reference
- See Also
When to Use This Guide¶
Use this when you want to query what happened in past sessions, inject historical context into planning, or analyze trends across your project. Start with the Querying Recipes table below — most common needs are one command.
Querying Recipes¶
| I want to know... | Command |
|---|---|
| Which files I touched in the last week | ll-session recent --kind file |
| All times I debugged authentication | ll-session search --fts "authentication" |
| Every correction Claude received about a topic | ll-session search --fts "rate limit" --kind correction |
| How long issue BUG-1759 took | ll-history-context BUG-1759 --effort |
| Which sessions worked on issue FEAT-42 | ll-history sessions FEAT-42 |
| A trend analysis for the last quarter | ll-history analyze --since 2026-01-01 --format markdown |
| All tools used across sessions | ll-session recent --kind tool --limit 20 |
| What the project summary looks like | ll-history summary |
| What shipped recently (commits with issue linkage) | ll-session recent --kind commit |
| Last pytest run on this branch | ll-session recent --kind test_run --limit 1 |
| Recent verifier verdicts (ready-issue, confidence-check, ...) | ll-session recent --kind verdict |
| Recent LLM token usage / cost by model | ll-session recent --kind usage |
| Per-issue outcomes from the latest automation batches | ll-session recent --kind orchestration_run |
| How often context handoff triggers / recent compaction events | ll-session recent --kind session_lifecycle |
| Which subagents a session spawned, or which agent type oscillates | ll-session recent --kind subagent_run |
| Which skills succeed vs. fail | ll-session skill-stats |
| Whether a hook fired, its exit code, or how long it took | ll-session recent --kind hook_event |
What Is history.db?¶
.ll/history.db is a per-project SQLite database that accumulates a long-lived event history across every Claude Code session. Where session JSONL files are ephemeral per-conversation snapshots, history.db is the persistent record: it indexes tool invocations, file modifications, issue state transitions, loop executions, user corrections, and session-to-message content across all sessions that have ever run in this project. Set LL_HISTORY_DB=/path/to/alt.db to override the default location (useful for test isolation or CI). To run an ll-* CLI without writing its per-invocation analytics row — or authoring the db at all — set LL_ANALYTICS_CAPTURE=0 (kill switch: no resolution, no file, no cli_events row; wins over LL_HISTORY_DB; per-invocation use, not a shell-profile export — ENH-3449).
The database is additive-only — backfill is idempotent (dedup indexes prevent duplicates on repeated runs) and nothing is deleted unless you explicitly prune. Schema migrations apply automatically on connect. Current schema version: 45, defined in scripts/little_loops/session_store/schema.py (_MIGRATIONS). Each version maps to the ENH/FEAT that introduced it:
| Version | Issue | Adds |
|---|---|---|
| v1 | — | Initial bootstrap: tool_events, file_events, issue_events, loop_events, user_corrections, search_index, meta |
| v2 | ENH-1621 | Issue completion-summary columns on issue_events; message_events table |
| v3 | ENH-1690 | Dedup index on issue_events(issue_id, transition) |
| v4 | ENH-1710 | sessions table (session ID → JSONL path) |
| v5 | ENH-1711 | issue_sessions view (timestamp-overlap join) |
| v6 | ENH-1830 | last_backfill_ts meta key for incremental backfill |
| v7 | ENH-1833 | skill_events table |
| v8 | ENH-1848 | cli_events table |
| v9 | ENH-1904 | Dedup index on user_corrections |
| v10 | FEAT-1712 | summary_nodes / summary_spans (LCM compaction DAG) |
| v11 | ENH-1942 | assistant_messages table |
| v12 | ENH-1953 | level column on summary_nodes for N-level DAG |
| v13 | ENH-2046 | correction_retirements table |
| v14 | ENH-2151 | issue_snapshots table |
| v15 | ENH-2460 | skill_events completion columns (exit_code, success, duration_ms) |
| v16 | ENH-2462 | Authoritative issue_events.session_id column |
| v17 | ENH-2458 | commit_events table |
| v18 | ENH-2459 | test_run_events table |
| v19 | ENH-2581 | raw_events source-of-truth table |
| v20 | ENH-2461 | usage_events table (real LLM token counts + cost) |
| v21 | FEAT-2478 | OTel invocation_id / provider_vendor attribution on usage_events |
| v22 | ENH-2492 | orchestration_runs table (per-issue batch outcomes) |
| v23 | ENH-2463 | loop_runs table (per-run FSM loop summaries) |
| v24 | ENH-2497 | agent_type discriminator column on tool_events |
| v25 | ENH-2511 | mcp_server/mcp_tool/mcp_outcome/latency_ms columns on tool_events |
| v26 | ENH-2466 | learning_test_events table (Learning Test Registry mirror) |
| v27 | ENH-2495 | session_lifecycle_events table (handoff/compaction/sweep transitions) |
| v28 | ENH-2505 | subagent_runs table (subagent Task/Agent spawn tree) |
| v29 | ENH-2723 | run_id column on usage_events |
| v30 | ENH-2506 | hook_events table (per-fire hook execution telemetry) |
| v31 | ENH-2739 | harness_events table (ll-harness/eval outcome telemetry; parent_id links DSL per-task rows, ENH-2740) |
| v32 | ENH-2498 | prompt_opt_events table (prompt-optimization offer/outcome telemetry) |
| v33 | ENH-2504 | verdict_events table (verifier verdict outcome telemetry) |
| v34 | ENH-2507 | context_pressure_events table (context-window pressure measurements) |
| v35 | ENH-2512 | review_events table (reviewer/audit outcome telemetry) |
| v36 | ENH-2771 | issue_num column on issue_events/issue_snapshots + collision-merge and type-blind dedup indexes; rebuilt issue_sessions view |
| v37 | ENH-2814 | loop_runs.failure_terminal column (NULL on pre-v37 rows — falls back to the legacy name check) |
| v38 | ENH-2866 | orchestration_runs.base_sha / base_dirty (dequeue-time base-state stamp) |
| v39 | ENH-141 | harness_events.target_content_hash / target_path / dirty (content-pinning a harness run) |
| v40 | ENH-2997 | prepatch_evidence table |
| v41 | ENH-3185 | idx_harness_semantic_verdict index on harness_events(semantic_verdict), backing the abstention-rate query (the cannot_judge verdict itself is just a value of the pre-existing semantic_verdict column, not a new one) |
| v42 | BUG-3236 | Idempotent rebuild of the issue_sessions view (repairs databases left with a pre-v36 view shape by an uncommitted working-tree migration) |
| v43 | BUG-3241 | Repairs databases missing idx_assistant_messages_dedup and/or idx_summary_nodes_retention_dedup; dedups any accumulated duplicate rows first, then re-creates the UNIQUE indexes, and re-asserts every non-UNIQUE index for good measure |
| v44 | ENH-230 | abstention_reason column on verdict_events plus a CHECK constraint restricting verdict to pass/fail/implement/cannot_judge (table rebuilt via rename/copy/drop, since SQLite can't ALTER TABLE a CHECK onto an existing column) |
| v45 | FEAT-3300 | advisor_consults table (advisor-consult telemetry) |
| v46 | ENH-2990 | research_triage_events table (live research-triage skip-rate telemetry) |
| v47 | ENH-3204 | credential_scope_events table (after-the-fact credential-scope audit) |
| v48 | FEAT-3404 | orchestration_runs.ll_version / loop_runs.ll_version (little-loops version stamp, defaulted by the writers from little_loops.__version__) |
| v49 | ENH-3406 | harness_events run-model columns (cell_key, repetition, attempt_kind, continuations, superseded_by) + harness_admissions table (schema foundation for distinguishing repetitions from infra retries, ENH-3397) |
v15–v18 and v20–v40 are EPIC-2457 coverage expansions and related observability migrations; all migrations are additive — no user action is required when the schema version advances. Migrations v37–v39 add columns without backfilling them, so rows written before those versions carry NULL in the new columns.
What Gets Recorded¶
| Table | What it stores |
|---|---|
raw_events |
Verbatim session-JSONL records — the source of truth every JSONL-derived cache table is replayed from (rebuild()). Carries a compacted flag set by ll-session compact; only compacted=1 rows are eligible for prune (ENH-2581, v19). |
tool_events |
Every tool call (Bash, Read, Write, etc.) with byte counts (bytes_in, bytes_out, result_size), cache_hit flag, and agent_type (nullable; populated with the dispatched subagent name for tool_name="Task" rows, NULL otherwise — ENH-2497) |
file_events |
File reads and writes with path, operation, and associated issue ID |
issue_events |
Issue state transitions: captured, started, completed, deferred. v16 added a session_id column (indexed) so the issue_sessions view no longer relies on timestamp overlap (ENH-2462). |
issue_snapshots |
Point-in-time snapshots of issue content at lifecycle transitions (open, done, cancelled); dedup index on (issue_num, transition) (v36, type-blind — ENH-2771); indexed for full-text search (FTS) via the search_index with kind="snapshot". Populated live by set_status and by ll-session backfill --snapshots for historical issues. Used by ll-history-context as a last-resort fallback when no corrections or FTS rows match an issue (ENH-2151). |
loop_events |
FSM (finite-state machine) loop transitions with loop name and retry count |
message_events |
User message content for FTS indexing |
assistant_messages |
Assistant response content with tool-use count |
user_corrections |
Messages matching correction patterns: message-start signals (no,/no!, don't, stop, revert, that's wrong, not like that, !remember) and anywhere-in-message phrases (instead, actually that/this/it, you missed, should be (excluding should be fine/ok/good/great/...), wrong approach, remember that, always use, never use, from now on, I meant...not, not...use); extend with analytics.capture.correction_patterns (see Configuration Reference) |
skill_events |
/ll: skill invocations with args. v15 added nullable exit_code, success, and duration_ms columns so ll-session skill-stats can compute per-skill success rates (ENH-2460). |
cli_events |
ll-* CLI commands with exit code and duration |
sessions |
Maps session IDs to their .jsonl file paths |
commit_events |
Git commit metadata: commit_sha (unique), parent_sha, message, author, branch, issue_id (linked when known), files_json. Populated live by the session-start backfill. Queryable via ll-session recent --kind commit (ENH-2458, v17). |
test_run_events |
Pytest runs: total, passed, failed, errored, skipped, duration_s, failing_names_json, head_sha, branch, command, env_label. Queryable via ll-session recent --kind test_run (ENH-2459, v18). |
usage_events |
Real LLM token counts per invocation: model, state (NULL on backfilled rows, populated on live-written rows), input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens, cost_usd (NULL for unpriced models), run_id (NULL until backfilled — v29). Populated two ways: post-hoc from raw_events by _backfill_usage_events() (parses message.usage on type == "assistant" records), and live at loop-run finish by record_usage_event() (FSMExecutor._finish(), one row per collected TokenUsage, ENH-2724). Queryable via ll-session recent --kind usage and history_reader.recent_usage_events()/aggregate_usage() (ENH-2461, v20; OTel attribution columns added in v21). |
orchestration_runs |
Final per-issue outcomes from ll-auto, ll-parallel, and ll-sprint: invocation-scoped run_id, driver, status, duration, failure reason, sprint wave label, optional PR URL, timestamps, git context, dequeue-time base_sha/base_dirty stamp (v38), and ll_version (v48, the little-loops version installed at write time). Retries UPSERT the same (run_id, issue_id) and refresh FTS. Queryable via ll-session recent --kind orchestration_run, FTS search, export, and history_reader.recent_orchestration_runs()/aggregate_orchestration_runs() (ENH-2492, v22). |
summary_nodes / summary_spans |
LCM compaction summary tree (summary_nodes = nodes, summary_spans = message-link table). Populated when history.compaction.enabled: true; surface via ll-history root --expand and ll-session expand/describe (v10 / v12). |
prompt_opt_events |
Prompt-optimization offer/outcome telemetry: ts, session_id, mode, offered, bypass_reason, raw_len, optimized_len, optimized_text, accepted. Live-written per prompt by user_prompt_submit.py::handle() (gated on analytics.enabled); optimized_len/optimized_text/accepted filled in later, in place, by _backfill_prompt_opt() when a parseable ENHANCED: block is found in the transcript. Queryable via ll-session recent --kind prompt_opt and history_reader.recent_prompt_opt_events()/prompt_opt_offer_rate() (ENH-2498, v32). |
verdict_events |
Verifier verdict outcome telemetry: ts, session_id, verdict_kind, target_kind, target_id, verdict, severity_counts (JSON), findings_count, confidence, head_sha, branch. Written best-effort from cli/action.py::cmd_invoke() for the nine ll-action-bridged verifiers (ready-issue, confidence-check, go-no-go, tradeoff-review-issues, refine-issue, format-issue, verify-issues, prioritize-issues, align-issues). Queryable via ll-session recent --kind verdict and history_reader.recent_verdict_events()/verdict_pass_rate() (ENH-2504, v33). |
correction_retirements |
Records corrections that have been "retired" by a matching decision rule (topic fingerprint → rule id). Lets ll-history analyze show how often a past correction is now auto-handled (v13). |
loop_runs |
One row per completed FSM loop run: run_id (archive-time identifier, unique), loop_name, started_at/ended_at, final_state, iterations, terminated_by, error, nullable evaluator_score/diagnostics_path, git context, failure_terminal (v37), and ll_version (v48, the little-loops version installed at write time). Written best-effort by FSMExecutor._finish(). Queryable via ll-session recent --kind loop_run and history_reader.recent_loop_runs()/find_loop_run()/aggregate_loop_runs() (ENH-2463, v23). |
learning_test_events |
Mirror of the Learning Test Registry (.ll/learning-tests/*.md): record_id (slugified target, unique), target, status, assertions_json, date, raw_output_path. Written best-effort by ll-learning-tests prove/mark-stale/orphans --mark-stale (UPSERT — re-proves overwrite in place); reconciled from disk for out-of-band edits by ll-session backfill. Queryable via ll-session recent --kind learning_test, FTS search, and history_reader.recent_learning_tests()/find_learning_test() (ENH-2466, v26). |
session_lifecycle_events |
Session-lifecycle/handoff transitions: session_id, event (handoff_needed/compaction/stale_ref_sweep, open TEXT — no CHECK constraint), detail (JSON), head_sha, branch. Written best-effort by record_session_lifecycle_event() from context-monitor.sh (80%-threshold crossing), pre_compact.handle() (after state persistence), and sweep_stale_refs.handle() (once per invocation, including zero findings). First-write-only — no historical backfill. Queryable via ll-session recent --kind session_lifecycle, FTS search, and history_reader.recent_lifecycle_events()/handoff_frequency() (ENH-2495, v27). |
subagent_runs |
Subagent (Task/Agent) spawn tree: parent_session_id, agent_id (spawn-local — scoped to its parent, not a sessions.session_id), agent_type, agent_transcript_path (nested <parent-transcript-dir>/subagents/agent-<id>.jsonl), started_at, ended_at, status (running/completed/failed/orphaned). Written by the SubagentStart/SubagentStop lifecycle hooks (record_subagent_run_start() INSERT OR IGNORE, record_subagent_run_stop() UPDATE, matching on the composite (parent_session_id, agent_id) key). Reconciled from disk for out-of-band spawns by ll-session backfill (all backfilled rows land as status="completed"). A running row whose SubagentStop hook never fired (e.g. the parent process group was reaped first) is swept to orphaned by reconcile_stale_subagent_runs(), called from the session_end-intent hook (hooks/sweep_stale_refs.py) on the next session's SessionStart (ENH-3210). Queryable via ll-session recent --kind subagent_run, FTS search, and history_reader.subagent_tree()/subagent_retries()/subagent_budget() (ENH-2505, v28). |
hook_events |
Per-fire hook execution telemetry: event_name, matcher, script, exit_code, duration_ms, stderr_preview (truncated to 512 bytes), session_id, head_sha, branch. Live-write-only — no raw_events source exists (the host doesn't emit hook execution results into the transcript), so there is no backfill and the table is excluded from rebuild(). Written by hook_event_context(), wrapped once around every Python-dispatched intent inside main_hooks(); Stop/SessionEnd (bash-only) go through the hooks/scripts/record-hook-event.sh shim instead. Queryable via ll-session recent --kind hook_event, FTS search, and history_reader.recent_hook_events()/hook_failure_rate()/hook_latency_p95() (ENH-2506, v30). |
harness_events |
ll-harness / eval-run outcome telemetry, including semantic_* scoring columns and a parent_id linking each DSL per-task row to its parent run (ENH-2740). v39 added target_content_hash, target_path, and dirty, which content-pin a run to the artifact it evaluated. v49 added the run-model columns cell_key, repetition, attempt_kind (repetition/infra_retry, CHECK-enforced), continuations (reserved, unpopulated), and superseded_by, plus a partial UNIQUE index on (cell_key, repetition) WHERE attempt_kind = 'repetition' guarding against duplicate repetition samples (ENH-3406) — schema-only, no producer populates these yet (ENH-3407). Live-write-only — excluded from rebuild(). Queryable via ll-session recent --kind harness (ENH-2739, v31). |
context_pressure_events |
Context-window pressure measurements emitted by context-monitor.sh. Live-write-only. Queryable via ll-session recent --kind context_pressure (ENH-2507, v34). |
review_events |
Reviewer/audit outcome telemetry. Live-write-only. Queryable via ll-session recent --kind review (ENH-2512, v35). |
prepatch_evidence |
Pre-patch evidence captured during implementation runs: issue_id, run_id, state, evidence_json, created_at; indexed by issue_id (ENH-2997, v40). |
advisor_consults |
One row per consult_for_trigger() invocation (advisor.py): task_key, signal, advisor_host, advisor_model, main_model, floor_status, outcome ("issued" or a skipped_reason value), latency_ms, input_tokens, output_tokens, confidence, verdict_body (nullable; only populated when advisor.store_verdict_body opts in). Live-write-only — no raw_events source exists, so the table is excluded from rebuild() (FEAT-3300, v45). |
research_triage_events |
One row per axis per ll-issues research-triage invocation (three rows share one ts + issue_id, written atomically by write_research_triage()): issue_id, axis (CHECK: locator/analyzer/pattern_finder), covered, reason (nullable, CHECK: no_qualified_refs/below_threshold/missing_symbol/stale/program_design_unmet/unreadable), refined_at (nullable — NULL means a first-refine invocation), evidence. Gated on analytics.capture.cli_commands. Live-write-only, not indexed into search_index — excluded from rebuild(). Queryable via ll-session recent --kind research_triage and history_reader.research_triage_stats() (ENH-2990, v46). |
credential_scope_events |
One row per declaring-state/spec dispatch, written via write_credential_scope() immediately before the spawn: run_id, state, scopes_json (declared scopes names), var_names_json (resolve_scopes()'s resolved env-var names). Names only, never values. No row for an undeclared state/spec. Written from two call sites: FSMExecutor (FSM shell states) and runner_spec.py::_run_cmd() (queued/CMD ActionSpec dispatch — run_id is entry.id when drained by ll-queue, else spec.name; BUG-3400). Live-write-only, not indexed into search_index — excluded from rebuild(). Queryable via ll-session recent --kind credential_scope (ENH-3204, v47). |
harness_admissions |
Append-only audit table recording when an infra retry was admitted and why: attempt_id/superseded_id (both harness_events.id), reason (CHECK-enforced: timeout/host_crash/harness_error/network). No run_id column by design — scoped via harness_events.parent_id/target instead. INSERT-only, like hook_events/commit_events — never UPDATE/DELETE. Live-write-only, not indexed into search_index — excluded from rebuild(). Schema-only in this migration; ENH-3407 adds the writer. Queryable via ll-session recent --kind harness_admission (ENH-3406, v49). |
Capture is controlled per-signal via analytics.capture.* config (scripts/little_loops/config-schema.json):
- analytics.capture.file_events (bool, default true) — gate file_events recording
- analytics.capture.corrections (bool, default true) — gate user_corrections recording
- analytics.capture.skills (array of glob patterns, default ["*"]) — which skill names get recorded to skill_events; e.g. ["create-sprint", "manage-issue"] records only those skills
- analytics.capture.cli_commands (array of glob patterns, default ["*"]) — which ll-* CLI command names get recorded to cli_events
- analytics.capture.hooks (bool, default true) — gate hook_events recording (ENH-2506)
- analytics.capture.usage_events (bool, default true) — gate usage_events recording (ENH-2461/ENH-2724)
- analytics.capture.correction_patterns (array of regex strings, default []) — additional patterns appended to the built-in correction detector (built-ins always remain active; see What Gets Recorded for the full built-in list)
Getting Started: Backfill¶
The database starts empty. Populate it by backfilling from your existing session JSONL files and issue directory.
Full backfill¶
Reads these sources sequentially:
- Issues directory (
.issues/*/) →issue_events,issue_snapshots - Loop state (
.loops/.running/,.loops/.history/) →loop_events - Git history (when the project has a
.git) →commit_events - Session JSONL files (discovered from your project folder) →
raw_events - Learning Test Registry (
.ll/learning-tests/) →learning_test_events - Subagent transcripts (under the sessions root) →
subagent_runs
Since ENH-2581, session JSONL lands in
raw_eventsand nowhere else.raw_eventsis the source of truth; the JSONL-derived cache tables (tool_events,message_events,assistant_messages,sessions,user_corrections,skill_events,summary_nodes/summary_spans,usage_events) are not populated by a plainbackfill. To (re)derive them, replayraw_events:ll-session backfill --rebuild # backfill, then replay raw_events into the cache tables ll-session rebuild # replay only; idempotent
rebuildwipes and re-derives those tables fromraw_events, so running it is safe and repeatable. If a query againsttool_eventsormessage_eventscomes back empty on a freshly-backfilled database, this is why.
Beyond backfill, issue_events rows also arrive through two live channels:
the EventBus-emitted issue.* path (SQLiteTransport.send(), FSM-loop/
issue-lifecycle events only), and a direct-call record_issue_event()
(session_store/writers.py) invoked from ll-issues set-status's transition
side-effect block — added by BUG-2770 so a manual/CLI status transition
produces the same row a bus-emitted one would, keeping issue_sessions and
issue_effort() populated regardless of which path closed the issue.
Output is a single human-readable summary line (not JSON):
Backfilled 1687 rows (issues=42, loops=8, raw_events=1204, snapshots=12, commits=389, learning_tests=32)
With --rebuild, the replayed cache-table counts are appended to the same line:
Backfilled 3402 rows (issues=42, loops=8, raw_events=1204, snapshots=12, commits=389, learning_tests=32, tools=1204, messages=389, sessions=23, corrections=17, summaries=0)
rebuild and compact do support --json; backfill does not.
Incremental backfill¶
Processes only session JSONL files modified after the given date. Faster than a full backfill and safe to run frequently. The session-start hook runs this automatically at the start of each session (ENH-1830), so the database stays current without manual intervention.
You can specify which host's session files to scan if you use multiple Claude Code hosts:
ll-session backfill --host claude-code
ll-session backfill --host codex
ll-session backfill --host opencode
ll-session backfill --host omp
--host defaults to None (auto-detect from LL_HOOK_HOST); valid choices also include pi, kimi-code, qwen, gemini, and omp.
Querying Sessions¶
Full-text search¶
ll-session search --fts "authentication middleware"
ll-session search --fts "rate limit" --kind correction
ll-session search --fts "worktree" --kind tool --limit 5
Returns BM25-ranked results across all event tables. Use --kind to restrict to one table type: tool, file, issue, loop, correction, message, skill, cli, snapshot, commit, test_run, usage, orchestration_run, loop_run, learning_test, session_lifecycle, subagent_run, hook_event, harness, prompt_opt, verdict, context_pressure, review, advisor_consult — 24 kinds in total, sourced from VALID_KINDS in session_store/schema.py. Note the kind for harness_events is harness, not harness_event.
Most recent events¶
ll-session recent --kind correction
ll-session recent --kind loop --limit 10
ll-session recent --kind issue --issue BUG-1759
--kind is required unless --issue is given (in which case sessions for that issue are listed instead). --kind + --issue together filters events of that kind to the issue.
All events for an issue¶
Returns every event (tools, files, corrections, loop transitions) linked to that issue ID, chronologically ordered.
Resolve a session's JSONL file¶
Useful when you want to open the raw session transcript.
Export tables as JSONL¶
ll-session export # all non-message tables, to stdout
ll-session export --tables issue_event correction # only these types
ll-session export --since 2026-06-01 -o export.jsonl # date-filtered, to a file
ll-session export --include-messages # also include message_events (~46K rows)
Dumps selected tables as newline-delimited JSON (one record per line, each tagged with a "type" field) for visualization or external tooling. --tables accepts one or more of: session, issue_event, issue_snapshot, skill_event, loop_event, correction, summary_node, message_event, commit_event, test_run_event, usage_event, orchestration_run, loop_run, session_lifecycle_event, harness_event, prompt_opt_event, verdict_event, context_pressure_event, review_event, advisor_consult_event — 20 in total, sourced from _EXPORT_TABLE_MAP in session_store/queries.py. When --tables is omitted, the default set is every type except message_event (pass --include-messages to add messages back, or select it explicitly via --tables). --since filters each table by its own timestamp column (started_at for session, created_at for summary_node, ended_at for orchestration_run and loop_run, ts for the rest) and accepts an ISO 8601 date or datetime. -o FILE / --output FILE writes to a file instead of stdout and prints a summary count on success; without it, records stream to stdout with no trailing summary (so output stays pipeable).
Issue ↔ Session Cross-References¶
The issue_sessions view joins issue lifecycle events with session messages. Since v16 (ENH-2462), the join is authoritative: every issue_events row carries a session_id column (indexed) recorded at write time, so the view no longer infers association from timestamp overlap. A legacy view legacy_issue_sessions_ts_overlap is retained as a backward-compat fallback for sessions recorded before the v16 migration — new code should use issue_sessions directly.
List sessions that worked on an issue:
Event stream for an issue filtered to one session:
Navigate within a session:
ll-session expand 42 # message_events under summary node 42 (if compaction enabled)
ll-session describe 42 # metadata for summary node 42
Planning Skill Injection¶
When you invoke a planning skill (/ll:create-sprint, /ll:manage-issue, /ll:scope-epic, /ll:review-epic), little-loops automatically injects a ## Historical Context block drawn from history.db. This surfaces past corrections, recently touched files, and completed issues relevant to what you're planning — so the agent doesn't repeat mistakes from prior sessions.
What the injected block looks like:
## Historical Context
- don't use HTTP-only cookies for refresh tokens (correction, 3 occurrences)
- authentication middleware needs CORS credentials flag (correction, 2 occurrences)
- file:src/middleware/auth.ts:write (7 days ago)
- file:src/utils/tokens.ts:write (7 days ago)
- completed: BUG-1759 — fix refresh token expiry (12 days ago)
How injection is gated:
The history.planning_skills config key controls which skills trigger injection. Default:
{
"history": {
"planning_skills": ["create-sprint", "scope-epic", "manage-issue", "review-epic"]
}
}
To add a skill or disable injection entirely:
{
"history": {
"planning_skills": ["create-sprint", "scope-epic", "manage-issue", "review-epic", "my-skill"]
}
}
Effort and velocity context:
Add --effort to get session count and cycle-time context for an issue:
How automation calls it:
Skills call ll-history-context --for-skill <name>, which exits 0 with no output if the skill is not in planning_skills. This makes the gate cheap: no DB query if the skill isn't configured for injection.
History Analytics¶
Project summary¶
ll-history summary
ll-history summary --json
ll-history summary --json --since 2026-08-01 # windowed, includes loop-run counts (ENH-3237)
Issue counts, completion rate, and age distribution. Quick health check.
--since/--until restrict the window and add loop_runs_started/
loop_runs_ended; --json output also names its source
("issue_events" or "files") since the two can disagree on counts.
Trend analysis¶
ll-history analyze
ll-history analyze --format markdown --period monthly
ll-history analyze --since 2026-01-01 --until 2026-06-01
Produces trend analysis: velocity, subsystem breakdown, tech debt signals. Useful for sprint retrospectives and capacity planning.
Export documentation from issue history¶
ll-history export "authentication"
ll-history export "rate limiting" --format narrative --output docs/rate-limiting-context.md
ll-history export "API design" --type FEAT --since 2026-01-01 --scoring hybrid
Generates prose documentation from completed issues matching the topic. The hybrid scoring mode combines BM25 keyword matching with semantic overlap. Useful for onboarding docs and ADRs.
Project root summary (requires compaction)¶
Shows the top-level condensed summary node when LCM compaction is enabled. --expand drills down to the underlying message events. See LCM Compaction below.
Test runs¶
ll-session recent --kind test_run --limit 5
# Filtering by branch is not supported on `recent` or `backfill`; query `test_run_events.branch` directly, e.g. via `ll-session export --tables test_run_event` and filter client-side.
Each row is a pytest invocation captured live during a session or by ll-session backfill from a recorded run: total, passed, failed, errored, skipped, duration_s, failing_names_json, head_sha, branch, command, env_label. Use this to spot a branch where tests started failing, or to find the commit that flipped a passing run red. (ENH-2459.)
Verifier verdicts¶
Each row is a ready-issue/confidence-check/go-no-go/etc. invocation with its verdict_kind, target_id, verdict, severity_counts, findings_count, and confidence. Use this to answer "how many issues passed readiness this week?" or "which verifier keeps blocking BUG-2501?" (ENH-2504.)
Skill success signal¶
Per-skill invocation count, completion count, and success rate, derived from the exit_code / success / duration_ms columns on skill_events (added in v15, ENH-2460). Use this to surface skills that users are pushing back on most, or to measure whether a recent change improved a skill's reliability.
Rework and agent-quality trends¶
ll-history rework # Reopen/follow-up/touch-back/revert rates
ll-history quality # Fix-rate/correction/cost/tokens/retry-inflation trends
Both answer "are things getting better or worse," not "what happened" — see
Quality Metric Definitions below for what each metric means, and
docs/reference/CLI.md's ll-history rework / ll-history quality sections for the full flag
tables.
ll-history quality --workspace (FEAT-3410) runs the same per-window analysis once per member
of a declared ll-workspace.yaml manifest, read-only, producing a per-repo breakdown plus a
skipped-with-reason list for any member whose history.db is missing or schema-skewed, plus a
workspace-wide totals number (FEAT-3418) computed over the union of every gated member's
tables — see docs/reference/CLI.md's "Cross-repo workspace aggregation" subsection for the
full behavior, including the cross-repo id-collision handling and the attach-limit fallback.
ll-history activity (FEAT-3446) is the third report in this family: per-repo and union
loop/issue activity counts over the same --workspace scope-flag convention, with
--format json as the machine-readable contract for downstream tooling. Note that it does
not share rework/quality's (calendar month, orchestrator) windowing convention — its
bounds are summary-style inclusive ISO-8601 timestamps (--since/--until, e.g.
2026-08-10T14:00:00Z, naive treated as UTC), which is what rolling consumer windows need.
Quality Metric Definitions¶
ll-history rework (FEAT-2867) and ll-history quality (FEAT-3183) share one
(calendar month, orchestrator) windowing convention and one min-sample/insufficient-history
gate, extracted into issue_history/_utils.py so the two reports read side by side and cannot
silently diverge. (ll-history activity, FEAT-3446, is related but deliberately outside this
convention — its window is summary-style inclusive ISO timestamps, not calendar months.)
This section states each metric's definition once; the CLI flag tables live in
docs/reference/CLI.md and are not restated here.
| Metric | Command | Formula | Notes |
|---|---|---|---|
| Reopen / follow-up / touch-back / revert rate | rework |
Share of closed issues in the window exhibiting the signal | issue_events dedups per (issue_num, transition), so a second done→open→done cycle collapses into the first |
| Quality-adjusted throughput | rework |
closed_count x (1 - max(reopen_rate, revert_rate)) |
The pinned rework-share formula; reused verbatim by quality's fix-rate |
| Fix-rate | quality |
1 - rework_share |
Verdict is derived from rework_share's own trend, not re-derived from the fix-rate value, since the two are related nonlinearly around the ±20% verdict band |
| Correction rate | quality |
Non-retired user_corrections attributed via session_id -> issue_sessions -> issue_num (split evenly across multi-issue sessions) ÷ closed issues |
user_corrections has no issue_id column; sessions with no recorded issue association are excluded from the numerator |
| Cost per issue | quality |
usage_events.cost_usd summed per issue (same session-split rule) ÷ closed issues |
Each window reports coverage (share of attributed rows with non-null cost_usd) and suppresses the verdict, not the number, below 50% coverage |
| Tokens per issue | quality |
The four usage_events token columns summed the same way ÷ closed issues |
Always computable — no pricing-table dependency, so it stays informative when cost coverage is poor |
| Retry inflation | quality |
Mean loop_runs.iterations per (calendar month, loop_name) |
Bucketed by loop, not orchestrator — loop_runs has no issue_id column, and the two-hop join needed to recover one is only partially reachable |
Why cost coverage matters: usage_events.cost_usd is None for any model absent from
pricing.MODEL_PRICING at write time, and that null rate is not evenly distributed across
time — the current-generation model is typically the least-priced one. Without the coverage
gate, a cost-per-issue trend would silently read "pricing-table lag" as "agents got more
expensive." --min-sample/insufficient_history guards against the symmetric failure: a
window built from too few closed issues (or loop runs) reporting a misleadingly confident ratio.
Every metric's formula, window, denominator, min-sample, verdict band, and caveats are also
emitted as a MetricDefinition object in ll-history quality's JSON/YAML payload — the same
data as this table, machine-readable for a downstream consumer.
That consumer now ships (FEAT-3405): ll-history quality also runs a prior-K-window baseline
regression detector over this table's series, flagging a drop in the latest eligible window of
each metric and attributing it to the model/host/ll_version whose composition shifted most.
See ll-history quality's entry in CLI.md for the
full detection/attribution rules and the --sensitivity/--baseline-windows/--all-windows
flags.
Session Log Tooling (ll-logs)¶
ll-logs operates directly on the host's session JSONL files rather than history.db. Use it for analysis that needs raw session-level data.
Invocation frequency and corrections¶
Skill invocation frequency ranked by usage or correction rate. Tells you which skills users are pushing back on most.
Mine failed commands for bugs¶
ll-logs scan-failures --project .
ll-logs scan-failures --project . --capture --window-days 14
ll-logs scan-failures --project . --skill review-epic --json
Finds failed ll-* CLI invocations in session logs, clusters by error signature, and optionally creates BUG issue files (--capture). --skill NAME scopes clusters to ll-* CLI failures that occurred while NAME was the enclosing skill.
Identify unused skills¶
Lists skills from the catalog with zero or few invocations in the given window. Useful for identifying candidates for pruning or deprecation.
Compare two sessions¶
Behavioral comparison: which skills were used in each session, tool-chain sequences, correction frequency, error rates. Good for understanding why one session solved a problem and another didn't.
Export eval fixtures¶
ll-logs eval-export --skill manage-issue --limit 50 --out fixtures/manage-issue.yaml
ll-logs eval-export --issue FEAT-1933 --out fixtures/feat-1933-turns.yaml
Extracts turn-pair fixtures from session logs for SFT training corpus construction. Filtered by skill name or issue ID. Invocations are reconstructed directly from JSONL session logs, not from history.db — the DB is consulted only for enrichment (history_reader.lookup_session_metadata(), e.g. correction/issue-outcome signal for fixture classification) and degrades gracefully to {} if history.db is missing or on an old schema, so there is no minimum schema version requirement.
Advanced: LCM Compaction¶
By default, history.db stores raw events only. Enable LCM-style compaction to additionally generate hierarchical summaries:
When enabled, ll-session backfill calls LLM summarization after ingesting session JSONL files. It produces:
- Per-session leaf nodes — compressed summaries of individual session content
- Per-session condensed nodes — bullet-point distillations when a session exceeds the token budget
- Cross-session condensed nodes — recursive summaries when enough per-session nodes accumulate
- Project root node — a single top-level summary accessible via
ll-history root
The compaction algorithm (LCM Algorithm 3) uses a three-level escalation: normal LLM → aggressive bullet-point → deterministic truncation.
Three optional keys tune the pass (see Configuration Reference): model and timeout control the summarization LLM calls, and max_level caps cross-session recursion depth (default: unbounded — recurses until a single root node remains).
Compaction is disabled by default because it makes background LLM calls during backfill. Enable it when you want
ll-history rootandll-session expand/describeto be useful.
Navigate the summary DAG:
ll-session describe 42 # show metadata for node 42
ll-session expand 42 # show original messages under node 42
ll-session grep "auth" --summary-id 42 # search within a node's scope
Retention & Pruning¶
history.db grows over time. prune reclaims space — but since ENH-2581 it is the second step of a two-step flow, and running it alone deletes nothing.
ll-session compact # step 1: fold aged raw_events into summary nodes, mark them compacted=1
ll-session prune --dry-run # step 2: show what would be deleted, without deleting
ll-session prune # step 2: apply
ll-session prune --json # machine-readable result
ll-session compact --and-prune # both steps in one invocation
prunerequirescompactfirst. A row is eligible for deletion only whencompacted = 1, whichll-session compactsets after folding the row into a per-sessionretentionsummary node. On a database that has never been compacted,prunepasses its gates, finds nothing eligible, and reports0deleted — which reads like a broken command but is the contract working as intended.
--dry-run counts eligible rows without deleting them (vacuumed is always false in this mode). --json prints the result dict instead of a human-readable summary.
What gets pruned: raw_events rows only, and only those already marked compacted = 1 and older than raw_event_max_age_days. Nothing else is ever pruned — the JSONL-derived cache tables (tool_events, cli_events, file_events, message_events, …) and the high-value tables (issue_events, user_corrections, …) are all untouched regardless of age. Pruning raw_events does not shrink those tables; it removes the verbatim source records that have already been summarised.
Gating: both minimums below must be exceeded before any row is deleted (dual-gated, not either/or):
analytics.retention.min_project_age_days(default: 365) — project age is measured asMIN(started_at)from thesessionstable, not wall-clock repo ageanalytics.retention.min_db_size_mb(default: 800) — measured as the.ll/history.dbfile size on disk
If either gate is unmet, prune returns a gate_unmet list explaining why and deletes nothing. If both gates pass but raw_event_max_age_days is null, pruning is considered to have "run" but no age cutoff is applied (no rows deleted). Otherwise, compacted raw_events rows older than the cutoff are deleted, the transaction is committed, and a VACUUM runs afterward on a separate connection (avoids transaction conflicts) to reclaim disk space.
Result shape (both human and --json output derive from this dict): pruned (bool, whether pruning executed), gate_unmet (list of human-readable reasons), project_age_days, db_size_mb, deleted, vacuumed (bool). deleted has exactly one key — {"raw_events": N} — or is empty {} when a gate was unmet or raw_event_max_age_days is null.
When to prune: If your project is under 1 year old, leave the defaults alone — the guards prevent premature pruning. Only lower raw_event_max_age_days if ll-session commands feel slow (consistently > 500ms), which indicates the database has grown large.
The raw event max age:
Configuration Reference¶
All keys live under history.* and analytics.* in .ll/ll-config.json.
| Key | Default | Description |
|---|---|---|
history.planning_skills |
["create-sprint", "scope-epic", "manage-issue", "review-epic"] |
Skills that trigger ## Historical Context injection |
history.velocity_window |
10 |
Issue count window for velocity calculations |
history.max_age_days |
null |
Global max age for all history queries (null = no limit) |
history.db_path |
null |
Override the default .ll/history.db location; relative paths resolve against the resolved project root (resolve_ll_dir()'s upward walk from cwd, ENH-2927) — not the bare working directory, so a command run from a subdirectory still anchors at the project's .ll/. The LL_HISTORY_DB env var takes precedence over this |
history.effort_fields |
["session_count", "cycle_time_days"] |
Fields extracted from history.db for ll-history-context --effort reporting |
history.session_digest.enabled |
true |
Inject project-wide digest block at session start |
history.session_digest.days |
7 |
Lookback window for session digest |
history.session_digest.char_cap |
1200 |
Max characters in injected context block |
history.session_digest.sections |
[] |
Ordered list of digest section providers to include; empty = all v1 providers |
history.compaction.enabled |
false |
LCM summarization during backfill |
history.compaction.budget_tokens |
4096 |
Token budget per summary node |
history.compaction.cross_session_enabled |
true |
Build cross-session condensed nodes |
history.compaction.model |
null |
Model override for compaction LLM calls (null = host default) |
history.compaction.timeout |
60 |
Timeout (seconds) per compaction LLM call; on timeout, escalation falls through to deterministic truncation |
history.compaction.max_level |
null |
Max cross-session condensation depth (null = recurse until one root node remains) |
history.evolution.feedback_min_recurrence |
2 |
Min recurrence count for a correction to surface in evolution analysis |
history.evolution.bypass_min_count |
2 |
Min bypass count threshold for evolution signal suppression |
history.go_no_go.correction_penalty |
-0.2 |
Score penalty applied per correction event in go/no-go scoring |
history.capture_issue.dup_overlap_threshold |
0.7 |
Overlap ratio above which a new captured issue is considered a duplicate |
analytics.retention.min_project_age_days |
365 |
Min project age before pruning is allowed |
analytics.retention.min_db_size_mb |
800 |
Min DB size before pruning is allowed |
analytics.retention.raw_event_max_age_days |
90 |
Age threshold for raw event deletion |
analytics.capture.file_events |
true |
Record file reads/writes |
analytics.capture.corrections |
true |
Record user correction messages |
analytics.capture.skills |
["*"] |
Glob patterns for skill names to record to skill_events |
analytics.capture.cli_commands |
["*"] |
Glob patterns for CLI command names to record to cli_events. Honored by ll-history and ll-session since ENH-3449 (they pass their project config to cli_event_context); most other ll-* binaries still capture unconditionally. A present-and-false analytics.enabled (the ll-init opt-out shape) suppresses the row the same way, and the LL_ANALYTICS_CAPTURE=0 env var is a per-invocation override of both |
analytics.capture.hooks |
true |
Record per-fire hook execution telemetry to hook_events (ENH-2506) |
analytics.capture.usage_events |
true |
Record per-invocation LLM token counts and cost to usage_events (ENH-2461/ENH-2724) |
analytics.capture.correction_patterns |
[] |
Additional regex patterns for correction detection |
See Also¶
- Session Handoff Guide — context monitoring and session continuation; the session-start hook that triggers incremental backfill
- Workflow Analysis Guide —
ll-messagesfor extracting and analyzing user message patterns - CLI Reference — complete flag listings for
ll-session,ll-history,ll-history-context,ll-logs