No description
Find a file
guochao 279d0e3faa fix: line valve freezes while suppression is active
Live testing exposed a valve conflict: suppressed lines still counted toward
maxLinesPerMinute, so a line-heavy flood was killed by the valve mid-
suppression (with a large 'last output' dump) before T1 could do its job.
The valve now counts delivered lines only; while delivery is paused the
breaker and the file cap own the kill decision. Adds regression test and
README note. 72/72 tests.
2026-09-24 01:14:14 +08:00
extensions fix: line valve freezes while suppression is active 2026-09-24 01:14:14 +08:00
tests fix: line valve freezes while suppression is active 2026-09-24 01:14:14 +08:00
.envrc chore: declare nix shell env (inotify-tools for file monitors, nodejs) with direnv entry 2026-09-24 00:10:57 +08:00
.gitignore feat: minimal event-driven monitor extension (rewrite of pi-event-monitor) 2026-09-24 00:03:00 +08:00
LICENSE feat: minimal event-driven monitor extension (rewrite of pi-event-monitor) 2026-09-24 00:03:00 +08:00
package.json feat: minimal event-driven monitor extension (rewrite of pi-event-monitor) 2026-09-24 00:03:00 +08:00
README.md fix: line valve freezes while suppression is active 2026-09-24 01:14:14 +08:00
shell.nix chore: declare nix shell env (inotify-tools for file monitors, nodejs) with direnv entry 2026-09-24 00:10:57 +08:00
tsconfig.json feat: minimal event-driven monitor extension (rewrite of pi-event-monitor) 2026-09-24 00:03:00 +08:00

pi-monitor

Minimal event-driven monitors for pi. Two tools, no timeouts: a monitor runs until stopped or the session ends, and its stdout lines wake the session only when something happens.

Tools

Tool Params Behavior
monitor_start command, maxLinesPerMinute? Spawns the command in a background shell; each stdout line is an event.
monitor_stop id (mon-N or all) Stops one monitor or all.

There is no list tool: every start/stop result and every stop notice ends with an inventory snapshot (Active monitors (N): …) so the agent always knows what is running.

Output files

Every monitor writes its complete stdout stream to ${XDG_RUNTIME_DIR:-tmpdir}/pi-monitor/<id>.<ts>.out.log (stderr goes to a sibling <id>.<ts>.stderr.log, capped at 1 MB). The file is never annotated — every byte is the command's real output. The transcript only ever sees bounded slices of it; the file holds everything. Files are kept after the monitor stops (runtime files are ephemeral across reboots; system tmp cleaners handle disk).

Event semantics

  • Lines arriving within 200 ms are batched into one wakeup (steer + turn trigger).
  • Each wakeup quotes output with | and labels it untrusted external data.
  • Lines cap at 4,000 chars; batches at 20,000 chars; unterminated stdout buffers flush (truncated) past 8,000 chars.
  • Only running monitors wake the session; stopping is already silent.
  • Auto-stops (valves, process exit, spawn error) notify with the reason, the last output, the stderr tail (if non-empty), the output-file path, and the inventory — and they wake the session. Tool-initiated and lifecycle stops are silent: the tool result or lifecycle context already covers them.

Overload tiers

  1. Batch + caps (always): 200 ms batching, per-line and per-batch caps.
  2. Suppression (soft): a sliding 10 s window over 24,000 chars pauses inline delivery — events keep flowing into the output file. A thin control notice (rate-limited to one per 60 s) reports the suppressed count, a sample line, the line range, and sed -n 'a,bp' <file> to extract exactly what was held back. Delivery auto-resumes when the output calms down.
  3. Circuit breaker (hard): 3 suppression spans within 10 minutes or 500 kB suppressed in that window kills the monitor with a notice.
  4. File cap (hard): an output file over 8 MB kills the monitor.
  5. maxLinesPerMinute (default 60, max 1000) stays as the explicit agent-set valve on delivered lines: exceeded → immediate stop, overflowing batch shown. While suppression is active the valve freezes — the breaker and the file cap own the kill decision until delivery resumes.

Bash guard

A tool_call handler blocks bash commands that would block the turn: tail -f/-F/--follow, watch, unbounded while/for ((;;)) loops with sleep, journalctl -f, container logs/events -f, kubectl … -w/--watch, gh … watch, and bare fswatch/inotifywait. timeout-wrapped commands are allowed; bounded retry loops are allowed.

Blocked commands receive a hint to use monitor_start instead. If blocking is intentional, retry the exact command with a # blocking-ok comment appended — the sentinel is visible in the transcript and approval flow. The guard is a speed bump with signage, not a security boundary.

Orphan detection

Each start/stop appends a pi-monitor:lifecycle custom entry to the session (pid, pgid, command, log paths, /proc start time). On session_start the extension reconciles those records across the whole session tree with live processes: started-without-stop or shutdown-marked records whose process is still alive are reported as orphans from a previous pi process, with the kill -- -pgid command to reap them. No separate state file — the session file is the state, and custom entries do not participate in LLM context.

Context tree

Monitors are processes: they follow the session, not the branch. After a session_tree move with active monitors, a lazy inventory hint is queued (delivered with the next prompt; costs nothing otherwise).

Lifecycle

  • Session switch and fork stop monitors (records closed). Session shutdown marks records orphaned — the detached processes keep running after pi exits, and the next session_start reports them. Extension reload kills leftovers from the previous instance in the same process. Stop is SIGTERM, then SIGKILL after a 3 s grace; lifecycle waits up to 4 s for children.
  • A SIGKILLed pi process leaves monitor process groups behind; the orphan report on the next session of the same file is the recovery path.

Security model

  • Approval is delegated to the platform tool_call flow. This extension never prompts; policy plugins decide what may run.
  • Monitor output is untrusted: quoted, labeled, and bounded before it reaches the model.
  • Monitors run with the user's permissions until stopped, so only start commands you would also run in a terminal.
  • Runtime files live in a 0700 directory under the user's runtime dir.

Development

npm test                 # node --test over extensions with fake children
pi -e .                  # load the extension in pi from this directory
pi -ne -e . --print hi   # clean load check (installed packages disabled)

The extension ships as TypeScript with explicit .ts relative imports; pi loads it through its own TypeScript loader. Peer dependencies (@earendil-works/pi-coding-agent, typebox) are supplied by pi.