Providers
Each provider is an adapter with two halves: a read-only parser over whatever that tool already wrote to disk, and a run surface that shells out to the tool’s own CLI (or, for opencode, its own HTTP server). threadle never writes into provider storage — see Trust.
From a terminal with the server up: threadle services (or health) prints process + every adapter; threadle providers is the adapters alone. See CLI.
| Provider | Storage (read-only) | Run / inject | Availability signal |
|---|---|---|---|
| Claude Code | ~/.claude/projects/<slug>/<uuid>.jsonl | claude CLI | ~/.claude/projects exists |
| opencode | ~/.local/share/opencode/opencode.db (SQLite) | opencode CLI + managed opencode serve | the db file exists |
| Cursor | ~/.cursor/projects/<slug>/agent-transcripts/… | agent CLI | storage dir exists or agent --version works |
Storage roots honor CLAUDE_CONFIG_DIR, OPENCODE_DATA_DIR, and CURSOR_CONFIG_DIR.
Parsers skip records they do not recognize instead of throwing. Every format here is undocumented and moves; a new message kind upstream must not break your session list.
Claude Code
Section titled “Claude Code”Storage. One JSONL file per session under ~/.claude/projects/<slug>/, where the slug is the project directory with / replaced by -. threadle also reads ~/.claude/history.jsonl (prompt index) and ~/.claude/sessions/*.json (live-status registry — each entry is confirmed with a kill(pid, 0) liveness probe, and stale files are ignored).
Parsing. Transcripts are streamed line by line; a partial or corrupt line is tolerated, because the file may be mid-append. A switch over the record type handles user, assistant, system (turn durations), ai-title, last-prompt, bridge-session, permission-mode, and file-history snapshots. Anything else falls through a default: break.
The on-disk order is not the conversation. Rewinds and compaction leave abandoned branches in the file, so the real thread is reconstructed by walking the parentUuid chain backwards from the last last-prompt leaf (with a cycle guard), falling back to file order when the leaf is missing. Blueprints and whole-session aggregation use a second path that keeps every branch.
Subagent turns (isSidechain) and meta records are excluded from the parent thread. Subagent runs live in their own files at <project>/<sessionId>/subagents/agent-*.jsonl with optional .meta.json sidecars, and appear as nodes with composite ids like <sessionId>/agent-<hex>.
Agents. Markdown files with YAML frontmatter (name, description, model): <projectDir>/.claude/agents/*.md first, then ~/.claude/agents/*.md. Plus three builtins — general-purpose, Explore, Plan.
Runs. The claude binary, resolved from PATH, always spawned with CLAUDE_CODE_ENTRYPOINT=threadle-inject so the runs are identifiable in your own logs.
| Mode | Argv shape |
|---|---|
| new session | --session-id <uuid> --append-system-prompt-file <ctx.md> -p <kickoff> --output-format json |
| resume-fork | --resume <id> --fork-session -p <prompt> --output-format json |
| agent run | --resume <id> or --session-id <uuid>, then -p <prompt>, --model, --output-format json |
| streaming | as above with --output-format stream-json --verbose |
Agent selection maps to --append-system-prompt-file (a subagent’s markdown, written to ~/.config/threadle/tmp), --permission-mode plan for Plan, or a read-only append-system-prompt for Explore. continue injection is not supported here — fork a session instead.
Quirks. The user-message count excludes array-content messages, so tool results do not inflate it. Per-model usage tracks the 1-hour cache-write share separately, because it bills at roughly twice the input rate rather than the 5-minute rate.
opencode
Section titled “opencode”Storage. A single SQLite database at ~/.local/share/opencode/opencode.db, opened readOnly: true. Sessions are rows; messages and parts are rows with a JSON data blob. Discovery is a SELECT, not a directory walk — top-level sessions are WHERE parent_id IS NULL, subagent runs are the children.
Schema churn. opencode’s schema moves, so threadle gates on it. At open time it reads pragma_table_info('session') and requires id, title, directory, parent_id, agent, time_updated. If any column is missing — or the database cannot be opened at all — it logs once and falls back to the CLI for every query:
opencode db --format json "<sql>"The direct connection is also retried up to three times on BUSY/LOCKED before dropping to the CLI. The fallback cannot bind parameters, so values are inlined with escaping; the SQL is always built internally, never from user input.
Parsing. Messages join their parts, ordered by creation time. Part types map as: text → text, reasoning → thinking, tool → a tool call plus its result, patch → a file-change marker. step-start/step-finish/compaction parts are consumed for token and cost accounting rather than rendered. Large tool outputs are spilled by opencode to tool-output/<id> files, which threadle dereferences and truncates at 64 000 characters.
Agents. Read from the managed server’s GET /agent when it is up; if that fails or is empty, opencode agent list is parsed instead.
Runs. opencode run --format json, with -s <sessionId> to continue, --agent, and -m <model>. New session ids are found by a bounded recursive scan of the JSON events for any value starting with ses_.
The managed server. For synthetic injection — writing context into a session without triggering a reply — threadle spawns opencode serve --port 0, scrapes the ephemeral URL from its output, and authenticates over HTTP Basic with a random per-process password passed via OPENCODE_SERVER_PASSWORD. The insert is POST /session/<id>/message with noReply: true and a synthetic text part. The child is single-flighted, killed on failure, killed on shutdown, and put on a 30-second cooldown after a failed start.
Quirks. Live status is a heuristic: any session row touched in the last 45 seconds is reported as running. opencode is the only provider whose reported cost is real money — it bills your own API keys, so it records what it actually spent.
Cursor
Section titled “Cursor”Storage. Transcripts at ~/.cursor/projects/<slug>/agent-transcripts/<uuid>/<uuid>.jsonl; chat metadata (title, cwd, timestamps) at ~/.cursor/chats/<hash>/<uuid>/meta.json. The metadata cwd wins over the slug when resolving the project directory. The model is not readable — Cursor keeps it in an encrypted store threadle does not touch.
Parsing. The transcript is read whole (these files are small) and parsed line by line, skipping bad lines. Records without a role are control frames like turn_ended and are skipped; blocks of unknown type are skipped. Message ids are synthetic (<file>.jsonl:<index>) because Cursor writes none.
Subagents. Cursor stores subagent runs as sibling transcripts with no parent link on disk. threadle reconstructs the tree heuristically: it extracts Task tool calls from parent transcripts, normalizes both the spawn prompt and each candidate child’s first user message (stripping <timestamp> and <user_query> wrappers, collapsing whitespace), and matches them within a project. Matched children are tagged linkedVia: "task-prompt" — a best-effort link, not a guarantee.
Agents. Three builtins, no filesystem scan: agent, plan, ask. The latter two run as agent --mode plan / --mode ask.
Runs. The binary is called agent, not cursor-agent. GUI-launched processes often lack ~/.local/bin on PATH, so threadle probes CURSOR_AGENT_PATH, then ~/.local/bin/agent, ~/.cursor/bin/agent, /usr/local/bin/agent, /opt/homebrew/bin/agent, before falling back to PATH.
agent -p --trust --workspace <projectDir> [--resume <id>] [--model <m>] <prompt>with --output-format json or stream-json appended. New sessions pin an id up front via agent create-chat and then --resume it, so the session is identifiable even if stdout parsing fails. Live status comes from agent persist list — any UUID in the output is marked running.
Usage. Cursor’s public JSONL carries no list-price usage, so token counts come from three tiers in order: the CLI’s own usage object captured after each run (cached in ~/.config/threadle/cursor-usage.json), turn_ended records in the transcript, and finally a character-count estimate. Which tier produced a number is recorded, so the UI can say so.
Billing honesty
Section titled “Billing honesty”Two numbers, never blended:
- Tracked cost — list price of the tokens used, from the models.dev price table.
- Actual spend — what you were actually billed.
Actual spend is $0 on subscription plans (Claude Pro/Max, Cursor) because the tokens are already paid for. opencode reports its real key spend. Claude’s billing mode is a setting — flip it to api and tracked cost becomes actual spend. Spend tripwires use tracked session dollars where available; they are a measurement, not a forecast.
When a provider is absent
Section titled “When a provider is absent”Availability is checked per provider and nothing cascades. A missing provider simply contributes no sessions and no agents; the rest of threadle works normally, and agent-less workflows (prompts, custom nodes, knots, gates) do not need any provider at all.
Storage and CLI are checked separately. Claude Code and opencode report available purely on their storage existing, so you can browse history on a machine where the CLI is gone — runs will then fail at spawn time with a clear error. Cursor accepts either signal. Version strings degrade to blank rather than failing, and a missing CLI during live-status polling yields an empty map, so sessions show as idle instead of erroring.
Related
Section titled “Related”- Agents & sessions — wiring providers onto the canvas
- CLI —
services/providers/agentsfrom the terminal - Trust model — the read-only guarantee in detail
- Install — prerequisites per provider