Skip to content

Commit 88ff290

Browse files
committed
Merge origin/master into npx quickstart PR
2 parents 3b2d0ca + f6fd8de commit 88ff290

34 files changed

Lines changed: 1659 additions & 1092 deletions

CHANGELOG.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed (breaking)
11+
12+
- Local `ctvs query` cache storage now uses local Iceberg tables under
13+
`.collectivus-query/cache` instead of Parquet sidecars under
14+
`.collectivus-query/parquet`. Config is now `query.cache.enabled` /
15+
`query.cache.dir`, and the CLI override is `--cache-dir`.
16+
- Refreshes keep per-source JSONL cursors and append from the last processed
17+
byte where possible. Source truncation/rewrite or collection schema drift
18+
starts a new source epoch instead of recreating every cache partition.
19+
- Glob-backed collection sources that disappear remain queryable as cache-only
20+
partitions instead of being pruned on refresh.
21+
1022
## [2.2.0] — 2026-05-14
1123

1224
### Added
@@ -76,7 +88,7 @@ new schema reference, working SQL patterns, and dataset semantics.
7688

7789
### Changed (breaking)
7890

79-
- **`ctvs query` allows stale Parquet cache by default.** Stale partitions
91+
- **`ctvs query` allows stale query cache by default.** Stale partitions
8092
(Parquet exists but may be outdated) now query successfully and emit a
8193
`warning: querying stale data; …` line to stderr instead of exiting non-zero.
8294
Stdout output (table, json, jsonl, markdown) is unchanged.

README.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ stay on this machine.
2121
per content block with agent identity and token usage.
2222
- **Application telemetry**: OTLP traces, metrics, and logs over HTTP,
2323
normalized to JSONL by signal and service.
24-
- **Local query**: Parquet-backed `ctvs query` commands and SQL over
24+
- **Local query**: Iceberg-backed `ctvs query` commands and SQL over
2525
`logs`, `traces`, `metrics`, `proxy_messages`, `gascity_messages`, and
2626
registered JSONL collections.
2727
- **Optional operations path**: export to local Parquet, archive daily
@@ -129,7 +129,7 @@ Pass a JSON config with `--config <path>` (a local path or url). The schema:
129129
"redact_headers": ["authorization", "x-api-key", "anthropic-api-key", "cookie", "set-cookie"]
130130
},
131131
"sink": { "type": "file", "dir": "./collectivus-data" },
132-
"query": { "parquet": { "enabled": true } }
132+
"query": { "cache": { "enabled": true } }
133133
}
134134
```
135135

@@ -141,7 +141,7 @@ Pass a JSON config with `--config <path>` (a local path or url). The schema:
141141
| `sink` | Root directory for Standalone JSONL recordings. Proxy rows land under `<sink.dir>/<gateway_id>/proxy/`; OTLP rows land under `<sink.dir>/<gateway_id>/<signal>/`. Required when `otel` or `proxy` is set in Standalone mode. Accepted but unused in Gateway mode. |
142142
| `central_server` | Gateway-mode Central server URL, identity settings, config poll interval, and optional `outbox_dir`. Gateway rows are first fsynced to this durable local outbox, then shipped to Central ingest. |
143143
| `upload` | Optional. Enables the daily S3 parquet drain. See [S3 upload](#s3-upload). |
144-
| `query` | Optional. Configures the local `ctvs query` Parquet cache. `query.parquet.enabled` defaults to `true`; `query.parquet.dir` defaults to `<recording-root>/.collectivus-query/parquet`. |
144+
| `query` | Optional. Configures the local `ctvs query` query cache. `query.cache.enabled` defaults to `true`; `query.cache.dir` defaults to `<recording-root>/.collectivus-query/cache`. |
145145

146146
`--print-config` loads, validates, and pretty-prints the resolved config:
147147

@@ -268,7 +268,7 @@ and [provider fields](https://developers.openai.com/codex/config-reference#model
268268
## Local query
269269

270270
`ctvs query` reads local recordings only. It never contacts S3 and it does not
271-
auto-refresh its Parquet cache unless you ask for that explicitly.
271+
auto-refresh its query cache unless you ask for that explicitly.
272272

273273
```bash
274274
ctvs query refresh /path/to/gw1/logs/2026-05-11.jsonl --config collectivus.json
@@ -283,17 +283,19 @@ ctvs collect --glob '.gc/runtime/**/*.jsonl' --name session-segments --config co
283283
ctvs query sql "select * from random_log" --config collectivus.json
284284
```
285285

286-
Cache files are written under
287-
`<recording-root>/.collectivus-query/parquet/<dataset>/gateway_id=<id>/date=<YYYY-MM-DD>/data.parquet`
288-
with `data.parquet.meta.json` sidecars.
286+
Cache cursors are written under
287+
`<recording-root>/.collectivus-query/cache/datasets/<dataset>/gateway_id=<id>/date=<YYYY-MM-DD>/cursor.json`.
288+
Rows live in local Iceberg tables under the same partition directory. Refreshes
289+
append from the last recorded JSONL cursor when possible; truncation, rewrite,
290+
or schema drift starts a new source epoch.
289291

290292
Freshness is treated asymmetrically (since v1.7.0):
291293

292294
| Partition state | Behavior |
293295
| --- | --- |
294296
| `fresh` | Query proceeds silently. |
295-
| `stale` (Parquet exists, may be outdated) | Query proceeds; a `warning: querying stale data; …` line is written to stderr. Stdout is unchanged. |
296-
| `missing` (no Parquet at all) | Query exits with the exact file-targeted `ctvs query refresh …` command to run when the source file is known. |
297+
| `stale` (cache exists, may be outdated) | Query proceeds; a `warning: querying stale data; …` line is written to stderr. Stdout is unchanged. |
298+
| `missing` (no cache table/cursor) | Query exits with the exact file-targeted `ctvs query refresh …` command to run when the source file is known. |
297299

298300
Use `ctvs query refresh <file.jsonl>` to refresh selected source files, or
299301
`ctvs query refresh --all [dataset]` when you explicitly want the broader
@@ -308,7 +310,7 @@ outdated data).
308310
> unchanged; the new warning is written only to stderr. `missing`
309311
> partitions still error.
310312
311-
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; `ctvs collect --glob <pattern> --name <name>` backs one table with many JSONL files. 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.
313+
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; `ctvs collect --glob '<pattern>' --name <name>` backs one table with many source files. 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. Deleted glob sources remain queryable from their cache-only partitions until the collection is removed. `ctvs query schema <dataset>` prints the schema, and `ctvs query catalog` shows which datasets have source and cached rows.
312314

313315
### Conversation log model
314316

@@ -617,7 +619,7 @@ supported in this build.
617619
| `ctvs detach [--client claude\|codex\|all]` | Revert Claude Code and/or Codex without uninstalling the daemon |
618620
| `ctvs status` | Print daemon (loaded / PID) and Claude Code (attached) state |
619621
| `ctvs export --config <path> [...]` | Convert recorded JSONL to local Parquet without invoking the upload scheduler |
620-
| `ctvs query <command> [...]` | Query local recordings through the explicit Parquet cache |
622+
| `ctvs query <command> [...]` | Query local recordings through the explicit query cache |
621623
| `ctvs collect <file.jsonl>\|--glob <pattern> --name <name>` | Register external JSONL as a dynamic query table |
622624
| `ctvs skills install [--client claude\|codex\|all]` | Install the bundled Collectivus query LLM skill |
623625

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"dependencies": {
5353
"hyparquet": "1.25.8",
5454
"hyparquet-compressors": "1.1.1",
55+
"icebird": "0.7.0",
5556
"squirreling": "0.12.19"
5657
}
5758
}

skills/collectivus-query/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Inspect local Collectivus recordings with the ctvs query CLI. Use w
55

66
# Collectivus Query
77

8-
Use `ctvs query` to inspect local Collectivus recordings. It reads local JSONL recordings and an explicit local Parquet cache; it does not query S3.
8+
Use `ctvs query` to inspect local Collectivus recordings. It reads local JSONL recordings and an explicit local query cache; it does not query S3.
99

1010
## Workflow
1111

@@ -39,7 +39,7 @@ ctvs collect <file.jsonl> --name <name>
3939
ctvs collect --glob '<pattern>' --name <name>
4040
```
4141

42-
`ctvs collect` registers an external local JSONL file as a dynamic SQL table and immediately refreshes its Parquet cache. Collection names are normalized for SQL (`random-log` -> `random_log`). Query them with `ctvs query sql "select * from random_log"`. Pass `--glob '<pattern>'` instead of a single path to back one logical table with many files; each matched file becomes its own cache partition and `_ctvs_source_path` tells you which file a row came from.
42+
`ctvs collect` registers an external local JSONL file as a dynamic SQL table and immediately refreshes its query cache. Collection names are normalized for SQL (`random-log` -> `random_log`). Query them with `ctvs query sql "select * from random_log"`. Pass `--glob '<pattern>'` instead of a single path to back one logical table with many files; each matched file becomes its own cache partition and `_ctvs_source_path` tells you which file a row came from.
4343

4444
## Proxy conversation log model
4545

@@ -107,7 +107,7 @@ Use `JSON_VALUE(<col>, '$.path')` to extract scalars from the `attributes` / `st
107107
## Guardrails
108108

109109
- Do not assume the cache auto-refreshes. Query commands default to `--refresh never`, and stale partitions return data with a stderr warning rather than refreshing themselves.
110-
- 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.
110+
- 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 cache rows, and the user should be told before drawing conclusions.
111111
- Do not paste `--config` into every command by habit. Use it when discovery shows the service is not using `~/.hyp/collectivus.json`.
112112
- Do not read arbitrary Parquet files directly for `ctvs query sql`; the CLI only allows logical tables.
113113
- Keep SQL read-only and use only logical datasets: `logs`, `traces`, `metrics`, `proxy_messages`, `gascity_messages`, and registered collection tables from `ctvs query catalog`.

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Collectivus Query CLI Reference
22

3-
`ctvs query` reads local Collectivus recordings. It materializes JSONL source files into a local Parquet cache under:
3+
`ctvs query` reads local Collectivus recordings. It materializes JSONL source files into a local query cache under:
44

55
```text
6-
<recording-root>/.collectivus-query/parquet/<dataset>/gateway_id=<id>/date=<YYYY-MM-DD>/data.parquet
6+
<recording-root>/.collectivus-query/cache/datasets/<dataset>/gateway_id=<id>/date=<YYYY-MM-DD>/cursor.json
77
```
88

99
External JSONL collections registered with `ctvs collect <file.jsonl> --name <name>` are recorded in:
@@ -12,15 +12,15 @@ External JSONL collections registered with `ctvs collect <file.jsonl> --name <na
1212
<recording-root>/.collectivus-query/collections.json
1313
```
1414

15-
Their Parquet cache lives under `<recording-root>/.collectivus-query/parquet/collections/<table>/data.parquet`. Names are normalized for SQL, so `--name random-log` exposes table `random_log`.
15+
Their query cache lives under `<recording-root>/.collectivus-query/cache/collections/<table>/source=<hash>/cursor.json`, with rows stored in local Iceberg tables below each source partition. Names are normalized for SQL, so `--name random-log` exposes table `random_log`.
1616

1717
The cache is explicit. Query commands do not refresh it unless `--refresh always` is passed.
1818

1919
Freshness is asymmetric (since v1.7.0):
2020

2121
- `fresh` — query proceeds silently.
22-
- `stale` (Parquet exists but may be outdated) — query proceeds and writes a `warning: querying stale data; N partition(s) outdated [...] — run '...' to update` line to stderr. Stdout is unchanged.
23-
- `missing` (no Parquet at all) — query exits with the exact `ctvs query refresh ...` command to run.
22+
- `stale` (cache exists but may be outdated) — query proceeds and writes a `warning: querying stale data; N partition(s) outdated [...] — run '...' to update` line to stderr. Stdout is unchanged.
23+
- `missing` (no cache table/cursor) — query exits with the exact `ctvs query refresh ...` command to run.
2424

2525
Pass `--strict-freshness` to restore the pre-1.7 behavior where stale partitions are a hard error.
2626

@@ -29,7 +29,7 @@ Commands default to `~/.hyp/collectivus.json`. If the running gateway or OTEL co
2929
## Shared Options
3030

3131
- `--config <path|url>`: Collectivus config. Defaults to `~/.hyp/collectivus.json`.
32-
- `--parquet-dir <dir>`: Override the query cache directory.
32+
- `--cache-dir <dir>`: Override the query cache directory.
3333
- `--from <timestamp>` / `--to <timestamp>`: Inclusive timestamp bounds.
3434
- `--since <duration>`: Relative lower bound such as `15m`, `2h`, or `7d`.
3535
- `--date <YYYY-MM-DD>`: Restrict to one UTC date partition.
@@ -48,8 +48,8 @@ Commands default to `~/.hyp/collectivus.json`. If the running gateway or OTEL co
4848
- `ctvs query status`: Inspect source partitions and cache freshness.
4949
- `ctvs query catalog`: List logical datasets, columns, source partitions, and cached row counts.
5050
- `ctvs query schema <dataset>`: Print static schema for a logical dataset.
51-
- `ctvs query refresh <file.jsonl>... [--force]`: Materialize selected JSONL source files into query-cache Parquet.
52-
- `ctvs query refresh --all [dataset] [--force]`: Materialize all matching JSONL source files into query-cache Parquet.
51+
- `ctvs query refresh <file.jsonl>... [--force]`: Materialize selected JSONL source files into the query cache.
52+
- `ctvs query refresh --all [dataset] [--force]`: Materialize all matching JSONL source files into the query cache.
5353
- `ctvs query sample <dataset>`: Show sample rows.
5454
- `ctvs query sql <select-sql>`: Run read-only SQL over logical datasets.
5555
- `ctvs query logs [count|tail]`: List logs, count logs, or tail live JSONL without requiring cache.
@@ -71,9 +71,9 @@ ctvs collect --glob '/path/to/segments/**/*.jsonl' --name segments
7171
ctvs query sql "select * from random_log" --format json
7272
```
7373

74-
`ctvs collect` stores the absolute source path (or glob) and immediately refreshes the Parquet cache. If the source file changes later, normal query freshness rules apply: stale cached data is queryable with a stderr warning, `--strict-freshness` turns that into an error, and `ctvs query refresh <file.jsonl>` refreshes selected files. Use `--refresh always` to refresh before running the query.
74+
`ctvs collect` stores the absolute source path (or glob) and immediately refreshes the query cache. If the source file changes later, normal query freshness rules apply: stale cached data is queryable with a stderr warning, `--strict-freshness` turns that into an error, and `ctvs query refresh <file.jsonl>` refreshes selected files. Use `--refresh always` to refresh before running the query.
7575

76-
With `--glob`, one logical table is backed by many source files: each matched file becomes its own cache partition under `.collectivus-query/parquet/collections/<table>/source=<hash>/data.parquet`, and only files whose mtime/size changed re-materialize on refresh. Files that no longer match the glob are pruned from the cache on the next refresh. Inside SQL, use `_ctvs_source_path` to see which file a row came from.
76+
With `--glob`, one logical table is backed by many source files: each matched file becomes its own cache partition under `.collectivus-query/cache/collections/<table>/source=<hash>/cursor.json`, and refresh appends from each file's recorded cursor when possible. Files that no longer match the glob remain queryable as cache-only partitions. Inside SQL, use `_ctvs_source_path` to see which file a row came from.
7777

7878
Collection tables always include `_ctvs_source_path`, `_ctvs_line_number`, and `_ctvs_raw`, plus inferred top-level JSON fields. Use `--timestamp-column <field>` when registering a file if `--from`, `--to`, `--since`, or `--date` should use a specific field.
7979

@@ -82,7 +82,7 @@ Collection tables always include `_ctvs_source_path`, `_ctvs_line_number`, and `
8282
- `logs`: OTLP log records. Common columns include `gateway_id`, `date`, `timestamp`, `observedTimestamp`, `severityNumber`, `severityText`, `serviceName`, `body`, `traceId`, `spanId`, `resource`, `scope`, and `attributes`.
8383
- `traces`: OTLP spans. Common columns include `gateway_id`, `date`, `traceId`, `spanId`, `parentSpanId`, `name`, `kind`, `startTimestamp`, `endTimestamp`, `durationMs`, `status`, `serviceName`, `resource`, `scope`, and `attributes`.
8484
- `metrics`: OTLP metric points. Common columns include `gateway_id`, `date`, `metricName`, `metricType`, `timestamp`, `startTimestamp`, `serviceName`, `value`, `valueInt`, `count`, `sum`, `unit`, `resource`, `scope`, and `attributes`.
85-
- `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.
85+
- `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 in the query cache.
8686
- `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.
8787

8888
Run `ctvs query schema <dataset> --format json` for the exact columns in the installed version.
@@ -120,7 +120,7 @@ Grain is one row per content part. Rows are deduplicated by `message_id` so a si
120120
| 25 | `status` | JSON | yes | Sparse: `tool_status` on tool_result, `finish_reason` on the last assistant part, `error_code` / `error_message` on error blocks | `null` when no key applies. |
121121
| 26 | `attributes` | JSON | yes | `request` settings (`max_tokens`, `thinking`, `output_config`, `context_management`, `stream`), `usage` (assistant only — `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`), `timing.latency_ms`, `client.claude_version`, `provider_raw.metadata` | `null` when no key applies. |
122122

123-
The on-disk Parquet additionally carries `gateway_id` (STRING, not null) as the leading column and `date` (STRING, not null) at the end. Both are partition keys; `date` is derived from `message_created_at` in UTC.
123+
The query-cache table additionally carries `gateway_id` (STRING, not null) as the leading column and `date` (STRING, not null) at the end. Both are partition keys; `date` is derived from `message_created_at` in UTC.
124124

125125
## Example SQL
126126

src/cli.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Commands:
5757
ctvs status Report daemon, config, recordings, attach state
5858
ctvs gascity <subcommand> [...] Manage gascity supervisor capture sources
5959
ctvs export --config <path|url> [...] Convert recorded JSONL to Parquet
60-
ctvs query <command> [...] Query local recordings through Parquet cache
60+
ctvs query <command> [...] Query local recordings through query cache
6161
ctvs collect <file.jsonl> --name <name> Add external JSONL as a query table
6262
ctvs skills install [...] Install the Collectivus query LLM skill
6363
ctvs ignore <add|remove|list> [path] Suppress Claude recording for a folder

0 commit comments

Comments
 (0)