# pi-subagent-timeout-safety

A reusable [Pi](https://github.com/badlogic/pi-mono) package that prevents a
`pi-subagents` writer from being killed by a caller-supplied wall-clock timeout
or turn budget while it may be in the middle of a file mutation.

It was originally designed against Pi `0.82.0` and `pi-subagents` `0.35.1`.
The current deployed compatibility matrix has also been verified with:

- Pi `0.82.1`
- `pi-subagents` `0.37.0`
- Node.js `24+`

This is currently version `0.3.0` of the package.

## Why

`pi-subagents` treats `timeoutMs` / `maxRuntimeMs` as a hard elapsed deadline.
When it expires, its runner terminates the child process and then escalates to
`SIGKILL`. A hard `turnBudget` can also terminate a child. Neither is a safe
boundary for an active `edit`, `write`, or mutating `bash` command.

This package changes only the **LLM-issued `subagent` tool-call path**:

- For a possibly mutating run it removes run-level `timeoutMs`,
  `maxRuntimeMs`, and `turnBudget` before the tool executes.
- It appends a checkpoint instruction to concrete writer tasks.
- It leaves limits intact only when the resolved agent is tool-restricted and
  cannot mutate; prompt wording alone cannot revoke an agent's tools.
- Unknown agent names fail closed: they are treated as capable of mutation.
- Agent discovery mirrors local user/project scope, configured extra user-agent
  directories, and nested agent directories before classifying a custom role; package-provided agent definitions are
  treated as unresolved and fail closed.
- It emits a durable `subagent-timeout-safety` audit message whenever it removes
  a limit.

It does not patch `pi-subagents` files.

## Install

Review the source before installing—Pi packages execute with your user account's
full permissions.

### Install from npm (recommended)

```bash
pi install npm:pi-subagent-timeout-safety
```

Update or remove the npm installation:

```bash
pi update npm:pi-subagent-timeout-safety
pi remove npm:pi-subagent-timeout-safety
```

For a project-local installation recorded in `.pi/settings.json`, add `-l`:

```bash
pi install -l npm:pi-subagent-timeout-safety
```

Use `pi update npm:pi-subagent-timeout-safety` to update the package; use
`pi remove -l npm:pi-subagent-timeout-safety` to remove that project-local installation.

### Test or develop from a local checkout

```bash
pi -e /absolute/path/to/pi-subagent-timeout-safety
```

### Install globally from a local checkout

```bash
pi install /absolute/path/to/pi-subagent-timeout-safety
```

Git sources remain an option when you need a specific repository revision for
development or debugging:

```bash
pi install git:gitlab.com/aloneio/pi-subagent-timeout-safety
```

Restart Pi or run `/reload` after changing the source/configuration.

To remove a globally installed local checkout:

```bash
pi remove /absolute/path/to/pi-subagent-timeout-safety
```

## Configuration

The package default is stored next to the extension entrypoint. In this source
checkout, the path is:

```text
extensions/subagent-timeout-safety/config.json
```

When this whole package directory is auto-discovered under
`~/.pi/agent/extensions/subagent-timeout-safety/`, the resulting live path is:

```text
~/.pi/agent/extensions/subagent-timeout-safety/extensions/subagent-timeout-safety/config.json
```

```json
{
  "version": 1,
  "enabled": true,
  "allowExplicitMutationTimeout": false,
  "unknownAgentMayMutate": true
}
```

Keep `allowExplicitMutationTimeout` set to `false`. Setting it to `true`
disables the protection for caller-supplied writer limits and reintroduces the
risk of a child being killed during mutation. Also leave `turnBudget` unset in
the `pi-subagents` global extension configuration: it is a downstream fallback
outside this package's interception boundary.

Use `/subagent-timeout-safety` in Pi to see the active policy and scan local
agent definitions for unsafe frontmatter defaults.

## Operational model

For long writer work, detach the run and bound only the **parent's wait**:

```ts
subagent({
  agent: "worker",
  async: true,
  task: "Implement the requested change and report checkpoints."
});

subagent_wait({ id: "run-id", all: true, timeoutMs: 1_200_000 });
```

`subagent_wait` timing out stops waiting; it does not terminate the child. Use
`status`, `steer`, `interrupt`, or `stop` when the supervising agent decides
what should happen next.

## Classification

The policy is intentionally capability-based rather than prompt-trust-based. A
custom agent whose resolved declared tools are all read-only can retain caller
limits. An agent that has `write`, `edit`, `bash`, or another unrecognized tool
is mutation-capable even if its task says `read-only` or its acceptance role is
`read-only`: text does not remove tool permissions. Built-in `pi-subagents`
roles are conservatively considered mutation-capable because their shipped tool
sets include `bash`, `write`, or `edit`. Unknown agent names fail closed.

## Important limitations

This is a supported Pi `tool_call` interception, not a full replacement for
`pi-subagents` timeout architecture.

1. **Covered:** normal model-issued `subagent(...)` calls, including
   `action: "schedule"` at creation time.
2. **Not covered:** direct internal invocation paths that do not execute the Pi
   `subagent` tool—currently slash-command bridges, extension-to-extension
   delegation, RPC `spawn`, a scheduled job firing later, and an authorized
   fanout child. These paths must not be given writer hard limits until the
   upstream plugin offers a checkpoint-safe policy at its executor boundary.
3. **Global defaults:** `pi-subagents` falls back to a configured global
   `turnBudget` after this hook. Do not configure a global `turnBudget` while
   mutation-capable agents are enabled; this package cannot distinguish those
   downstream fallback limits. Similarly, do not set per-agent timeout or
   turn-budget defaults on mutation-capable roles.
4. **Agent defaults:** `pi-subagents` resolves custom-agent `timeoutMs` and
   `turnBudget` frontmatter after Pi's `tool_call` hook. The command reports
   detected local frontmatter defaults, but package-supplied agent defaults are
   treated as unresolved/fail-closed.
5. **Other termination causes remain:** explicit `stop`, parent/session abort,
   protocol-output safety limits, provider failure, OS failure, and system
   shutdown can still end a child.

## Development and verification

```bash
npm test
npm run check
```

The tests use no network and do not launch an LLM. They cover policy
classification, limit removal, local agent discovery/scope, package loading,
child exclusion, and the extension handler's mutation of Pi's validated tool
input.

## Security and distribution

The package contains no credentials, provider settings, models, user agent
profiles, session state, or unrelated global extensions. `package.json` marks
`@earendil-works/pi-coding-agent` as a peer dependency, per Pi package guidance.

## License

MIT. See [LICENSE](LICENSE).
