Skip to content

Throughline runtime state files

The Reflection Pass daemon (daemon/reflection_pass.py) and the MCP tools (mcp_server/tools/) communicate via JSON state files under $THROUGHLINE_STATE_DIR/ (default ~/throughline_runtime/state/). The daemon is the only writer; MCP tools and the --inspect / --explain diagnostics are pure readers.

This doc lists every file, its schema, when it's written, who reads it, and how to inspect it.


TL;DR — file inventory

File Writer Readers Refresh cadence
reflection_pass_state.json daemon --inspect, doctor every pass (non-dry-run)
reflection_cluster_names.json daemon stage 3 daemon (cache), --inspect when new clusters formed
reflection_backfill_state.json daemon stage 4 daemon (cache), --inspect when card mtime changes
reflection_open_threads.json daemon stage 5 find_open_threads MCP tool every pass
reflection_positions.json daemon check_consistency, get_position_drift MCP tools every pass
reflection_writeback_preview.json daemon stage 8 --diff-preview, future --commit-writeback every pass
reflection_contradictions.json daemon stage 6 (NOT YET) future check_consistency enrichment every pass once stage 6 ships
reflection_drift.json daemon stage 7 (NOT YET) future get_position_drift enrichment every pass once stage 7 ships

Run python -m daemon.reflection_pass --inspect to see a live summary of what's present and how stale.


reflection_pass_state.json — per-pass watermark

Writer: daemon/reflection_pass.py:run_pass at end of each non-dry-run pass.

Shape: serialized PassResult dataclass.

{
  "started_at": "2026-04-28T12:00:00+00:00",
  "finished_at": "2026-04-28T12:00:30+00:00",
  "cards_scanned": 2477,
  "cards_reflectable": 72,
  "cards_excluded": 2405,
  "cards_with_position_signal": 0,
  "cards_clustered": 72,
  "clusters_count": 24,
  "cluster_names_resolved": 24,
  "backfill_completed": 72,
  "open_threads_detected": 8,
  "contradictions_detected": 0,
  "drift_phases_computed": 0,
  "cards_updated": 0,
  "dry_run": false,
  "stages_completed": [...],
  "stages_skipped": [...],
  "errors": []
}

Use cases: doctor checks that a pass ran in the last N hours; --inspect shows last-run age and per-stage counters; future incremental mode uses started_at as a watermark for "what changed since last pass".


reflection_cluster_names.json — stage 3 cache

Writer: stage 3 (_stage_resolve_cluster_names) when --enable-llm-naming is set. Mutated in-place during the pass; written at end if the pass is non-dry-run.

Shape: cluster_signature -> snake_case_name.

{
  "0|10_Tech/foo.md,10_Tech/bar.md": "pricing_strategy",
  "1|20_Health/a.md,20_Health/b.md,20_Health/c.md": "b1_thiamine_therapy"
}

The signature combines cluster_id with sorted member paths so re-clustering with shifted membership invalidates the cache naturally. When the pass re-runs and a cluster's membership hasn't changed, the cache hit avoids the LLM call entirely.


reflection_backfill_state.json — stage 4 cache

Writer: stage 4 (_stage_backfill_position_signal) when --enable-llm-backfill is set.

Shape: <card_path>|<mtime_int> -> {claim_summary, open_questions}.

{
  "/path/to/card.md|1714512345": {
    "claim_summary": "Use B1 daily for nerve repair",
    "open_questions": ["What's the right dose escalation?"]
  }
}

Cache invalidates when mtime shifts (user edited the card), ensuring re-runs only fire LLM calls for new or changed content.


reflection_open_threads.json — stage 5 output

Writer: every pass (after stage 5), regardless of dry-run. Stage 5 is pure structural — no LLM, no I/O risk.

Reader: mcp_server/tools/find_open_threads.py.

Shape:

{
  "generated_at": "2026-04-28T12:00:00+00:00",
  "vault_root": "/Users/.../ObsidianVault",
  "dry_run": false,
  "open_threads": [
    {
      "card_path": "/path/to/card.md",
      "topic_cluster": "pricing_strategy",
      "open_questions": [
        "How to handle freemium conversion?",
        "What happens to LTV at scale?"
      ],
      "last_touched": "2026-01-15",
      "context_summary": "Discussing freemium pricing for early-stage SaaS"
    }
  ]
}

Sorted by last_touched DESC. The find_open_threads tool applies optional topic filter and limit; total count comes from the entries length.


reflection_positions.json — comprehensive position database

Writer: every pass. The widest state file — every reflectable card's stance/reasoning/date/cluster keyed by cluster.

Readers: mcp_server/tools/check_consistency.py, mcp_server/tools/get_position_drift.py.

Shape:

{
  "generated_at": "2026-04-28T12:00:00+00:00",
  "vault_root": "/.../ObsidianVault",
  "dry_run": false,
  "clusters": [
    {
      "cluster_id": "0",
      "topic_cluster": "pricing_strategy",
      "size": 3,
      "cards": [
        {
          "card_path": "/path/to/early.md",
          "title": "Early discussion",
          "stance": "Against usage-based for early-stage SaaS",
          "reasoning": ["LTV math is unpredictable", "Churn risk severe pre-PMF"],
          "open_questions": ["What changes once past PMF?"],
          "date": "2025-12-01",
          "is_open_thread": false,
          "is_backfilled": true
        }
      ]
    }
  ]
}

Cards within each cluster are sorted chronologically by date (using card_timestamp() priority: frontmatter.date > .updated > file mtime > "0").

When stage 4 (back-fill) hasn't run, stance/reasoning/ open_questions are null/empty and is_backfilled: false. Tools degrade gracefully with explicit "no back-fill yet" messages rather than crashing or returning empty.


reflection_writeback_preview.json — stage 8 preview

Writer: every pass (stage 8). Stage 8 in current release NEVER mutates vault files — this preview is the only output. The actual atomic frontmatter rewrite is gated to a future commit with --commit-writeback flag.

Shape:

{
  "generated_at": "2026-04-28T12:00:00+00:00",
  "dry_run": false,
  "cards_would_be_modified": 72,
  "diffs": [
    {
      "card_path": "/path/to/card.md",
      "additions": {
        "position_signal": {
          "topic_cluster": "pricing_strategy",
          "stance": "...",
          "reasoning": [...],
          "conditions": null,
          "confidence": "asserted",
          "emit_source": "refiner_inferred",
          "topic_assignment": "daemon_canonicalized"
        },
        "open_questions": [...],
        "reflection": {
          "status": "open_thread",
          "last_pass": "2026-04-28T12:00:00+00:00"
        }
      },
      "skipped_fields": ["position_signal"]
    }
  ]
}

additions lists fields that would be ADDED to frontmatter. skipped_fields lists fields already present in current frontmatter (would not be overwritten).

reflection is treated specially: it's daemon-managed metadata and is always added even if a prior reflection block exists (daemon refreshes last_pass on every run).


reflection_contradictions.json — stage 6 output (NOT YET)

Status: path reserved by daemon/state_paths.py, file not yet written. Stage 6 (LLM contradiction judgment) is a stub.

When implemented, schema will be:

{
  "generated_at": "...",
  "card_pairs": [
    {
      "card_a": "/path/to/early.md",
      "card_b": "/path/to/late.md",
      "topic_cluster": "pricing_strategy",
      "is_contradiction": true,
      "reasoning_diff": "...",
      "addressed_by": null
    }
  ]
}

check_consistency MCP tool will use this to filter from "all historical positions in cluster" down to "specifically contradicting positions" with explanation.

Until stage 6 ships, check_consistency returns full cluster positions and lets the host LLM (Claude / Cursor) judge contradiction in conversation — soft-mode default.


reflection_drift.json — stage 7 output (NOT YET)

Status: path reserved, file not yet written. Stage 7 (LLM drift segmentation) is a stub.

When implemented, schema will be:

{
  "generated_at": "...",
  "topics": {
    "pricing_strategy": {
      "phases": [
        {
          "phase_name": "value-based pre-PMF",
          "stance": "...",
          "reasoning": [...],
          "started": "2025-04-15",
          "ended": "2025-10-20",
          "card_paths": [...]
        }
      ],
      "drift_kind": "healthy_evolution"
    }
  }
}

get_position_drift MCP tool will use this to upgrade from "per-card trajectory" (current V1) to "per-phase trajectory" where each phase represents a coherent stance period with transition reasoning.


Inspecting state files

Three CLI helpers:

# Pretty summary of everything
python -m daemon.reflection_pass --inspect

# Per-card diagnostic — dumps ALL state's view of one card
python -m daemon.reflection_pass --explain "S:/obsidian/card.md"

# State directory directly (raw JSON):
ls $THROUGHLINE_STATE_DIR/
cat $THROUGHLINE_STATE_DIR/reflection_pass_state.json

--inspect reports stale-ness via humanized timestamps ("3h ago"), file sizes, per-stage counters, and sample entries.

--explain CARD_PATH is invaluable when an MCP tool returns a result you don't expect — it shows you exactly which state file has what about that card.


Where state files come from in the architecture

┌─────────────────────────────────────────────────────────────┐
│  vault/                                                      │
│  └── card.md  (frontmatter + body)                           │
└────────────────────┬────────────────────────────────────────┘
                     │ daemon walks + parses
┌─────────────────────────────────────────────────────────────┐
│  daemon/reflection_pass.py — 8-stage pass                    │
│    1. load                                                   │
│    1.5 reflectable filter (slice_id || managed_by)           │
│    2. cluster (bge-m3 via rag_server)                        │
│    3. cluster naming (LLM, opt-in) → cluster_names.json      │
│    4. back-fill (LLM, opt-in) → backfill_state.json          │
│    5. open-thread detection (no LLM) → open_threads.json     │
│    6. contradiction judgment (LLM, NOT YET) → contradictions.json
│    7. drift segmentation (LLM, NOT YET) → drift.json         │
│    8. writeback preview (no vault mutation) → writeback_preview.json
│       + writes positions.json (every-pass)                   │
│       + writes pass_state.json (every-pass watermark)        │
└────────────────────┬────────────────────────────────────────┘
                     │ MCP tools read state files
┌─────────────────────────────────────────────────────────────┐
│  mcp_server/tools/                                           │
│    find_open_threads     ← reads open_threads.json           │
│    check_consistency     ← reads positions.json              │
│                            (contradictions.json when stage 6) │
│    get_position_drift    ← reads positions.json              │
│                            (drift.json when stage 7)         │
└─────────────────────────────────────────────────────────────┘

State files are throughline's contract between offline LLM work (daemon) and realtime user-facing surfaces (MCP tools). LLM cost happens in the daemon, never on the MCP hot path.