Skip to content

make real-time capture opt-in: default capture.realtime off, drop PostToolUse observe from adapters #602

Description

@plind-junior

what you're trying to do

capture should not run in real time by default. today every adapter ships a
PostToolUse hook that shells out to vouch capture observe after every
tool call, plus a Stop hook that shells out to vouch capture answer after
every turn. on a busy session that's hundreds of python process spawns, each
one loading the kb store, to append a line to an ephemeral buffer that only
matters once — at SessionEnd.

the knowledge itself is already batched: capture.answer_mode defaults to
session (capture.py:45), so claims are
extracted once at finalize from the full transcript, and the per-turn Stop
hook is a pure deferral — it fires, decides it is in session mode, and exits.
the real-time half of capture is doing work whose only consumer is a
once-per-session rollup.

there is also a correctness angle. the buffer is per-tool-call hearsay: a
Command failed: line, a truncated grep -rn "delete" src/vouch/*.py | head.
the transcript that finalize already reads is strictly richer and is the
receipt-bearing artifact. two sources of truth for "what happened in this
session", one of them lossy.

what you've tried

capture.enabled: false is the only existing knob and it is all-or-nothing —
it turns off the session summary too, which is the part worth keeping. there is
no way to say "capture, but only at the end".

suggested shape

  1. new config key capture.realtime, default false. when off,
    capture observe returns a {"skipped": "realtime-disabled"} no-op
    immediately, before touching the store.
  2. drop the PostToolUse block from the shipped adapter templates
    (adapters/claude-code/.claude/settings.json and the equivalent in the
    other host adapters), and drop Stopcapture answer while we're there
    — under the default answer_mode: session it can never file anything.
    SessionStartfinalize-all, UserPromptSubmitcontext-hook, and
    SessionEndfinalize are the three that carry weight.
  3. reconstruct tool activity from the transcript inside finalize instead of
    from the buffer, so the summary body keeps its "files modified / activity /
    notable commands" sections without the per-call hook.
  4. capture.realtime: true restores today's behaviour for anyone who wants
    the buffer as a crash-resistant backstop.

compatibility considerations

this is the part that needs care. session_split.summarize gates on
len(observations) + len(changed_files) < cfg.min_observations (default 3,
session_split.py:131). with the buffer
empty, only the git-diff backstop counts — so any session touching fewer than
3 files would file no summary at all. item 3 above is therefore not
optional; it is what keeps the gate meaningful. the alternative is counting
transcript exchanges toward total.

existing kbs are unaffected — no stored artifact changes shape. adapter
templates are regenerated by install_adapter, so installed hosts keep their
current settings.json until they re-run the installer; worth a line in the
changelog either way.

not a surface change, so no VEP: capture observe stays on the method list
and stays callable, it just defaults to a no-op.

alternatives

  • keep the hook but make it write nothing when answer_mode: session — same
    saving in effect, but leaves the confusing "hook fires, does nothing"
    shape and still pays the process-spawn cost.
  • debounce observe (write at most every N seconds). cheaper to build, but
    keeps two sources of truth and still spawns a process per tool call to
    decide it should not write.

Metadata

Metadata

Assignees

No one assigned

    Labels

    adaptersagent host adapters and install manifestsenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions