hooks: answer each message, so the extension sends a report once
the pi extension (herdr's protocol) waits for a reply line and sends a
report again after 0.5 s without one; dingger never replied. every report
arrived twice and took about 2 s, delaying the next one (the live log: 196
of 400 reports repeated the previous one, 0.49 s later). the daemon now
answers each line with {"ok":true}; senders that do not read replies
(dingger poke, importance, the claude hook) are unaffected.
e2e step 21: one report, done in 0.38 s; without replies, two, 2.08 s.
Igor Soarez igor@soarez.org
Mon, 26 Oct 2026 16:40:50 +0200
2 files changed,
23 insertions(+),
0 deletions(-)
M
crates/dinggerd/src/hooks.rs
→
crates/dinggerd/src/hooks.rs
@@ -18,6 +18,8 @@ //! Reports are state-change *events*: the watcher applies every one in
//! order, so a turn that starts and finishes between two polls is still a //! completion, and each report wakes the watcher immediately. //! +//! Each line is answered with `{"ok":true}`. +//! //! `{"method":"poke"}` only wakes the watcher; `dingger focus` and tmux's //! focus-change hooks send it so attendance updates without waiting for the //! next poll. The last reported state stays authoritative for its pane@@ -123,8 +125,18 @@ importance: Option<String>,
} fn handle_stream(stream: UnixStream) { + // Each message gets a reply line: an extension waits for one and sends + // the message again without it (herdr's protocol), so a report would + // arrive twice and take seconds. + let mut replies = stream.try_clone().ok(); for line in BufReader::new(stream).lines() { let Ok(line) = line else { return }; + if let Some(out) = replies.as_mut() { + use std::io::Write; + if out.write_all(b"{\"ok\":true}\n").is_err() { + replies = None; // the sender does not read replies + } + } let Ok(message) = serde_json::from_str::<HookMessage>(&line) else { warn!(%line, "invalid hook message"); continue;
M
tests/e2e.sh
→
tests/e2e.sh
@@ -909,6 +909,17 @@ expect "shown in pi's footer" "grep -c '^status dingger-importance importance: high$' $EXTLOG" 1
expect "announced to other extensions" "grep -c '^event dingger:importance {\"level\":\"high\"}$' $EXTLOG" 1 report "$I2" "" normal expect "imp: set back by hand" "opt $I2 @dingger_importance" "" + # Unanswered, the extension sends a report again after 0.5 s, and gives + # up on it after 2 s. + working() { sed 's/\x1b\[[0-9;]*m//g' "$DIR/daemon.log" | grep -c "hook report pane=$I2 state=Working"; } + before=$(working) + start=$(python3 -c 'import time; print(time.time())') + iext "$I2" agent_start + took=$(python3 -c 'import time,sys; print(round(time.time() - float(sys.argv[1]), 2))' "$start") + sleep 0.6 + expect "the daemon answers a report: the extension sends it once" "echo \$((\$(working) - before))" 1 + expect "and is done with it at once ($took s, with the harness's 0.3 s pause)" \ + "python3 -c 'import sys; print(float(sys.argv[1]) < 1.5)' $took" True 1 iext "$I2" session_start expect "a new pi on the session reports it again" "opt $I2 @dingger_importance" high # pi starts a session before any command runs.