Skip to content

Fold hooks: JSON-RPC summaries for large added folds, concurrent streaming - #1

Merged
sidkmenon merged 19 commits into
mainfrom
diff-preprocess
Sep 10, 2026
Merged

sidkmenon merged 19 commits into
mainfrom
diff-preprocess

Conversation

@sidkmenon

Copy link
Copy Markdown
Contributor

Summary

Adds a user-configurable fold hook that replaces fold placeholders with richer text, such as model-written pseudocode, before each streaming file event. Along the way, streaming now diffs files on a worker pool and emits them as they finish.

  • Worker pool. --format ndjson diffs --jobs files at once (default 16) on a rayon pool and emits results in completion order. Clients lay out slots from the manifest already on start. 5x faster on a 1382-file debug-build diff.
  • Fold hook. A [folds.hook] table names a trusted command. diffr starts it once per invocation as a JSON-RPC 2.0 server on a loopback port (DIFFR_HOOK_PORT), waits for it to listen, and calls summarize once per file with that file's large novel folds on the after side. The worker diffing that file blocks on its own reply; in-flight calls multiplex on a typed jsonrpsee client over a small tokio runtime. Failures keep the diff and add hook_error to the file event.
  • Wire. Every fold gains a nullable summary beside its placeholder; the TUI schema accepts it. No rendering yet.
  • Reference hook. examples/hooks/summarize.py: an aiohttp + jsonrpcserver server that asks Gemini 3.8 Flash, thinking off, for Python-style pseudocode, concurrently on one asyncio loop with a shared httpx client.
  • Config-relative paths. Hooks run in the directory containing the config file, so one config works from any checkout. DIFFR_WORKSPACE carries the repo root.

Design notes and the full protocol are in docs/streaming.md.

Test plan

  • cargo test --bin diffr (173 passed), including hook unit tests against a stdlib JSON-RPC server: id routing under 8 concurrent callers, timeout, startup exit, error objects, invalid responses
  • tests/streaming/check.py: hook success, error object, startup exit, unstartable command, config outside the repo with a relative script path, --jobs 1 ordering
  • bun test in tui/ (42 passed)
  • End to end against Gemini: 13 folds across 4 files in about 5s wall, no hook errors
  • cargo fmt, cargo clippy, ruff format and ruff check clean on touched files

AI disclosure

Implemented with Claude Code per AI_POLICY.md; every change was read and verified by a human-directed session, and the design was iterated in review.

https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN

Introduce LoadedFile so sources are read on the session thread and the
diff itself can later run elsewhere. No behavior change.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: 01a08a73-9b4c-7bd1-bb9c-e2efe577c764
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
Streaming output no longer waits for earlier files: a rayon pool of --jobs
workers (default 16) pulls sources through the session serially and diffs
them concurrently. The file manifest on the start event already lets clients
lay out results in any order.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: 01a08a73-9b4c-7bd1-bb9c-e2efe577c764
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
Folds gain a summary field, always null for now, on the domain model, the
wire format, and the terminal frontend schema. No behavior change.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: 01a08a73-9b4c-7bd1-bb9c-e2efe577c764
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
A [folds.hook] table names a trusted command plus tag, size and timeout
filters. Parsed and validated only; nothing invokes it yet.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: 01a08a73-9b4c-7bd1-bb9c-e2efe577c764
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
Streaming spawns the [folds.hook] command once and sends each file's
qualifying rhs folds as one NDJSON request; the worker blocks on the reply
and fills fold summaries before emitting the file event. Replies are routed
by request id so files in flight overlap freely. A failed or timed-out
request leaves placeholders in place and adds hook_error to that file.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: 01a08a73-9b4c-7bd1-bb9c-e2efe577c764
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: 01a08a73-9b4c-7bd1-bb9c-e2efe577c764
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
examples/hooks/summarize.py answers diffr fold requests concurrently through
OpenRouter, defaulting to google/gemini-3.1-flash-lite with reasoning
disabled, which answered a three-fold request in under two seconds.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: 01a08a73-9b4c-7bd1-bb9c-e2efe577c764
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
A wrapper such as uv run can be killed while its child keeps the stdout pipe
open, which left diffr waiting forever on the reader thread at exit. Closing
stdin first lets the hook finish on EOF; the reader now ends on its own.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: 01a08a73-9b4c-7bd1-bb9c-e2efe577c764
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: 01a08a73-9b4c-7bd1-bb9c-e2efe577c764
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
OpenRouter refuses to disable reasoning for Gemini 3.8, so the hook calls
the Gemini API with thinking budget zero and a fixed response schema.
A three-fold request answers in about two seconds.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a08a73-9b4c-7bd1-bb9c-e2efe577c764
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
Relative command paths now resolve against diffr.toml or the --config file
rather than the diffed repository, so one hook config works from any
checkout. The repository root is passed as DIFFR_WORKSPACE.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a08a73-9b4c-7bd1-bb9c-e2efe577c764
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
Replace the thread pool and urllib with a single event loop, a shared
AsyncClient and a semaphore; replies are written whole between awaits.
Three concurrent requests answer in about two seconds.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a08a73-9b4c-7bd1-bb9c-e2efe577c764
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
Dependency-only change ahead of moving hook communication to JSON-RPC.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
The hook is now a server: diffr reserves a port, passes it as
DIFFR_HOOK_PORT, waits for it to listen, then calls a typed jsonrpsee
client on a small tokio runtime. Rayon workers block on their own call
while in-flight requests multiplex on the client. The hand-written
framing, id map, reader thread and stdin lock are gone. Hook stdout is
discarded so it cannot disturb the event stream; hooks log to stderr.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
One POST route hands each body to jsonrpcserver's async dispatch; the
summarize method awaits Gemini on the shared httpx client under a
semaphore. No hand-written protocol code remains on either side.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
Sorted imports, literal dicts, an explicit check flag on subprocess.run,
and executable bits on the shebang scripts. No behavior change.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: a8ed5d94-5d5f-4d7a-ae57-7c90ef8c7d6b
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
Both findings predate this branch: one misspelling in the review examples
README, and a Git blob hash in a fixture patch that spells a word.

AI assistance: implemented with Claude Code.

Claude-Session: https://claude.ai/code/session_015ZLcD4fU7YTe4CTcB1Q7oN
Agent-Session: 01a089c9-50b5-7593-81bc-8024113a43d4
Agent-Session: c0881bbc-fcf4-466b-b42b-d04811dd045e
Agent-Session: 26b74d72-c5f7-46ce-9404-849b5bb0a0ae
Agent-Session: 01a08ccf-048f-7790-9e74-4889a953c39f
Agent-Session: 118d399f-d820-431e-8b32-8c27fbbc44eb
@sidkmenon
sidkmenon merged commit 99720d3 into main Sep 10, 2026
20 of 30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant