feat(capture): make real-time capture opt-in, drop the per-tool-call hook - #690
feat(capture): make real-time capture opt-in, drop the per-tool-call hook#690dripsmvcp wants to merge 2 commits into
Conversation
…hook new config key `capture.realtime`, default false. `capture observe` returns immediately when it is off, before any file i/o, and the shipped claude-code adapter no longer registers a `PostToolUse` hook at all. on a busy session that hook was hundreds of python process spawns, each loading the kb store, to append a line to an ephemeral buffer whose only consumer is the once-per-session rollup — the knowledge itself is already batched, since `capture.answer_mode` has defaulted to `session` since passive answer memory landed. the `Stop` -> `capture answer` hook goes with it. under the default answer mode it is a pure deferral: it fires, decides it is in session mode, and exits. `SessionStart`, `UserPromptSubmit` and `SessionEnd` are the three that carry weight. removing the buffer without replacing it would have cost small sessions their summary — `summarize` gates on `len(observations) + len(changed_files) < min_observations`, so with an empty buffer only the git-diff backstop counts. `finalize` now reconstructs the session's tool activity from the transcript it already reads (`observations_from_transcript`), pairing each tool_use with the tool_result carrying its id so a failed bash still reads as failed, and masking secrets the same way `observe` does. same observation shape, same rollup, one more front door — codex_rollout already does this for codex. with `realtime: true` both sources describe the same calls, so `_merge_observations` dedups on `tool_use_id` (falling back to tool/summary/cmd for older buffers) and orders by timestamp: a call recorded twice counts once, in the buffer's position. no stored artifact changes shape, and installed hosts keep their current settings.json until they re-run `vouch install-mcp`.
…erge the diff-coverage gate wants 100% of changed python. the uncovered lines were all the tolerant branches — which are the ones worth a test anyway, since a transcript is the host's file and one malformed row must not cost the session its summary. one messy transcript exercises every skip in a single pass: blank and whitespace lines, undecodable json, a bare-string document, a non-dict message, a non-list content, a bare-string block, tool_use with a missing and a non-string name, an orphan tool_result, and an unparseable timestamp degrading to 0.0. plus the max_observations ceiling. for the merge: id-based dedup, the no-id fallback on tool/summary/cmd for buffers that predate tool_use_id, timestamp ordering, and the short-circuit when either side is empty.
|
the argument holds and the compatibility half is the part that makes it shippable — checked the sibling-adapter question, since removing a hook from one adapter usually leaves another behind: the thing i would want resolved is what that leaves a config key whose "on" state is unreachable through any shipped path, which is the shape that ages into a dead flag. either the merge-order note is worth stating too: #679 drives correction capture from |
|
duplicated |
closes #602
what
capture.realtimeis a new config key, defaultfalse.vouch capture observereturns immediately when it is off — before any file i/o — and theshipped claude-code adapter no longer registers a
PostToolUsehook at all.on a busy session that hook was hundreds of python process spawns, each
loading the kb store, to append a line to an ephemeral buffer whose only
consumer is the once-per-session rollup. the knowledge itself has been batched
since passive answer memory landed:
capture.answer_modedefaults tosession, so claims are extracted once at finalize from the full transcript.the
Stop→vouch capture answerhook goes with it, per the issue. under thedefault answer mode it is a pure deferral — it fires, decides it is in session
mode, and exits.
SessionStart,UserPromptSubmitandSessionEndare thethree that carry weight, and that is what the adapter ships now.
the compatibility half — item 3, which the issue is right to call
non-optional
summarizegates onlen(observations) + len(changed_files) < min_observations, so an empty buffer would have left any session touchingfewer than 3 files with no summary at all.
capture.finalizenow reconstructs the session's tool activity from thetranscript it already reads (
capture.observations_from_transcript):tool_useblock is paired with thetool_resultcarrying its id, sothe Bash branch still tells a failed command from a clean one —
is_erroris folded into the text
summarize_toolgreps, and it wins over contentobservemasks them, because these recordsroll into a committed page and the append-only audit log
[]rather than costing the session itssummary
same observation shape, same rollup, one more front door onto it —
codex_rolloutalready does precisely this for codex rollouts.when realtime is on
both sources then describe the same tool calls.
session_split._merge_observationsunions them on
tool_use_id(falling back to tool/summary/cmd identity forolder buffers that predate the id) and orders by timestamp, so a call recorded
by both counts once against
min_observationsand renders once in theactivity list.
compatibility
no stored artifact changes shape. installed hosts keep their current
.claude/settings.jsonuntil they re-runvouch install-mcp— noted in thechangelog.
capture observestays on the CLI and stays callable, it justdefaults to a no-op, so this is not a surface change and needs no VEP.
tests
new in
tests/test_capture.py: the default is off and a quoted"true"stillenables it;
observewrites no buffer file when off; reconstruction returnsthe right tools in transcript order with ids, timestamps, the failed-Bash
summary and the
files/cmdfields, skips unobserved tools, masks secrets,and survives a missing path;
finalizewith realtime off clearsmin_observationson reconstructed activity alone and renders it into thepage body; and with realtime on, a call in both the buffer and the transcript
is counted and rendered once.
existing suites that seed the buffer through
observenow opt intorealtimeexplicitly (fixtures intest_capture.py/test_session_split.py,call-site configs in
test_capture_answer.py/test_adopt.py/test_session_transcript.py), and the adapter-shape assertions intest_capture.py/test_install_adapter.pynow pin the absence ofPostToolUse/Stoprather than their presence.full gate green locally:
pytest tests/ --ignore=tests/embeddings,mypy src,ruff check src tests.