Summary
When a process notification with attention turn fires while the agent is idle, sendProcessNotificationMessage calls pi.sendMessage(message, { triggerTurn: true, deliverAs: "steer" }). On an idle host, Pi starts that run through sendCustomMessage → _runAgentPrompt, which skips before_agent_start (earendil-works/pi#5581, still open on Pi main). Any extension that sets or extends the system prompt in before_agent_start is missing from that turn.
With pi-claude-bridge the turn is rejected outright, and the session stops until the user types something:
Error: prompt-capture: no capture for this 69174-char system prompt, and it embeds none of the 10 known.
Closest known match diverges at offset 69174 (71847-char key, last recorded at turn_start). ...
The prompt is the captured one minus its final 2673 characters: exactly the section other extensions append in before_agent_start. With other providers the turn runs, but silently without those additions (see the cache-cost reports in pi#5581).
Reproduce
Pi 0.99.1 / 0.99.2, @aliou/pi-processes 0.12.0, any extension that returns systemPrompt from before_agent_start (pi-claude-bridge 0.9.0 shows it as a hard error):
- Start a short process with default attention (
onSuccess: "turn"), e.g. sleep 5.
- Let the agent end its turn and go idle.
- When the process exits, the woken turn runs without
before_agent_start.
main (510e9bc) is affected at extensions/processes/notification-sender.ts:26 (return { triggerTurn: true, deliverAs: "steer" }) and :39 (pi.sendMessage(...)). #116 keeps the same turn options, so it stays affected after that rewrite.
Suggested fix
This is the workaround documented in pi#5581 (comment by @tenshiak), already used by gentle-pi (Gentleman-Programming/gentle-shell#1595). When the host is idle, queue the notification for the next turn and start the turn through sendUserMessage, which goes through prompt() and therefore runs before_agent_start:
if (options.triggerTurn && ctx?.isIdle()) {
pi.sendMessage(message, { deliverAs: "nextTurn" });
pi.sendUserMessage(" "); // prompt() → before_agent_start
} else {
pi.sendMessage(message, options); // unchanged while a run is active
}
Two details matter in practice:
- Coalesce wakes.
prompt() awaits input handlers, auth, compaction and before_agent_start before it marks the run active, so isIdle() still reads true while the first wake is being prepared. A second process exiting in that window would submit a second sendUserMessage, which is rejected ("Agent is already processing"). A synchronous reservation fixes it: set it on the first wake, release it on agent_start/session_start, and let it expire after ~60 s. Later notifications are only queued as nextTurn, and the pending prompt carries them.
- Where to read
isIdle(). The notification fires outside any handler, so it needs the latest ExtensionContext. Tracking it from session_start/agent_start/turn_end/agent_end, registered at extension load in registerNotificationDelivery, works. If no context has been seen yet, keep the original sendMessage call.
I'm running this as a local patch (about 40 lines in notification-sender.ts plus one call in handlers/notifications.ts). I checked it with a small harness: busy host unchanged, idle host woken once, a second idle notification coalesced, the reservation released on agent_start, and context attention unchanged. Happy to open a PR with tests, on main or on top of #116, whichever you prefer.
Summary
When a process notification with attention
turnfires while the agent is idle,sendProcessNotificationMessagecallspi.sendMessage(message, { triggerTurn: true, deliverAs: "steer" }). On an idle host, Pi starts that run throughsendCustomMessage → _runAgentPrompt, which skipsbefore_agent_start(earendil-works/pi#5581, still open on Pimain). Any extension that sets or extends the system prompt inbefore_agent_startis missing from that turn.With pi-claude-bridge the turn is rejected outright, and the session stops until the user types something:
The prompt is the captured one minus its final 2673 characters: exactly the section other extensions append in
before_agent_start. With other providers the turn runs, but silently without those additions (see the cache-cost reports in pi#5581).Reproduce
Pi 0.99.1 / 0.99.2,
@aliou/pi-processes0.12.0, any extension that returnssystemPromptfrombefore_agent_start(pi-claude-bridge 0.9.0 shows it as a hard error):onSuccess: "turn"), e.g.sleep 5.before_agent_start.main(510e9bc) is affected atextensions/processes/notification-sender.ts:26(return { triggerTurn: true, deliverAs: "steer" }) and:39(pi.sendMessage(...)). #116 keeps the sameturnoptions, so it stays affected after that rewrite.Suggested fix
This is the workaround documented in pi#5581 (comment by @tenshiak), already used by gentle-pi (Gentleman-Programming/gentle-shell#1595). When the host is idle, queue the notification for the next turn and start the turn through
sendUserMessage, which goes throughprompt()and therefore runsbefore_agent_start:Two details matter in practice:
prompt()awaits input handlers, auth, compaction andbefore_agent_startbefore it marks the run active, soisIdle()still reads true while the first wake is being prepared. A second process exiting in that window would submit a secondsendUserMessage, which is rejected ("Agent is already processing"). A synchronous reservation fixes it: set it on the first wake, release it onagent_start/session_start, and let it expire after ~60 s. Later notifications are only queued asnextTurn, and the pending prompt carries them.isIdle(). The notification fires outside any handler, so it needs the latestExtensionContext. Tracking it fromsession_start/agent_start/turn_end/agent_end, registered at extension load inregisterNotificationDelivery, works. If no context has been seen yet, keep the originalsendMessagecall.I'm running this as a local patch (about 40 lines in
notification-sender.tsplus one call inhandlers/notifications.ts). I checked it with a small harness: busy host unchanged, idle host woken once, a second idle notification coalesced, the reservation released onagent_start, andcontextattention unchanged. Happy to open a PR with tests, onmainor on top of #116, whichever you prefer.