Skip to content

Commit 3b16fe5

Browse files
philcunliffeclaude
andcommitted
Add in-repo hypaware-query skill with graph-vs-raw query routing guidance
Brings the hypaware-query agent skill into the repo (per the tooling-lives-in-repo convention) and extends it with context-graph routing guidance distilled from a 15-query empirical comparison of the graph route (node/edge datasets, hyp graph neighbors) against raw ai_gateway_messages SQL: - A generic routing rule: route to the graph when the answer is a set of identifiers, the predicate is derived rather than stored, the question crosses 2+ relationships, it is an inventory question, or identity needs normalizing; route to raw for counts, content, ordering, and row-level attributes. - A two-stage strategy (graph picks the scope, raw reads inside it) with the graph-to-content drill-down recipe (Session natural_key = session_id; tool_result_for pairs results to calls; message_index windows for context). - Raw-route footguns found during the evaluation: LIKE on JSON-typed columns silently matches nothing, json_extract throws on malformed rows without a tool_name pre-filter, correlated IN subqueries can run past 2 minutes, and session identity is session_id (not the mostly-null conversation_id). - Graph caveats: File-node identity splits between repo-scoped and absolute keys, neighbors prints display-truncated node ids and ignores --format json, and SQL over edge tables needs literal node_id anchors to avoid pathological join plans. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f68c793 commit 3b16fe5

1 file changed

Lines changed: 138 additions & 0 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
---
2+
name: hypaware-query
3+
description: Inspect local HypAware recordings with the hyp query CLI and the context graph. Use when the user asks about recorded logs, traces, metrics, AI gateway exchanges, query cache freshness, wants SQL over local HypAware data (including collected JSONL tables), or asks entity-level activity questions like which sessions touched a file, ran a skill, invoked a program, or used a model.
4+
---
5+
6+
# HypAware Query
7+
8+
Use `hyp query` to inspect local HypAware recordings. It reads local JSONL recordings and an explicit local query cache; it does not query the central server.
9+
10+
## Workflow
11+
12+
1. Run `hyp query status` first to verify the recording root and cache state.
13+
2. If the command cannot find the intended config, discover the service config once with `hyp status`, a LaunchAgent/systemd unit, or the user, then reuse `--config <path>` only for that setup.
14+
3. Cache freshness is handled asymmetrically:
15+
- **Stale partitions are queried by default** and the CLI prints a `warning: query cache last refreshed at …` line to stderr. Read stderr alongside stdout, and surface the refresh timestamp to the user so they know the cache may not include newer source rows. Prefer the file-targeted `hyp query refresh <file.jsonl>` command the CLI prints when updating cache data; use `--refresh always` only when the query should refresh before it runs.
16+
- **Missing partitions still error.** Run the exact `hyp query refresh …` command the CLI prints, or rerun the target query with `--refresh always`.
17+
- Broad manual refreshes are explicit: `hyp query refresh --all [dataset]`. Do not run a broad refresh when the printed file-targeted command is enough.
18+
4. Prefer structured output for analysis: use `--format json` for follow-up reasoning and `--format markdown` when showing a table to the user. Inline output is context-budgeted, not row-capped: each string cell is truncated to ~200 code points (a `…(+N)` marker shows how much was elided) and rows are dropped once a byte budget (~32KB) is hit, with a `notice: showing X of Y rows …` line on stderr. To get a full, untruncated result, spill it to a file with `--output <file>` (prints only a receipt to stdout - the data never floods context) and post-process the file. Override the caps with `--max-cell <n>` / `--max-bytes <n>` (`0` disables either).
19+
5. Use high-level query commands before custom SQL. Switch to `hyp query sql` only when the built-in commands cannot answer the question.
20+
6. For unfamiliar SQL tables, run `hyp query schema <table> --format json` before querying.
21+
22+
## Common Commands
23+
24+
```bash
25+
hyp query status
26+
hyp query catalog --format json
27+
hyp query logs --since 1h --format json
28+
hyp query traces slow --limit 20 --format json
29+
hyp query metrics list --format json
30+
hyp query metrics series <metric-name> --format json
31+
hyp query schema <table> --format json
32+
hyp query sql "<sql>" --format json
33+
hyp query refresh <file.jsonl>
34+
hyp query refresh --all logs
35+
hyp collect <file.jsonl> --name <name>
36+
hyp collect --glob '<pattern>' --name <name>
37+
```
38+
39+
## AI gateway message model
40+
41+
Recorded AI-gateway traffic is exposed through one dataset: `ai_gateway_messages`. Each row is a normalized message content part owned by the HypAware AI gateway schema.
42+
43+
Key columns:
44+
45+
- `session_id` (NOT NULL) - the session identity; filter and group by this. `conversation_id` is nullable and often null; do not use it as the session key.
46+
- `message_id`, `message_index`, `part_id`, `part_index` - stable message and part identity.
47+
- `cwd`, `git_remote`, `git_branch`, `repo_root`, `head_sha` - per-row workspace context; useful for repo-scoped raw filters.
48+
- `provider`, `model`, `role`, `part_type`, `content_text` - normalized provider/message content fields.
49+
- `tool_name`, `tool_call_id`, `tool_args`, `status` - tool-call/result joins and sparse status such as `finish_reason`.
50+
- `attributes` (JSON) - request settings, usage, propagated `dev_run_id`, and gateway diagnostics under `attributes.gateway`.
51+
52+
Claude transcript enrichment adds `provider_uuid`, `parent_uuid`, `request_id`, `entrypoint`, `client_version`, `user_type`, `permission_mode`, and `hook_event` when the local Claude Code JSONL transcript can be matched.
53+
54+
Run `hyp query schema ai_gateway_messages --format markdown` for the authoritative column reference.
55+
56+
## Context graph: route entity questions through the graph first
57+
58+
The activity graph is a structural index projected from `ai_gateway_messages` into two small datasets, `node` and `edge`. It answers "which sessions / files / skills / repos relate to X" questions in one sub-second lookup that would otherwise be a fragile LIKE-scan over hundreds of thousands of raw message rows. Decide the route before writing SQL:
59+
60+
**The routing rule.** Ask: does answering require *reading* rows, or only knowing they *exist and connect*? Route to the graph when the question is any of: (1) the answer is a set of identifiers, not text (membership/reachability); (2) the predicate is derived, not stored - the concept has no column and is computed by canonical projection rules, so ad hoc reconstruction is slower AND disagrees with the canonical answer; (3) it crosses two or more relationships (co-occurrence, indirect association) - the graph pre-materialized the join the raw route would express as a brittle correlated subquery; (4) it is an inventory/existence question - the node table is a pre-computed DISTINCT over all history; (5) identity needs normalizing across raw spellings. Route to the raw table when the question needs quantity/frequency (edges are existence-only), content, ordering/time, or row-level attributes. Compose them asymmetrically: graph decides WHICH scope matters, raw reads WHAT happened inside it - a scoped raw query is as fast as the graph, an unscoped one grows with history.
61+
62+
**Reach for the graph first when the question is entity-shaped** (the answer is a set of sessions, files, skills, programs, models, or repos, not message content):
63+
64+
- "Which sessions touched file X?" - `hyp graph neighbors <key> --direction in` (~0.2s vs ~0.9s raw, and the raw route has footguns - see below).
65+
- "What did session S do?" (files, tools, skills, programs, model, repo) - `hyp graph neighbors <session-id> --direction out`, optionally `--edge-type touched|ran|invoked|used`.
66+
- "Which sessions ran skill X / invoked program Y?" - these facets are derived at projection time (multi-surface skill-activation detection, argv[0] extraction with wrapper unwrapping). Ad hoc reconstruction from raw rows measurably disagrees with the canonical derivation (a 3-surface LIKE approximation returned 52 sessions where the graph's strict rules give 44) and a first-token approximation of "programs" returned 470 garbage tokens against the graph's 86 clean ones. Always use the graph for skill/program questions.
67+
- Inventories: "what skills / models / programs / repos appear in the recordings?" - `select natural_key from node where node_type = 'Skill'` (~0.13s; even `select distinct model` on the messages table costs ~0.6s and scales with the cache).
68+
- Co-occurrence and multi-hop: "what files co-occur with X via shared sessions", "which repos does skill Z run against" - `graph neighbors <seed> --depth 2 --direction both --edge-type <t>...`. This is the graph's biggest win: the raw equivalent needed a correlated subquery that either threw on a malformed row or timed out (>120s vs 0.17s).
69+
- Cross-client joins: Skill and Program nodes are keyed to converge across claude and codex, so per-skill and per-program questions span both clients for free. Repo nodes normalize remote-URL forms that a raw `git_remote LIKE` misses (graph found 312 sessions in a repo; the LIKE found 240).
70+
71+
**Stay in `ai_gateway_messages` when the question is content- or event-shaped**: what was said, prompt/tool text, ordering and timing inside a session, token usage and other `attributes`, and any "how many times" question. Graph edges are deduplicated per (session, entity) pair - a `touched` edge means "at least once", never a count. Session-scoped raw aggregates are fast (~0.15s with a `session_id` filter), so once you have a session id there is no reason to avoid the messages table.
72+
73+
**Default strategy is two-stage**: use the graph to discover which sessions or entities matter, then drill into content with `ai_gateway_messages` filtered by `session_id`. Coverage of the two surfaces is near-identical when the graph is freshly projected, but they can drift (the graph only updates on `hyp graph project`; messages rows can be pruned by retention), so treat an empty drill-down as "check freshness", not "no data".
74+
75+
### From graph results to session content
76+
77+
The join is direct: a graph `Session` node's `natural_key` IS the `session_id` column in `ai_gateway_messages`. The standard drill-down for analyzing a tool's usage across chats:
78+
79+
```bash
80+
# 1. Which sessions used the tool (human-readable; session UUIDs printed inline)
81+
hyp graph neighbors <ToolName> --type Tool --direction in
82+
# 1b. Machine-readable session list (resolve the node_id first; see pitfall below)
83+
hyp query sql "select s.natural_key session_id from edge e join node s on e.src_id=s.node_id \
84+
where e.edge_type='used' and e.dst_id='<full-node-id>'" --format json
85+
# 2. The calls themselves, with args
86+
hyp query sql "select message_index, tool_call_id, json_extract(tool_args,'\$.<arg>') a \
87+
from ai_gateway_messages where session_id='<uuid>' and part_type='tool_call' and tool_name='<ToolName>'" --format json
88+
# 3. Each call's result: tool_result rows point back via tool_result_for
89+
hyp query sql "select content_text from ai_gateway_messages \
90+
where session_id='<uuid>' and tool_result_for='<tool_call_id>'" --format json
91+
# 4. Surrounding conversation: window on message_index
92+
hyp query sql "select message_index, role, part_type, content_text from ai_gateway_messages \
93+
where session_id='<uuid>' and message_index between <n-3> and <n+3> order by message_index, part_index" --format json
94+
```
95+
96+
Two pitfalls in this handoff: `graph neighbors` ignores `--format json` (parse the table, or use the SQL form for machine-readable lists), and the bracketed id it prints (`[1a585e06adf4]`) is **display-truncated** - pasting it into SQL matches nothing; resolve the full `node_id` from the `node` table by `natural_key`. Note that for a single known tool, discovery via raw SQL is also fine (`tool_name` is a real column); the graph earns its keep when the tool question chains into other entities (which repos, which skills, co-used tools).
97+
98+
### Raw-route footguns (when you do query messages directly)
99+
100+
- **`tool_args` and `attributes` are JSON-typed columns. `tool_args LIKE '%x%'` silently matches zero rows** - it is not an error, it just returns nothing. Extract first: `json_extract(tool_args, '$.file_path') = '...'` or `json_extract(...) LIKE '%...%'`.
101+
- **`json_extract` throws on malformed rows** (`invalid JSON string`) when scanned broadly. Always pre-filter to the tool names whose args you are extracting (`tool_name in ('Read','Edit','Write','MultiEdit','NotebookEdit')`) so the scan never reaches a bad row.
102+
- **Avoid correlated `IN (select ...)` subqueries over `json_extract`** - observed to run past a 2-minute timeout on a 143k-row table. If you need a two-stage raw query, resolve the inner set first and inline the literal values, or route the discovery hop through the graph.
103+
104+
### Graph shape
105+
106+
Node types: `Session`, `App` (claude/codex), `Model`, `Tool`, `File`, `Skill`, `Program`, `Repo`, `Commit`. Edges are Session-rooted: `via` -> App, `used_model` -> Model, `used` -> Tool, `touched` -> File, `ran` -> Skill, `invoked` -> Program, `in` -> Repo, `at` -> Commit (plus Commit -`in`-> Repo). Direction is load-bearing: `--direction out` from a Session lists its resources; `--direction in` from a resource lists the sessions that touched it; depth-2 `both` from a resource is co-occurrence.
107+
108+
Natural keys (what you pass as a seed): Session = session UUID; File = `owner/repo:path` when the session had git metadata, absolute path otherwise; Skill = bare skill name; Program = lowercased basename; Model = model id; Repo = `owner/repo`. Seeds resolve by node_id, then natural_key, then label; an ambiguous seed errors and lists candidates (pass the fuller key). `ran` edges carry `dispatch_*` boolean props saying how the skill was activated.
109+
110+
**File-node identity is split**: the same physical file can exist as both a repo-scoped node (`hyparam/hypaware:src/x.js`) and one or more absolute-path nodes (including worktree and tmp-dir copies). For a complete "who touched this file" answer, enumerate the keys first, then walk each:
111+
112+
```bash
113+
hyp query sql "select node_id, natural_key from node where node_type='File' and natural_key like '%src/core/runtime/bundled.js'" --format json
114+
hyp graph neighbors <each-key> --direction in
115+
```
116+
117+
### Graph commands
118+
119+
```bash
120+
hyp graph neighbors <seed> [--depth N] [--type <NodeType>] [--edge-type <t>...] [--direction out|in|both] [--limit N]
121+
hyp graph project # (re)project the graph from source datasets; idempotent
122+
hyp query sql "select ... from node ..." --format json
123+
hyp query sql "select ... from edge e join node n on ..." --format json
124+
```
125+
126+
### Graph caveats
127+
128+
- **Availability**: `node`/`edge` must appear in `hyp query catalog`. If missing, the graph plugins are not in the active config (fleet-locked installs may need a separate config via `HYP_CONFIG`); discover once, as with `--config`, and reuse.
129+
- **Eventual freshness**: the graph only updates when `hyp graph project` runs; there is no daemon hook. For recency-sensitive questions, check `max(first_seen)` in `node` (or just re-run `hyp graph project` - it is idempotent and cheap) before trusting the graph.
130+
- **SQL over `node`/`edge` is for weights and aggregation only, and has sharp performance tiers** (measured): `graph neighbors` traversal ~0.2s; an edge self-join anchored on a **literal node_id** ~3s; the same join with a scalar subquery (`e1.dst_id = (select node_id from node where ...)`) ~33s. Resolve seed node_ids in a separate query first and inline them. Use SQL only when you need per-edge weights (`count(distinct e.src_id)`) that `neighbors` (a deduplicating BFS) cannot report.
131+
- **SQL join planner**: multi-join queries over `edge`/`node` usually work, but the planner has intermittently failed non-trivial edge self-joins with `Column ... not found`. If that happens, keep the edge self-join adjacent and early, or materialize it as a subquery and join `node` in the outer query; resolve a second node lookup in a separate query.
132+
133+
## Guardrails
134+
135+
- Do not assume the cache auto-refreshes. Query commands default to `--refresh never`.
136+
- Always read stderr. A successful exit code does not mean the cache is current.
137+
- Keep SQL read-only and use only query tables from `hyp query catalog`.
138+
- `hyp query sql` inline output is context-budgeted (cells truncated to ~200 chars, rows dropped past ~32KB) and emits a `notice:` on stderr when it withholds rows - it is not a fixed row cap. Prefer aggregates/filters for analysis; use `--output <file>` for a complete, untruncated result and read it back from the file rather than from stdout.

0 commit comments

Comments
 (0)