Hook-driven claim extraction for guard mode (precise mode, opt-in)#115
Hook-driven claim extraction for guard mode (precise mode, opt-in)#115justinstimatze wants to merge 3 commits into
Conversation
|
Smoke-tested against the live API after all — borrowed an Anthropic key for a single call. Confirms: SDK 0.92.0 accepts the request shape (cache_control 1h TTL + tool schema + tool_choice), the model returns the v7 format, basis classification looks sensible, intra-batch edges resolve correctly, latency ~3.7s for a 5-turn transcript fits the Stop-hook budget. |
|
@justinstimatze feedback from codex and claude code Both reviewers agree: Approve with minor suggestions Shared findings (flagged independently by both) ` Shared findings (flagged independently by both) ┌───────────────────┬──────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ Additional from Codex ┌───────────────────┬──────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ Additional from my review ┌───────────────────────────────┬──────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────┐ Both reviewers praised
|
|
Bottom line One actionable item before merge: combine readRecentTranscript and countTranscriptTurns into a single pass to eliminate the double-read and the race |
Guard mode currently relies on the host model voluntarily calling buddy_observe with claims/edges. As context grows past ~100k tokens the model drops these calls and the graph goes silent. This adds an opt-in precise mode: the Stop hook reads the transcript JSONL directly (with a 2MB tail seek for long sessions), calls Anthropic with a v7 extraction prompt + 1h prompt cache, and feeds the structured output into the existing runGuardPipeline. UserPromptSubmit drains pending findings into the next prompt as a [buddy observation] block. Independent of the host model's attention budget; works through long sessions. Opt-in via BUDDY_EXTRACTION_KEY (or ANTHROPIC_API_KEY). Without a key, guard mode falls back to today's behavior — no regression. Highlights - Per-host-session cursor + cross-batch existing-claims context so consecutive Stop hooks don't re-extract the same window. - Persistent extraction telemetry (reasoning_extraction_stats) visible across the MCP server / Stop hook process boundary; in-memory counters reset every Stop hook process so the doctor needs DB-backed state to see hook-side activity. - WAL journaling, atomic JSON1 increments in recordFailure, cross- process graph-cache invalidation via PRAGMA data_version (catches the case where the Stop hook's writes don't bump the MCP server's in-process generation counter). - Backoff after sustained failure with graceful fallback to accepting model claims so the graph doesn't go silent during outages. - Mute respected: hook skips extraction, UserPromptSubmit skips delivery while mood='muted'. - buddy_forget all clears the new tables so the cursor doesn't point past an empty graph. - Doctor + buddy_reasoning_status surface mode, stats, backoff state. - BUDDY_DEBUG=1 logs per-claim drop reasons in writeClaims and a shape snapshot at the buddy_observe boundary. Schema - New tables: reasoning_extraction_state (per-host-session cursor), reasoning_extraction_stats (per-companion telemetry). - New column on reasoning_observe_seq: last_delivered_finding_id. - All idempotent — CREATE TABLE IF NOT EXISTS and try/wrapped ALTER. Dep - @anthropic-ai/sdk added (loaded only when an extraction key is present at hook fire time; not loaded otherwise). Tests - ~90 new across the extractor, persistent state, multi-call incremental flow (regression test for the duplication bug), end- to-end with stubbed SDK, cross-connection cache invalidation, doctor regressions, mute respect, BUDDY_DEBUG output. - 826/829 in the full suite pass; the 3 remaining are pre-existing penguin animation / oldBuddy stats randomness on master. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapses readRecentTranscript and countTranscriptTurns into one parse
that returns { chunk, totalTurns }. Closes the race window where the
transcript could grow between the two reads, advancing the cursor past
turns the LLM never saw — and drops the redundant read.
The tail-seek micro-optimization (when sinceTurn==0) is gone with it;
once a cursor exists every fire was already doing a full scan, so the
savings only applied to the first fire and complicated the cursor
advance for marginal benefit. Full reads on multi-MB transcripts parse
in well under 50ms.
Per maintainer feedback on PR fiorastudio#115.
ebd427c to
c268f24
Compare
|
Done in c268f24 — Side effect: the 2MB tail-seek on the first fire dropped out with it. Once a cursor exists every subsequent fire was already doing a full scan, so the optimization only applied to fire #1 and made the cursor advance fiddly to keep race-free. Full reads on multi-MB transcripts parse in well under 50ms either way. Tests updated (29 passing across the affected suites; full repo at 827/830, same 3 pre-existing failures as before). |
- stop-handler: pipeline-throw stderr is no longer BUDDY_DEBUG-gated. Backoff/API-failure logs stay gated (expected, recurring, captured in stats), but a pipeline throw is rare and silently loses a batch since the cursor was already bumped — worth a visible warn line. - server/index.ts: move describeKind below the import block instead of splitting it. Per reviewer comments on PR fiorastudio#115.
|
Worked through the consolidated review: Already addressed (c268f24)
Just landed (03d9b7c)
Verified, no change needed
Acknowledged but not changing
|
This tries to fix the lossy claim extraction symptom we talked about. Guard mode today relies on the host model calling
buddy_observewith claims/edges, which it stops doing past ~100k tokens of context. The graph goes silent mid-session — exactly the report.Approach mirrors slimemold's hook architecture: move extraction out of band into the Stop hook so it doesn't depend on the host model's attention budget. It's opt-in via
BUDDY_EXTRACTION_KEY(orANTHROPIC_API_KEY). Without a key, guard mode keeps working exactly as before — no regression for existing users.What's here
runGuardPipeline.[buddy observation]block before each prompt.reasoning_extraction_statstable) sobuddy_doctoractually sees hook-side activity — the in-memorytelemetry.tscounters reset every Stop hook process.recordFailure, cross-process graph-cache invalidation viaPRAGMA data_version.buddy_forget allclears the new tables.BUDDY_DEBUG=1logs per-claim drop reasons inwriteClaimsplus a shape snapshot at thebuddy_observeboundary. Should help debug the 0 claims via MCP path you mentioned — running with the env set will print which field is makingwriteClaimssilently drop.Compatibility
Strictly additive. MCP protocol unchanged,
runGuardPipelinesignature unchanged, the existing model-driven path is preserved (and still the only path when no key is set). New tables areCREATE TABLE IF NOT EXISTS, new column is an idempotentALTER. No manual migration.@anthropic-ai/sdkadded as a runtime dep, loaded only when an extraction key is present at hook fire time.Tests
~90 new (extractor reader, shape conversion, key/model resolver, persistent state, multi-call incremental flow, end-to-end with stubbed SDK, cross-connection cache invalidation, doctor regressions, mute respect). 826/829 in the full suite pass — the 3 remaining are pre-existing penguin animation / oldBuddy stats randomness on master, not touched by this branch.
I didn't run a real-API smoke test — no key on hand. The SDK call shape matches the typed interface, but if you want to gate the merge on a manual run, say the word and I'll add a tiny smoke script.
A few things I left for your call
claude-haiku-4-5. Override viaBUDDY_EXTRACTION_MODELorextraction.modelin~/.buddy/config.json. I didn't touch the install script's onboard messaging — felt like that's your voice to write.BUDDY_INSTRUCTIONSstill injects the extraction-prompt guidance into CLAUDE.md at install time. With precise mode active that guidance is wasted prompt budget every turn (model tries to comply, mybuddy_observegate discards). Could be made conditional, but felt out of scope.Honest disclosure: most of this was written with Claude. I reviewed and iterated heavily but a senior engineer's eyes on it would not be wasted.