omp packages

pi-smart-timeout0.1.2

Content-aware shell command timeouts for Pi: caps every bash/powershell call so a hung command cannot stall the agent, while long-running builds keep the room they need.

GitHub Actions Published 26 days ago MIT ~469 downloads/mo

Untested on omp

This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.

Did it work for you?

Result

One report per person. A new report replaces your old one.

Reports 0

No reports yet.

Readme

pi-smart-timeout

CI npm license

Content-aware shell command timeouts for Pi.

Pi's bash tool has no default timeout — timeout is an optional parameter the model has to remember to pass, and it usually doesn't. When a command hangs (a bare REPL, tail -f, a stalled network call, an interactive prompt), nothing kills it and the agent sits there until you interrupt by hand.

This extension caps every bash and powershell call so Pi's own process-tree kill fires and the tool returns Command timed out after N seconds. The model sees the error and recovers on its own.

Install

pi install npm:pi-smart-timeout

Or load it for a single run:

pi -e npm:pi-smart-timeout

Why "smart"

A single blunt number has an obvious failure mode: low enough to catch hangs and it kills your cargo build; high enough to let builds finish and it doesn't catch hangs. So the cap is chosen per command.

Decision order — first match wins:

# Condition Applied cap
1 mode is off nothing injected
2 the model supplied timeout used as-is, clamped to maxSeconds
3 the command contains timeout N ... min(N + graceSeconds, maxSeconds)
4 the command looks long-running longSeconds (default 1800)
5 anything else defaultSeconds (default 120)

Hangs are caught, and a 20-minute build is not.

How this differs from the other timeout extensions

Three Pi timeout packages exist, and the difference is what they do with the command text. @jamiefutch/pi-timeout sorts commands into safe / run / unknown and never caps safe — but its safe set includes tail -f and npm install, which is exactly where hangs live:

Command Safe/never-capped classifier pi-smart-timeout
tail -f app.log safe → never capped 1800s → killed
npm install safe → never capped 1800s
git status safe → never capped 120s
grep -rn 'make sure' src/ — 120s (heuristics ignore quoted text and English words)

@rukaachan/pi-timeout guards a different failure mode: a model passing 60000 when it meant 60 seconds. It clamps that number but applies the same cap to every command. pi-extend-timeout changes the timeout of a command already running, which is a recovery tool rather than a preventive one.

This package keeps two buckets so a low cap catches hangs without killing builds, and treats tail -f-style blocking commands as the long-running case they are.

Case 3 in detail

If the command already guards itself, the outer cap gets out of the way so the inner guard fires first and you get a precise error instead of an ambiguous kill:

timeout 2m cargo build     ->  150   (120 + 30 grace)
timeout -s KILL 90 job     ->  120
/usr/bin/timeout 45 job    ->   75
timeout 1h backup.sh       -> 3600   (3630 clamped to maxSeconds)
echo "timeout 5"           ->  120   (quoted, not a guard)
sh -c 'echo "a"; timeout 7 x' -> 37   (quote state resets per segment)
timeout 0 npm install      -> 1800   (coreutils: `timeout 0` means no limit)
timeout 99999 cat          -> 3600   (huge inner guard clamped to maxSeconds)

Option values are tokenized properly rather than matched with one regex, so -s KILL and --kill-after=5s are not misread as the duration. The inner guard is clamped to maxSeconds like every other path: when N exceeds the ceiling the inner guard could not fire before it anyway, so clamping never races the guard.

It also tells the model

The policy is appended to the system prompt each turn, so instead of being silently killed the model knows to pass timeout whenever a command might outlast the default cap, and knows not to re-run a timed-out command unchanged. The long bucket is framed as what commands known to run long receive, not as a blanket allowance, so the model asks for more time explicitly rather than relying on a promoted default. This turns a hard kill into a recoverable error.

Configuration

Settings are merged over built-in defaults. Project settings only apply to trusted projects.

~/.pi/agent/settings.json (global) or <cwd>/.pi/settings.json (project):

{
  "smartTimeout": {
    "mode": "long",
    "defaultSeconds": 120,
    "longSeconds": 1800,
    "maxSeconds": 3600,
    "graceSeconds": 30,
    "longPatterns": ["\\bmy-slow-tool\\b", "scripts/nightly\\.sh"]
  }
}
Key Default Meaning
mode "long" "long", "short" (cap everything at defaultSeconds), or "off" (never inject)
defaultSeconds 120 Cap for ordinary commands
longSeconds 1800 Cap for long-running commands
maxSeconds 3600 Hard ceiling applied even to model-supplied values. 0 disables it
graceSeconds 30 Headroom added on top of a detected inner timeout N
longPatterns [] Extra regexes (matched against the raw command) marking it long-running

Environment variables work too and sit below settings files: PI_BASH_TIMEOUT_SEC, PI_BASH_TIMEOUT_LONG_SEC, PI_BASH_TIMEOUT_MAX_SEC, PI_BASH_TIMEOUT_MODE, PI_BASH_TIMEOUT_LOG (append one line per decision — useful for verifying what was applied).

Invalid values are rejected with a warning and fall back to defaults; a malformed settings.json never breaks startup.

Commands

/bash-timeout                   show current configuration
/bash-timeout off               disable capping for this session
/bash-timeout short             cap everything at defaultSeconds
/bash-timeout long              restore content-aware capping
/bash-timeout reload            re-read settings.json without restarting

What it does not do

  • It can't make Pi have a default timeout. Pi's bash schema is timeout?: number with no default; only injecting the field changes behavior.
  • It won't kill deliberately detached work. nohup x & leaves a process outside the killed tree. That is usually what you want.
  • It doesn't hang-proof interactive network tools. Bare ssh and ssh-keygen and curl/wget stay in the default bucket: they hang on prompts and unreachable hosts far more often than they legitimately run long, so a low cap catches the hang. Pass an explicit timeout when you really do need a long session or a big download.
  • It doesn't promote while true loops. A while true; do ...; done loop is a hang by definition and gets the default cap. Only bounded poll loops (until ping -c1 host; do sleep 10; done) get the long bucket.
  • It doesn't stop output flooding. It caps duration, not volume. Pi truncates output on its own (2000 lines / 50KB, full output saved to a temp file).
  • timeout 0 cmd is not treated as a guard, because coreutils reads it as "no timeout" — so the outer cap still applies.

Tests

npm install
npm test          # fast: decision table + config + notifications   (~2s)
npm run test:e2e  # slow: real kill path, spawns and kills processes (~2min)
Suite What it covers
tests/logic.test.mts ~75-case decision table: classification, inner-guard parsing, clamping, mode switching, system-prompt injection
tests/config.test.mts settings.json layering, project trust gating, validation, malformed input, /bash-timeout reload
tests/ui.test.mts notifications, hasUI: false, malformed tool input
tests/e2e.test.mts drives Pi's real createBashTool: verifies 15 blocking scenarios are actually killed, that no orphaned grandchildren survive, and that fast commands and 3s commands under a 6s cap are not killed

The e2e suite asserts on real process state, not mocks: it reads taskkilled process tables via WMI to confirm the whole tree dies.

License

MIT