# beval-pi-lazy-loader

> A [pi](https://github.com/earendil-works/pi-mono) extension that defers loading of non-critical extensions until after the interactive session is ready, keeping startup fast.

[![npm version](https://img.shields.io/npm/v/beval-pi-lazy-loader.svg)](https://www.npmjs.com/package/beval-pi-lazy-loader)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Why

When your pi setup accumulates many extensions (cost trackers, runtime tracers, web tooling, sound notifiers, …), loading them all synchronously at startup blocks the prompt. This package moves that work to the background: it listens to `session_start`, then discovers and loads every extension in the agent's `extensions/lazy/` directory after a configurable delay — so you get a responsive prompt first, and the rest loads in ~20–50ms without blocking.

```
pi startup
  ├─ load core extensions (sync)
  ├─ scan skills
  ├─ prompt ready ← user can type immediately
  └─ background: lazy-loader loads deferred extensions
```

## Install

```bash
pi install npm:beval-pi-lazy-loader
```

Or add it to `~/.pi/agent/settings.json` manually:

```json
{
  "packages": ["npm:beval-pi-lazy-loader"]
}
```

Then move the extensions you want deferred into the lazy directory (create it if missing):

```bash
mkdir -p ~/.pi/agent/extensions/lazy
# e.g. move a deferred extension
mv ~/.pi/agent/extensions/cost-tracker ~/.pi/agent/extensions/lazy/
```

Each lazy extension is either a directory with `index.ts` (or `index.js`) or a standalone `.ts`/`.js` file (not named `index.*`) inside `extensions/lazy/`. Type declaration files (`.d.ts`/`.d.js`) are skipped automatically.

## How it works

On `session_start`, the loader:

1. Reads its config from the `lazyLoader` key in the agent `settings.json`.
2. Scans `<agentDir>/extensions/lazy/` (override with `lazyDir`).
3. Filters by whitelist/blacklist, then loads every surviving extension in parallel via `Promise.allSettled`.
4. Guards each load with a per-extension timeout so one slow extension can't stall the rest (see *Timeout semantics* below).
5. Reports a one-line status (`N/M extensions loaded in Xms`) and clears it after 2s. Failures go to `console.error`.
6. Optionally watches the lazy directory for new files and hot-loads them.

All paths resolve against the agent config directory (`~/.pi/agent` by default, overridable via `PI_CODING_AGENT_DIR`), so this package behaves identically whether installed globally or per-project.

### ⚠️ What *not* to defer (first-turn visibility)

Lazy extensions are loaded **after** `session_start` — i.e. after the UI is ready but in the background. The system prompt for the **first user turn** is assembled around that same point in time, so any tool, command, or prompt snippet a lazy extension registers is **not guaranteed** to be present in that first turn if the user types fast.

**Keep in the synchronous `extensions/` directory** (not `extensions/lazy/`):

- extensions that register tools the LLM must be able to call from the very first message (e.g. a web-access tool the user expects to use immediately),
- provider proxies or model overrides that must change the available model list before the first turn,
- anything that patches the system prompt and needs to be visible immediately.

**Safe to defer** into `extensions/lazy/`:

- passive listeners (cost trackers, runtime tracers, evolve trackers),
- notifiers and non-critical helpers whose absence on turn 1 is harmless,
- tools the user won't reach for until later in the conversation.

### Timeout semantics

The `timeout` is a *decision deadline*, not a cancellation. A running extension factory generally cannot be safely aborted (it may have already registered side-effects). Therefore:

- If a factory has not resolved within `timeout`, the result is reported as `timedOut`, but the underlying load is **kept running** and awaited.
- If it eventually **completes successfully**, it is still recorded as loaded — so hot reload will **not** attempt to register it a second time. The result carries a `completedLate` flag (visible in the error log as `Timeout (>Nms) but completed later`).
- This avoids the previous bug where a timed-out extension would silently load in the background yet be missing from the loaded set, causing hot reload to double-register it.

## Configuration

Add a `lazyLoader` object to `~/.pi/agent/settings.json`:

```json
{
  "lazyLoader": {
    "enabled": true,
    "startDelay": 0,
    "timeout": 5000,
    "lazyDir": "~/.pi/agent/extensions/lazy",
    "hotReload": false,
    "whitelist": [],
    "blacklist": ["sound-notifier"]
  }
}
```

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | boolean | `true` | Master switch. Set `false` to disable lazy loading entirely. |
| `startDelay` | number (ms) | `0` | Delay before the background load begins. `0` loads as soon as the session is ready. |
| `timeout` | number (ms) | `5000` | Per-extension load *decision* deadline. See *Timeout semantics* above. |
| `lazyDir` | string | `<agentDir>/extensions/lazy` | Override the directory scanned for lazy extensions. |
| `whitelist` | string[] | `[]` | If non-empty, only these extension names are loaded. |
| `blacklist` | string[] | `[]` | Extensions to skip. |
| `hotReload` | boolean | `false` | Watch the lazy directory and hot-load newly added `.ts` files. |

## Notes

- **Deferred extensions must be self-contained.** They are loaded with the same `ExtensionAPI` instance as core extensions, so they can register tools, commands, event handlers, etc. — but anything that must be ready *before* the first prompt (e.g. a provider proxy that changes available models) should stay in the synchronous `extensions/` directory, not `extensions/lazy/`.
- **Failures are isolated.** One broken lazy extension logs an error and is skipped; the rest still load.
- **No bundled extensions.** This package ships only the loader. Your `extensions/lazy/` content is your own.

## Compatibility

Requires pi (`@earendil-works/pi-coding-agent`) with the `getAgentDir` export (0.80+). The pi core package is declared as an optional peer dependency — pi provides it at runtime, so you don't need to install it separately.

## License

MIT © BevalZ
