Skip to content

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

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

ll-session backfill

Reads these sources sequentially:

  1. Issues directory (.issues/*/) → issue_events, issue_snapshots
  2. Loop state (.loops/.running/, .loops/.history/) → loop_events
  3. Git history (when the project has a .git) → commit_events
  4. Session JSONL files (discovered from your project folder) → raw_events
  5. Learning Test Registry (.ll/learning-tests/) → learning_test_events
  6. Subagent transcripts (under the sessions root) → subagent_runs

Since ENH-2581, session JSONL lands in raw_events and nowhere else. raw_events is 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 plain backfill. To (re)derive them, replay raw_events:

ll-session backfill --rebuild   # backfill, then replay raw_events into the cache tables
ll-session rebuild              # replay only; idempotent

rebuild wipes and re-derives those tables from raw_events, so running it is safe and repeatable. If a query against tool_events or message_events comes 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

ll-session backfill --since 2026-05-01

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

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

ll-session related BUG-1759

Returns every event (tools, files, corrections, loop transitions) linked to that issue ID, chronologically ordered.

Resolve a session's JSONL file

ll-session path abc123-def456
# → /Users/you/.claude/projects/my-project/abc123-def456.jsonl

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:

ll-history sessions BUG-1759

Event stream for an issue filtered to one session:

ll-session recent --kind tool --issue BUG-1759

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"]
  }
}
{
  "history": {
    "planning_skills": []
  }
}

Effort and velocity context:

Add --effort to get session count and cycle-time context for an issue:

ll-history-context BUG-1759 --effort

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)

ll-history root
ll-history root --expand

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

ll-session recent --kind verdict --limit 5

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

ll-session skill-stats
ll-session skill-stats --since 2026-08-01

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.

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

ll-logs stats --project .
ll-logs stats --project . --sort corrections --window-days 30

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

ll-logs dead-skills --project .
ll-logs dead-skills --project . --threshold 5 --window-days 90

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

ll-logs diff SESSION_A SESSION_B

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:

{
  "history": {
    "compaction": {
      "enabled": true,
      "budget_tokens": 4096
    }
  }
}

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 root and ll-session expand/describe to 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

prune requires compact first. A row is eligible for deletion only when compacted = 1, which ll-session compact sets after folding the row into a per-session retention summary node. On a database that has never been compacted, prune passes its gates, finds nothing eligible, and reports 0 deleted — 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 as MIN(started_at) from the sessions table, not wall-clock repo age
  • analytics.retention.min_db_size_mb (default: 800) — measured as the .ll/history.db file 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:

{
  "analytics": {
    "retention": {
      "raw_event_max_age_days": 90
    }
  }
}

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 Guidell-messages for extracting and analyzing user message patterns
  • CLI Reference — complete flag listings for ll-session, ll-history, ll-history-context, ll-logs