feat(pi): signal herdr:blocked while a revdiff review owns the terminal - #319
feat(pi): signal herdr:blocked while a revdiff review owns the terminal#319nicdumz wants to merge 2 commits into
Conversation
umputun
left a comment
There was a problem hiding this comment.
the diagnosis is right and the problem is real, but the signal can't arrive while the review is open, so this doesn't do what it says yet.
blocking: the blocked state can't reach herdr during the review. The emit at plugins/pi/extensions/revdiff.ts:104 and the spawnSync inside runDirectReview are separated by zero event-loop turns: runDirectReview runs synchronously into ctx.ui.custom(factory), and pi calls that factory inside its new Promise executor, where tui.stop() and spawnSync run. On herdr's side the listener is invoked synchronously but the transport is not: herdr:blocked goes to publishState -> queueState -> drainStateQueue -> sendRequestAttempt, which does net.createConnection and only writes from the "connect" callback. That callback needs a loop turn spawnSync denies for the whole review, so herdr gets blocked and then working back to back after revdiff has already exited. Sidebar, notification and agent wait --until blocked all still miss. Fix: the state has to be flushed before the terminal handoff, or the review has to stop blocking the loop (an async spawn awaited with the loop live). That is a design call against pi's TUI teardown, not a line edit.
blocking: this breaks the pi regression test. The harness's fakePi() in app/plugin_exit_code_test.go:881 has no events, so revdiff_review.execute throws TypeError: Cannot read properties of undefined (reading 'emit') and TestPiExtensionExecutableBehavior fails. It skips locally when bun is absent, which is why you wouldn't have seen it, and CI on this PR hasn't run yet. Fix: add an events: { emit(){}, on(){ return () => {}; } } stub to fakePi(), and assert the active:true / active:false pair brackets the review.
plugins/pi/extensions/revdiff.ts:92 - the blocked window opens after resolveLaunchSpec, so the ctx.ui.select prompt it can reach is not covered and a bare /revdiff on a dirty branch still reports working while waiting on the user. This is the one part that would work today, since the loop is live there. Fix: cover every await that waits on the user, and keep active:false firing exactly once on each exit path including the early !launch return.
worth checking before you rework this here: herdr's own omp integration already handles tool_execution_start / tool_execution_end, gated on toolName !== "ask". Adding revdiff_review to that gate would get the same state from herdr's side with no change in revdiff, assuming it clears the same timing problem.
on the design: every herdr touchpoint in this repo is gated on HERDR_ENV and this emit is unconditional. I know it is inert without a subscriber, but I want to settle whether revdiff should own a channel name another project versions before taking it.
per CONTRIBUTING.md a first PR needs an issue and a maintainer reply before code. Issue #318 went up two minutes before this one, so the design was never agreed.
Addresses maintainer feedback on umputun#319: - extend the blocked window to wrap resolveLaunchSpec too, so the ui.select prompt on a dirty branch is covered, not just runDirectReview; a single try/finally guarantees active:false fires exactly once on every exit path, including the early !launch return - yield two macrotask ticks (flushPendingIOBestEffort) before the spawnSync handoff so herdr's queued socket write gets a turn to run before the loop freezes; best-effort, no delivery acknowledgement exists to make this a guarantee - gate the emit and the flush on HERDR_ENV=1, matching every other herdr touchpoint in this repo, so this is a genuine no-op outside herdr rather than just harmless-because-unobserved - fix fakePi() in the regression test harness: it had no events stub, so revdiff_review.execute threw a TypeError the moment it tried to emit; add coverage for the blocked/unblocked pairing across a completed review, a launch-resolution failure, an invalid cwd, and the HERDR_ENV-unset no-op case
I main have misunderstood but it seems awkward to, in herdr itself, add special-casing for a specific extension. As in, a little bit backwards: I would expect the extension to integrate well with herdr. @umputun I've tried adding a (The downside of making the review not synchronous is... having to deal with potential async events doing other things in the meantime, i'm not sure i'd be equipped to fix that) |
|
the test breakage and the select-prompt gap are fixed, and the the channel name. the gate bounds when whether the emit is needed here at all. I checked the package: if it is not, then the rest stands. blocking: two this is one of the two routes I named last round, so the approach is not the problem. Two turns is probable-before-handoff, not observable-before-handoff, and when it misses the symptom is #318 coming back silently and reading as environmental. the flush has no test. delete
nits, none blocking:
|
|
@umputun herdrdev/herdr@ca4270b#diff-82789c89eb0b273638986575b634c01addd0d47bb32503217c47949ddb22eeeb is the commit which added the Herder<>Pi integration. You can see there that src/integration/assets/pi/herdr-agent-state.ts wants to be the authoritative state of wether or not a session is blocked. We need to find a way to update this state, otherwise we'd be fighting herdr's own Pi integration. There is Rust code in the herdr binary / server about state, and the Pi Herdr extension communicates with herdr via It's really important to notice that nothing in that original PR emits Can we settle on this being the right approach? It seems the past review turns are "doubting" that |
I understand this suggestion as changing https://github.com/herdrdev/herdr/blob/ffc4e263168f9e81d5bbc14db4b16ca9818d684a/src/integration/assets/omp/herdr-agent-state.ts#L416 from: to some: To me, changing the main herdr codebase to be aware of the revdiff tool, an optional extension, seems like the wrong change. I don't think this is the clean fix and I doubt that herdr maintainers would accept this kind of hardcoding (if we think about it this means that any of the thousands of extension authors could then ask special-casing, this doesnt scale). |
|
you're right on both design points.
enumerating consumers in herdr doesn't scale either. Won't ask for that. timing still blocks, and it's worse than a tight tick count. I went looking for a synchronous send, since herdr's cursor/droid/kimi hooks do a blocking that leaves an ack on that one is generic, which is your own objection satisfied. If you want a herdr maintainer involved, that's the question to put to them. I'm not merging the two-tick flush meanwhile. It's probable-before-handoff, not observable, and when it misses #318 returns silently and reads as environmental. Worse than the bug because it looks fixed. Still untested too: delete happy to leave this open while the herdr side is settled. If the ack is accepted there, this PR shrinks to a bounded wait plus a delivery-order test and a minimum-version note. If herdr declines, say so and I'll close without prejudice, #318 keeps tracking it. and the |
Title: feat(pi): signal herdr:blocked while a revdiff review owns the terminal
Problem
revdiff_reviewruns revdiff in-place viaspawnSync(..., { stdio: "inherit" }), parking pi's agent loop inside the tool call for the whole review. External supervisors that consume pi lifecycle state (herdr's omp/pi integration, which replaces screen detection for the pane) reportworkingthe entire time — no blocked state, no notification,agent wait --until blockednever fires. Fixes #318. Refs herdrdev/herdr#2758 for the same blind spot with hook confirmation dialogs.Change
Emit
herdr:blockedon pi's shared extension event bus aroundrunDirectReview:{ active: true, label }before,{ active: false }in afinally. herdr's installed omp/pi integration listens for this channel and drives sidebar/notifications/waits from it;EventBus.emitwith no subscriber is a no-op, so this costs nothing when herdr isn't involved. 13 lines, no signature changes.(Disclosure: prepared by an AI coding agent on behalf of the human author, who reviewed and approved this change and text before submission.)