feat(parser): add charm crush agent harness support - #1724
Conversation
Charm Crush stores one SQLite database per project under <project>/.crush/crush.db with a projects.json registry at its global data directory. The new provider expands that registry into roots, parses the parts-based message JSON (text, reasoning, tool_call, tool_result, finish), pairs tool results into system messages, links subagent sessions through parent_session_id, and emits one aggregate usage event per session from the cumulative token totals and recorded cost. Timestamps are decoded as Unix seconds, matching what Crush actually writes despite its schema comments claiming milliseconds. Windows users get the confirmed %LOCALAPPDATA%/crush data dir. plans/ records the plan this implementation followed.
Review against the add-new-agent checklist found the provider claimed summary-message handling in the provenance doc without implementing it. Summary rows (is_summary_message=1) now emit as compact-boundary system messages with the original row's model attribution cleared. Freshness gains a per-session row hash with FingerprintHashRequiredForFreshness so same-second edits are not missed by the store's second-resolution timestamps, and watcher events use bounded rowid cursors over sessions and messages so work stays proportional to inserted rows instead of re-enumerating the whole project database. The malformed parts policy (fail that session's parse, matching goose) and cursor behavior are now documented in the provenance entry.
The sync engine writes usage rows only from ParseResult.UsageEvents, but the crush provider left the aggregate session events on the ParsedSession field, which feeds ID validation only, so crush sessions synced with no token usage or cost on the usage page. The provider now carries the events on the ParseResult, verified end to end against a scratch archive. Also wires subagent tool-call linking: crush names subagent sessions <own-uuid>$$<spawning-tool-call-id>, so a parent's tool call now gets SubagentSessionID and renders the inline child transcript.
The plans/ directory is a local advisor workspace documenting design and review decisions; it does not belong in the upstream history.
ecdb657 to
9126700
Compare
The branch's rebase onto main changed the shared parser APIs the crush provider predates: dbBackedProviderSpec.parse takes a context.Context, provider specs are built per StableSourceSnapshots flag, SQLite stores open with immutable=1 for stable snapshots, and inferToolSkillName takes a context. Thread all of them through the crush parser and provider, preserving the WAL-corruption fallback (a stable-snapshot open now fails instead of silently degrading the snapshot guarantee) and fixing the test call sites.
roborev: Combined Review (
|
- Preserve registry root for reconciliation scope resolution by overriding ResolveReconciliationScopes to expand registry roots to their data directories before scope matching - Feature-detect optional message columns (provider, finished_at, is_summary_message) in crushSessionFingerprint so valid older Crush stores without newer columns do not fail fingerprinting - Retain registry path from projects.json for correct project attribution when data directories do not follow the default layout - Honor CRUSH_GLOBAL_DATA and XDG_DATA_HOME environment variables for Crush default directory resolution
roborev: Combined Review (
|
- Feature-detect optional columns (provider, is_summary_message, finished_at) in loadCrushMessages and crushRowIdentityExpression so older valid Crush stores without these columns do not fail parsing or change tracking - Use ExtractProjectFromCwdWithBranchContext in parseCrushSession so stable-snapshot contexts skip local filesystem probing for foreign project paths - Replace for-range loop with maps.Copy in normalizeCrushRoots to satisfy the modernize linter - Fix TestCrushProviderDiscoveryAndRoots on Windows by using filepath.ToSlash to avoid invalid JSON escape sequences in test registry paths - Add TestCrushParseSessionWithoutOptionalColumns to verify parsing works with minimal schema
roborev: Combined Review (
|
Crush keeps many sessions in one SQLite database. The generic sync path parsed every session again even when its stored fingerprint still matched. Use the database-backed sync path so repeated scans leave unchanged archive rows untouched.
AgentCrush was provider-authoritative but missing from the dedicated DB-backed full-sync dispatch and persisted-freshness predicates, so unchanged sessions were reparsed on every full sync. Reconciliation also failed to map configured crush.db roots onto the normalized data directory, leaving deleted sessions stale when the database file was the request root.
roborev: Combined Review (
|
Live crush.db opens must not silently downgrade to immutable mode, which ignores WAL content and can overwrite the archive with stale sessions. Crush sources discovered from a registry or crush.db root also now keep the original configured spelling on ConfiguredRoot so source-machine mapping does not fall back to the expanded data dir.
roborev: Combined Review (
|
…l updated_at Scoped reconciliation providers were rebuilt from expanded data directories, losing registry expansion, project mapping, and the original configured root for machine attribution. TraversalRoots now carry the original configured spelling so NewProvider reconstruction restores that metadata. Fingerprint queries no longer require messages.updated_at, which validateCrushSchema does not demand; minimal accepted stores parse but previously failed every fingerprint.
Bring the PR branch's newer Crush parser and sync work into the local fix before publication. The combined history preserves Crush database roots for reconciliation and uses the database-backed sync path. Missing source sessions remain in the archive. Generated with Codex Co-authored-by: Codex <codex@openai.com>
Bring the contributor branch's newest reconciliation and fingerprint fixes into the combined branch before publication. Scoped reconciliation keeps the configured Crush root when it rebuilds a provider. Fingerprints also work for valid stores without messages.updated_at. Generated with Codex Co-authored-by: Codex <codex@openai.com>
roborev: Combined Review (
|
A Crush registry can change a project's path without changing its data directory or SQLite rows. The old fingerprint treated that session as unchanged and left its archived project metadata stale. Include the resolved project directory in the fingerprint so the next sync updates the session while unchanged registry entries still skip parsing. Generated with Codex Co-authored-by: Codex <codex@openai.com>
Crush derives parent tool-call links from child session rows. The old fingerprint skipped the parent after a child appeared, so the archive kept an empty link. A registry can also enumerate sibling data directories during scoped reconciliation. Keep those directories inside the traversal boundary while proof remains limited to the requested project.
roborev: Combined Review (
|
Summary
Adds Charm Crush (
charmbracelet/crush) as a supported agent.Crush keeps one SQLite store per project at
<project>/.crush/crush.db, with a project registry (projects.json) in its global data directory. The provider expands registry entries into roots, parses the parts-based message JSON (text,reasoning,tool_call,tool_result,finish), pairs tool results into system messages keyed by call ID, links subagent sessions throughparent_session_idand the<uuid>$$<spawning-tool-call-id>child-ID convention, and emits one aggregate usage event per session from cumulative token totals and the recorded provider cost.Notable decisions:
sessionsandmessagesmean a database event re-parses only sessions with new rows; a scheduled reconciliation pass covers metadata-only edits and deletes.messages.partscolumn identifies a Crush store; the vendored goose migration table (goose_db_version) proves nothing and must not be mistaken for the Goose agent's format.Limits: Crush stores no per-request usage, so there is no per-message token attribution or cache-token breakdown; projects registered after startup are picked up by scheduled reconciliation; unknown future
partstypes are skipped rather than failing the session.Where to look:
internal/parser/crush.go(parser),internal/parser/crush_provider.go(discovery, cursors, fingerprint),internal/parser/crush_test.go(16 tests incl. provider-path usage and watcher-bounds coverage), the wiring intypes.go/provider.go/taxonomy.go, frontend color/label, and the pinned provenance entry indocs/internal/session-format-sources.md.