
Cass
- 23 installs
- 416 repo stars
- Updated August 5, 2026
- boshu2/agentops
cass is a skill that mines past agent sessions for working prompts, decisions, and patterns via the self-describing cass search binary.
About
cass is a skill that mines past agent sessions for working prompts, decisions, and patterns. A developer uses it for session archaeology, prior-art checks, and recovering context after a crash. It wraps the upstream self-describing cass binary and adds an operating doctrine for when and how to search history.
- Search past agent sessions for prompts, decisions, and patterns
- Semantic, keyword, or hybrid search over session history
- Discovery workflow: search, view, expand, then cluster related sessions
Cass by the numbers
- 23 all-time installs (skills.sh)
- Ranked #10,032 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
cass capabilities & compatibility
- Capabilities
- casr · cass memory · codebase briefing report
- Use cases
- research · web search · memory · orchestration
What cass says it does
Mine past agent sessions for working prompts, decisions, and patterns.
Your repeated prompts are your best prompts. If you typed it 10+ times, it works. Mine your history.
Prior-art check before inventing a new approach, plan, or prompt
npx skills add https://github.com/boshu2/agentops --skill cassAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 23 |
|---|---|
| repo stars | ★ 416 |
| Last updated | August 5, 2026 |
| Repository | boshu2/agentops ↗ |
What it does
Find a prompt or decision from an earlier agent session instead of reinventing it.
When should I use this skill?
You ask 'what did I ask?', 'find that prompt', or need session archaeology and agent history.
What you get
Prior prompts, rituals, and scope decisions are recovered from session history.
By the numbers
- fastembed model bundle ~90MB for semantic mode
- 5-step discovery workflow
Files
cass Session Search
Core Insight: Your repeated prompts are your best prompts. If you typed it 10+ times, it works. Mine your history.
cass is an upstream (Dicklesworthstone) tool and is self-describing — do not re-learn its surface from this skill. Discover it live:
cass capabilities --json # features/connectors/limits of the installed binary
cass introspect --json # full schema of every command + response
cass robot-docs guide|commands|examples|schemas|contracts # machine-targeted docsThis skill carries only the AgentOps operating doctrine: when to reach for cass, the discovery workflow, the recovery posture, and the anti-patterns we have actually hit.
When to Use
- "What did I ask last time?" / "find that prompt that worked" — session archaeology
- Prior-art check before inventing a new approach, plan, or prompt
- Scope archaeology: "when did we decide NOT to do X?"
- Post-context-loss recovery: what was searched for after a crash = what mattered
Folded triggers (ag-s43tg wave 1): casr + cass-memory route here
- `casr` → cross-harness resume. Cross Agent Session Resumer: convert and resume sessions across Claude Code, Codex, Gemini, and other providers —
cass resumeplus RESUME.md own this lane (resolve subagent logs to their parent viacass contextfirst; subagent files are not resumable). - `cass-memory` → `cm` procedural memory. Use when starting non-trivial work, mining lessons, or preventing repeated mistakes with cm procedural memory — mine past sessions here first, then promote the durable lessons through
cminstead of re-deriving them each session.
The Goldmine Principle
Your conversation history contains:
- Refined prompts — Every rephrase that worked better was captured
- Working rituals — Prompts repeated 10+ times ARE your methodology
- Scope decisions — "When did we decide NOT to do X?"
- Recovery moments — What you searched for after context loss = what mattered
The insight: Mining your past beats inventing new approaches. In the AgentOps loop the goal is prior-art first: mine as a research-phase move before writing a fresh plan or prompt, and feed what you find back into the corpus instead of re-deriving it.
THE EXACT PROMPT — Discovery Workflow
1. Bootstrap: Check health, refresh index, get project overview
cass status --json && cass index --json
cass search "*" --workspace /data/projects/PROJECT --aggregate agent,date --limit 1 --json
2. Find prompts: Search for keywords, filter to user prompts (lines 1-3)
cass search "KEYWORD" --workspace /data/projects/PROJECT --json --fields minimal --limit 50 \
| jq '[.hits[] | select(.line_number <= 3)]'
3. Follow hits: View the actual content
cass view /path/from/source_path.jsonl -n LINE -C 20
4. Expand context: See the full conversation flow
cass expand /path/from/source_path.jsonl --line LINE --context 3
5. Discover related: Find the whole work cluster
cass context /path/from/source_path.jsonl --jsonWhy it works: aggregations first (know the terrain), --fields minimal (5x smaller output), line_number <= 3 (user prompts live at the top), context clustering (one good hit → many related sessions). >10 matches for a prompt = a ritual; document and reuse it.
Operating Doctrine: Stale ≠ Broken
Three index states matter — never conflate them:
| State | Meaning | Do |
|---|---|---|
cass health exit 0 | Healthy | Search immediately |
stale (index.stale=true) | Usable but old | Search NOW; refresh in background with a wall-clock cap: ( timeout 600 cass index --json &>/tmp/cass-bg.log </dev/null & ) — NEVER a bare &, cass index can hang |
broken (database.exists=false or documents=0) | Truly uninitialized | cass doctor --fix --json, then cass index --full --json |
The trap: treating stale as broken triggers an unneeded 8–25s full rebuild when a 1–3s incremental (or a stale-but-correct query) would do. scripts/recover.sh implements the full decision tree with timeouts. Detailed symptom→fix tables (issue #196 hang, stale locks, database is busy race, etc.): RECOVERY.md, OBSERVABILITY.md, PITFALLS.md.
Version Pinning
cass evolves quickly; the released binary may lack HEAD features. When a flag returns "unrecognized", do not guess — probe: cass capabilities --json and cass introspect --json | jq '.commands[].name', and check cass --version.
Anti-Patterns (Don't Do These)
| Anti-pattern | Why it's wrong | Do instead |
|---|---|---|
| Asking the user "should I rebuild the index?" | They have agents waiting; rebuild is safe and idempotent | Just run cass doctor --fix --json (preserves source data) |
Running cass index --full whenever status says unhealthy | A 25s rebuild for a 30-min stale index is wasteful | Check index.stale separately from database.exists; prefer incremental |
Running bare cass to "see what's there" | Launches blocking TUI in the agent's session | Always --json or --robot; never bare |
Piping cass export into head/jq | Broken-pipe panic on large sessions | cass export ... -o /tmp/x.json first, then operate on the file |
| Treating subagent files as parent sessions | Subagents are separate logs with their own line-2 prompt; also NOT resumable | Filter by `select(.source_path \ |
Using --limit 0 for "no limit" | Earlier cass panics | Use a real limit (--limit 50); --limit 1 minimum for aggregations |
Trusting 0 hits with --workspace /X | Workspace strings are case- and trailing-slash-sensitive | Re-run with --aggregate workspace --limit 1 to discover the canonical key |
Skipping --fields minimal on wide scans | ~3KB per hit × 100 hits = 300KB context burn | --fields minimal for wide passes; upgrade to summary/full for keepers |
Reading session files with cat | Loads the full conversation into context | cass view PATH -n LINE -C 5 or cass expand PATH --line LINE --context 3 |
| Re-indexing on every search | Index is shared across processes | Refresh only when status says stale |
Falling back to manual find/grep when cass misbehaves | Recovery is autonomous; skipping cass loses the corpus | Walk the recovery tree in RECOVERY.md. One real exception: terms inside tool stdout/stderr are skipped at index time — there rg -n "TERM" /path.jsonl is correct |
Long-form versions with mined evidence: ANTI_PATTERNS.md.
Safety Boundaries
Pre-authorized (rebuilds derived index data only, never destroys source sessions): cass doctor --fix --json, cass index --full --force-rebuild --json, cass sources doctor/sync, cass models install/verify.
Do NOT without explicit permission: delete core.NNNNN coredumps, delete .beads/, git reset --hard, or hand-edit ~/.config/cass/sources.toml — the CLI commands above already do everything safely. Never run bare cass (blocking TUI) inside an agent loop.
Reference Index
| Need | Reference |
|---|---|
| Full command reference | COMMANDS.md |
| Workflow recipes | RECIPES.md |
| jq patterns | PATTERNS.md |
| Pitfalls & fixes | PITFALLS.md |
| Session file formats | SESSION_FORMATS.md |
| Remote sources, multi-machine search | REMOTE_SOURCES.md |
| Semantic / hybrid / models | SEMANTIC_AND_HYBRID.md |
| Token / tool / model analytics | ANALYTICS.md |
| Cross-harness session resume | RESUME.md |
| Doctor + autonomous recovery | RECOVERY.md |
| Mined gold-standard prompts | PROMPTS.md |
| Anti-patterns (long form) | ANTI_PATTERNS.md |
| Health vs status vs index nuance | OBSERVABILITY.md |
| Pages encrypted archive + HTML export | PAGES_AND_EXPORT.md |
Harness exclusion (disabled_agents) | HARNESS_EXCLUSION.md |
| Schema introspection contracts | INTROSPECTION.md |
When the right reference isn't obvious from titles, grep -ni "SYMPTOM" references/*.md — cheaper than loading whole files into context.
Scripts
Scripts live under scripts/. They execute, never load — zero context tokens. None mutate state without explicit confirmation.
| Script | Usage |
|---|---|
./scripts/quick_analysis.sh /path | One-command project overview (status → aggregate agent/date → top prompts) |
./scripts/prompt_miner.py --workspace /path | Find repeated prompts (ritual detection) |
./scripts/validate.sh | Validate cass install + skill structure |
./scripts/recover.sh | Autonomous recovery decision tree (READY → STALE_BUT_USABLE → BROKEN); wraps every cass index in timeout |
./scripts/multi_machine_search.sh "QUERY" [host…] | Parallel fan-out across the fleet; merges + dedups hits |
Validation
# Quick health check
cass status --json | jq '.index.fresh'
# Should return: trueIf false, run: cass index --json
Token, Tool & Model Analytics
One-liner: cass already has every Claude/Codex/Gemini API call you've made. cass analytics rolls them up into per-day, per-tool, per-model usage tables — no separate billing pipeline needed.Contents
- Five Subcommands
- Status First
- Token Usage Reports
- Tool Usage
- Model Usage
- Rebuild Strategy
- Validation
- Pitfalls
---
Five Subcommands
cass analytics status --json # Coverage + freshness of rollup tables
cass analytics tokens --json # Token usage time-series + dim breakdowns
cass analytics tools --json # Per-tool invocation counts
cass analytics models --json # Top models + coverage stats
cass analytics rebuild --json # Backfill / rebuild rollup tables
cass analytics validate --json # Drift detection between raw rows and aggregates---
Status First
cass analytics status --json | jq '.data'Look for:
coverage.api_token_coverage_pct— % of messages with real API token data (vs estimate)coverage.estimate_only_pct— inverse; <10% is healthycoverage.message_metrics_coverage_pct— % with full per-message statsdrift.signals— empty array means rollups match raw datadrift.track_a_fresh/track_b_fresh— are both rollup tracks currentrecommended_action— "none" or specific ("rebuild","validate")
If track_a_fresh=false or coverage.api_token_coverage_pct < 90, run:
cass analytics rebuild --json | jq '.data.summary'---
Token Usage Reports
--group-by is time-only (hour|day|week|month). To slice by agent or model, use the dedicated subcommands or filter flags.
# Last 30 days, daily buckets
cass analytics tokens --days 30 --group-by day --json | jq '.data.buckets'
# Hour granularity for the past day
cass analytics tokens --days 1 --group-by hour --json | jq '.data.buckets'
# Filter to one agent (slug from cass capabilities --json | jq '.connectors')
cass analytics tokens --days 30 --agent claude_code --json | jq '.data.totals'
# Specific date range
cass analytics tokens --since 2026-04-01 --until 2026-04-22 --jsonReal Output Shape
{ "data": {
"bucket_count": 30,
"buckets": [{
"bucket": "2026-04-22",
"counts": {"message_count": 4374, "user_message_count": 74, "assistant_message_count": 0, "tool_call_count": 681, "plan_message_count": 0},
"content_tokens": {"est_total": 45724, "est_user": 612, "est_assistant": 0},
"api_tokens": {"total": 145060939, "input": 1670, "output": 160042, "cache_read": 143693750, "cache_creation": 1205477, "thinking": 0},
"plan_tokens": {"content_est_total": 0, "api_total": 0},
"coverage": {"api_coverage_message_count": 1052, "api_coverage_pct": 24.05},
"derived": {"api_tokens_per_assistant_msg": null, "tool_calls_per_1k_api_tokens": 0.0047, ...}
}, ...],
"totals": {...}
}}Note: input/output/cache_read/cache_creation live under .api_tokens, not flat at the row level. cache_read typically dwarfs input for active prompt-caching workloads — count it when estimating spend.
Cost Estimation Pattern (per-day across all models)
# Rough $ across all activity at Sonnet-4 list prices
# (cache_read is ~10% the price of input; check current Anthropic pricing)
cass analytics tokens --days 30 --group-by day --json \
| jq '.data.buckets[] | {
day: .bucket,
input_M: (.api_tokens.input // 0) / 1e6,
output_M: (.api_tokens.output // 0) / 1e6,
cache_read_M: (.api_tokens.cache_read // 0) / 1e6
}'For per-model spend, use cass analytics models --json (next section); tokens does not break down by model. For Claude Max / GPT Pro flat-rate accounts, the dollar number is irrelevant — what matters is throughput per account (use caam to plan account allocation).
---
Tool Usage
cass analytics tools --days 30 --json | jq '.data.rows[0:20]'Returns rows with: key (agent slug), tool_call_count, message_count, api_tokens_total, tool_calls_per_1k_api_tokens, tool_calls_per_1k_content_tokens. Note: rows are keyed by agent, not by tool name — this is "tool-use intensity per agent." Useful for:
- Finding which tools dominate your workflow
- Detecting newly broken tools (sudden error spike)
- Justifying which
--allowedToolsto pre-approve in.claude/settings.json
---
Model Usage
cass analytics models --json \
| jq '.data.by_api_tokens.rows[0:10] | map({model: .key, tokens: .value, msgs: .message_count, derived})'Returns under .data.by_api_tokens.rows[] with: key (model name), value (api_total tokens), message_count, derived{api_coverage_pct, tool_calls_per_1k_api_tokens, plan_message_pct}. There's also .data.timeseries.buckets[] for time-aware model usage.
Spot patterns like "Haiku is doing 80% of the work but Opus is doing all the spending" — then tune your skill triggers accordingly.
---
Rebuild Strategy
After bulk operations that shuffle data:
# After cass sources sync
cass sources sync --source css --json
cass analytics rebuild --json
# After cass import chatgpt
cass import chatgpt /path/to/conversations.json --json
cass analytics rebuild --json
# After a long indexing campaign
cass index --full --force-rebuild --json
cass analytics rebuild --jsonrebuild accepts --since/--until/--days to scope the backfill window, plus --agent, --workspace, and --source local|remote|all|<host> filters. There's no --force flag — to fully recompute, use a wide window like --days 9999. cass analytics validate --json afterward confirms invariants.
---
Validation
cass analytics validate --json | jq '.data.invariants'Checks:
- Sum(daily) == raw_messages_count for each day
- Token totals match cross-track
- No orphan rollup rows for missing conversations
If signals is non-empty, run analytics rebuild --force and re-validate.
---
Pitfalls
- Wrong field path is the #1 mistake.
tokensreturns.data.buckets[],toolsreturns.data.rows[],modelsreturns.data.by_api_tokens.rows[]. They are not consistent — always probe withjq 'keys'first when scripting against a new subcommand. - No `--group-by model` / `--group-by agent`. Time-only enum (
hour|day|week|month). For per-model breakdowns usecass analytics models; for per-agent usecass analytics tools(rows are keyed by agent slug despite the name). - Analytics rollups are derived data.
cass doctor --fixdoes NOT rebuild analytics rollups — only the lexical/FTS index. To repair analytics, usecass analytics rebuild --json. - Coverage <90% usually means legacy sessions before the agent emitted token-usage events. Estimates fill the gap; rebuild can't recover what was never recorded.
cass analytics tokensreads from the rollup tables; it won't reflect sessions added in the last few minutes until the next rollup tick. Runcass analytics rebuild --days 1for a fresh view.
Anti-Patterns (Long Form)
Why this exists: Mined sessions show the same wasteful behaviors repeating across hundreds of agent runs. Each item below has a real-world cost; the "instead" is the move that actually works.
Contents
- 1. Asking the User to Do What You're Authorized To Do
- 2. Dropping the Whole Index
- 3. Treating "Stale" as "Broken"
- 4. `--limit 0` for "no limit"
- 5. Piping `cass export` Into Anything
- 6. Searching Without a Workspace, Then Filtering Client-Side
- 7. Workspace Path Drift
- 8. Searching for Tool Output
- 9. Default Fields on Wide Scans
- 10. Ignoring `_meta` and `_warning`
- 11. Bare `cass`
- 12. Running `cass index --full` Every Loop
- Summary
---
1. Asking the User to Do What You're Authorized To Do
Bad:
"Your cass index is stale. Could you run cass index --full and let me know when it's done?"Why bad: The user has 22 agents waiting. Every "could you" multiplies their interrupt cost.
Instead:
cass doctor --fix --json # safe by default; preserves all source data
cass index --json & # background refresh while you proceedThen proceed and mention the rebuild ran in passing.
Authorization scope: Anything under ~/.local/share/coding-agent-search/ is yours to manage. Source session files (~/.claude/projects/, ~/.codex/sessions/) are NOT — those are user data.
---
2. Dropping the Whole Index
Bad:
rm -rf ~/.local/share/coding-agent-search
cass index --full --json # rebuild from scratchWhy bad: Throws away 4M+ messages of indexed history. Rebuild from raw sessions takes 25min on a typical fleet. Loses analytics rollups (no easy recovery).
Instead:
cass doctor --fix --json # rebuilds only what's brokendoctor --fix already backs up corrupt DBs, so you never lose data even when the original is bad.
---
3. Treating "Stale" as "Broken"
Bad: Seeing cass status say healthy: false, recommended_action: "Run cass index" and immediately running cass index --full --force-rebuild (25s blocking).
Why bad: The index is stale only because it's older than the threshold (default 30 min). The data is still correct — it just doesn't have sessions from the last 30 min indexed yet.
Instead:
state=$(cass status --json | jq -r '.index | "\(.fresh)/\(.stale)/\(.documents // "N")"')
case "$state" in
true/*) echo "fresh — search now" ;;
false/true/*) echo "stale but usable"
# ALWAYS wrap bg cass index in `timeout` — without it, a hung
# rebuild silently strands forever. See scripts/recover.sh.
( timeout 600 cass index --json >/tmp/cass-bg.$$.log 2>&1 </dev/null & ) 2>/dev/null ;;
*/*/0|*null) echo "broken" && timeout 60 cass doctor --fix --json ;;
esacThe agent's first search returns immediately on the still-correct stale index. The background cass index finishes in 1–3s for incremental refreshes.
---
4. --limit 0 for "no limit"
Bad: cass search "X" --limit 0 --json (worked in earlier cass versions; no-ops or returns RAM-capped result in v0.3+, panicked before).
Why bad: Unbounded scans burn context. Even with the modern RAM cap, you get random truncation.
Instead: Pick a real number.
- Aggregations:
--limit 1and parse.aggregations.*only - Wide scans:
--limit 50+--fields minimal+ iterate via--cursor - Sampling:
--limit 5 --fields summary
---
5. Piping cass export Into Anything
Bad:
cass export /path.jsonl --format json | jq '.[0:50]'Why bad: Large exports trigger broken-pipe panic when the consumer closes early.
Instead:
cass export /path.jsonl --format json --include-tools -o /tmp/export.json
jq '.[0:50]' /tmp/export.jsonAlways -o. The file is cheap; the panic isn't.
---
6. Searching Without a Workspace, Then Filtering Client-Side
Bad:
cass search "auth" --json --limit 500 \
| jq '[.hits[] | select(.workspace == "/data/projects/myrepo")]'Why bad: Server returned 500 hits, you keep maybe 50. 10x context waste.
Instead:
cass search "auth" --workspace /data/projects/myrepo --json --limit 50Server-side filtering is free. Client-side filtering is paid in tokens.
---
7. Workspace Path Drift
Bad: cass search "X" --workspace /data/projects/myrepo/ — note trailing slash. Returns 0 hits. You assume the corpus is empty.
Why bad: Workspace strings are case-sensitive and trailing-slash-sensitive. The canonical key may be /data/projects/myrepo (no slash) or different case.
Instead: Probe first.
cass search "X" --aggregate workspace --limit 1 --json \
| jq '.aggregations.workspace.buckets[] | select(.key | contains("myrepo"))'Use the exact key from the bucket.
---
8. Searching for Tool Output
Bad: Looking for the exact bytes of a Bash tool's stdout via cass search. Finds nothing. Concludes the session doesn't exist.
Why bad: cass deliberately skips large tool outputs at index time to keep the corpus searchable on prompts and replies. Tool outputs are still in the source file.
Instead:
cass search "near-by user-prompt phrase" --json --fields minimal --limit 10 \
| jq -r '.hits[0].source_path' | xargs rg -n "the exact tool output bytes"Find the session via prompt, then rg for the bytes inside.
---
9. Default Fields on Wide Scans
Bad: cass search "X" --json --limit 100 (no --fields). Returns ~3KB per hit × 100 = 300KB of context. With a 200K context budget, you've burned 1.5% on one tool call.
Instead:
cass search "X" --json --fields minimal --limit 100 # ~60KB totalUpgrade to --fields summary only for the few hits you decide to inspect.
---
10. Ignoring _meta and _warning
Bad: Reading .hits and reporting confidently — without checking that the index was fresh, the search wasn't truncated, and no fallback happened.
Instead:
cass search "X" --robot-meta --json | jq '{
warning: ._warning,
fresh: ._meta.index_freshness.fresh,
fallback: ._meta.fallback_mode,
clamped: ._meta.hits_clamped,
total: .total_matches,
shown: .count
}'If _warning is non-null, mention it. If hits_clamped: true, paginate. If fallback_mode != null, your --mode hybrid actually ran lexical-only.
---
11. Bare cass
Bad: Running cass with no args inside an agent session. Launches the interactive TUI, blocks the agent's terminal, requires the user to Ctrl+C.
Instead: Always use --json or --robot. If you genuinely need TUI semantics from automation, cass tui --once --asciicast /tmp/snap.cast renders a single frame and exits.
---
12. Running cass index --full Every Loop
Bad: Pre-flight in a tight loop runs cass index --full --json every iteration. 25s × 60 iter = 25 min wasted.
Instead:
# Once at startup
cass status --json | jq -e '.index.fresh' >/dev/null || cass index --json
# Or use watch mode and never refresh inline
cass index --watch --json & # one daemon, all agents share the index---
Summary
The pattern across these anti-patterns: lack of trust in cass. Trust the autonomous-recovery commands. Trust the safe-by-default doctor --fix. Trust the stale-but-correct lexical index. Trust the server to filter. Then your agent stops bothering the user.
cass Command Reference
Freedom Key: LOW = exact syntax required | MEDIUM = some flexibility | HIGH = multiple approaches
Contents
- [Lifecycle Commands [LOW freedom]](#lifecycle-commands-low-freedom)
- [Search Command [MEDIUM freedom]](#search-command-medium-freedom)
- [View & Expand [LOW freedom]](#view--expand-low-freedom)
- [Export [LOW freedom]](#export-low-freedom)
- [Context [MEDIUM freedom]](#context-medium-freedom)
- [Timeline [MEDIUM freedom]](#timeline-medium-freedom)
- [Pagination [LOW freedom]](#pagination-low-freedom)
- [Chained Searches [MEDIUM freedom]](#chained-searches-medium-freedom)
- Output Schemas
- Exit Codes
---
Lifecycle Commands [LOW freedom]
cass status --json # Health check — is index current?
cass index --json # Incremental refresh (fast, use first)
cass index --full --json # Full rebuild (when stale)
cass capabilities --json # What this install supports
cass diag --json # Detailed diagnostics
cass doctor # Repair (safe, won't delete sources)Status Output
{
"database": {"conversations": 4827, "messages": 664027},
"index": {"fresh": true, "stale": false},
"recommended_action": "Index is up to date"
}---
Search Command [MEDIUM freedom]
Basic Form
cass search "QUERY" --workspace /path --json --fields minimal --limit NEvery search needs: --json, --fields minimal, --limit N (N > 0)
Filtering
| Filter | Example |
|---|---|
| By workspace | --workspace /data/projects/foo |
| By agent | --agent claude_code |
| By mode | --mode lexical (default), semantic, hybrid |
| From session list | --sessions-from /tmp/sessions.txt |
Output Control
| Flag | Effect | Token Impact |
|---|---|---|
--fields minimal | source_path, line_number, agent | 5x smaller |
--fields summary | + title, score | 2x smaller |
--max-content-length N | Truncate snippets | Reduces size |
--max-tokens N | Soft token budget | Caps output |
Aggregations [LOW freedom]
cass search "*" --workspace /path --aggregate agent,date --limit 1 --jsonMUST use `--limit 1` (not 0). Ignore .hits, parse .aggregations only.
{
"aggregations": {
"agent": {"buckets": [{"key": "claude_code", "count": 925}]},
"date": {"buckets": [{"key": "2026-01-16", "count": 112}]}
}
}Available: agent, date, workspace
Robot Formats
| Flag | Output | Use Case |
|---|---|---|
--robot-format sessions | Session paths only | Chaining searches |
--robot-format jsonl | Streaming NDJSON | Large result sets |
--robot-meta | Include _meta block | Check freshness |
Debug
cass search "QUERY" --workspace /path --dry-run --explain---
View & Expand [LOW freedom]
View (Line-Oriented)
cass view /path/to/session.jsonl -n LINE -C CONTEXT| Flag | Meaning |
|---|---|
-n LINE | Center on this line |
-C N | N lines before AND after |
-A N | N lines after only |
-B N | N lines before only |
Expand (Message-Oriented)
cass expand /path/to/session.jsonl --line LINE --context NUse `expand` for conversation flow, `view` for raw debugging.
---
Export [LOW freedom]
cass export /path.jsonl --format json --include-tools -o /tmp/output.jsonCRITICAL: Always export to file first. Piping causes broken pipe panic.
# WRONG (may panic):
cass export /path.jsonl --format json | head -100
# RIGHT:
cass export /path.jsonl --format json -o /tmp/out.json
jq '.[0:100]' /tmp/out.json| Flag | Effect |
|---|---|
--format json | Machine parsing |
--format markdown | Human review |
--include-tools | Include tool calls (hidden by default) |
-o FILE | Output file (required) |
---
Context [MEDIUM freedom]
cass context /path/to/session.jsonl --jsonFinds related sessions: same workspace, same day, same agent.
{
"related_sessions": [...],
"same_workspace": 12,
"same_day": 8,
"same_agent": 15
}---
Timeline [MEDIUM freedom]
cass timeline --since 2026-01-14 --until 2026-01-17 --workspace /path --json
cass timeline --since 7d --jsonPrefer aggregations — more predictable JSON:
cass search "*" --workspace /path --aggregate date --limit 1 --json---
Pagination [LOW freedom]
# First page
cass search "KEYWORD" --json --robot-meta --limit 50 --request-id run-1
# Next page (use _meta.next_cursor)
cass search "KEYWORD" --json --robot-meta --limit 50 --cursor "eyJ..." --request-id run-1b---
Chained Searches [MEDIUM freedom]
# Step 1: Get sessions matching coarse filter
cass search "COARSE" --workspace /path --robot-format sessions > /tmp/sessions.txt
# Step 2: Search within only those
cass search "SPECIFIC" --sessions-from /tmp/sessions.txt --json --fields minimal
# Or via pipe:
cass search "COARSE" --robot-format sessions | cass search "SPECIFIC" --sessions-from ----
Output Schemas
Search Response
{
"hits": [
{
"source_path": "/path/to/session.jsonl",
"line_number": 42,
"agent": "claude_code",
"title": "First few words...",
"score": 15.234,
"created_at": "2026-01-16T10:30:00Z"
}
],
"total_matches": 125,
"aggregations": {...}
}Field Presets
| Preset | Fields |
|---|---|
minimal | source_path, line_number, agent |
summary | source_path, line_number, agent, title, score |
full | All fields including content |
---
Exit Codes
| Code | Meaning | Action |
|---|---|---|
0 | Success | Continue |
1 | Error (index, query) | Check cass status, re-index |
2 | Invalid args, special chars | Quote query, simplify |
Persistent Harness / Connector Exclusion
One-liner: When a connector (e.g.,openclaw,chatgpt) floods the index with low-value or looped sessions, persistently exclude it viacass sources agents. The setting survives across runs.
Contents
- The Three Subcommands
- Connector Slugs
- What Exclude Actually Does
- When to Use
- Verifying
- Manual sources.toml Edit (Fallback)
- Pitfalls
---
The Three Subcommands
cass sources agents list --json
cass sources agents exclude <agent-slug>
cass sources agents exclude <agent-slug> --keep-indexed-data
cass sources agents include <agent-slug>---
Connector Slugs
cass capabilities --json | jq '.connectors' returns the canonical list. As of v0.3.6:
codex, claude_code, gemini, clawdbot, vibe, opencode, amp,
cline, aider, cursor, chatgpt, pi_agent, factory, openclaw,
kimi, copilot, copilot_cli, qwen, crushUse those exact slugs. Aliases (claude → claude_code) are accepted but get normalized.
---
What Exclude Actually Does
cass sources agents exclude openclaw:
1. Writes disabled_agents = ["openclaw"] to ~/.config/cass/sources.toml (creating the file if absent) 2. Halts indexing of openclaw sessions on future scans, syncs, and watch cycles 3. By default purges already-indexed openclaw data from the local archive and rebuilds lexical search → reclaims space immediately 4. With --keep-indexed-data: leaves prior data alone, only blocks future ingestion
The setting survives upgrades, restarts, and machine reboots. To unset: cass sources agents include openclaw.
---
When to Use
| Trigger | Action |
|---|---|
| One harness is producing 80% of the index volume with low-value content | exclude it |
| openclaw / vibe / experimental agent is looping | exclude immediately |
| You only care about Claude+Codex on this machine | exclude everything else |
| You're temporarily debugging that harness | exclude --keep-indexed-data so a re-include restores quickly |
---
Verifying
# Current exclusions
cass sources agents list --json | jq '.disabled_agents'
# Confirm new sessions from excluded harness are skipped
cass index --json 2>&1 | grep -i "skipping excluded"
# Confirm searches no longer return excluded-agent hits
cass search "*" --aggregate agent --limit 1 --json | jq '.aggregations.agent.buckets'
# excluded slugs should be absent---
Manual sources.toml Edit (Fallback)
The cass sources agents subcommand landed in commit 82d8d70e (2026-04-20). It is not present in the v0.3.6 release binary but is in source HEAD. If cass sources agents errors with "unrecognized subcommand 'agents'", you're on a build older than that commit — edit the config directly:
# ~/.config/cass/sources.toml
disabled_agents = ["openclaw", "vibe"]
[[sources]]
name = "..."
# ...Then trigger a one-off cleanup of already-indexed data:
cass index --full --force-rebuild --json # rebuilds excluding the disabled listTo check what version added the subcommand:
cd /dp/coding_agent_session_search && git log --oneline --all -- src/lib.rs | grep -i "agents\|disabled_agents" | head -5---
Pitfalls
disabled_agentsis case-sensitive in normalization. The CLI normalizes; manual edits should use lowercase slugs.- Excluding an agent does not delete the source files on disk — only the indexed copies. The user can always re-include and re-index without data loss.
- If you exclude a harness whose sessions live in an indexed remote source (
cass sources sync), the remote sessions are still rsync'd to disk; only the indexing step skips them. Usecass sources remove --purgeto stop syncing entirely. - The default purge-on-exclude triggers a lexical rebuild. On big corpora that's a 25s blocking step. Pass
--keep-indexed-dataif you want exclude to be instant.
Schema Introspection & Robot-Mode Contracts
One-liner: cass is fully self-describing. Every command, flag, response, and error code is queryable at runtime. Use this when the skill or your memory is uncertain.
Contents
- The Discovery Trinity
- introspect Schema
- capabilities Schema
- Robot Output Conventions
- Robot-Format Output Modes
- Response `_meta` Fields
- Agent Self-Configuration Pattern
---
The Discovery Trinity
cass --robot-help # Top-level machine help
cass robot-docs <topic> # Topic-scoped docs
cass introspect --json # Full schema dump (commands + responses)
cass capabilities --json # Static features + limitsrobot-docs Topics
guide — Quickstart for automation
commands — Every subcommand + arg
examples — Copy-paste workflows
schemas — Auto-generated response schemas
contracts — Output stream conventions (stdout=data, stderr=diag)If a topic returns "Could not parse arguments", that topic is unsupported in your installed cass version.
---
introspect Schema
cass introspect --json | jq '{
api_version,
contract_version,
commands: .commands | map(.name),
responses: .response_schemas | keys
}'Returns:
commands[]— every command withname,description,arguments[],has_json_outputarguments[]per command —name,description,arg_type(flag/option),value_type,required,default,enum_values,repeatableresponse_schemas— declared field shapes for every JSON-emitting commandglobal_flags— top-level flags valid for all subcommands
This is the source of truth — newer than any handwritten skill.
Programmatic Discovery
# What commands have a JSON response?
cass introspect --json | jq '.commands[] | select(.has_json_output) | .name'
# Find commands that accept --workspace
cass introspect --json | jq '.commands[] | select(.arguments[]?.name == "workspace") | .name'
# What enum values does --mode accept?
cass introspect --json \
| jq '.commands[] | select(.name == "search") | .arguments[] | select(.name == "mode") | .enum_values'---
capabilities Schema
{
"crate_version": "0.3.6",
"api_version": 1,
"contract_version": "1",
"features": [
"json_output", "jsonl_output", "robot_meta", "time_filters",
"field_selection", "content_truncation", "aggregations",
"wildcard_fallback", "timeout", "cursor_pagination",
"request_id", "dry_run", "query_explain", "view_command",
"status_command", "state_command", "api_version_command",
"introspect_command", "export_command", "expand_command",
"timeline_command", "highlight_matches"
],
"connectors": [...],
"limits": {
"max_limit": 0,
"max_content_length": 0,
"max_fields": 50,
"max_agg_buckets": 10
}
}max_limit: 0 means no enforced cap (RAM-clamped at runtime). max_*: 0 consistently means "uncapped".
Version-Aware Patterns
features[] enumerates capabilities; output formats are NOT in features[] — they live in each subcommand's per-arg enum. --robot-format is per-subcommand, not global (global_flags only contains db, robot-help, trace-file, quiet, verbose, color, progress, wrap, nowrap).
# Probe a feature (real entry in features[])
HAS_AGG=$(cass capabilities --json | jq -r '.features | index("aggregations") != null')
# Probe a robot-format value for `cass search` specifically
cass introspect --json \
| jq -r '.commands[]
| select(.name=="search")
| .arguments[]
| select(.name=="robot-format")
| .enum_values[]'
# json jsonl compact sessions toon---
Robot Output Conventions
stdout = data only (parseable by --json)
stderr = diagnostics, progress events, warnings
exit 0 = success
exit 1 = recoverable error (retry with different args)
exit 2 = invalid args / contract violationAlways 2>/dev/null when you need clean JSON on stdout, unless you're consuming progress events from stderr (during indexing).
---
Robot-Format Output Modes
--robot-format json # default; pretty-printed
--robot-format jsonl # one event per line; streams big result sets
--robot-format compact # single-line JSON; smallest token cost
--robot-format sessions # one source_path per line; pipe into `cass search --sessions-from -`
--robot-format toon # token-optimized object notation (saves ~30% vs JSON)Chained Searches via sessions format
# Step 1: get sessions matching a coarse filter
cass search "auth" --workspace /repo --robot-format sessions > /tmp/auth-sessions.txt
# Step 2: search within only those sessions
cass search "JWT" --sessions-from /tmp/auth-sessions.txt --json --fields minimal
# Or pipe directly:
cass search "auth" --robot-format sessions | cass search "JWT" --sessions-from -Two-pass is a 10x speedup vs one big query when your filter is narrow.
---
Response _meta Fields
When you pass --robot-meta, every search response gets:
"_meta": {
"elapsed_ms": 42,
"wildcard_fallback": false,
"cache_stats": {"hits": 12, "misses": 3, "shortfall": 0},
"tokens_estimated": 850,
"max_tokens": 1200,
"next_cursor": "eyJ...",
"hits_clamped": false,
"fallback_mode": null, // "lexical" if --mode hybrid degraded
"index_freshness": {
"fresh": true,
"age_seconds": 122,
"stale": false,
"last_indexed_at": "2026-04-22T19:45:06Z",
"pending_sessions": 0
},
"state": {"index": "...", "database": "..."},
"request_id": "run-1"
}_warning (top-level) is set when the index is stale enough to cast doubt on results. Always check it; surface to the user verbatim.
---
Agent Self-Configuration Pattern
Use introspection to auto-tune behavior to whatever cass version is installed:
caps=$(cass capabilities --json)
intro=$(cass introspect --json)
VERSION=$(jq -r '.crate_version' <<< "$caps")
HAS_HYBRID=$(jq -r '.features | index("hybrid_search") != null' <<< "$caps")
SEARCH_FIELDS=$(jq -r '.commands[] | select(.name=="search") | .arguments[] | select(.name=="fields") | .description' <<< "$intro")
echo "Running cass $VERSION; hybrid=$HAS_HYBRID; fields-help=$SEARCH_FIELDS"This way a single skill file works across cass versions — the agent adapts.
Health, Status, and Index Freshness
One-liner: cass exposes three overlapping health surfaces. Knowing which to use prevents the #1 mistake (treating "stale" as "broken").
Contents
- The Four Commands
- Status Schema (the one you'll actually parse)
- The Three States, Read Off Status
- NDJSON Progress on stderr
- Liveness Stack
- Capabilities Self-Check
---
The Four Commands
| Command | Latency | Output | Best For |
|---|---|---|---|
cass health | <50ms | Exit code 0/1 | Pre-flight gating in hooks/cron |
cass status --json | ~100-700ms | Full JSON | Branching agent logic |
cass diag --json | ~500ms-2s | Path/size diagnostics | Bug reports, deep triage |
cass capabilities --json | <10ms | Static feature list | Version-aware fallbacks |
status and state are aliases for the same command.
---
Status Schema (the one you'll actually parse)
{
"status": "healthy|unhealthy|rebuilding|initializing",
"healthy": true,
"initialized": true,
"explanation": "...", // null when healthy
"recommended_action": "...", // null when nothing to do
"index": {
"exists": true,
"status": "fresh|stale|missing|rebuilding",
"fresh": true,
"stale": false,
"age_seconds": 1234,
"stale_threshold_seconds": 1800,
"rebuilding": false,
"documents": 51214,
"fingerprint": {
"current_db_fingerprint": "content-v1:51214:51214:4711459",
"checkpoint_fingerprint": "content-v1:51214:51214:4711459",
"matches_current_db_fingerprint": true
},
"checkpoint": {
"present": true,
"completed": true,
"db_matches": true,
"schema_matches": true,
"page_size_compatible": true
}
},
"database": {
"exists": true,
"opened": true,
"conversations": 4827,
"messages": 664027,
"open_error": null,
"open_retryable": false,
"counts_skipped": false
},
"pending": {
"sessions": 0,
"watch_active": true,
"orphaned": false
},
"rebuild": {
"active": true,
"orphaned": false,
"pid": 3472773,
"mode": "incremental",
"job_id": "lexical_refresh-...",
"job_kind": "lexical_refresh",
"phase": "indexing",
"started_at": "2026-04-22T20:18:45Z",
"updated_at": "2026-04-22T20:21:09Z",
"processed_conversations": 12,
"total_conversations": 145,
"indexed_docs": 410
},
"semantic": {
"status": "missing|partial|installed",
"available": false,
"can_search": false,
"fallback_mode": "lexical",
"preferred_backend": "fastembed",
"embedder_id": "minilm-384",
"hint": "Run 'cass models install'..."
},
// active_index appears ONLY while a rebuild is running.
// For the always-present pid/phase, read .rebuild instead.
"active_index": {
"pid": 3472773,
"data_dir": "/home/.../coding-agent-search",
"db_path": "/home/.../agent_search.db",
"started_at": "2026-04-22T20:18:45.804+00:00",
"job_id": "lexical_refresh-...",
"job_kind": "lexical_refresh",
"phase": "index"
}
}---
The Three States, Read Off Status
# Fresh & ready
.healthy=true && .index.fresh=true
# Stale-but-usable (most common — DON'T panic)
.healthy=false && .index.stale=true && .database.exists=true && .database.messages > 0
# Truly broken
.database.exists=false # never indexed
OR .database.open_error != null
OR .index.documents=0 && .database.messages > 0
OR .index.fingerprint.matches_current_db_fingerprint=falseDecision Function (Bash)
cass_classify() {
local s=$(cass status --json)
local fresh=$(echo "$s" | jq -r '.index.fresh')
local db_exists=$(echo "$s" | jq -r '.database.exists')
local docs=$(echo "$s" | jq -r '.index.documents // 0')
local msgs=$(echo "$s" | jq -r '.database.messages // 0')
if [ "$fresh" = "true" ]; then
echo "READY"
elif [ "$db_exists" = "true" ] && [ "$msgs" != "0" ] && [ "$docs" != "0" ]; then
echo "STALE_BUT_USABLE"
else
echo "BROKEN"
fi
}---
NDJSON Progress on stderr
When you run cass index --json, stderr streams progress events:
{"event":"started","mode":"incremental","full":false}
{"event":"phase","phase":"preparing","elapsed_ms":6000}
{"event":"phase","phase":"indexing","total":145,"current":12,"elapsed_ms":12000,"rate_per_sec":1.0,"eta_seconds":133}
{"event":"completed","conversations":145,"elapsed_ms":25000}Tune the cadence: --progress-interval-ms 1000 (clamped 250–60000). Disable: --no-progress-events or CASS_INDEX_NO_PROGRESS_EVENTS=1.
This is how you detect issue #196 (stuck indexing): if current doesn't advance for >30s of progress events, kill and retry with --full --force-rebuild.
---
Liveness Stack
Layer 1 — The Trinity:
cass health --json # 50ms, exit-code only
cass status --json # ~500ms, structured JSON
cass diag --json # ~1-2s, paths/sizes/diskLayer 2 — The Process:
# .rebuild is always present (active=false when idle); .active_index appears only during a run
cass status --json | jq '{rebuild, active_index: (.active_index // null)}'
PID=$(cass status --json | jq -r '.rebuild.pid // empty')
[ -n "$PID" ] && ps -p "$PID" || echo "no rebuild running"Layer 3 — Observability hooks:
cass --trace-file /tmp/cass-trace.jsonl search "X" --json # span timing---
Capabilities Self-Check
Before issuing a flag your agent isn't sure exists, gate on capabilities:
HAS_HYBRID=$(cass capabilities --json | jq -r '.features | index("hybrid_search") != null')
if [ "$HAS_HYBRID" = "true" ]; then
cass search "X" --mode hybrid --json
else
cass search "X" --json # fall back to lexical
fifeatures[] enumerates: json_output, jsonl_output, robot_meta, time_filters, field_selection, content_truncation, aggregations, wildcard_fallback, timeout, cursor_pagination, request_id, dry_run, query_explain, view_command, status_command, state_command, api_version_command, introspect_command, export_command, expand_command, timeline_command, highlight_matches, ....
Limits live in limits{max_limit, max_content_length, max_fields, max_agg_buckets}. max_limit=0 means "no enforced cap" (clamped at runtime by RAM).
Encrypted Archives & HTML Export
One-liner:cass export-htmlmakes one shareable, optionally-password-protected HTML file.cass pagesmakes a fully-encrypted searchable archive that can be hosted on GitHub Pages with no server.
Contents
- HTML Export (One Session, Easy)
- Pages — Encrypted Searchable Archive
- Disaster Recovery
- When to Use Which
- Pitfalls
---
HTML Export (One Session, Easy)
# Plain HTML
cass export-html /path/to/session.jsonl -o /tmp/session.html
# Password-protected (AES-256-GCM, PBKDF2 600k iter)
cass export-html /path/to/session.jsonl -o /tmp/session.html --password "use-strong-passwords"
# Read password from stdin to keep it out of shell history
read -rs PW && cass export-html /path/to/session.jsonl -o /tmp/session.html --password-stdin <<< "$PW"The output is a single self-contained HTML file:
- Inlined CSS / JS, opens offline
- Tailwind + Prism enhanced via CDN when online (graceful degrade)
- Markdown rendering, syntax highlighting, role-colored bubbles
Use case: hand a teammate "the conversation that solved X" as one file, no installation needed.
---
Pages — Encrypted Searchable Archive (Many Sessions, Static Hosting)
cass pages encrypt ~/.local/share/coding-agent-search/agent_search.db \
--output /tmp/cass-archive \
--with-recoveryThis produces a directory:
cass-archive/
├── config.json # key slots, payload metadata
├── payload/
│ ├── chunk-00000.bin # AES-256-GCM encrypted, content-addressed
│ ├── chunk-00001.bin
│ └── ...
├── search/ # client-side search index
└── viewer/ # static HTML+JS viewerDrop the whole directory under any static host (GitHub Pages, S3, Netlify). Visitors authenticate in the browser with the password (or recovery key) and get a fully searchable view of the corpus.
Key Architecture
| Layer | Crypto |
|---|---|
| Per-slot KEK from password | Argon2id (64MB, 3 iter, parallelism 4) |
| Per-slot KEK from recovery secret | HKDF-SHA256 |
| Wrapped DEK | AES-256-GCM |
| Payload chunks | AES-256-GCM, per-chunk nonce |
Multi-Slot Operations
cass pages key list --archive ./archive
cass pages key add-password --archive ./archive
cass pages key add-recovery --archive ./archive
cass pages key revoke --archive ./archive --slot 1
cass pages key rotate --archive ./archive --keep-recovery
cass pages key show-recovery --archive ./archive --qr # printable backupConstraints:
- Cannot revoke the only remaining slot
- Cannot revoke the slot you're authenticating with
- Revoked slot IDs are never reused
Verification
cass pages verify --archive ./archive --check-integrityValidates that all files in config.json exist and SHA-256 hashes match integrity.json.
---
Disaster Recovery
| Scenario | Move |
|---|---|
| Forgot password, have recovery key | cass pages decrypt ./archive --recovery |
Corrupted config.json | Restore from backup (no backup = unrecoverable) |
| Corrupt payload chunks | cass pages verify --archive ./archive to identify; restore from backup |
| Need to share access | cass pages key add-password (requires existing auth first) |
Full recovery procedures: see source-of-truth at /dp/coding_agent_session_search/docs/RECOVERY.md.
---
When to Use Which
| Goal | Tool |
|---|---|
| Share one conversation | cass export-html --password |
| Publish a redacted corpus on GitHub Pages | cass pages encrypt --with-recovery |
| Internal team archive | cass pages + corp SSO at the storage layer |
| Estate-planning backup of work history | cass pages + recovery key in safe deposit box |
| Quick markdown handoff | cass export FILE --format markdown -o /tmp/x.md (no encryption) |
---
Pitfalls
cass pages encryptindexes the entire DB. For a 4M-message corpus expect 5–15 min and ~1.5x DB size in chunks.- The static viewer requires JavaScript and ~50ms of in-browser key derivation per session load — slow on low-end devices.
- Recovery keys provide full access. Treat them like the password — print + safe deposit box, not in email.
- Never use
--passwordwith the literal password on the command line in a shared shell — use--password-stdinor password manager integration. Shell history leaks. cass export(markdown/json) is plaintext. Usecass export-html --passwordif confidentiality matters.
jq Extraction Patterns
Copy-paste reference for parsing cass output and raw session files.
Contents
- Quick Reference Card
- cass Search Output
- Pattern Detection
- Raw Session File Parsing
- Tool Call Extraction
- Subagent Prompt Extraction
- Safe Access Patterns
- Composite Recipes
- Debugging jq
- One-Liners for Common Tasks
---
Quick Reference Card
| Goal | Pattern |
|---|---|
| User prompts (lines 1-5) | select(.line_number <= 3) |
| Subagent sessions | contains("subagent") |
| Total match count | .total_matches |
| Safe access with default | // [] or // "default" |
| Sort descending | sort_by(-.field) |
| Group and count | `group_by(.) \ |
| Claude Code user message | .type == "user" |
| Codex/Gemini user message | .role == "user" |
| Tool call | .type == "tool_use" |
---
cass Search Output
Basic Hit Extraction
# First 5 hits
| jq '.hits[0:5]'
# Just source paths (for follow-up)
| jq '.hits[].source_path' -r
# Path, line number, title
| jq '[.hits[] | {path: .source_path, line: .line_number, title: .title[0:80]}]'
# Total match count
| jq '.total_matches'User Prompt Extraction (The Most Important Pattern)
User prompts appear at lines 1-5. Filter by line_number:
# Get user prompts only
| jq '[.hits[] | select(.line_number <= 3)]'
# With formatted output
| jq '[.hits[] | select(.line_number <= 3)] | .[] | {path: .source_path, line: .line_number, title: .title[0:80]}'
# Just titles (for scanning)
| jq '[.hits[] | select(.line_number <= 3) | .title]'
# Count user prompts
| jq '[.hits[] | select(.line_number <= 3)] | length'Subagent Session Extraction
# Find subagent sessions
| jq '[.hits[] | select(.source_path | contains("subagent"))]'
# Just paths (unique)
| jq '[.hits[] | select(.source_path | contains("subagent"))] | .[].source_path' -r | sort -u
# User prompts in subagent sessions
| jq '[.hits[] | select(.source_path | contains("subagent")) | select(.line_number <= 3)]'Aggregation Parsing
# Agent breakdown
cass search "*" --workspace /path --aggregate agent --limit 1 --json \
| jq '.aggregations.agent.buckets'
# Date breakdown
cass search "*" --workspace /path --aggregate date --limit 1 --json \
| jq '.aggregations.date.buckets'
# Formatted timeline
| jq '.aggregations.date.buckets | sort_by(.key) | .[] | "\(.key): \(.count) hits"' -r
# Multiple aggregations
| jq '{agents: .aggregations.agent.buckets, dates: .aggregations.date.buckets}'---
Pattern Detection
Find Repeated Prompts (Ritual Detection)
# Group prompts by title, count, sort by frequency
cass search "*" --workspace /path --json --limit 500 \
| jq '[.hits[] | select(.line_number <= 3) | .title[0:80]] | group_by(.) | map({prompt: .[0], count: length}) | sort_by(-.count) | .[0:20]'Filter by Count Threshold
# Only patterns appearing 5+ times
| jq '[.hits[] | select(.line_number <= 3) | .title[0:80]] | group_by(.) | map({prompt: .[0], count: length}) | map(select(.count >= 5)) | sort_by(-.count)'Check Total Matches (Is It a Ritual?)
cass search "First read ALL" --workspace /path --json --limit 100 | jq '.total_matches'
# > 10 = ritual, document it
# < 3 = one-off, ignore---
Raw Session File Parsing
Claude Code Format
# Extract user messages
jq 'select(.type == "user") | .message.content' session.jsonl
# Handle content arrays (common)
jq 'select(.type == "user") | .message.content | if type == "array" then [.[] | select(.type == "text") | .text] | join(" ") else . end' session.jsonl
# With timestamps
jq 'select(.type == "user") | {ts: .timestamp, content: .message.content}' session.jsonl
# First user prompt only (the ritual opener)
jq -s '[.[] | select(.type == "user")][0] | .message.content' session.jsonl
# All user messages sorted
jq -s '[.[] | select(.type == "user")] | sort_by(.timestamp)' session.jsonlCodex/Gemini Format
# Extract user messages
jq 'select(.role == "user") | .content' session.jsonl
# With timestamp
jq 'select(.role == "user") | {ts: (.timestamp // .created_at), content}' session.jsonl
# First user prompt
jq -s '[.[] | select(.role == "user")][0] | .content' session.jsonlDetect Format and Extract
# Check format
head -1 session.jsonl | jq -e '.type == "user"' && echo "claude_code"
head -1 session.jsonl | jq -e '.role == "user"' && echo "codex"---
Tool Call Extraction
Find Tool Usage in Claude Code
# All tool calls
jq 'select(.type == "assistant") | .message.content[] | select(.type == "tool_use") | {name, input}' session.jsonl
# Specific tool (e.g., Write)
jq 'select(.type == "assistant") | .message.content[] | select(.type == "tool_use" and .name == "Write")' session.jsonl
# Tool results
jq 'select(.type == "tool_result")' session.jsonl
# Count tool calls by type
jq -s '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | .name] | group_by(.) | map({tool: .[0], count: length}) | sort_by(-.count)' session.jsonl---
Subagent Prompt Extraction
Subagent logs have THE prompt at line 2:
# View line 2 via cass
cass view /path/to/subagents/agent-XXXXX.jsonl -n 2 -C 1
# Extract with sed + jq
sed -n '2p' /path/to/subagents/agent-XXXXX.jsonl | jq '.message.content'
# Extract with jq slurp
jq -s '.[1].message.content' /path/to/subagents/agent-XXXXX.jsonl---
Safe Access Patterns
Avoid null Errors
# With default
| jq '.hits // []'
| jq '.aggregations.agent.buckets // []'
| jq '.total_matches // 0'
# Check before access
| jq 'if .hits | length == 0 then "no results" else .hits[0:5] end'
# Safe iterate
| jq '(.hits // [])[]'Check JSON Structure
# Top-level keys
| jq 'keys'
# First hit structure
| jq '.hits[0] | keys'
# Check field exists
| jq '.hits[0] | has("source_path")'---
Composite Recipes
Full Prompt Mining Pipeline
# 1. Search all sessions
cass search "*" --workspace /path --json --limit 500 \
| jq '
[.hits[] | select(.line_number <= 3) | .title[0:80]]
| group_by(.)
| map({prompt: .[0], count: length})
| sort_by(-.count)
| map(select(.count >= 2))
| .[0:30]
'Extract Conversation Flow from Session
jq -s '[.[] | {
type: (.type // .role),
ts: (.timestamp // .created_at),
preview: (if .message then .message.content else .content end | tostring[0:100])
}]' session.jsonlFind Sessions with Specific Tool Usage
cass search "Write" --workspace /path --json --fields minimal --limit 50 \
| jq '[.hits[] | .source_path] | unique'Complete Source Path → First Prompt Pipeline
# 1. Get source paths
cass search "KEYWORD" --workspace /path --json --fields minimal --limit 20 \
| jq '.hits[].source_path' -r > /tmp/paths.txt
# 2. Extract first prompt from each
while read path; do
echo "=== $path ==="
jq -s '[.[] | select(.type == "user")][0] | .message.content[0:200]' "$path" 2>/dev/null
done < /tmp/paths.txt---
Debugging jq
The Golden Rule: Simplify Rather Than Debug
When a complex jq command fails silently or returns nothing:
Don't: Spend time debugging the complex filter. Do: Simplify to basics, verify data exists, then rebuild.
# Complex filter fails silently:
| jq '[.hits[] | select(.line_number <= 3 and .source_path | contains("subagent"))] | ...'
# No output, no error. Now what?
# SIMPLIFY FIRST:
| jq '.hits | length' # Do we have hits at all?
| jq '.hits[0]' # What does a hit look like?
| jq '.hits[0] | keys' # What fields exist?
# THEN rebuild step by stepWhy this works: The JSON structure varies slightly between cass versions. Complex filters compound errors. Simple filters reveal the actual structure.
Build Up Incrementally
# Start simple
| jq '.hits[0:5]'
# Add projection
| jq '[.hits[] | {path: .source_path, line: .line_number}]'
# Add filter
| jq '[.hits[] | select(.line_number <= 3)]'
# Combine (last)
| jq '[.hits[] | select(.line_number <= 3) | select(.source_path | contains("subagent"))]'Check Intermediate Counts
| jq '.hits | length' # Total hits
| jq '[.hits[] | select(.line_number <= 3)] | length' # After line filter
| jq '[.hits[] | select(.source_path | contains("subagent"))] | length' # After path filter---
One-Liners for Common Tasks
| Task | One-Liner |
|---|---|
| User prompt titles | `jq '[.hits[] \ |
| Source paths only | jq '.hits[].source_path' -r |
| Agent counts | jq '.aggregations.agent.buckets' |
| Date counts | jq '.aggregations.date.buckets' |
| First hit details | jq '.hits[0]' |
| Total matches | jq '.total_matches' |
| Unique titles | `jq '[.hits[].title] \ |
| Subagent paths | `jq '[.hits[] \ |
cass Pitfalls & Troubleshooting
Quick lookup: Ctrl+F for your error message or symptom.
Contents
- Quick Diagnosis
- Index Problems
- Query Errors
- Output Problems
- Content Not Found
- jq Problems
- Diagnostic Workflow
- Exit Codes
- Pro Tips
---
Quick Diagnosis
| Symptom | Likely Cause | Fix |
|---|---|---|
| 0 results but content exists | Workspace path mismatch | Use --aggregate workspace to find exact path |
--limit 0 panic | Invalid limit | Always use --limit 1 minimum |
| Exit code 2 | Special characters in query | Quote query or use nearby anchor |
| Broken pipe on export | Piping large output | Export to file first with -o |
| Tool calls hidden | Missing flag | Add --include-tools to export |
| Stale results | Index needs refresh | Run cass index --json |
| jq returns null | Wrong field name | Use jq 'keys' to check structure |
| Can see in file, cass finds nothing | Content not indexed | Fallback to rg on raw file |
---
Index Problems
"Index is stale" / Missing Recent Sessions
cass status --json # Check state
cass index --json # Incremental refresh (try first)
cass index --full --json # Full rebuild (if still stale)Index Shows 0 Conversations
# Find session files
find ~/.claude/projects -name "*.jsonl" | head -5
# Full rebuild
cass index --full --json
# Check capabilities
cass capabilities --jsonDatabase Corruption
cass doctor # Safe repair — won't delete source sessions
cass index --full --json---
Query Errors
--limit 0 Panic
# WRONG:
cass search "*" --workspace /path --aggregate agent --limit 0 --json
# RIGHT:
cass search "*" --workspace /path --aggregate agent --limit 1 --jsonWorkspace Path Mismatch (0 Results)
Paths must match exactly — case-sensitive, no trailing slash.
# Discover exact path:
cass search "your_keyword" --aggregate workspace --jsonSpecial Characters Fail
Problem chars: --, |, ", ', *, ?, \
# Quote the term
cass search '"--fields"' --workspace /path --json
# Use nearby anchor without special chars
cass search "fields minimal" --workspace /path --json
# Escape in double-quotes
cass search "\"role\":\"user\"" --workspace /path --jsonQuery With Leading Dashes Fails
# Wrap in quotes
cass search '"--workspace"' --workspace /path --json
# Or search without dashes
cass search "workspace /data" --workspace /path --json---
Output Problems
Export Piping Causes Broken Pipe Panic
# WRONG (may panic):
cass export /path.jsonl --format json | head -100
# RIGHT:
cass export /path.jsonl --format json -o /tmp/out.json
jq '.[0:100]' /tmp/out.jsonOutput Too Large
--fields minimal # 5x smaller
--fields summary # 2x smaller
--max-content-length 200 # Truncate snippets
--max-tokens 1200 # Soft cap
--aggregate date,agent # Counts only---
Content Not Found
String Exists But cass Returns 0 Hits
cass doesn't index everything. Large tool outputs (stdout/stderr) are skipped.
# Fallback to direct grep:
rg -n "the exact string" /path/from/source_path.jsonlSubagent Content Not Found
Subagent sessions are separate files.
cass search "KEYWORD" --workspace /path --json --fields minimal \
| jq '[.hits[] | select(.source_path | contains("subagent"))]'---
jq Problems
jq Returns null
| jq 'keys' # Top-level keys
| jq '.hits[0] | keys' # Hit structure
| jq '.hits // []' # Safe access with defaultComplex Filter Fails Silently
Simplify rather than debug:
| jq '.hits | length' # Do we have hits?
| jq '.hits[0]' # What does a hit look like?
| jq '.hits[0] | keys' # What fields exist?---
Diagnostic Workflow
When cass isn't working:
# 1. Health check
cass status --json
# 2. Refresh index
cass index --json
# 3. Diagnostics
cass diag --json
# 4. Simple query test
cass search "*" --workspace /path --limit 5 --json
# 5. Check workspace paths
cass search "test" --aggregate workspace --json
# 6. Fallback to raw grep
rg -n "exact string" /path/to/session.jsonlTimestamp Reality Check
cass search "*" --workspace /path --aggregate date --limit 1 --json \
| jq '.aggregations.date.buckets | sort_by(.key) | reverse | .[0]'
# Should show today's date if you've been working today---
Exit Codes
| Code | Meaning | Action |
|---|---|---|
0 | Success | Continue |
1 | Error (index, query) | Check cass status, review query |
2 | Invalid args, special chars | Quote query, simplify, check flags |
---
Pro Tips
Parallel Searches Find More
Lexical matching is literal. Different phrasings = different hits.
cass search "cass search" --workspace /path --json --fields minimal &
cass search "aggregate" --workspace /path --json --fields minimal &
cass search "line_number" --workspace /path --json --fields minimal &
waitTogether = full coverage.
Mined Gold-Standard Prompts
Source: Real prompts from this user's session corpus that have been re-used 5+ times. These are tested, working entry points to cass — copy/paste, then adapt the keyword.
Contents
- Discovery Openers
- Compile-A-File Prompts (Aggregation Tasks)
- Subagent Mining (Line 2 = THE Prompt)
- Cross-Machine Recall
- "What Worked Last Time?"
- Decision Archaeology
- Ritual Detection
- Cost & Usage Reports
- Sentinel Phrases (Triggers for /cass)
- Anti-Templates
---
Discovery Openers
read AGENTS.md and use /cass to find the session history with codex for this projectUse /cass to search session history for context on issues that were closed but never actually fixed.i distinctly recall making a project called <NAME> ; can you look on /cass for what happened with itread AGENTS.md ; /cass-session-search use cass to see how I used <TOOL> for <PROJECT>Reread AGENTS.md so it's still fresh in your mind. Use /cass to search the project sessions history for ...---
Compile-A-File Prompts (Aggregation Tasks)
read AGENTS.md. I need you to use /cass to compile a file LIST_OF_INPUT_MESSAGES.md that contains all the user prompts for project X, in chronological order.Use cass to extract every "first read ALL of AGENTS.md" prompt across this workspace and group by week.---
Subagent Mining (Line 2 = THE Prompt)
Use cass to find all subagent sessions in the last 30 days where the prompt mentions "deep dive" — give me the line-2 text from each.Implementation:
cass search "deep dive" --workspace /path --json --fields minimal --limit 50 \
| jq -r '[.hits[] | select(.source_path | contains("subagent"))] | unique_by(.source_path) | .[].source_path' \
| xargs -I{} sh -c 'echo "=== {} ==="; sed -n "2p" "{}" | jq -r ".message.content"' ---
Cross-Machine Recall
Search cass on css, csd, ts1, and ts2 for any mention of <KEYWORD> and dedup by source_path.(For execution see SKILL.md → "Cross-Machine Search" or the REMOTE_SOURCES.md reference, Approach C — load it explicitly with the Read tool when you need the full recipe.)
---
"What Worked Last Time?"
Use cass to find the most-recent successful run of <TASK> and resume that session.HIT=$(cass search "<TASK>" --workspace /repo --json --fields summary --limit 1 \
| jq -r '.hits[0].source_path')
cass resume "$HIT" --shell---
Decision Archaeology
When did we decide NOT to support <X>? Use cass with terms like "EXCLUDE", "out of scope", "skip for now".Find the earliest session where we discussed adopting <LIBRARY>, and the conversation that finalized the choice.---
Ritual Detection
What prompts have I used 10+ times across all my agent sessions? Surface the top 20.cass search "*" --workspace /path --json --limit 500 \
| jq '[.hits[] | select(.line_number <= 3) | .title[0:80]]
| group_by(.) | map({prompt: .[0], count: length})
| sort_by(-.count) | .[0:20]'---
Cost & Usage Reports
What did I spend on Claude API across all projects last month? Break down by model.# Per-model token totals (`cass analytics tokens` is time-only; use `models` for per-model)
cass analytics models --json | jq '.data.by_api_tokens.rows[0:10]'Which agent is doing most of the tool-calling? Pull the top 10 over the last 60 days.---
Sentinel Phrases (Triggers for /cass)
These literal phrases are reliable triggers for the skill in this user's vocabulary:
- "Use /cass to ..."
- "use cass to find ..."
- "look on /cass for ..."
- "session history" + "<TASK>"
- "find that prompt"
- "what did I ask"
- "scope archaeology"
- "what worked last time"
When you hear any of these, jump straight to the Two-Step Bootstrap and start mining.
---
Anti-Templates
These prompts trigger /cass but produce poor results — rewrite them before executing:
| Bad prompt | Why bad | Better |
|---|---|---|
| "Search cass for everything about X" | unbounded; will return 10k hits | Add --workspace /repo and --days 30 |
| "Find all sessions" | no filter; useless | Pick a keyword OR an aggregate (--aggregate agent,date) |
| "What's in the index?" | not actionable | cass status --json + cass search "*" --aggregate workspace --limit 1 --json |
| "Re-extract all my prompts" | duplicates work cass already does | `cass search "*" --json --limit 500 \ |
Workflow Recipes
Priority: Your prompts first. They're replicable. Agent execution varies.
Contents
| Recipe | When |
|---|---|
| Session Bootstrap | Start of every cass session |
| Ritual Discovery | Find reusable prompts |
| User Prompt Extraction | What did I ask? |
| Subagent Mining | Find extraction prompts |
| Scope Archaeology | When did we decide X? |
| Multi-Agent Analysis | Who did what? |
| Timeline Construction | What happened when? |
| Session Clustering | Find related work |
| Context Recovery | Where did forgotten context resurface? |
| Artifact Origin Tracing | When was this doc created? |
| Meta-Pattern: cass-on-cass | Find which cass queries worked |
| Full Example | End-to-end workflow demo |
---
Session Bootstrap
Always start here:
# 1. Health check
cass status --json
# 2. Refresh index
cass index --json
# 3. Project overview
cass search "*" --workspace /data/projects/PROJECT --aggregate agent,date --limit 1 --json---
Ritual Discovery
Goal: Find prompts you used repeatedly (these work).
# Count suspected ritual
cass search "First read ALL" --workspace /path --json --limit 100 | jq '.total_matches'
# > 10 = RITUAL — document it
# Find most repeated prompts
cass search "*" --workspace /path --json --limit 500 \
| jq '[.hits[] | select(.line_number <= 3) | .title[0:80]] | group_by(.) | map({prompt: .[0], count: length}) | sort_by(-.count) | .[0:20]'Common Rituals to Search
| Pattern | Purpose |
|---|---|
"First read ALL" | Context loading |
"read AGENTS.md" | Project rules |
"comprehensive deep dive" | Thorough analysis |
"think super hard" | Quality mode |
"ultrathink" | Quality mode |
"extract all" | Data extraction |
---
User Prompt Extraction
Goal: Find what you asked, in what order.
# User prompts mention keyword (lines 1-3)
cass search "KEYWORD" --workspace /path --json --limit 100 \
| jq '[.hits[] | select(.line_number <= 3)] | .[] | {path: .source_path, line: .line_number, title: .title[0:80]}'
# Count user prompts with term
cass search "KEYWORD" --workspace /path --json --limit 100 \
| jq '[.hits[] | select(.line_number <= 3)] | length'
# View actual prompt
cass view /path/from/hit.jsonl -n 1 -C 5---
Subagent Mining
Goal: Extract deep dive prompts — line 2 of subagent logs is THE prompt.
# Find subagent sessions
cass search "deep dive" --workspace /path --json --fields minimal \
| jq '[.hits[] | select(.source_path | contains("subagent"))] | .[].source_path' -r | sort -u
# View the prompt (line 2)
cass view /path/to/subagents/agent-XXXXX.jsonl -n 2 -C 1
# Extract just the text
sed -n '2p' /path/to/subagents/agent-XXXXX.jsonl | jq '.message.content'Subagent Structure
Line 1: Metadata
Line 2: THE PROMPT (gold — copy-paste ready)
Line 3+: Execution---
Scope Archaeology
Goal: Find where scope decisions were made.
Exclusion Decisions
cass search "EXCLUDE" --workspace /path --json --limit 50
cass search "NOT porting" --workspace /path --json --limit 50
cass search "skip for now" --workspace /path --json --limit 50
cass search "out of scope" --workspace /path --json --limit 50Inclusion Decisions
cass search "we DO need" --workspace /path --json --limit 50
cass search "must include" --workspace /path --json --limit 50
cass search "actually necessary" --workspace /path --json --limit 50Scope Reduction
cass search "less invasive" --workspace /path --json --limit 50
cass search "simplify" --workspace /path --json --limit 50
cass search "reduce scope" --workspace /path --json --limit 50---
Multi-Agent Analysis
Goal: Understand which agent did which work.
# Overview by agent
cass search "*" --workspace /path --aggregate agent --limit 1 --json \
| jq '.aggregations.agent.buckets'
# Search within specific agent
cass search "KEYWORD" --workspace /path --agent claude_code --json --limit 50
cass search "KEYWORD" --workspace /path --agent codex --json --limit 50
# Compare activity over time
cass search "*" --workspace /path --agent claude_code --aggregate date --limit 1 --jsonAgent Patterns
| Agent | Typical Work |
|---|---|
| Claude Code (Opus) | Complex reasoning, architecture, specs |
| Codex | Fast extraction, high-volume, code gen |
| Gemini | Research, varied tasks |
---
Timeline Construction
Goal: Build chronological understanding of work.
Method 1: Date Aggregation (Recommended)
cass search "*" --workspace /path --aggregate date --limit 1 --json \
| jq '.aggregations.date.buckets | sort_by(.key) | .[] | "\(.key): \(.count) hits"' -rMethod 2: Timeline Command
cass timeline --since 2026-01-14 --until 2026-01-17 --workspace /path --json
cass timeline --since 7d --jsonNote: Aggregations usually have simpler, more predictable JSON.
Method 3: Manual Grouping
cass search "*" --workspace /path --json --limit 200 \
| jq '[.hits[] | {date: .created_at[0:10], path: .source_path}] | group_by(.date) | .[] | {date: .[0].date, count: length}'---
Session Clustering
Goal: Find all related work from one good hit.
# 1. Find one relevant session
cass search "KEYWORD" --workspace /path --json --fields summary --limit 5
# Get source_path
# 2. Discover related
cass context /path/from/hit.jsonl --json
# 3. Iterate over related_sessionsWhy: Work happens in clusters. One good session → whole cluster.
---
Context Recovery
Goal: Find where forgotten context was recovered (reveals what mattered).
cass search "we already DID" --workspace /path --json --limit 50
cass search "wait we already" --workspace /path --json --limit 50
cass search "I think we discussed" --workspace /path --json --limit 50
cass search "earlier session" --workspace /path --json --limit 50
cass search "use cass to find" --workspace /path --json --limit 50---
Artifact Origin Tracing
Goal: Find when/how specific documents were created.
Find References to Spec Documents
cass search "PLAN_TO_PORT_" --workspace /path --json --fields summary
cass search "EXISTING_" --workspace /path --json --fields summary
cass search "PROPOSED_ARCHITECTURE" --workspace /path --json --fields summaryFind Creation Prompts
cass search "create a spec" --workspace /path --json
cass search "document this" --workspace /path --json
cass search "write to file" --workspace /path --json---
Meta-Pattern: cass-on-cass
Goal: Find which cass queries worked.
cass search "cass search" --workspace /path --json --fields minimal
cass search "aggregate" --workspace /path --json --fields minimalInsight: Queries that appear multiple times = queries that worked.
---
Full Example
# 1. Health
cass status --json
# 2. Overview (925 sessions)
cass search "*" --workspace /data/projects/beads_rust --aggregate agent --limit 1 --json
# 3. Find ritual opener
cass search "First read ALL of AGENTS.md" --workspace /data/projects/beads_rust --json --limit 100 \
| jq '.total_matches'
# Result: 50+ = RITUAL
# 4. Extract ritual
cass view $(cass search "First read ALL" --workspace /data/projects/beads_rust --json --limit 1 | jq -r '.hits[0].source_path') -n 1 -C 5
# 5. Find scope decisions
cass search "EXCLUDE" --workspace /data/projects/beads_rust --json --limit 50
# 6. Discover related
cass context /path/to/interesting/session.jsonl --jsonDoctor & Autonomous Recovery
The contract: cass doctor --fix --json is safe by default. It rebuilds derived data (Tantivy index, FTS table) from source SQLite — it never deletes source session files. Use it without asking the user.Contents
- When to Run Each Doctor Mode
- Output Schema (top-level keys, no `.summary` wrapper)
- Real-World Recovery Recipes
- What `--fix` Does NOT Do
- Disk Cleanup (ALWAYS ask first)
- Pre-Flight Hook Pattern
---
When to Run Each Doctor Mode
cass doctor --json # Read-only diagnosis
cass doctor --json --verbose # Show passed checks too
cass doctor --fix --json # Apply safe rebuilds (USE THIS)
cass doctor --fix --force-rebuild --json # Same + force index rebuild even if healthy--fix runs a 7-step protocol:
1. Data directory — Create if missing 2. Stale lock files — Remove .index.lock if older than 1h 3. Database open + quick_check — Backup to .corrupt.<ts> and rebuild if quick_check fails 4. FTS table — Verify fts_messages is queryable via frankensqlite 5. Tantivy index — Rebuild from SQLite if empty/missing/stale 6. Config files — Validate config.toml and sources.toml parse 7. Session directories — Detect ~/.claude, ~/.codex, etc. for visibility
Backup format: agent_search.db.corrupt.20260315_154822_759 (sortable timestamp). Corruption salvage is preserved for forensic review.
---
Output Schema (top-level keys, no .summary wrapper)
{
"status": "healthy|unhealthy",
"healthy": true,
"initialized": true,
"explanation": null,
"recommended_action": null,
"needs_rebuild": false,
"issues_found": 0,
"issues_fixed": 0,
"warnings": [],
"failures": [],
"auto_fix_applied": false,
"auto_fix_actions": [],
"checks": [
{"name": "database", "status": "pass|warn|fail",
"message": "...", "fix_available": true, "fix_applied": false},
...
],
"_meta": {...}
}Parse failures[] (top-level array of failed check names) for blocking issues. Anything in auto_fix_actions happened automatically. Do not look for a `.summary` key — it doesn't exist.
cass doctor --fix --json | jq '{
ok: .healthy,
issues_found, issues_fixed,
applied: .auto_fix_actions,
failed: [.checks[] | select(.status=="fail") | .name]
}'---
Real-World Recovery Recipes
Index empty but DB has rows
# Symptom
cass status --json | jq '.database.messages, .index.documents'
# 664027 0
# Fix
cass doctor --fix --json | jq '.summary.auto_fix_actions'
# ["Rebuilt search index from database"]Database file unreadable
# Symptom: cass status returns counts_skipped=true and open_error
cass status --json | jq '.database.open_error'
# "database disk image is malformed"
# Fix
cass doctor --fix --json
# Backs up bad DB to .corrupt.<ts>, then rebuilds index from corrupt-salvage if possibleStale lock from crashed indexer
# Symptom: "Index rebuild is already in progress" but no cass process exists
ps -p $(cass status --json | jq -r '.active_index.pid // empty')
# (no such process)
# Fix (doctor handles >1h-old locks; for fresher locks, force it)
cass doctor --fix --force-rebuild --jsonIncremental index hangs at current:0 (OPEN issue #196)
# Workaround until fixed upstream
pkill -f "cass index"
cass index --full --force-rebuild --jsoncass status keeps showing rebuilding after the kill? cass doctor --fix clears the run lock.
Full rebuild "succeeds" then fails on last_indexed_at write (FIXED at HEAD as of 2026-04-22)
Status: Fixed by commit e06342f2 (bead coding_agent_session_search-zz8ni, closed). Affects v0.3.6 and earlier. Once you're on a build that includes the fix, the rebuild reports {"success": true} and the missing-marker case logs a deferred-update warning instead of bubbling out as failure. The recovery recipe below remains valid as a workaround for older binaries.
Symptom: cass index --full --force-rebuild --json runs for 3–5 minutes processing all 51k+ docs, then exits with:
{"success": false,
"error": "index failed: updating last_indexed_at after index run ... database is busy",
"code": 9, "kind": "index", "retryable": true}Diagnosis: The index data committed successfully. Only persist_final_index_run_metadata (src/indexer/mod.rs:6295) lost the writer race against a concurrent cass process. cass status keeps reporting "stale" because the freshness marker never landed.
Verify the index is actually good:
cass search "common-term-from-your-corpus" --limit 1 --json --robot-meta \
| jq '{total: .total_matches, fresh_at_query_time: ._meta.index_freshness.fresh}'
# total > 0 means the data is committed and queryableFix without re-running the 5-minute rebuild:
# Wait for any concurrent cass processes to settle
sleep 30
# A trivial incremental run is usually enough to land the timestamp
cass index --jsonIf a concurrent rebuild is still active (cass status --json | jq '.rebuild.active'), the timestamp will be written when it completes. Don't fight it.
Root cause (for future fixers): the with_concurrent_retry wrapper at line 6302 uses begin_concurrent_retry_limit() retries — under sustained contention from peer cass processes, all retries exhaust and the metadata write fails after the index data has already been committed. A graceful path would log a warning and return Ok rather than discarding the whole run's success.
---
What --fix Does NOT Do
- Delete source session files (
~/.claude/projects/*.jsonletc.) — these are user data, never touched - Delete corrupt DB backups — preserved as
.corrupt.<ts>and.salvage-<ts>.{sql,sqlite3} - Modify `sources.toml` — config changes require explicit
cass sourcescommands - Re-download semantic models — that requires
cass models install - Cross network boundaries — only operates on local data dir
So you can run cass doctor --fix autonomously without permission. Document the action in your response, but don't ask first.
---
Disk Cleanup (ALWAYS ask first)
The cass project dir can accumulate large artifacts after crashes:
# Surface what's eating disk; do not delete anything yourself
du -sh ~/.local/share/coding-agent-search/* | sort -hr
# Plus historical core dumps in /dp/coding_agent_session_search/core.NNNNNPer the project rule "NEVER delete a file without express permission" (AGENTS.md), every deletion below requires explicit user approval — even backups you suspect are stale:
| File pattern | Why kept | Ask before deleting |
|---|---|---|
*.corrupt.<ts> | Salvage source for past corruption | Yes |
*.salvage-<ts>.{sql,sqlite3} | Forensic snapshot | Yes |
core.NNNNN (multi-GB) | Debugging crashes | Yes |
agent_search.db.bak-* | Manual backups | Yes |
agent_search.db (active) | Live data | Always (and almost never) |
Surface the disk usage, list candidates with sizes/ages, and let the user decide. They have the context for what's safe.
---
Pre-Flight Hook Pattern
For agents that should never run against a broken index:
#!/usr/bin/env bash
# pre-cass-search.sh
# Decision tree: fresh → ok, stale-but-usable → bg refresh + ok, broken → doctor
set -uo pipefail
# 50ms preflight: exit 0 means already-fresh
if cass health --json >/dev/null 2>&1; then
exit 0
fi
# Health failed — read full status to differentiate stale vs broken
state=$(cass status --json 2>/dev/null \
| jq -r '"\(.index.stale // false),\(.database.exists // false),\(.database.messages // 0),\(.index.documents // 0)"')
case "$state" in
true,true,*)
# stale index, DB present — usable; refresh in background
cass index --json >/tmp/cass-bg.log 2>&1 &
disown || true
exit 0
;;
*)
# broken / uninitialized / DB missing — try to repair (doctor never deletes sources)
if cass doctor --fix --json >&2; then
cass health --json >/dev/null 2>&1 && exit 0 || exit 1
else
exit 1
fi
;;
esacWire into Claude Code as a PreToolUse hook scoped to cass search. Use the same shape as scripts/recover.sh for the inline decision tree.
Remote Sources & Multi-Machine Search
One-liner:cass sourceslets you treat sessions oncss,csd,ts1,ts2, etc. as part of your local searchable corpus. Three approaches, ordered by long-term value.
Contents
- Approach A — Configured Sources (preferred)
- Approach B — One-Shot SSH Query (no setup)
- Approach C — Parallel Fan-Out
- Diagnostics
- When to Use Which
- Pitfalls
---
Approach A — Configured Sources (preferred)
Persist remote machines so every cass search automatically spans them.
# 1. Discover SSH hosts and probe each one
cass sources discover --json
cass sources setup # interactive wizard; auto-skips configured hosts
# 2. Or add manually
cass sources add ssh://ubuntu@css --name css --preset linux-defaults
cass sources add ssh://ubuntu@csd --name csd --preset linux-defaults
cass sources add ssh://ubuntu@ts1 --name ts1 --preset linux-defaults
cass sources add ssh://ubuntu@ts2 --name ts2 --preset linux-defaults
# 3. Sync (rsync remote → local + reindex)
cass sources sync --json # all sources
cass sources sync --source css --json # one source
cass sources sync --dry-run --json # preview only
# 4. Confirm
cass sources list --json
cass sources doctor --json # connectivity + path probeAfter sync, cass search queries return hits with origin_host: "css" mixed in. Always preserve origin_host when reporting back to the user — it tells them which machine the prompt lives on.
Source Schedule
# ~/.config/cass/sources.toml
[[sources]]
name = "css"
type = "ssh"
host = "css"
paths = ["~/.claude/projects", "~/.codex/sessions"]
sync_schedule = "manual" # or "hourly", "daily"
platform = "linux"Path Mappings
When a workspace path differs between machines (e.g., /home/user1/dp ↔ /data/projects):
cass sources mappings list css --json
cass sources mappings add css --from /home/user1/dp --to /data/projectsMapped paths are rewritten so --workspace /data/projects/foo matches both local and remote sessions.
---
Approach B — One-Shot SSH Query (no setup)
Fastest path when you only need one query against one host.
ssh css 'cass search "KEYWORD" --json --fields minimal --limit 10' \
| jq '[.hits[] | {host: "css", path: .source_path, line: .line_number}]'Trade-off: every query incurs SSH latency (~300ms cold) + remote cass startup. Use Approach A for >3 queries per session.
---
Approach C — Parallel Fan-Out (when speed matters)
Run identical query against the whole fleet simultaneously.
HOSTS="css csd ts1 ts2"
for h in $HOSTS; do
ssh "$h" 'cass search "KEYWORD" --json --fields minimal --limit 20' > "/tmp/cass-$h.json" &
done
wait
# Merge + dedup (same source_path + line counts as the same hit)
jq -s '
[.[] | .hits[] // empty]
| unique_by(.source_path + ":" + (.line_number|tostring))
| sort_by(-.score)
| .[0:30]
' /tmp/cass-*.jsonUse this when you don't want to write to local disk (sources sync writes ~tens of MB per host) or when you're ok with snapshot-in-time results.
---
Diagnostics
cass sources list --verbose --json # full config + sync state
cass sources doctor --source css --json
# Common failures:
# - "host unreachable" → check ssh connectivity
# - "remote cass not found" → ssh in and `install.sh`
# - "rsync arg protection mismatch" → CLOSED issue #191; update macOS rsync to 3.4.1+
# Per-host probe (lightweight)
ssh css 'cass health --json'---
When to Use Which
| Need | Approach |
|---|---|
| One query, exploratory | B (ssh one-shot) |
| Mining a project across the fleet over a session | A (configured sources) |
| Latency matters more than dedup | C (parallel fan-out) |
| Building cross-machine analytics | A + cass analytics rebuild after sync |
---
Pitfalls
cass sources syncrsyncs session files, not the index. Re-indexing happens automatically; if you pass--no-index, you must runcass index --jsonyourself.- Path mappings rewrite workspace paths only. The
source_pathof remote hits still references the remote filesystem — pass back to the user verbatim, don't try to open them locally. - Removing a source:
cass sources remove NAME --purgedeletes the synced data too. Without--purge, it just stops future syncs but keeps already-indexed sessions. - If
sources doctorreports paths as missing on macOS but they exist on the remote, that's CLOSED issue #190 — update cass past v0.3.1.
Cross-Harness Session Resume
One-liner: cass resume PATH resolves any indexed session into the exact command its native CLI uses to continue the conversation. Works across Claude Code, Codex, Gemini CLI, OpenCode, pi_agent.Contents
- The Three Modes
- Per-Harness Behavior
- The Subagent Trap
- The Resume → Search Loop
- When Resume Won't Work
- What `cass resume` is NOT
---
The Three Modes
# 1. Print argv tokens, one per line (for the caller to wrap)
cass resume /path/to/session.jsonl
# claude
# resume
# 8efcc298-90d8-4764-9144-944c40f1a321
# 2. Emit a single shell-escaped command line
cass resume /path/to/session.jsonl --shell
# claude resume '8efcc298-90d8-4764-9144-944c40f1a321'
eval "$(cass resume /path/to/session.jsonl --shell)"
# 3. Replace the current process (mutually exclusive with --shell/--json)
cass resume /path/to/session.jsonl --exec---
Per-Harness Behavior
cass resume detects the harness from the file path and emits the command its native CLI expects. Don't memorize the argv shape — just read what cass resume PATH --shell prints.
| Detected Agent | Source path layout | --agent override |
|---|---|---|
| Claude Code | ~/.claude/projects/<workspace>/<uuid>.jsonl | claude / claude-code / claude_code |
| Codex | ~/.codex/sessions/<YYYY/MM/DD>/rollout-*.jsonl | codex |
| OpenCode | ~/.opencode/... | opencode |
| Gemini | ~/.gemini/... | gemini |
| pi_agent (mono) | ~/.pi/... | pi_agent / pi-agent (auto) or pi (force) |
| Oh My Pi | ~/.pi/... | omp / oh-my-pi / ohmypi |
Override the auto-detected harness with --agent:
cass resume /weird/path.jsonl --agent claude # force Claude Code resume form
cass resume /weird/path.jsonl --agent omp # force Oh My Pi---
The Subagent Trap
Subagent files are NOT resumable — they're orchestrated by a parent session.
cass resume /home/x/.claude/projects/<ws>/subagents/agent-a0b4d4b58a1fd73da.jsonl --json
# {"error":{"code":5,"kind":"session_id_not_found",
# "message":"filename stem 'agent-a0b4d4b58a1fd73da' does not look like a Claude Code session UUID (expected 8-4-4-4-12 hex)",
# "hint":"Did you pass a project directory or notes file instead..."}}Recover the parent session via cass context. The schema is:
{
"source": {"path": "...", "agent": "...", "workspace": "...", ...},
"counts": {"same_workspace": 12, "same_day": 8, "same_agent": 15},
"related": {
"same_workspace": [{"path": "...", "agent": "...", "title": "...", ...}, ...],
"same_day": [...],
"same_agent": [...]
}
}Note: items use .path (not .source_path) and related is an object, not a flat array.
# Find the parent (first non-subagent file in same_workspace)
cass context /path/to/subagents/agent-XXXXX.jsonl --json \
| jq -r '.related.same_workspace[]
| select(.path | contains("subagents") | not)
| .path' \
| head -1Then cass resume that path.
---
The Resume → Search Loop
A common pattern: search for a past task, resume the agent that did it, hand off the next prompt.
# 1. Find the right past session
HIT=$(cass search "implement auth flow" --workspace /myrepo --json --fields summary --limit 1 \
| jq -r '.hits[0].source_path')
# 2. Print the command without executing
cass resume "$HIT" --shell
# 3. Drop the user into the resumed conversation
cass resume "$HIT" --execOr for inspection only:
cass expand "$HIT" --line 1 --context 5 # see the original prompt---
When Resume Won't Work
| Symptom | Cause | Fix |
|---|---|---|
session_id_not_found for agent-*.jsonl | Subagent file | Use cass context to find parent |
unknown harness | Path doesn't match any connector layout | Pass --agent explicitly |
| Resumed session won't open | The native CLI was upgraded and changed its session schema | Try the harness's own --list to see if the ID is still valid; the source jsonl is your fallback |
cross_agent_session_resumer#9 style: Codex → Pi resumption broken | Cross-harness resume requires casr (separate tool) | Use the matching native CLI; cass resume only does same-harness |
---
What cass resume is NOT
It is not a cross-CLI translator. Resuming a Codex conversation always uses the Codex CLI; Claude → Claude; etc. For genuine cross-CLI continuation, invoke the standalone casr skill (cross-skill reference — load via casr rather than following a path; the file at ../..casr/SKILL.md is documentation only and not part of this skill's progressive-disclosure tree).
Semantic & Hybrid Search
One-liner: Lexical (BM25) is the default and is sufficient for >90% of agent queries. Enable semantic only when you don't know the exact wording.
Contents
- Decision Tree
- Models — Three States
- Building & Refreshing the Vector Index
- Querying
- Background Backfill
- Pitfalls
---
Decision Tree
Need to find something?
│
├─ I know exact words / file names / IDs → --mode lexical (default; do nothing)
├─ I want "things conceptually like X" → --mode semantic (needs MiniLM)
├─ Mix of both / not sure → --mode hybrid (RRF combines results)cass uses Reciprocal Rank Fusion for hybrid: score = Σ 1 / (60 + rank_i). Top-of-list lexical hits stay near the top, but conceptually similar items the lexical index missed get surfaced.
---
Models — Three States
cass models status --json | jq '{state, installed_size_bytes, total_size_bytes}'| State | Meaning | Action |
|---|---|---|
not_installed | No model files; semantic falls back to hash embedder (lexical-overlap only) | cass models install to enable real semantic |
partial | Some files present, others missing | cass models verify then re-install missing |
installed | All files present and SHA256 verified | Use freely |
The hash embedder is deterministic and instant but only matches token overlap — it doesn't know "car ≈ automobile". For real semantic understanding you need the MiniLM bundle (~90MB).
Install / Verify / Remove
cass models install # downloads from HuggingFace (default model)
cass models install --mirror <URL> # use a different mirror (HF flaky on Windows / corp networks)
cass models install --from-file <DIR> # air-gapped: install from a pre-downloaded model dir
cass models verify # SHA256 check, no network
cass models remove -y # frees ~90MB; semantic falls back to hash
cass models check-update # see if a newer model rev existsIf cass models install fails on Windows with WSAENOTCONN (closed issue #193), retry once. If still failing, use --mirror to switch endpoints or --from-file with a model dir you copied from a working host. The required files are listed in the README's Semantic Search section.
---
Building & Refreshing the Vector Index
# After install, build the FSVI vector index
cass index --semantic --json
# Add HNSW for O(log n) approximate search (recommended for >10k sessions)
cass index --semantic --build-hnsw --json
# Subsequent runs are incremental
cass index --semantic --json # only new conversations get embeddedThe vector index lives at ~/.local/share/coding-agent-search/vector_index/index-minilm-384.fsvi. It's memory-mapped — opening a 1GB index doesn't read 1GB into RAM.
---
Querying
# Lexical (default; fastest)
cass search "tantivy index" --mode lexical --json
# Semantic (requires --semantic-built index OR falls back to hash)
cass search "ways to make search faster" --mode semantic --json
# Hybrid (best for "I'll know it when I see it")
cass search "stuck index recovery" --mode hybrid --json
# Approximate semantic via HNSW (10–100x faster on big corpora)
cass search "QUERY" --mode semantic --approximate --jsonIf --mode semantic is used but no model is installed and CASS_SEMANTIC_EMBEDDER=hash is unset, cass silently degrades to lexical — the response includes _meta.fallback_mode: "lexical". Always check that field before claiming semantic worked.
---
Background Backfill
For very large corpora, semantic embedding can take minutes. cass schedules low-impact background backfill that respects idle/load budgets:
# Status of backfill (in-progress, completed, idle)
cass status --json | jq '.semantic'
# Force foreground build (skip backfill scheduler)
cass index --semantic --jsonWhen semantic.progressive_ready=true but hnsw_ready=false, you can still query with --mode semantic; it'll do a brute-force vector scan (slower but correct).
---
Pitfalls
- Always check fallback mode. A query that looks semantic may have run lexically:
cass search "X" --mode hybrid --robot-meta --json | jq '._meta.fallback_mode // "ok"'- Hash embedder ≠ semantic. It's deterministic and lexical-overlap only. Useful for env-pinning tests; not a substitute for MiniLM.
- Vector index is per-embedder. Switching embedders requires a rebuild:
cass index --semantic --embedder fastembed --json. - Disk pressure: the FSVI index can grow to ~1.5x the source SQLite size. If
df -h ~/.local/shareis tight, semantic backfill silently pauses. - Daemon mode (Unix only):
cass daemonruns the model in-memory across queries to avoid 500ms model-load cost per call. Worth it if you're running >50 semantic queries/min.
Session File Formats by Agent
Critical knowledge for parsing raw session logs. Each agent stores conversations in JSONL with different structures.
Contents
- Quick Detection
- Claude Code Format
- Codex CLI Format
- Gemini CLI Format
- Subagent Sessions (Critical)
- Universal Extraction Patterns
- File Location Cheat Sheet
- Quick Reference
---
Quick Detection
# Detect agent type from first line
head -1 /path/to/session.jsonl | jq -e '.type == "user"' && echo "claude_code"
head -1 /path/to/session.jsonl | jq -e '.role == "user"' && echo "codex_or_gemini"---
Claude Code Format
Location: ~/.claude/projects/<escaped-workspace-path>/*.jsonl
Path encoding: Workspace /data/projects/foo becomes -data-projects-foo
Example path:
~/.claude/projects/-data-projects-beads_rust-2d7a3b1/session-20260116-143022.jsonlMessage Structure
{"type": "user", "message": {"content": "...", "role": "user"}, "timestamp": "2026-01-16T14:30:22Z"}
{"type": "assistant", "message": {"content": [...], "role": "assistant"}, "timestamp": "..."}
{"type": "tool_result", "tool_use_id": "...", "content": "...", "timestamp": "..."}Content Formats
User content — can be string OR array:
// Simple string
{"type": "user", "message": {"content": "Help me fix this bug"}}
// Array with text blocks (common with images/files)
{"type": "user", "message": {"content": [{"type": "text", "text": "Help me fix this bug"}]}}Assistant content — always array with mixed types:
{"type": "assistant", "message": {"content": [
{"type": "text", "text": "I'll help you fix that..."},
{"type": "tool_use", "id": "toolu_01...", "name": "Read", "input": {"file_path": "/path/to/file"}}
]}}Extract User Messages
# Simple extraction (handles both string and array content)
jq 'select(.type == "user") | .message.content | if type == "array" then [.[] | select(.type == "text") | .text] | join(" ") else . end' session.jsonlExtract Tool Calls
# All tool calls
jq 'select(.type == "assistant") | .message.content[] | select(.type == "tool_use") | {name, input}' session.jsonl
# Specific tool (e.g., Write)
jq 'select(.type == "assistant") | .message.content[] | select(.type == "tool_use" and .name == "Write")' session.jsonl
# Tool results
jq 'select(.type == "tool_result")' session.jsonlFirst User Prompt (The Ritual Opener)
# Line 1-3 typically contains the opening prompt
jq -s '[.[] | select(.type == "user")][0] | .message.content' session.jsonl---
Codex CLI Format
Location: ~/.codex/**/*.jsonl (varies by installation)
Message Structure
{"role": "user", "content": "...", "timestamp": "2026-01-16T14:30:22Z"}
{"role": "assistant", "content": "...", "created_at": "..."}Key Differences from Claude Code
| Aspect | Claude Code | Codex |
|---|---|---|
| Type field | .type == "user" | .role == "user" |
| Timestamp | .timestamp | .timestamp or .created_at |
| Content structure | Often array | Usually string |
| Tool calls | Embedded in .content[] | Varies |
Extract User Messages
jq 'select(.role == "user") | .content' session.jsonlExtract with Timestamp
jq 'select(.role == "user") | {ts: (.timestamp // .created_at), content}' session.jsonl---
Gemini CLI Format
Location: ~/.gemini/**/*.jsonl (varies by installation)
Similar to Codex format. Uses .role instead of .type.
jq 'select(.role == "user") | .content' session.jsonl---
Subagent Sessions (Critical)
What: When Claude Code spawns a Task agent, it creates a separate session log.
Location: ~/.claude/projects/<workspace>/subagents/agent-<id>.jsonl
Subagent Structure
Line 1: Session metadata (type, model info)
Line 2: THE USER PROMPT (this is gold — the extraction prompt that worked)
Line 3+: Agent execution and responsesWhy Subagents Matter
Deep dive extraction prompts live here. The prompt at line 2 is copy-paste ready — it's the exact instruction that produced the extraction.
Extract Subagent Prompt
# View prompt with context
cass view /path/subagents/agent-XXXXX.jsonl -n 2 -C 1
# Extract just the text
sed -n '2p' /path/subagents/agent-XXXXX.jsonl | jq '.message.content'
# Or with jq slurp
jq -s '.[1].message.content' /path/subagents/agent-XXXXX.jsonlFind All Subagent Sessions
# Via cass search
cass search "KEYWORD" --workspace /path --json --fields minimal \
| jq '[.hits[] | select(.source_path | contains("subagent"))] | .[].source_path' -r | sort -u
# Via filesystem
find ~/.claude/projects -path "*/subagents/*.jsonl" | head -20---
Universal Extraction Patterns
Detect and Extract (Any Agent)
#!/bin/bash
# extract_prompts.sh — works with any agent format
FILE="$1"
# Detect format and extract
if jq -e '.[0].type == "user"' "$FILE" >/dev/null 2>&1; then
# Claude Code format
jq -s '[.[] | select(.type == "user")] | .[] | .message.content' "$FILE"
elif jq -e '.[0].role == "user"' "$FILE" >/dev/null 2>&1; then
# Codex/Gemini format
jq -s '[.[] | select(.role == "user")] | .[] | .content' "$FILE"
else
echo "Unknown format"
fiCount Messages by Type
# Claude Code
jq -s 'group_by(.type) | map({type: .[0].type, count: length})' session.jsonl
# Codex/Gemini
jq -s 'group_by(.role) | map({role: .[0].role, count: length})' session.jsonlExtract Conversation Flow
# Timeline of who said what
jq -s '[.[] | {
type: (.type // .role),
ts: (.timestamp // .created_at),
preview: (if .message then .message.content else .content end | tostring[0:100])
}]' session.jsonl---
File Location Cheat Sheet
| Agent | Session Location | Subagent Location |
|---|---|---|
| Claude Code | ~/.claude/projects/<escaped-path>/*.jsonl | .../subagents/agent-*.jsonl |
| Codex | ~/.codex/**/*.jsonl | Varies |
| Gemini | ~/.gemini/**/*.jsonl | Varies |
Find Session Files for a Workspace
# Claude Code: convert workspace to escaped pattern
WORKSPACE="/data/projects/beads_rust"
ESCAPED=$(echo "$WORKSPACE" | tr '/' '-' | sed 's/^-//')
find ~/.claude/projects -name "*.jsonl" | grep -i "$ESCAPED"---
Quick Reference
| Task | Claude Code | Codex/Gemini |
|---|---|---|
| Is user message? | .type == "user" | .role == "user" |
| Get content | .message.content | .content |
| Get timestamp | .timestamp | .timestamp or .created_at |
| Tool call? | .type == "tool_use" | Varies |
| First prompt | `jq -s '[.[] \ | select(.type=="user")][0]'` |
#!/usr/bin/env bash
# multi_machine_search.sh — fan-out a cass search across the fleet
#
# Usage: ./multi_machine_search.sh "QUERY" [host1 host2 ...]
# Default hosts: css csd ts1 ts2
#
# - Local + remote searches run in parallel
# - One bad/unreachable host doesn't kill the rest (set -e disabled)
# - Query is passed via stdin to ssh, never interpolated into the command line,
# so quotes/specials in the query are safe
set -uo pipefail
shopt -s nullglob # unmatched globs expand to empty so `jq -s "$TMPDIR"/*.json` is safe
QUERY="${1:?usage: $0 \"QUERY\" [host1 host2 ...]}"
shift
HOSTS=("$@")
[ ${#HOSTS[@]} -eq 0 ] && HOSTS=(css csd ts1 ts2)
# Per-host wall-clock cap (post-connect). ssh's ConnectTimeout only covers TCP
# handshake; if the remote `cass search` hangs we'd block indefinitely.
PER_HOST_TIMEOUT="${CASS_FANOUT_TIMEOUT:-30}"
# Required tools.
for tool in jq timeout ssh; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "error: '$tool' not on PATH" >&2
exit 2
fi
done
# Refuse multi-line queries early — `read -r q` on the remote only sees the first line.
case "$QUERY" in
*$'\n'*)
echo "error: multi-line queries are not supported (only the first line would be sent to remotes)" >&2
exit 2
;;
esac
TMPDIR=$(mktemp -d -t cass-fanout-XXXXXX)
cleanup() {
echo "cass fan-out diagnostics retained: $TMPDIR" >&2
}
trap cleanup EXIT
# Local-host search runs in a function so a local cass failure produces "[]"
# rather than a missing file (which would later break the merge glob).
# Wrap every cass invocation in `timeout` — cass search has been observed
# to hang on certain inputs (e.g. --limit 0).
local_search() {
local raw
raw=$(timeout "$PER_HOST_TIMEOUT" cass search "$QUERY" --json --fields summary --limit 20 2>/dev/null) || raw=""
if [ -z "$raw" ]; then
echo "[]" > "$TMPDIR/local.json"
return
fi
printf '%s' "$raw" \
| jq '[(.hits // [])[] | . + {origin_host: "local"}]' > "$TMPDIR/local.json" \
|| echo "[]" > "$TMPDIR/local.json"
}
# Pass the query via stdin so it's never spliced into the command line.
# `timeout` here covers post-connect hangs (ssh ConnectTimeout only covers TCP).
remote_search() {
local h="$1"
local raw
# shellcheck disable=SC2016 # Remote shell expands $q after reading it from stdin.
raw=$(timeout "$PER_HOST_TIMEOUT" ssh -o ConnectTimeout=5 -o BatchMode=yes "$h" \
'IFS= read -r q && cass search "$q" --json --fields summary --limit 20 2>/dev/null' \
<<<"$QUERY" 2>"$TMPDIR/$h.err") || raw=""
if [ -z "$raw" ]; then
echo "[]" > "$TMPDIR/$h.json"
return
fi
printf '%s' "$raw" \
| jq --arg h "$h" '[(.hits // [])[] | . + {origin_host: $h}]' > "$TMPDIR/$h.json" \
|| echo "[]" > "$TMPDIR/$h.json"
}
echo "→ local" >&2
local_search &
for h in "${HOSTS[@]}"; do
echo "→ $h" >&2
remote_search "$h" &
done
wait
# Surface ssh errors (don't fail; just inform)
for h in "${HOSTS[@]}"; do
if [ -s "$TMPDIR/$h.err" ]; then
echo " ! $h: $(head -c 200 "$TMPDIR/$h.err" | tr '\n' ' ')" >&2
fi
done
# Merge + dedup by source_path:line, sort by score
files=( "$TMPDIR"/*.json )
if [ ${#files[@]} -eq 0 ]; then
echo "[]"
exit 0
fi
jq -s '
(add // [])
| map(select(type == "object"))
| unique_by((.source_path // "") + ":" + ((.line_number // 0)|tostring))
| sort_by(-(.score // 0))
| .[0:50]
| map({
host: (.origin_host // "?"),
agent: (.agent // ""),
line: (.line_number // null),
score: (.score // null),
title: ((.title // "") | .[0:100]),
path: (.source_path // "")
})
' "${files[@]}"
#!/usr/bin/env python3
"""
Prompt Miner — Extract and cluster prompts across agent session logs.
Mines user prompts from Claude Code, Codex CLI, and Gemini CLI sessions,
clusters them by similarity, and identifies "ritual" prompts (repeated patterns
that indicate working workflows).
Usage:
python prompt_miner.py --workspace /data/projects/PROJECT [OPTIONS]
Examples:
# Find repeated prompts in a project
python prompt_miner.py --workspace /data/projects/beads_rust --top 30
# Mine all sessions with custom glob
python prompt_miner.py --glob "~/.claude/projects/**/*.jsonl" --top 50
# Only show rituals (10+ occurrences)
python prompt_miner.py --workspace /path --min-count 10
# Output as JSON for further processing
python prompt_miner.py --workspace /path --json
# Filter by agent
python prompt_miner.py --workspace /path --agent claude_code
"""
import json
import re
import glob
import argparse
import os
import sys
from datetime import datetime, timezone
from typing import Optional
def normalize(s: str) -> str:
"""Normalize whitespace for clustering."""
return re.sub(r"\s+", " ", s.strip())
def truncate(s: str, max_len: int = 100) -> str:
"""Truncate string with ellipsis."""
if len(s) <= max_len:
return s
return s[:max_len-3] + "..."
def parse_iso(ts: str) -> Optional[datetime]:
"""Parse ISO timestamp, handling various formats."""
if not ts:
return None
# Handle Z suffix
if ts.endswith("Z"):
ts = ts[:-1] + "+00:00"
# Handle missing timezone
if "+" not in ts and "-" not in ts[-6:]:
ts = ts + "+00:00"
try:
dt = datetime.fromisoformat(ts)
if dt.tzinfo is None:
return dt.replace(tzinfo=timezone.utc)
return dt
except ValueError:
return None
def extract_text_from_content(content) -> str:
"""Extract text from various content formats."""
if isinstance(content, str):
return content
elif isinstance(content, list):
parts = []
for c in content:
if isinstance(c, dict):
if "text" in c:
parts.append(str(c["text"]))
elif "content" in c:
parts.append(extract_text_from_content(c["content"]))
return " ".join(parts)
elif isinstance(content, dict):
if "text" in content:
return str(content["text"])
elif "content" in content:
return extract_text_from_content(content["content"])
return ""
def detect_agent(path: str, obj: dict) -> str:
"""Detect which agent produced this session."""
path_lower = path.lower()
if ".claude" in path_lower:
return "claude_code"
elif "codex" in path_lower:
return "codex"
elif "gemini" in path_lower:
return "gemini"
# Fallback: check object structure
if obj.get("type") == "user":
return "claude_code"
elif obj.get("role") == "user":
return "codex" # or gemini
return "unknown"
def mine_prompts(
glob_pattern: str,
agent_filter: Optional[str] = None
) -> list[tuple[datetime, str, str, str]]:
"""
Mine user prompts from session logs.
Returns list of (timestamp, source_path, prompt_text, agent).
"""
items = []
expanded_pattern = os.path.expanduser(glob_pattern)
for path in glob.glob(expanded_pattern, recursive=True):
try:
with open(path, "r", encoding="utf-8") as f:
for line in f:
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue
if not isinstance(obj, dict):
continue
try:
agent = detect_agent(path, obj)
if agent_filter and agent != agent_filter:
continue
text = None
ts = None
# Claude Code format
if obj.get("type") == "user":
msg = obj.get("message", {})
if not isinstance(msg, dict):
continue
content = msg.get("content", "")
text = extract_text_from_content(content)
ts = obj.get("timestamp")
# Codex/Gemini format
elif obj.get("role") == "user" and "content" in obj:
text = extract_text_from_content(obj["content"])
ts = obj.get("timestamp") or obj.get("created_at")
if text and text.strip():
dt = parse_iso(ts) or datetime.now(timezone.utc)
items.append((dt, path, text.strip(), agent))
except Exception:
continue
except Exception:
# Skip unreadable files
pass
items.sort(key=lambda x: x[0])
return items
def find_repeated_prompts(
items: list[tuple],
top_n: int = 30,
min_count: int = 2
) -> list[dict]:
"""Find most repeated prompts with metadata."""
# Group by normalized text
groups = {}
for dt, path, text, agent in items:
key = normalize(text)
if key not in groups:
groups[key] = {
"text": text, # Keep original (first occurrence)
"count": 0,
"agents": set(),
"first_seen": dt,
"last_seen": dt,
"paths": []
}
groups[key]["count"] += 1
groups[key]["agents"].add(agent)
groups[key]["last_seen"] = max(groups[key]["last_seen"], dt)
if path not in groups[key]["paths"] and len(groups[key]["paths"]) < 3: # Keep first 3 unique paths
groups[key]["paths"].append(path)
# Convert to list and sort
results = []
for key, data in groups.items():
if data["count"] >= min_count:
results.append({
"prompt": data["text"],
"count": data["count"],
"agents": sorted(data["agents"]),
"first_seen": data["first_seen"].isoformat() if data["first_seen"] else None,
"last_seen": data["last_seen"].isoformat() if data["last_seen"] else None,
"example_paths": data["paths"],
"is_ritual": data["count"] >= 10
})
results.sort(key=lambda x: -x["count"])
return results[:top_n]
def workspace_to_glob(workspace: str) -> str:
"""Convert workspace path to glob pattern for session files."""
workspace = os.path.expanduser(workspace)
# Claude Code stores sessions in ~/.claude/projects/-path-to-project/
# Convert /data/projects/foo to -data-projects-foo pattern
escaped = workspace.replace("/", "-").lstrip("-")
return f"~/.claude/projects/*{escaped}*/**/*.jsonl"
def main():
parser = argparse.ArgumentParser(
description="Mine prompts from agent session logs",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --workspace /data/projects/beads_rust --top 30
%(prog)s --glob "~/.claude/**/*.jsonl" --min-count 10
%(prog)s --workspace /path --json > prompts.json
"""
)
parser.add_argument(
"--workspace",
help="Project workspace path (auto-generates glob pattern)"
)
parser.add_argument(
"--glob",
help="Glob pattern for session files (overrides --workspace)"
)
parser.add_argument(
"--top",
type=int,
default=30,
help="Number of top repeated prompts to show (default: 30)"
)
parser.add_argument(
"--min-count",
type=int,
default=2,
help="Minimum repetition count to include (default: 2)"
)
parser.add_argument(
"--agent",
choices=["claude_code", "codex", "gemini"],
help="Filter by agent type"
)
parser.add_argument(
"--json",
action="store_true",
help="Output as JSON"
)
parser.add_argument(
"--rituals-only",
action="store_true",
help="Only show ritual prompts (10+ occurrences)"
)
args = parser.parse_args()
if args.top <= 0:
print("ERROR: --top must be greater than 0", file=sys.stderr)
return 1
if args.min_count <= 0:
print("ERROR: --min-count must be greater than 0", file=sys.stderr)
return 1
# Determine glob pattern
if args.glob:
glob_pattern = args.glob
elif args.workspace:
glob_pattern = workspace_to_glob(args.workspace)
else:
glob_pattern = "~/.claude/projects/**/*.jsonl"
# Set min count for rituals-only mode
min_count = args.min_count
if args.rituals_only:
min_count = max(min_count, 10)
# Mine prompts
if not args.json:
print(f"Mining prompts from: {glob_pattern}")
items = mine_prompts(glob_pattern, args.agent)
if not args.json:
print(f"Found {len(items)} user prompts")
# Find repeated prompts
repeated = find_repeated_prompts(items, args.top, min_count)
# Output
if args.json:
print(json.dumps({
"glob_pattern": glob_pattern,
"total_prompts": len(items),
"repeated_prompts": repeated
}, indent=2, default=str))
else:
print(f"\nTop {len(repeated)} repeated prompts (count >= {min_count}):\n")
for item in repeated:
ritual_marker = " [RITUAL]" if item["is_ritual"] else ""
display = truncate(item["prompt"], 100)
agents = ", ".join(item["agents"])
print(f"{item['count']:3d}x ({agents}){ritual_marker}: {display}")
if __name__ == "__main__":
sys.exit(main())
#!/bin/bash
#
# Quick Analysis — One-command project overview using cass
#
# Usage:
# ./quick_analysis.sh /data/projects/PROJECT_NAME
#
# Output:
# - Index health
# - Session counts by agent
# - Activity by date
# - Top 5 ritual opener candidates
#
# Requires: cass, jq
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROMPT_MINER="$SCRIPT_DIR/prompt_miner.py"
WORKSPACE="${1:-}"
if [ -z "$WORKSPACE" ]; then
echo "Usage: $0 /data/projects/PROJECT_NAME"
echo ""
echo "Examples:"
echo " $0 /data/projects/beads_rust"
echo " $0 /data/projects/rich_rust"
exit 1
fi
# Expand path
WORKSPACE=$(realpath "$WORKSPACE" 2>/dev/null || echo "$WORKSPACE")
if ! command -v cass >/dev/null 2>&1; then
echo "Error: cass is not installed or not in PATH"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq is not installed or not in PATH"
exit 1
fi
echo "=============================================="
echo "CASS QUICK ANALYSIS: $WORKSPACE"
echo "=============================================="
echo ""
# 1. Health check
echo "--- Index Health ---"
cass status --robot-format json 2>/dev/null | jq '{
conversations: .database.conversations,
messages: .database.messages,
index_fresh: .index.fresh,
rebuilding: (.index.rebuilding // .rebuild.active // false),
recommended: .recommended_action
}' 2>/dev/null || echo "Error: Could not get cass status"
echo ""
# 2. Refresh index (quick, incremental)
echo "--- Refreshing Index ---"
cass index --json 2>/dev/null | jq '.indexed // "Index refreshed"' -r 2>/dev/null || echo "Index refresh attempted"
echo ""
# 3. Agent breakdown
echo "--- Sessions by Agent ---"
cass search "*" --workspace "$WORKSPACE" --aggregate agent --limit 1 --json 2>/dev/null \
| jq '.aggregations.agent.buckets[] | "\(.key): \(.count) sessions"' -r 2>/dev/null \
|| echo "No sessions found for this workspace"
echo ""
# 4. Date breakdown (last 7 days of activity)
echo "--- Recent Activity (by date) ---"
cass search "*" --workspace "$WORKSPACE" --aggregate date --limit 1 --json 2>/dev/null \
| jq '.aggregations.date.buckets | sort_by(.key) | reverse | .[0:7] | .[] | "\(.key): \(.count) hits"' -r 2>/dev/null \
|| echo "No date information available"
echo ""
# 5. Ritual opener candidates (prompts at lines 1-3 that appear multiple times)
echo "--- Ritual Opener Candidates ---"
echo "(Prompts appearing at session start, sorted by frequency)"
echo ""
# Search for common ritual opener patterns
for pattern in "First read ALL" "AGENTS.md" "comprehensive deep dive" "ultrathink" "think super hard"; do
count=$(cass search "$pattern" --workspace "$WORKSPACE" --json --limit 100 2>/dev/null \
| jq '.total_matches // 0' 2>/dev/null || echo "0")
if [ "$count" -gt 2 ]; then
printf " %3dx: \"%s\"\n" "$count" "$pattern"
fi
done
echo ""
# 6. Quick tips
echo "--- Next Steps ---"
echo "1. Find ritual opener: cass search \"First read ALL\" --workspace $WORKSPACE --json --limit 5"
echo "2. View a session: cass view /path/to/session.jsonl -n 1 -C 10"
echo "3. Find user prompts: cass search \"KEYWORD\" --workspace $WORKSPACE --json | jq '[.hits[] | select(.line_number <= 3)]'"
echo "4. Mine all prompts: python \"$PROMPT_MINER\" --workspace $WORKSPACE"
echo ""
echo "=============================================="
#!/usr/bin/env bash
# validate.sh — structural check for the cass skill.
#
# CI should prove the skill artifact is valid, not require an operator's local
# cass index. Set AGENTOPS_VALIDATE_LIVE_TOOLS=1 to run the live cass smoke.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
SKILL_MD="$SKILL_DIR/SKILL.md"
SPEC_JSON="$SKILL_DIR/skill.spec.json"
fail=0
err() { printf 'FAIL: %s\n' "$1" >&2; fail=1; }
ok() { printf 'ok: %s\n' "$1"; }
[ -f "$SKILL_MD" ] || { err "SKILL.md missing"; exit 1; }
head -n1 "$SKILL_MD" | grep -qx -- '---' || err "frontmatter must open with ---"
grep -q '^name: cass$' "$SKILL_MD" || err "name must be cass"
grep -q '^description:' "$SKILL_MD" || err "description missing"
grep -q '^skill_api_version:' "$SKILL_MD" || err "skill_api_version missing"
grep -q 'cass search' "$SKILL_MD" || err "cass search workflow missing"
grep -q 'cass status' "$SKILL_MD" || err "cass status workflow missing"
if [ -f "$SPEC_JSON" ]; then
python3 -m json.tool "$SPEC_JSON" >/dev/null || err "skill.spec.json is not valid JSON"
ok "skill.spec.json valid JSON"
else
ok "skill.spec.json sidecar absent"
fi
if [ "${AGENTOPS_VALIDATE_LIVE_TOOLS:-0}" = "1" ]; then
command -v cass >/dev/null 2>&1 || err "cass is not installed or not in PATH"
command -v jq >/dev/null 2>&1 || err "jq is not installed or not in PATH"
if command -v cass >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
status="$(cass status --robot-format json 2>/dev/null)" || err "cass status failed"
printf '%s' "$status" | jq -e . >/dev/null 2>&1 || err "cass status returned invalid JSON"
cass search "*" --json --limit 1 --fields minimal >/dev/null 2>&1 || err "basic cass search failed"
cass search "*" --json --aggregate agent --limit 1 --fields minimal >/dev/null 2>&1 || err "cass aggregation search failed"
fi
else
ok "live cass smoke skipped (set AGENTOPS_VALIDATE_LIVE_TOOLS=1 to enable)"
fi
if [ "$fail" -eq 0 ]; then
printf '\nPASS: cass skill artifact is valid.\n'
exit 0
fi
printf '\nFAILED: cass skill artifact validation failed.\n' >&2
exit 1
Related skills
FAQ
What does cass search over?
It mines your past agent conversation history for refined prompts, working rituals, scope decisions, and recovery moments.
Does cass support semantic search?
Yes, via a fastembed model bundle for --mode semantic / hybrid; keyword search works without it.