Skip to content

Herdr Blocked on Question

The herdr-blocked-on-question extension is a small companion to herdr-agent-state for Herdr users. While pi has an ask_user_question open, it tells Herdr the agent is blocked (needs attention) — so hardware readouts such as the opendeck-herdr Stream Deck plugin turn red instead of showing busy/blue.

⚠️ Status: provisional — needs addressing later. This is a stopgap for one specific gap. See Status & what still needs addressing for the known limitations and the eventual upstream fix.

Be precise about the scope, because it’s easy to misread:

  • The ask_user_question tool itself was not modified. No pi source, no tool definition, no built-in behaviour was touched. There is nothing to find “in the tool”.
  • A new companion extension was added (herdr-blocked-on-question.ts) that observes the tool’s execution lifecycle and emits a signal on pi’s shared event bus. The existing Herdr-managed herdr-agent-state.ts already knew how to turn that signal into Herdr’s blocked state — so no change was needed there either (and that file is Herdr-managed and overwritten on update, so it must not be edited).

The extension listens to three pi events:

Event Action
tool_execution_start (toolName === "ask_user_question") emit herdr:blocked { active: true, label: "awaiting answer" }
tool_execution_end (toolName === "ask_user_question") emit herdr:blocked { active: false }
agent_end drain any question still open (abort safety net)

herdr-agent-state.ts consumes herdr:blocked on the same bus, increments its blocked counter, and reports Herdr state blocked. When the question is answered, the counter returns to zero and — because pi is still mid-turn — state goes back to working. End-to-end:

ask_user_question opens → herdr:blocked active → Herdr "blocked" → readout RED
user answers → herdr:blocked inactive → Herdr "working" → readout BLUE

It tracks open toolCallIds in a Set, so nested/repeated questions and duplicate starts stay balanced. It is inert unless pi is running under Herdr (HERDR_ENV=1); with no listener on the bus the emit is a harmless no-op.

Prerequisites

  1. The Herdr pi integration is installed (it ships the herdr-agent-state.ts bridge that consumes the signal):

    Terminal window
    herdr integration install pi
  2. pi is launched from within Herdr (herdr, or herdr --session <name>) so the HERDR_ENV / HERDR_SOCKET_PATH / HERDR_PANE_ID variables are set.

Install the extension — pick one (all land in pi’s auto-discovered ~/.pi/agent/extensions/):

Terminal window
# Option 0: one-liner from GitHub (auto-discovers every .ts in the repo,
# including this one — re-run to update)
curl -fsSL https://raw.githubusercontent.com/JohannesBertens/jreb-pi-extensions/main/install.sh | sh
# Option 1: copy
mkdir -p ~/.pi/agent/extensions
cp herdr-blocked-on-question.ts ~/.pi/agent/extensions/
# Option 2: symlink from a checkout (edits propagate on /reload)
mkdir -p ~/.pi/agent/extensions
ln -sf ~/projects/jreb-pi-extensions/herdr-blocked-on-question.ts ~/.pi/agent/extensions/

Activate — there is no toggle command; it activates automatically on every session_start when HERDR_ENV=1:

/reload

To verify end-to-end, trigger an ask_user_question under Herdr: the connected Stream Deck tile/dial should flip to red while the question is open and back to blue once answered. (opendeck-herdr polls every 2 s, so allow up to that for the colour change.)

This extension is a provisional fix for one symptom: an open ask_user_question is mid-turn, so Herdr correctly reports working and the readout says “busy”. It is not the durable solution.

  • Only ask_user_question is covered. Permission / confirmation prompts (ctx.ui.confirm / select / input) and the built-in permission gate are not — they lack a single, stable hook to listen on.
  • Couples to a Herdr-managed contract. It emits herdr:blocked, which herdr-agent-state.ts consumes. If Herdr renames or drops that event, this silently stops working.
  • The real fix is upstream. The durable solution is for pi core (or the Herdr integration) to emit herdr:blocked for all awaiting-input states, at which point this extension becomes redundant and should be removed.