Metadata-Version: 2.4
Name: ipinb
Version: 0.1.0
Summary: Persistent IPython notebooks for MCP agents
Author-email: Nima Shoghi <nimashoghi@gmail.com>
License-Expression: MIT
Project-URL: Repository, https://github.com/nimashoghi/ipi
Keywords: agent,mcp,ipython,jupyter,coding-agent
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: beniget<0.6,>=0.5
Requires-Dist: exceptiongroup<2,>=1.2.0; python_version < "3.11"
Requires-Dist: ipykernel<8,>=6.4.2
Requires-Dist: ipython<10,>=7.23.1
Requires-Dist: jupyter-client<9,>=7.0.0
Requires-Dist: nbformat<6,>=5.0.2
Requires-Dist: packaging<27,>=23.0
Requires-Dist: pydantic<3,>=2.0
Requires-Dist: requests<3,>=2.32
Requires-Dist: tomli<3,>=1.1.0; python_version < "3.11"
Requires-Dist: toolfuncs
Requires-Dist: traitlets<6,>=5.0.0
Requires-Dist: typing-extensions<5,>=4.6.1
Provides-Extra: mcp
Requires-Dist: tomlkit==0.15.1; extra == "mcp"
Requires-Dist: beniget==0.5.0; extra == "mcp"
Requires-Dist: exceptiongroup==1.3.1; python_version < "3.11" and extra == "mcp"
Requires-Dist: fastmcp==4.0.3; extra == "mcp"
Requires-Dist: ipykernel==7.2.0; extra == "mcp"
Requires-Dist: ipython==8.39.0; python_version < "3.11" and extra == "mcp"
Requires-Dist: ipython==9.13.0; python_version >= "3.11" and extra == "mcp"
Requires-Dist: jupyter-client==8.8.0; extra == "mcp"
Requires-Dist: jsonschema==4.26.0; extra == "mcp"
Requires-Dist: mcp==2.2.0; extra == "mcp"
Requires-Dist: nbformat==5.10.4; extra == "mcp"
Requires-Dist: packaging==26.2; extra == "mcp"
Requires-Dist: pydantic==2.13.3; extra == "mcp"
Requires-Dist: requests==2.34.2; extra == "mcp"
Requires-Dist: starlette==1.3.1; extra == "mcp"
Requires-Dist: tomli==2.4.1; python_version < "3.11" and extra == "mcp"
Requires-Dist: toolfuncs; extra == "mcp"
Requires-Dist: traitlets==5.14.3; extra == "mcp"
Requires-Dist: typed-agent-hooks==0.2.5; extra == "mcp"
Requires-Dist: typing-extensions==4.15.0; extra == "mcp"
Provides-Extra: publish
Requires-Dist: jupyter-collaboration==4.4.1; extra == "publish"
Requires-Dist: jupyter-server==2.18.2; extra == "publish"
Requires-Dist: jupyter-server-ydoc==2.4.1; extra == "publish"
Requires-Dist: notebook==7.5.6; extra == "publish"
Requires-Dist: pycrdt==0.13.1; extra == "publish"
Requires-Dist: tornado==6.5.5; extra == "publish"
Dynamic: license-file

# IPi

IPi gives MCP agents persistent IPython kernels, Jupyter notebooks, and durable
local output artifacts. It is designed for Codex and Claude Code.

IPi is not an LLM harness. It has no provider client, agent turn loop, terminal
UI, prompt manager, or context-window manager. The external harness owns the
conversation; IPi owns notebook execution and typed hooks into that harness.

The PyPI distribution is [`ipinb`](https://pypi.org/project/ipinb/); the Python import and command remain `ipi`.

## Run

IPi supports Linux and macOS and requires Python 3.10 or newer plus `uv`. Run
the published package without installing it permanently:

```bash
uvx --isolated --from "ipinb[mcp]" ipi
```

The bare `ipi` command starts a stdio MCP server. Configure MCP startup and
host-event forwarding together from the project directory:

```bash
uvx --isolated --from "ipinb[mcp]" \
  ipi install --provider all
```

This writes Codex's `.codex/config.toml` and `.codex/hooks.json`, and Claude
Code's `.mcp.json` and `.claude/settings.json`. Select `--provider codex` or
`--provider claude_code` for one host, or `--scope user` for personal configuration.
Project configuration can be committed: launchers use `uvx` through PATH and
contain no interpreter or checkout paths. Review native project trust and MCP
approval, then restart the host.

The installer preserves unrelated settings and rejects a conflicting `ipi`
server entry. Review it before rerunning with `--replace`. Repeated installation
is idempotent. Files are replaced individually and atomically; rerun an
interrupted installation to finish it.

The default MCP requirement uses the installed Git commit or index version.
Local/editable installs need `--requirement 'ipinb[mcp] @ git+https://...@<ref>'`.
Use that option to select a different source or add the `publish` extra. Keep
credentials in Git's authentication configuration. Forwarding uses a separately
pinned `typed-agent-hooks` runtime. Existing manually configured MCP servers can
still use `ipi install-hooks --provider codex --scope project` for hooks alone.

Set `IPI_INSTALL_REQUIREMENT` to that same authenticated requirement when child
kernels must reconstruct a private Git-installed IPi host. The value is used
only at launch and is removed from published runtime state. `IPI_UV_WITH` is a
deprecated fallback for existing launchers.

Linux and macOS correlate the harness over a Unix socket and process ancestry.
When stdio IPi is launched outside a recognized Codex or Claude Code process,
the notebook server remains available but hook forwarding is inactive.

## Tools And Storage

The same core MCP catalog serves stdio and HTTP:

- `create_kernel`, `list_kernels`, and `close_kernel` manage explicit kernel resources.
- `ipython(kernel_id, code)` accepts Python and observes it for a bounded interval. Calls to one kernel may overlap and share globals. `yield_time_ms=0` always returns an accepted cell handle.
- `ipdb` provides optional exception inspection and real breakpoint/stepping control. Ordinary failures finish normally and include one inspection hint.
- `read_cell` retrieves Python state and output or Markdown source and metadata, optionally waiting for Python output or completion; `interrupt` requests cancellation of one cell. Each reader owns its output cursor; cancelling observation does not cancel Python.
- `list_cells` and `markdown` inspect and author retained notebook work.
- `publish(kernel_id, cells={cell_id: caption})` retains a full live executable Notebook 7 publication and optionally highlights terminal Python cells with captions.

Wait for a predecessor to succeed before submitting dependent code. `interrupt` is best effort and never escalates to killing a sibling or kernel. `close_kernel` deliberately discards one kernel's live state while preserving retained records and other kernels. There is no active-kernel selector or agent-facing whole-service shutdown.

Bash is disabled by default. Set `IPI_MCP_ENABLE_BASH_TOOL=1` before startup to expose it; `0` disables it. Optional Bash and plugin tools also require `kernel_id`. The `ipdb` tool requires `kernel_id` on every call. Add `cell_id` for execution-specific inspection or control; omit it to configure kernel-wide breakpoints and exception stops. See [Debugging](docs/debugging.md).

The notebook skill includes locally readable guides for [execution and result recovery](docs/execution-and-results.md), [kernel environments and SSH](docs/kernel-environments.md), [debugging](docs/debugging.md), [publishing](docs/notebook-publishing.md), and [rich output](docs/rich-output.md). Agents load the relevant guide when needed. `docs/` owns their content; matching copies ship in the skill and package, with packaging checks guarding against drift.

For Codex, IPi maps each tool call's
`_meta["x-codex-turn-metadata"]["thread_id"]` to `CODEX_THREAD_ID` in the
named kernel at launch. Notebook code and its child
processes therefore observe the same standard thread environment as Codex's
native shell tools, without an IPi-specific metadata API.

Execution requires an explicit `kernel_id`. The default observation window is five seconds for `ipython` and zero for `read_cell`; both accept `yield_time_ms` from zero through 30,000. Zero returns an accepted handle for `ipython` and an immediate snapshot for `read_cell`. Python `read_cell` replies contain state and output without source or submission metadata; Markdown replies include their source and metadata. Observation is independent of execution lifetime.

Managed kernels keep IPython history in memory because the notebook and registry are the durable execution record. Kernel creation is explicit, apart from optional discoverable startup prewarming. A closed or lost handle never creates a replacement.

Handles are registry-local counters beginning with `k` for kernels and `c` for cells, such as `k0` and `c1`. Pass them unchanged to their originating registry; IDs are never reused within that registry. Tool responses provide one structured JSON payload with typed resource, handle, cell, inventory, or error envelopes. Output cursors belong to each reader. Large sources and events provide immutable MCP resource URIs and byte counts. Use `read_cell(..., include_artifact_metadata=True)` for artifact filesystem paths, SHA-256 checksums, and MIME types.

`ipython` returns its bounded cell record plus any images the cell displayed as MCP
image content, so `plt.show()` is one step instead of a path the model has to
fetch. `[tools] inline_images = "off"` disables that; images past
`inline_image_max_px` or `inline_image_max_bytes` are skipped rather than
resized, since IPi carries no imaging dependency.

Each notebook runtime gets a new session. Local sessions are stored under:

```text
<initial-cwd>/.agents/sessions/<session-id>/
  session.jsonl
  kernels/k1/notebook.ipynb
  kernels/k1/outputs/manifest.json
  kernels/k1/outputs/c1/g-<sha256-prefix>/input.py
  kernels/k1/outputs/c1/g-<sha256-prefix>/o1-stdout.txt
```

Every input and every retained Jupyter MIME output has a durable local
destination. Output available for the first MCP result is persisted before IPi
builds that bounded response. Returned and recorded paths identify immutable
response-time generations. Browser, yielded, and late output publishes a new
generation as it arrives; `manifest.json` atomically points to the latest
reduced state, including Jupyter clear and display updates. Unfinished agent or
browser cells are recorded as aborted during shutdown. Kernels and Python state
live until the MCP process exits; IPi does not resume them after restart.
Execution and plugins are trusted local Python, not a sandbox.

Generation directories normally use eight hexadecimal prefix characters and
extend on collision. Their `.sha256` file and the manifest retain the complete
digest.

## Importing Python Scripts From Paths

IPi re-exports `toolfuncs.import_path` for importing one local script addressed by the kernel filesystem:

```python
import ipi

tool = ipi.import_path("scripts/tool.py")
```

The source may be an ordinary `.py` file or extensionless script. Its filename determines the module name. A relative path resolves in the kernel's current working directory, including on an SSH target. Packages, projects, distributions, URLs, and explicit module-name overrides are not supported; install a package normally and use `importlib.import_module` for those cases.

The script needs no shebang, executable bit, `App`, or toolfuncs metadata. When optional [PEP 723](https://peps.python.org/pep-0723/) metadata exists, its complete dependency list is installed into the running kernel before import. A caller may replace the default current-interpreter pip operation with a callable that accepts `list[str]` and returns `None`:

```python
tool = ipi.import_path(
    "scripts/tool.py",
    dependency_installer=install_requirements,
)
```

The default dependency installation modifies the disposable environment of the running kernel, exactly like `%pip`; it does not create a per-script environment. Repeated imports of the same source return the existing module, and `importlib.reload()` works. Importing executes the source inside the kernel process, so review untrusted source and metadata first.

## Publishing a Notebook

Kernels always write an archival `notebook.ipynb`. Publish a specific live
notebook only when someone needs to open it:

```text
publish(kernel_id="k1", cells={"c1": "Result"})
```

The tool starts one private Notebook 7 child attached to the existing IPi
kernel, starts one Cloudflare quick tunnel, and returns the complete URL with
its access token. The recipient can view and execute the notebook against the
same live Python state. Browser interrupts are forwarded; browser launch,
restart, and shutdown actions cannot create or destroy IPi-owned kernels.

Publication is lazy, explicit, and per kernel. There is no global daemon,
dashboard, rendezvous file, registration table, or `[jupyter]` configuration.
An unpublished kernel owns no Jupyter or tunnel process. A published kernel is
exempt from ordinary idle expiry until the kernel is
killed or its owning IPi process exits; teardown closes the Jupyter child and
tunnel and invalidates the URL. Repeating `publish` returns the same
URL while it remains live and repairs a failed child or tunnel otherwise.

The Notebook 7 stack is a lazy `publish` extra, not part of normal MCP-host startup. The first publication builds one process-cached wheel from the exact running IPi code and starts it in an isolated `uv run` environment. Set `UV_CACHE_DIR` to node-local storage when the home directory is remote. Jupyter configuration, runtime data, IPython state, logs, and collaboration data always live in a private operating-system temporary directory and are removed with the publication.

Quick tunnels require an installed `cloudflared`. Publication fails without
returning a local fallback if Jupyter or the public tunnel cannot become ready.
Treat the returned URL as a bearer credential and share it only with intended
recipients. See [Notebook publishing](docs/notebook-publishing.md) for the
lifecycle and security contract.

## HTTP

Stdio is the primary transport. FastMCP HTTP is also available:

```bash
uvx --isolated --from "ipinb[mcp]" \
  ipi --transport http --host 127.0.0.1 --port 8000
```

The process registry owns kernels and cells independently of HTTP transport sessions. Reconnecting clients use the same explicit `kernel_id` and `cell_id`; resource IDs are not authentication credentials. Both transports use the same schemas, observation windows, and lifecycle. No `server.session_mode` setting is needed.

`server.running_capacity` defaults to eight execution lanes per kernel, `server.queue_capacity` to 64 waiting submissions, and `server.kernel_capacity` to eight live or starting kernels. Saturation rejects a new submission before acceptance. The registry retains terminal records and retry keys indefinitely by default. Set `server.registry_root` to retain and reopen the same on-disk registry across server restarts; live Python state is reported lost, never reconstructed implicitly. Only one process may own that directory at a time.

Unpublished, inactive kernels expire after `server.kernel_idle_timeout_s` (one hour by default; zero disables it). Published kernels are retained until explicit close or process exit. The repeatable `-c/--config key=value` option overrides strict configuration fields using dotted keys and TOML values.


Set `IPI_MCP_HTTP_AUTH_TOKEN` to require the token as either a bearer token or
the `token` query parameter. Authentication does not define runtime ownership;
logical session IDs do. The Codex/Claude harness hook bridge is currently
attached only for stateful stdio.

Pass `--connection-file PATH` when another local process needs a machine-readable
endpoint. IPi removes a stale file before startup, waits until the HTTP origin
is accepting requests, and atomically publishes a private `0600` JSON manifest:

```json
{
  "headers": {"Authorization": "Bearer <token>"},
  "transport": "http",
  "url": "http://127.0.0.1:8000/mcp?token=%3Ctoken%3E",
  "version": 1
}
```

The URL includes the query token only when query authentication is enabled, and
the header appears only when bearer authentication is enabled. Without a token,
`headers` is empty and the URL has no token. The last successfully published
manifest remains after shutdown as a diagnostic record; the next launch removes
it before starting. Treat the file as a secret whenever authentication is
enabled.

An installed `cloudflared` can expose the MCP endpoint directly. Public tunnels
require `IPI_MCP_HTTP_AUTH_TOKEN`:

```bash
IPI_MCP_HTTP_AUTH_TOKEN='<token>' \
uvx --isolated --from "ipinb[mcp]" \
  ipi --transport http --host 127.0.0.1 --port 0 --tunnel
```

Tunnels are accountless Cloudflare quick tunnels, so the public
`*.trycloudflare.com` URL changes every time the tunnel starts. IPi
prints the public endpoint to stderr after the local origin and tunnel are both
ready; tunnel setup failure stops the public server instead of silently falling
back to a local-only URL. Combining `--tunnel` with `--connection-file` delays
manifest publication until the public endpoint is ready and records that public
URL.

## SSH Kernels

`create_kernel(cwd=...)` accepts either a local absolute path or a per-kernel
SSH URI:

```python
create_kernel(cwd="/Users/me/local-project")
create_kernel(cwd="ssh://gpu-box/opt/projects/model")
create_kernel(cwd="ssh://alice@gpu-box:2222/opt/projects/model", venv=".venv")
```

The URI path is the absolute working directory on the target. Host aliases,
keys, agents, ports, `ProxyJump`, and host-key policy come from normal OpenSSH
configuration. Authentication is non-interactive: IPi never accepts a password
in the URI, prompts for credentials or host trust, weakens host-key checking, or
forwards the SSH agent. Establish host trust and verify `ssh -T <alias>` works
before creating the kernel.

Each remote kernel owns a private OpenSSH master, five target-loopback Jupyter
forwards, and a mode-`0700` target session under `/tmp`. IPi resolves the latest
official portable `uv` release at launch, verifies its publisher checksum on
the host and target, stages the exact running IPi build, and removes the staged
runtime at shutdown. The target project does not need a host mirror and IPi
does not install target system software. The target must provide `python3`
3.10 or newer for the isolated, standard-library-only session owner that guards
the workspace until the exact staged supervisor takes ownership. Normal `uv` Python and dependency
resolution still needs network or pre-populated artifacts; failures retain the
bounded `uv` diagnostic.

Remote kernels run built-in plugins only. Project, user, and installed external
plugins are parsed as data but are not imported, executed, or staged for that
kernel. Creation emits one `The following plugins have been ignored` warning
with every ignored plugin and a redacted source. A single server can mix local
kernels with kernels on multiple SSH targets; listing, switching, recreation,
idle reaping, notebook attachment, PDB, shell history, images, and
cleanup retain each kernel's immutable location.

Windows, WSL path mapping, the `ipi-windows`/`ipi-remote` provisioners, and all
process-wide `IPI_KERNEL_*` settings have been removed. If a removed variable
is present, IPi exits with migration guidance to use an `ssh://` cwd. See
[Architecture](docs/architecture.md) for transport ownership and
[Breaking Changes](BREAKING_CHANGES.md) for migration details.

### SSH troubleshooting and cleanup

Before launching, inspect the effective alias with `ssh -G <alias>` and prove
non-interactive authentication with `ssh -o BatchMode=yes -T <alias> true`.
Resolve an unknown host key or changed trust record through ordinary OpenSSH
administration first; IPi deliberately does not prompt, disable checking, or
edit `known_hosts`. A configured local agent may authenticate the client, but
IPi does not forward that agent to the target. Existing `ProxyJump`, port, and
identity settings remain OpenSSH's responsibility.

Resolving the latest portable `uv` requires host HTTPS access to GitHub. Later
Python and dependency resolution runs on the target and therefore needs its
normal `uv` cache or target network access. Both failures identify which side
needs access. A lost SSH connection may prevent immediate target verification;
the supervisor's owner lease then kills its kernel and removes its private
`/tmp/ipi-session.*` directory after roughly 60 seconds. Cleanup diagnostics
name the exact redacted SSH URI and owned paths. Audit only those exact paths
and processes; never delete a broad `/tmp` pattern. Passwords, Jupyter keys,
tokens, private-key paths, and signed query strings are not included in IPi
diagnostics.

## Configuration And Plugins

IPi merges `~/.ipi/config.toml` with `<cwd>/.ipi.toml`, with project values
winning. Each `create_kernel(cwd)` resolves that cwd's configuration and
plugins independently. A missing config file is optional; invalid, unreadable,
or otherwise inaccessible config fails loudly.

```toml
[server]
kernel_idle_timeout_s = 0     # keep idle kernels alive; set a positive timeout to reap them
prewarm_kernel = true         # start the default kernel in the background at startup

[tools]
bash = true
inline_images = "all"

[kernel]
install_default_dependencies = true
additional_dependencies = []

[publication]
shared_dependencies = []

[plugins]
installed = []
disabled = []
```

Fresh kernels pre-import `ipi`. They also make a small general-purpose module set available: `typing_extensions`, `attrs`, `packaging`, `platformdirs`, `pydantic`, `requests`, `dateutil`, `psutil`, `tqdm`, `filelock`, `more_itertools`, `yaml`, `tenacity`, and `dotenv`. The last six come from bare, unversioned overlay requirements for `tqdm`, `filelock`, `more-itertools`, `PyYAML`, `tenacity`, and `python-dotenv`. Set `kernel.install_default_dependencies = false` to omit those six, or add PEP 508 requirements with `kernel.additional_dependencies`. A caller's explicit `additional_dependencies` value wins when it names the same distribution.

Use `publication.shared_dependencies` for packages that must be installed in both the kernel and the lazily created Notebook 7 environment, such as packages that ship a JupyterLab frontend extension. Shared entries are automatically included in every kernel, so do not duplicate them under `kernel.additional_dependencies`. If a `create_kernel` call explicitly supplies a different requirement for the same distribution, that requirement is also used by its publisher. Ordinary kernel-only packages remain in `kernel.additional_dependencies`; installing one live with `%pip` requires no publication change.

Kernel overlays do not edit `pyproject.toml`, `uv.lock`, or the project virtual environment. As with uv's `--with` behavior generally, an overlay distribution can take import precedence over a project distribution inside that kernel. Session-start and re-grounding context lists the known import names and configured kernel and shared publication requirement strings.

When `tools.view_image` is omitted, the public tool is disabled on stdio and
enabled on HTTP, SSE, and streamable HTTP. Set it explicitly to `true` or
`false` to override that transport default. Kernel-side image-output guidance
follows the same resolution: it recommends `view_image` only when the tool is
actually registered, and otherwise lists absolute paths for the client's own
file/image reading.

IPi's bundled Notebook 7 extension presents the notebook as a sequence of turns. A turn anchor contains the user's initial prompt and any later steering messages for that turn, followed by a nested agent section whose chronological activities can include grouped consecutive reasoning summaries, agent messages, kernel or host shell commands, IPython executions, authored Markdown, and compact records for other tools. User and agent prose is rendered as untrusted Markdown through the notebook's own rendermime registry, while each raw trace cell retains a readable plain-text fallback for clients without the extension. Closed historical prose keeps a lightweight plain-text preview and creates its Markdown renderer only when revealed. The published notebook retains JupyterLab's native `contentVisibility` windowing and output-trimming behavior instead of forcing every cell or thousands of discrete outputs to render eagerly.

Every turn, user section, agent section, thinking group, agent message, and execution has its own disclosure control. Compact tools are deliberately concise terminal rows because no arbitrary tool payload is copied into the notebook. A reader's manual choice is retained across later metadata updates. `publish(kernel_id, cells={cell_id: caption})` opens the target execution and every enclosing agent, execution, and turn disclosure once; the reader can collapse them again until a later promotion revision explicitly requests another reveal. Shell commands run through the harness's own tool are recorded as foreign cells because they ran in the harness's shell rather than this kernel. User prompts, portable shell/tool activity, and Claude Code's final agent message are captured for Codex and Claude Code; reasoning capture is Codex-only and requires `model_reasoning_summary` in the Codex config. Exact request-scoped parentage for native IPython, kernel Bash, and authored Markdown cells currently depends on Codex's MCP turn metadata.

Plugins are ordinary `Plugin(server=..., environment=..., kernel=...)` values.
They can come from built-ins, selected `ipi.extensions` entry points,
`~/.ipi/extensions/`, or `<cwd>/.ipi/extensions/`. See
[Developing External IPi Plugins](docs/your-first-extension.md) for the API and
[Architecture](docs/architecture.md) for ownership and lifecycle details.
Coding agents that create or debug plugins can opt into the
[`developing-ipi-plugins` skill](ipi/skills/developing-ipi-plugins/SKILL.md);
it is not auto-read by the MCP server.
Harness handlers, native observers, and context providers use `async def` and
run on a dedicated IPi-owned daemon event-loop worker. The five-second callback
and 25-second dispatch limits are hard response deadlines: timed-out callbacks
fail open and late results are ignored. `HarnessRuntime` exposes only immutable
`snapshot`, `environment`, and `config` state. Callbacks should propagate
cancellation and must not capture async objects bound to FastMCP's serving loop.
Hook dispatch never discovers plugins: it uses the startup environment or an
pre-resolved kernel environment with the exact event cwd, and otherwise fails open
without plugin output.

## Development

```bash
uv sync --all-extras --all-groups
uv run ruff check .
uv run ruff format --check .
uv run basedpyright
uv run pytest -m "not kernel and not live_api" -q
uv run pytest -m kernel -q
uv run python benchmarks/model_output_tokens.py
```

Breaking API changes are recorded in [BREAKING_CHANGES.md](BREAKING_CHANGES.md).
Release procedure is documented in [docs/releasing.md](docs/releasing.md).

## Optional notebook startup in Codex

When the host owns MCP configuration, `ipi install-hooks --provider codex --async-startup` installs native asynchronous SessionStart and SubagentStart forwarding. The bridge readiness wait defaults to 300 seconds and can be set with `--startup-wait`. Other event forwarders do not wait for a missing bridge. The Python `install_hooks` API exposes `async_startup=True` and `startup_wait=` too.

Configure the notebook MCP server as optional and set a positive `mcp_optional_startup_grace_ms` in Codex to let the first turn proceed while it starts. Zero disables the grace deadline and waits for startup. Completed background context and newly ready tools appear at subsequent native context/catalog boundaries. Ordinary chat remains available if optional notebook startup fails.
