Skip to content

Commit 158734f

Browse files
philcunliffeclaude
andauthored
feat(gascity): register gascity_messages in query catalog + ctvs-gascity skill (co-276c.6) (#110)
Wires the daemon-owned `~/.collectivus/sink/gascity_messages/` parquet store into `ctvs query` as the new `gascity_messages` logical dataset and teaches the `ctvs-gascity` skill how to use it alongside `events`, `session_segments`, and the wire-level `proxy_messages`. - `src/query/schema.js`: add `gascity_messages` to QUERY_DATASETS with the GASCITY_MESSAGES_COLUMNS list (47 cols including agent identity + token hoist) and `gascity` source signal. - `src/query/paths.js`: `discoverGascityPartitions` walks the Hive layout `date=YYYY-MM-DD/city=<name>/part-*.parquet` and yields one CachePartition per part-file. Routed into `expectedCachePartitions` ahead of the JSONL flow so gascity bypasses the parquet-cache layer entirely. Partitions are always-fresh (no `.meta.json` sidecar; the daemon owns writes). - `src/query/sql.js`: `parquetDataSource` skips the `numRows` hint for gascity so squirreling falls back to scan-based COUNT(*) instead of reading 0 from the absent meta sidecar. - `src/query/refresh.js`: `refreshQueryCache` reports gascity partitions as already-fresh (no-op) — the source IS the parquet, nothing to materialize. - `src/cli/query.js`: `catalog`/`status`/`doctor`/`ensureCacheReady` know about gascity — `source_signal` derives from sourceSignalForDataset(), status counts gascity part-files as both source and fresh cache, cache-disabled gates only fire for cache-backed datasets. - `src/cli/init_presets/gascity_skill.md`: third table section with schema highlights, gascity-native example queries (mayor tool calls today, token usage by rig with cache breakdown, tool-result errors, cross-source UNION with proxy_messages), and a "when to use which source" guide. - `README.md` + `skills/collectivus-query/`: short gascity source section and dataset listings updated. - `test/cli/query.test.js`: 6 new tests using the real ParquetWriter to produce fixture sinks under HOME-swapped tmp dirs — catalog/schema, count + group-by SQL, cache-disabled gascity-only queries, refresh no-op, cross-source UNION with proxy_messages, --gateway-id semantics. Acceptance verified: - `ctvs query catalog --format markdown` lists gascity_messages with source_signal=gascity, columns=50, source_partitions counted from disk. - `ctvs query schema gascity_messages --format markdown` returns the full v1 schema (47 typed columns). - All bead-3 example queries run cleanly against fixture data; cross-source UNION uses `part_type` instead of `role` since gascity_messages has no role column (deviation from the bead description, documented in the skill doc). Out of scope (deferred per bead): gemini provider, retention/TTL on parquet store, web dashboard integration. Closes co-276c.6 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fe21900 commit 158734f

11 files changed

Lines changed: 674 additions & 22 deletions

File tree

README.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ tail -f "collectivus-data/$USER/proxy/$(date -u +%F).jsonl"
105105

106106
Full step-by-step: [`docs/walkthrough-claude-code.md`](docs/walkthrough-claude-code.md).
107107

108+
To capture agent-attributed transcripts from a gascity supervisor (separate
109+
from the proxy capture above), attach a city to the same daemon:
110+
111+
```bash
112+
ctvs gascity attach hyptown --api-url http://127.0.0.1:8372
113+
ctvs query sql "select gascity_template, count(*) as parts from gascity_messages group by 1 order by parts desc"
114+
```
115+
116+
See [Gascity source (`gascity_messages`)](#gascity-source-gascity_messages) below.
117+
108118
## Configuration
109119

110120
Pass a JSON config with `--config <path>` (a local path or url). The schema:
@@ -552,7 +562,7 @@ outdated data).
552562
> unchanged; the new warning is written only to stderr. `missing`
553563
> partitions still error.
554564
555-
Logical datasets are `logs`, `traces`, `metrics`, and `proxy_messages`. `ctvs collect <file.jsonl> --name <name>` registers an external JSONL file as a dynamic table; names are normalized for SQL, so `--name random-log` becomes table `random_log`. Collection tables include `_ctvs_source_path`, `_ctvs_line_number`, `_ctvs_raw`, and inferred top-level JSON fields. `ctvs query schema <dataset>` prints the schema, and `ctvs query catalog` shows which datasets have source and cached rows.
565+
Logical datasets are `logs`, `traces`, `metrics`, `proxy_messages`, and `gascity_messages`. `ctvs collect <file.jsonl> --name <name>` registers an external JSONL file as a dynamic table; names are normalized for SQL, so `--name random-log` becomes table `random_log`. Collection tables include `_ctvs_source_path`, `_ctvs_line_number`, `_ctvs_raw`, and inferred top-level JSON fields. `ctvs query schema <dataset>` prints the schema, and `ctvs query catalog` shows which datasets have source and cached rows.
556566

557567
### Conversation log model
558568

@@ -566,6 +576,36 @@ JSON columns (`attributes`, `status`, `tools`, `tool_args`) carry sparse structu
566576

567577
For the full per-column derivation table see [skills/collectivus-query/references/query-cli.md](skills/collectivus-query/references/query-cli.md).
568578

579+
### Gascity source (`gascity_messages`)
580+
581+
`ctvs gascity` is a separate listener that subscribes to a gascity supervisor's
582+
REST API, normalizes provider frames (Claude / Codex), and writes one row per
583+
content block (text / thinking / tool_use / tool_result / attachment) directly
584+
to Parquet at `~/.collectivus/sink/gascity_messages/date=<YYYY-MM-DD>/city=<name>/`.
585+
There is no JSONL stage and no `.meta.json` sidecar: the sink IS the queryable
586+
store, so `ctvs query gascity_messages` is always reading what the daemon has
587+
flushed up to the moment of the call.
588+
589+
```bash
590+
ctvs gascity attach hyptown --api-url http://127.0.0.1:8372
591+
ctvs gascity list
592+
ctvs query schema gascity_messages --format markdown
593+
ctvs query sql "select gascity_template, count(*) from gascity_messages group by 1"
594+
```
595+
596+
`gascity_messages` carries agent-identity columns the proxy can't see —
597+
`gascity_template`, `gascity_rig`, `gascity_alias` — plus per-frame token usage
598+
with cache breakdown (`input_tokens`, `cache_read_input_tokens`,
599+
`cache_creation_input_tokens`). Use it when you need agent-attributed cost
600+
analysis or tool-call inspection; use `proxy_messages` for HTTP-level retry
601+
visibility and request timing. They UNION cleanly via `gateway_id` (a constant
602+
`gascity-scribe` on every gascity row tags the source).
603+
604+
The bundled [`ctvs-gascity` skill](src/cli/init_presets/gascity_skill.md) — installed per-workspace by
605+
`ctvs init gascity` — teaches Claude Code and Codex how to query all three
606+
gascity-aware tables (`events`, `session_segments`, `gascity_messages`) and
607+
their cross-source joins with `proxy_messages`.
608+
569609
### LLM skill
570610

571611
Install the bundled `collectivus-query` skill so Claude Code and Codex know how

skills/collectivus-query/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ Use `JSON_VALUE(<col>, '$.path')` to extract scalars from the `attributes` / `st
107107
- Always read stderr. A successful exit code does not mean the data is fresh — a `warning: querying stale data; …` line on stderr means stdout reflects outdated Parquet, and the user should be told before drawing conclusions.
108108
- Do not paste `--config` into every command by habit. Use it when discovery shows the service is not using `~/.hyp/collectivus.json`.
109109
- Do not read arbitrary Parquet files directly for `ctvs query sql`; the CLI only allows logical tables.
110-
- Keep SQL read-only and use only logical datasets: `logs`, `traces`, `metrics`, `proxy_messages`, and registered collection tables from `ctvs query catalog`.
110+
- Keep SQL read-only and use only logical datasets: `logs`, `traces`, `metrics`, `proxy_messages`, `gascity_messages`, and registered collection tables from `ctvs query catalog`.
111111
- Use UTC dates with `--date YYYY-MM-DD`.
112112
- Use `--service`, `--gateway-id`, `--from`, `--to`, or `--since` to narrow broad investigations.
113113

skills/collectivus-query/references/query-cli.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ Collection tables always include `_ctvs_source_path`, `_ctvs_line_number`, and `
8080
- `traces`: OTLP spans. Common columns include `gateway_id`, `date`, `traceId`, `spanId`, `parentSpanId`, `name`, `kind`, `startTimestamp`, `endTimestamp`, `durationMs`, `status`, `serviceName`, `resource`, `scope`, and `attributes`.
8181
- `metrics`: OTLP metric points. Common columns include `gateway_id`, `date`, `metricName`, `metricType`, `timestamp`, `startTimestamp`, `serviceName`, `value`, `valueInt`, `count`, `sum`, `unit`, `resource`, `scope`, and `attributes`.
8282
- `proxy_messages`: One row per LLM proxy content part (text block, tool call, tool result, etc.), globally deduped by content-derived `message_id`. See **proxy_messages columns** below for the full 26-column schema; `gateway_id` and `date` are added as partition columns on the on-disk Parquet.
83+
- `gascity_messages`: One row per content block from gascity-captured agent sessions (text, thinking, tool_use, tool_result, attachment). Captured by the `ctvs gascity` supervisor source — agent-attributed (`gascity_template` / `gascity_rig` / `gascity_alias`) and includes per-frame token usage with cache breakdown. Always fresh: the daemon writes Parquet directly to `~/.collectivus/sink/gascity_messages/date=<YYYY-MM-DD>/city=<name>/` (no JSONL stage, no `.meta.json` sidecar). The constant `gateway_id = 'gascity-scribe'` tags the source for cross-source UNIONs with `proxy_messages`. Run `ctvs query schema gascity_messages --format markdown` for the full 47-column schema.
8384

8485
Run `ctvs query schema <dataset> --format json` for the exact columns in the installed version.
8586

src/cli/init_presets/gascity_skill.md

Lines changed: 95 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,55 @@
11
---
22
name: ctvs-gascity
3-
description: Query the gascity event log and session-reconciler segments registered by `ctvs init gascity` in this workspace. Use when the user asks about gc agents, beads, orders, mail, sessions, or session-reconciler decisions.
3+
description: Query the gascity event log, session-reconciler segments, and `gascity_messages` agent transcripts. Use when the user asks about gc agents, beads, orders, mail, sessions, session-reconciler decisions, agent tool calls, or LLM token usage by rig/template.
44
---
55

66
# Gascity Query
77

8-
This workspace has been registered with the `ctvs query` cache via `ctvs init gascity`. Two collection tables are available alongside the built-in datasets:
8+
This workspace has been registered with the `ctvs query` cache via `ctvs init gascity`. Three tables are available alongside the global built-in datasets:
99

10-
- **`events`** — one row per gascity event from `.gc/events.jsonl` (bead lifecycle, order execution, mail, sessions, controller events). Single source file.
11-
- **`session_segments`** — one row per tracepoint from `.gc/runtime/session-reconciler-trace/segments/**/*.jsonl` (baseline, decision, mutation, operation records per session reconciler cycle). Glob-backed; many source files, one cache partition each.
10+
- **`events`** — one row per gascity event from `.gc/events.jsonl` (bead lifecycle, order execution, mail, sessions, controller events). Single source file, registered as a collection.
11+
- **`session_segments`** — one row per tracepoint from `.gc/runtime/session-reconciler-trace/segments/**/*.jsonl` (baseline, decision, mutation, operation records per session reconciler cycle). Glob-backed collection; many source files, one cache partition each.
12+
- **`gascity_messages`** — one row per content block from gascity-captured agent sessions (text, thinking, tool_use, tool_result, attachment). Captured by the `ctvs gascity` source from the supervisor REST API; provider-native frames preserved verbatim in `raw_frame`. Built-in dataset (no `ctvs init gascity` needed) — partitioned at `~/.collectivus/sink/gascity_messages/date=<YYYY-MM-DD>/city=<name>/`.
1213

13-
Refer to the global [`collectivus-query`](../collectivus-query/SKILL.md) skill for cache freshness rules, `--format` options, and built-in datasets like `proxy_messages`.
14+
Refer to the global [`collectivus-query`](../collectivus-query/SKILL.md) skill for cache freshness rules, `--format` options, and the wire-level `proxy_messages` dataset.
1415

1516
## Agent targeting
1617

17-
There is no `cwd` column on these tables. Agent identity lives in different columns per table:
18+
Each table identifies the originating agent through a different column. There is no `cwd` on `events` or `session_segments`; on `gascity_messages` the `cwd` is the agent's working directory at frame time.
1819

1920
- `events.actor` — e.g. `hypcity-overrides.mayor`, `hypcity-overrides.refinery`, `hypcity-overrides.deacon`.
2021
- `session_segments.template` — e.g. `hypcity-overrides.mayor` (city-scoped) or `collectivus/hypcity-overrides.polecat` (rig-scoped: `<rig>/<pack>.<agent>`).
22+
- `gascity_messages.gascity_template` — same shape as `session_segments.template`. Pair with `gascity_rig` and `gascity_alias` for finer cuts. The provider-side session id is `gascity_session_id` / `provider_session_id`.
2123

2224
Matching patterns:
2325

2426
```sql
2527
-- a specific agent (city-scoped)
2628
where actor = 'hypcity-overrides.mayor'
29+
where gascity_template = 'hypcity-overrides.mayor'
2730

2831
-- any rig's refinery
2932
where template LIKE '%/hypcity-overrides.refinery'
33+
where gascity_template LIKE '%/hypcity-overrides.refinery'
3034

3135
-- everything in the collectivus rig
3236
where template LIKE 'collectivus/%'
37+
where gascity_rig = 'collectivus'
3338
```
3439

35-
To cross-reference an agent's actions with its proxied LLM calls, join `template` (this skill) against `cwd` (global `proxy_messages` table) via the workspace path embedded in the rig prefix.
40+
To cross-reference an agent's actions with its proxied LLM calls, join `template` against `cwd` (`proxy_messages`) via the workspace path embedded in the rig prefix, or join `gascity_messages.provider_session_id` to `proxy_messages.conversation_id` when both sources captured the same Claude Code session.
41+
42+
## `gascity_messages` schema highlights
43+
44+
Grain is one row per content block (text, thinking, tool_use, tool_result, attachment). Run `ctvs query schema gascity_messages --format markdown` for the authoritative column list — there are 40+ columns; the highlights below are the ones you reach for first.
45+
46+
- **Identity:** `gascity_template`, `gascity_rig`, `gascity_alias`, `gascity_session_id`, `provider`, `provider_session_id`, `provider_uuid`, `gateway_id` (constant `gascity-scribe` so cross-source unions can tag the source).
47+
- **Frame metadata:** `part_type` (`text` / `thinking` / `tool_use` / `tool_result` / `attachment`), `part_index`, `message_id`, `message_created_at`, `conversation_started_at`, `cwd`, `git_branch`, `permission_mode`, `is_sidechain`, `parent_uuid`, `prompt_id`, `request_id`.
48+
- **Assistant-only hoist (null elsewhere):** `model`, `stop_reason`, `stop_details`, `input_tokens`, `output_tokens`, `cache_creation_input_tokens`, `cache_read_input_tokens`, `ephemeral_1h_input_tokens`, `ephemeral_5m_input_tokens`, `service_tier`, `inference_geo`, `speed`.
49+
- **Content-block specific:** `content_text`, `thinking_signature`, `tool_name`, `tool_call_id`, `tool_args` (JSON), `caller_type`, `tool_result_for`, `is_error`, `attachment_type`, `hook_event`.
50+
- **Overflow:** `attributes` (unmapped fields, JSON) and `raw_frame` (verbatim original envelope, JSON) keep nothing the supervisor sent from being lost.
51+
52+
> Note: `gascity_messages` does not carry a `role` column. Use `part_type` to split assistant-side frames (`text` / `thinking` / `tool_use`) from user/tool-side frames (`tool_result`).
3653
3754
## Common queries
3855

@@ -74,10 +91,80 @@ ORDER BY ts;
7491

7592
Use `_ctvs_source_path` to see which segment file a row came from, and `_ctvs_line_number` for the line inside that file.
7693

94+
## `gascity_messages` queries
95+
96+
```sql
97+
-- All tool calls by the mayor today
98+
SELECT message_created_at, tool_name, JSON_VALUE(tool_args, '$.command') AS command
99+
FROM gascity_messages
100+
WHERE gascity_template = 'hypcity-overrides.mayor'
101+
AND part_type = 'tool_use'
102+
AND date = strftime(NOW(), '%Y-%m-%d')
103+
ORDER BY message_created_at DESC
104+
LIMIT 50;
105+
106+
-- Token usage by rig over the last 24h. `input_tokens` is set only on
107+
-- assistant frames, so summing across the whole table picks up exactly the
108+
-- assistant-side cost (NULL inputs from tool_use / tool_result / text are
109+
-- skipped by SUM).
110+
SELECT gascity_rig,
111+
SUM(input_tokens) AS input,
112+
SUM(output_tokens) AS output,
113+
SUM(cache_read_input_tokens) AS cache_hits,
114+
SUM(cache_creation_input_tokens) AS cache_creates
115+
FROM gascity_messages
116+
WHERE message_created_at >= NOW() - INTERVAL '1 day'
117+
AND input_tokens IS NOT NULL
118+
GROUP BY gascity_rig
119+
ORDER BY input DESC;
120+
121+
-- Tool result that came back as an error, last hour, any agent
122+
SELECT gascity_template, tool_name, content_text
123+
FROM gascity_messages
124+
WHERE part_type = 'tool_result'
125+
AND is_error = true
126+
AND message_created_at >= NOW() - INTERVAL '1 hour'
127+
ORDER BY message_created_at DESC;
128+
129+
-- Sanity-check: for a Claude Code session captured by both sources,
130+
-- compare token counts. `proxy_messages.conversation_id` is the Claude
131+
-- Code session id, which gascity records as `provider_session_id`.
132+
SELECT
133+
COALESCE(p.conversation_id, g.provider_session_id) AS session,
134+
SUM(CAST(JSON_VALUE(p.attributes, '$.usage.input_tokens') AS BIGINT)) AS proxy_input,
135+
SUM(g.input_tokens) AS gascity_input
136+
FROM proxy_messages p
137+
FULL OUTER JOIN gascity_messages g
138+
ON p.conversation_id = g.provider_session_id
139+
WHERE p.role = 'assistant'
140+
OR g.input_tokens IS NOT NULL
141+
GROUP BY session
142+
ORDER BY proxy_input DESC NULLS LAST
143+
LIMIT 20;
144+
145+
-- Cross-source UNION: text content from both sources for one Claude Code session
146+
SELECT 'proxy' AS source, message_index AS idx, content_text
147+
FROM proxy_messages
148+
WHERE conversation_id = '<session-id>' AND role = 'assistant' AND part_type = 'text'
149+
UNION ALL
150+
SELECT 'gascity' AS source, part_index AS idx, content_text
151+
FROM gascity_messages
152+
WHERE provider_session_id = '<session-id>' AND part_type = 'text'
153+
ORDER BY source, idx;
154+
```
155+
156+
## When to use which source
157+
158+
- Use **`gascity_messages`** when you want: agent identity (`gascity_template` / `gascity_rig`), structured content blocks, tool calls + arguments + results in one table, per-frame token usage with cache breakdown, no need for HTTP wire detail.
159+
- Use **`proxy_messages`** when you want: HTTP retry visibility, request timing, response status codes, end-user attribution via the Anthropic `user_id`, conversation-grain dedup of replayed history.
160+
- Use **both** (UNION ALL or FULL OUTER JOIN) for cross-source aggregations, sanity checks, or to recover content that one source missed (e.g., gascity captured an in-process supervisor frame the proxy never saw).
161+
77162
## Freshness
78163

79164
`session_segments` is glob-backed: new segment files only appear in the cache after a refresh. Run `ctvs query refresh session_segments` to pick up new segments, or use `--refresh always` on any query. Deleted segment files are pruned from the cache on the next refresh.
80165

81166
`events` is append-only single-file; mtime/size changes trigger re-materialization on refresh.
82167

83-
Full schemas: `ctvs query schema events --format markdown`, `ctvs query schema session_segments --format markdown`. Catalog: `ctvs query catalog --format markdown`.
168+
`gascity_messages` is **always fresh** — the daemon writes Parquet directly into the sink (no JSONL stage, no `.meta.json` sidecar), so query-time discovery picks up every part-file the writer has flushed. `ctvs query refresh gascity_messages` is a documented no-op (it lists existing partitions as already-fresh). To pull in newly-flushed rows simply rerun the query.
169+
170+
Full schemas: `ctvs query schema events --format markdown`, `ctvs query schema session_segments --format markdown`, `ctvs query schema gascity_messages --format markdown`. Catalog: `ctvs query catalog --format markdown`.

0 commit comments

Comments
 (0)