Skip to content

Idle turn notifications skip before_agent_start (pi#5581): breaks pi-claude-bridge and drops system-prompt extensions #121

Description

@BGamboa13

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):

  1. Start a short process with default attention (onSuccess: "turn"), e.g. sleep 5.
  2. Let the agent end its turn and go idle.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions