Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
138 commits
Select commit Hold shift + click to select a range
b71a85f
Run public REPL calls in disposable processes
Deicyde Oct 5, 2026
c0bae84
Validate REPL imports with the selected Lean parser
Deicyde Oct 5, 2026
02f3c68
Qualify the selected REPL package target
Deicyde Oct 5, 2026
c04c2c2
Pin the consumer REPL dependency in setup
Deicyde Oct 5, 2026
bcd8ebd
Add REPL fault injection and pinned integration CI
Deicyde Oct 5, 2026
b2da264
Merge branch 'split/repl-lean-header-validation' into split/repl-faul…
Deicyde Oct 5, 2026
132d636
Preserve cancellation and validate warmup imports
Deicyde Oct 5, 2026
f90e053
Inject header cancellation and cleanup failures
Deicyde Oct 5, 2026
2ceeca6
Merge branch 'split/repl-lean-header-validation' into split/repl-faul…
Deicyde Oct 5, 2026
e50637e
Unblock waiting REPL calls during shutdown
Deicyde Oct 5, 2026
92564e3
Merge branch 'split/repl-disposable-lifecycle' into split/repl-lean-h…
Deicyde Oct 5, 2026
6820410
Merge branch 'split/repl-lean-header-validation' into split/repl-faul…
Deicyde Oct 5, 2026
4e1f9ec
Preserve Lean module and prelude headers
Deicyde Oct 5, 2026
d00c03b
Merge remote-tracking branch 'upstream/split/repl-lean-header-validat…
Deicyde Oct 5, 2026
048e88e
Exercise directive-safe REPL composition
Deicyde Oct 5, 2026
8c9b702
Retry quarantined REPL cleanup before replacement
Deicyde Oct 5, 2026
3b7dedc
Merge remote-tracking branch 'upstream/split/repl-disposable-lifecycl…
Deicyde Oct 5, 2026
05aa715
Merge remote-tracking branch 'upstream/split/repl-lean-header-validat…
Deicyde Oct 5, 2026
d16caf5
Preserve cancellation through REPL quarantine
Deicyde Oct 5, 2026
26d277d
Retain cache lease through REPL settlement
Deicyde Oct 5, 2026
c5cfd79
Log unexpected REPL settlement retries
Deicyde Oct 5, 2026
76c6eb0
Merge remote-tracking branch 'upstream/split/repl-disposable-lifecycl…
Deicyde Oct 5, 2026
70ed623
Merge remote-tracking branch 'upstream/split/repl-lean-header-validat…
Deicyde Oct 5, 2026
9eea324
Merge remote-tracking branch 'origin/main' into split/repl-disposable…
Deicyde Oct 5, 2026
db02051
Collapse REPL pool settlement into one cleanup loop
Deicyde Oct 5, 2026
2ce2d8b
Merge remote-tracking branch 'origin/main' into split/repl-disposable…
Deicyde Oct 5, 2026
097168f
Merge remote-tracking branch 'origin/split/repl-disposable-lifecycle'…
Deicyde Oct 5, 2026
4552041
Run the Lean header parser as the slot's process generation
Deicyde Oct 5, 2026
f3da058
Tighten Lean header validation tests
Deicyde Oct 5, 2026
2318897
Merge remote-tracking branch 'origin/split/repl-lean-header-validatio…
Deicyde Oct 5, 2026
7ae9370
Withhold a dirty REPL slot instead of quarantining the pool
Deicyde Oct 6, 2026
1fe0d62
Merge remote-tracking branch 'origin/split/repl-disposable-lifecycle'…
Deicyde Oct 6, 2026
2c9e611
Merge remote-tracking branch 'origin/split/repl-lean-header-validatio…
Deicyde Oct 6, 2026
fcfcabb
Share the header cleanup fault injector and compact REPL tests
Deicyde Oct 6, 2026
c6f770e
Merge remote-tracking branch 'origin/main' into split/repl-disposable…
Deicyde Oct 6, 2026
208e48d
Merge remote-tracking branch 'origin/split/repl-disposable-lifecycle'…
Deicyde Oct 6, 2026
0a1e822
Merge remote-tracking branch 'origin/split/repl-lean-header-validatio…
Deicyde Oct 6, 2026
20cbba4
Document the real REPL CI job
Deicyde Oct 6, 2026
579e9e3
Merge remote-tracking branch 'origin/main' into split/repl-disposable…
Deicyde Oct 6, 2026
f96c69e
Merge remote-tracking branch 'origin/split/repl-disposable-lifecycle'…
Deicyde Oct 6, 2026
88997e2
Merge remote-tracking branch 'origin/split/repl-lean-header-validatio…
Deicyde Oct 6, 2026
0ff7b7c
Merge current main into disposable REPL lifecycle
Deicyde Oct 6, 2026
0105df7
Restack Lean header validation on disposable REPL lifecycle
Deicyde Oct 6, 2026
4f4f50f
Restack REPL fault coverage on header validation
Deicyde Oct 6, 2026
23a129c
Integrate retained cleanup with disposable REPL calls
Deicyde Oct 6, 2026
3db313a
Constrain the legacy header parser guard
Deicyde Oct 6, 2026
bbd67cc
Compare legacy header parser interpretations
Deicyde Oct 6, 2026
6ec70b0
Compare all suspect legacy header closes
Deicyde Oct 6, 2026
378fa77
Reject ambiguous import module components
Deicyde Oct 6, 2026
8bfdf44
Bound legacy header compatibility checks
Deicyde Oct 7, 2026
cb790a7
Preserve warmup composition contracts
Deicyde Oct 7, 2026
ade5186
Separate warmup composition from import policy
Deicyde Oct 7, 2026
e47ef0e
Read REPL headers with Lean's own header parser
Deicyde Oct 11, 2026
c0d59a3
Compose the Mathlib warmup from the parsed header kind
Deicyde Oct 12, 2026
055af77
Install the real-REPL job's Elan from the pinned release
Deicyde Oct 12, 2026
5fb08be
Say when the Lean runtime refuses to start
Deicyde Oct 12, 2026
b85d800
Report diagnostics in generated imports at line 1
Deicyde Oct 12, 2026
b892420
Escape each import component the header parser prints
Deicyde Oct 13, 2026
1278fbe
Say that the header check costs one Lean process per call
Deicyde Oct 13, 2026
9bddb70
Merge main now that #168 and #111 have landed
Deicyde Oct 13, 2026
03d7346
Name both remedies when Lake cannot find the REPL package
Deicyde Oct 13, 2026
2322c97
Check the header parser on the oldest supported Lean in CI
Deicyde Oct 13, 2026
c716f99
Default the REPL timeout to a lowered maximum
Deicyde Oct 13, 2026
482418a
Report the last output line of a failed header command
Deicyde Oct 13, 2026
2522637
Rename the REPL header command field to header_command
Deicyde Oct 13, 2026
27756d1
Refuse header output that parse_header.lean does not print
Deicyde Oct 13, 2026
1decfbf
Tell a failed header check apart from a rejected header
Deicyde Oct 13, 2026
b7b1297
Cover each fail-closed branch of the header decoder
Deicyde Oct 13, 2026
1d051b6
Test the header output cap across stdout and stderr together
Deicyde Oct 13, 2026
78d0892
Spawn the REPL and the header parser through one helper
Deicyde Oct 13, 2026
2e5cc6a
Explain the prelude index and drop a redundant with_traceback
Deicyde Oct 13, 2026
76a23c0
Test how run_disposable maps each REPL response outcome
Deicyde Oct 13, 2026
de9dcf4
Delete the resident REPL path that the runtime no longer reaches
Deicyde Oct 13, 2026
77ecebd
Describe the disposable REPL model in the stale docstrings
Deicyde Oct 13, 2026
46bdd2b
Say in run_lean_code what agents need to know
Deicyde Oct 13, 2026
33f33c4
Drop the 240-second cap on AUTOFORM_MAX_REPL_REQUEST_SECONDS
Deicyde Oct 13, 2026
b95bbc4
State the caller's budget when a REPL call times out
Deicyde Oct 13, 2026
3e6f1cd
Put the REPL require remedy ahead of the failure details
Deicyde Oct 13, 2026
d044ddf
Say that LEAN_REPL_CMD needs a runtime restart and applies to all
Deicyde Oct 13, 2026
cb4fc5e
Say that a call refused by a shut-down REPL pool can be retried
Deicyde Oct 13, 2026
f20f96a
Revert the unrelated reformatting in the REPL pool
Deicyde Oct 13, 2026
125064c
Word the header-command error as the check the runtime makes
Deicyde Oct 13, 2026
ffb93cb
Assert no cleanup was refused in the slow-reap timeout test
Deicyde Oct 13, 2026
98b4523
State the caller's budget when a queued REPL call times out
Deicyde Oct 13, 2026
5a1ec6a
Let the cold-slot shutdown test fail instead of hanging
Deicyde Oct 13, 2026
5cb7e4d
Make the runtime warmup test fail when no prefix is added
Deicyde Oct 13, 2026
66ca297
Drop two REPL core test cases that repeat other cases
Deicyde Oct 13, 2026
899c500
Remove test stubs and setup that no test exercises
Deicyde Oct 13, 2026
a69b172
Fix stale test names and an environment-dependent assertion
Deicyde Oct 13, 2026
ac12a80
Model the idle-slot wait with a controlled clock in pool tests
Deicyde Oct 13, 2026
c8b6337
Assert that the wheel ships the REPL header parser
Deicyde Oct 13, 2026
b49f78a
Give REPL isolation its own README paragraph
Deicyde Oct 13, 2026
f4be78b
Say the REPL import allowlist is not a sandbox
Deicyde Oct 13, 2026
75336aa
State the per-call cost of disposable REPL calls
Deicyde Oct 13, 2026
81c7038
Call the per-project REPL pool a slot pool throughout
Deicyde Oct 13, 2026
4915e16
Say the REPL commands apply to every project the runtime serves
Deicyde Oct 13, 2026
50130f5
Describe the REPL environment variables as the runtime reads them
Deicyde Oct 13, 2026
3c80a3c
Drop the 240-second REPL cap from the servers README
Deicyde Oct 13, 2026
b20638b
Say how to run the real-REPL tests locally
Deicyde Oct 13, 2026
e8d5a04
Say which REPL revision to pin for a project's Lean release
Deicyde Oct 13, 2026
d7e7135
List the example's lake manifest among its pins
Deicyde Oct 13, 2026
4fbb732
Tie the example's REPL pin to the fixture the real-REPL job builds
Deicyde Oct 13, 2026
5c4958c
Name and bound the real-REPL job's last two steps
Deicyde Oct 13, 2026
590d6e6
Say what to do when a project has no REPL require
Deicyde Oct 13, 2026
4ce3ea4
Test the require hint when Lake exits before the frame is sent
Deicyde Oct 13, 2026
51a531e
Name the import whose module components are refused
Deicyde Oct 13, 2026
0a37adc
Say the header output cap counts stdout and stderr
Deicyde Oct 13, 2026
b072c9a
Pin the fallback order of a failed header command's detail
Deicyde Oct 13, 2026
323d01f
Drop the REPL startup timeout that no longer bounds startup
Deicyde Oct 13, 2026
a86649e
Catch a stderr backlog at the frame and map it once
Deicyde Oct 13, 2026
13bfc71
State the budget when a REPL call times out before dispatch
Deicyde Oct 13, 2026
a56d476
Stop describing a REPL process that serves another request
Deicyde Oct 13, 2026
3a6281c
Let the post-send timeout test use the real close
Deicyde Oct 13, 2026
375d833
Say a frame Lake never reads is not fully sent
Deicyde Oct 13, 2026
00a0f1e
Make the REPL start stubs take no arguments
Deicyde Oct 13, 2026
e1f49f4
Drop the second-request name from the stdout recheck test
Deicyde Oct 13, 2026
4bac7da
Drop the command-boundary rationale from the REPL stderr comments
Deicyde Oct 13, 2026
af6c3eb
Pin the shutdown wording at every pool raise site
Deicyde Oct 13, 2026
4061b6a
Say seconds and slot in the REPL pool's queue timeout
Deicyde Oct 13, 2026
2087358
Start each FIFO pool waiter only after the last one has parked
Deicyde Oct 13, 2026
01e2aa3
Say what the REPL pool's memory figure counts
Deicyde Oct 13, 2026
c8f3261
Stop calling the REPL pools expensive process pools
Deicyde Oct 13, 2026
0556603
Say in run_lean_code what a call costs and what timeout covers
Deicyde Oct 13, 2026
5eb1ca6
Start the runtime tests from the default configuration
Deicyde Oct 13, 2026
9035375
Name the allowlisted import roots in the servers README
Deicyde Oct 13, 2026
1ad2787
Break up the runtime configuration text in the servers README
Deicyde Oct 13, 2026
a5020dd
Say that a set LEAN_REPL_HEADER_CMD overrides the lake default
Deicyde Oct 13, 2026
38936de
Say how a changed REPL command reaches the restarted runtime
Deicyde Oct 13, 2026
35c02f8
Say that a project without a REPL require fails every REPL call
Deicyde Oct 13, 2026
067db5f
Tell example adopters to move the REPL pin with the Lean version
Deicyde Oct 13, 2026
48dae8d
Check the REPL locks and the tag the docs name against one commit
Deicyde Oct 13, 2026
4d63549
Scope the post-slot shutdown patch to the call under test
Deicyde Oct 13, 2026
e3d7b65
Put the node-wide README pin on one line
Deicyde Oct 13, 2026
80e0631
Drop the REPL environment id that no call passes
Deicyde Oct 13, 2026
4da89ed
Reflow the servers README's REPL command paragraph
Deicyde Oct 13, 2026
a9e609a
Merge main abae7671 into split/repl-disposable-lifecycle
Deicyde Oct 13, 2026
e2b7a74
Merge main 6e4c0037 into split/repl-disposable-lifecycle
Deicyde Oct 13, 2026
dab0fd5
Merge main 937e19e6 into split/repl-disposable-lifecycle
Deicyde Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,47 @@ jobs:
AUTOFORM_REQUIRE_REAL_LEAN_TESTS: "1"
AUTOFORM_REAL_LEAN_MATHLIB_PROJECT: ${{ runner.temp }}/autoform-created-project

real-repl:
name: real REPL (fixture toolchain)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: "0.12.1"
python-version: "3.13"
enable-cache: true
- name: Install pinned Elan
run: |
set -euo pipefail
timeout --signal=TERM --kill-after=15s 2m curl -sSfL \
https://github.com/leanprover/elan/releases/download/v4.2.3/elan-x86_64-unknown-linux-gnu.tar.gz \
-o elan.tar.gz
echo "df0b2b3a439961ffcbb3985214365ffe40f49bc871df04dff268c7d8e21ca8b2 elan.tar.gz" \
| sha256sum --check --strict
tar xzf elan.tar.gz
./elan-init -y --default-toolchain none
echo "$HOME/.elan/bin" >> "$GITHUB_PATH"
- name: Build the REPL fixture
working-directory: tests/fixtures/repl-smoke
run: |
set -euo pipefail
timeout --signal=TERM --kill-after=30s 5m \
elan toolchain install "$(tr -d '\r\n' < lean-toolchain)"
timeout --signal=TERM --kill-after=30s 8m lake build Mathlib Secret @repl/repl
- name: Install the minimum supported Lean toolchain
run: |
set -euo pipefail
timeout --signal=TERM --kill-after=30s 5m \
elan toolchain install "$(tr -d '\r\n' < tests/fixtures/minimum-lean/lean-toolchain)"
- name: Install locked development environment
run: timeout --signal=TERM --kill-after=30s 3m uv sync --frozen --extra dev --extra repl
- name: Run the REPL suite against the pinned upstream REPL
run: timeout --signal=TERM --kill-after=30s 10m uv run pytest -q tests/test_real_repl.py
env:
AUTOFORM_RUN_REAL_REPL_TESTS: "1"

windows-hardening:
name: Windows inspection, subprocess, and publication hardening
runs-on: windows-latest
Expand Down
14 changes: 13 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ CI (`.github/workflows/tests.yml`) runs the same four steps on Python 3.10 and
3.13 for every push and pull request. A separate job runs
`tests/test_skeleton.py` and one `tests/test_project_inspect.py` test against a
real Lean toolchain; another runs the inspection, bounded-subprocess, and
transactional-output tests on Windows. Locally, tests that need Lean skip when
transactional-output tests on Windows; a third builds
`tests/fixtures/repl-smoke` and runs `tests/test_real_repl.py` against the
pinned upstream REPL, also checking the header parser on the oldest Lean release
Autoform supports. Locally, tests that need Lean skip when
`lake` is not on `PATH`. The `tests/test_skeleton.py` ones also skip when the
toolchain pinned in `tests/fixtures/skeleton-project/lean-toolchain` is
missing; the others let elan download it. The `tests/test_library_generate.py`
Expand All @@ -28,6 +31,15 @@ job does; a missing toolchain or a failed download then fails them. Run
`lake build` in `skills/setup/assets/cabannes-thesis-project` when you change
the example's Lean sources or declarations.

`tests/test_real_repl.py` skips unless `AUTOFORM_RUN_REAL_REPL_TESTS=1`. To run
it locally, build its fixture and install the oldest supported toolchain first:

```bash
(cd tests/fixtures/repl-smoke && lake build Mathlib Secret @repl/repl)
elan toolchain install "$(cat tests/fixtures/minimum-lean/lean-toolchain)"
AUTOFORM_RUN_REAL_REPL_TESTS=1 uv run pytest -q tests/test_real_repl.py
```

## Pull Requests
We actively welcome your pull requests.

Expand Down
80 changes: 67 additions & 13 deletions servers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,37 @@ when useful.
Plugin hosts start the two stdio MCP processes automatically. They are
lightweight adapters: the first Lean tool call race-safely starts a detached
runtime for the current AutoformBot installation, Unix user, and compute node.
That runtime owns one resident REPL pool and LSP session per active Lean
project, so sessions using that installation reuse the same warmed processes.
Closing the session that started it does not stop it; after a crash, the next
tool call starts it again. Runtime sockets include a code fingerprint, so an
in-place upgrade gracefully replaces the older build.
That runtime owns one REPL slot pool and LSP session per active Lean
project. Closing the session that started the runtime does not stop it; after a
crash, the next tool call starts it again. Runtime sockets include a code
fingerprint, so an in-place upgrade gracefully replaces the older build.

REPL and LSP processes remain lazy. A cold tool call stays pending while Lean
warms up, so no `/repl-start`, `/lsp-start`, or model-side sleep is needed. Idle
project processes are closed after 30 minutes by default, while the small
runtime remains available. Its lifecycle is also explicit:
LSP sessions stay warm; each public REPL call gets a fresh child and no
process-owned environment or proof-state handle survives the response. Before
that child starts, the selected Lake toolchain runs Lean's own header parser,
which the REPL also uses, on the submitted source. Every import it reports must
have one of the allowlisted roots `Mathlib`, `Aesop`, `Batteries`, or
`LeanSearchClient` (a plain `Init` import is ignored), and a rejected or
unrecognized parser response fails closed.
Allowlisted imports require plain, nonempty canonical module components; names
that remain quoted or path-like after Lean parsing are refused instead of being
reduced to a misleading allowlisted root. The allowlist is a policy check, not a
sandbox: each call runs with the runtime user's privileges, and `Init` alone
provides file and process IO, for example `#eval IO.Process.run`. The header
check runs as one extra short Lean process per call. Execution names the
package-qualified `@repl/repl` target so a project target cannot shadow it.

REPL children and LSP processes remain lazy. A tool call stays pending while
Lean starts, so no `/repl-start`, `/lsp-start`, or model-side sleep is needed.
Every REPL call starts Lean afresh and, unless its header starts with `module`
or `prelude`, imports Mathlib, so it takes at least as long as `import Mathlib`
on the node. Calls to the same project run one at a time unless
`AUTOFORM_REPL_WORKERS_PER_PROJECT` is raised. The REPL `timeout` is one total
post-admission budget for an idle-slot wait, header validation, child startup,
generated imports, and submitted code; it and its cap default to 240 seconds.
Project-cache admission uses the daemon response budget separately. Idle project
state is closed after 30 minutes by default, while the small runtime remains
available. Its lifecycle is also explicit:

```bash
uv run autoform-lean-runtime start
Expand All @@ -45,7 +66,40 @@ The private socket lives below `$XDG_RUNTIME_DIR/autoform`, falling back to a
uid-specific directory in `/tmp`; the rotating runtime log is beside it.
`AUTOFORM_RUNTIME_DIR` overrides that location. Node-wide limits are controlled
by `AUTOFORM_REPL_TOTAL_WORKERS`, `AUTOFORM_REPL_WORKERS_PER_PROJECT`,
`AUTOFORM_MAX_LEAN_PROJECTS`, and `AUTOFORM_LEAN_IDLE_SECONDS`. The first
process to start the runtime supplies those settings until it is stopped.
`AUTOFORM_RUNTIME_RESPONSE_TIMEOUT` can raise the client/daemon response budget
when unusually large worker pools need more than the default 15 minutes to warm.
`AUTOFORM_MAX_LEAN_PROJECTS`, and `AUTOFORM_LEAN_IDLE_SECONDS`. A REPL worker is
one concurrent call slot; an idle slot holds no Lean process. The first process
to start the runtime supplies those settings until it is stopped.

`LEAN_REPL_CMD` selects the disposable child command. Its default,
`lake exe @repl/repl`, finds the REPL only through a require named `repl`. For a
require with any other name, rename the require to `repl`, or restart the
runtime with `LEAN_REPL_CMD="lake exe @<name>/repl"` for a require named
`<name>`, which applies to every project the runtime serves. A project with no
REPL require, as `autoform project new` and `init` leave it, fails every REPL
call until it adds one as the [setup skill](../skills/setup/SKILL.md) describes.
When `LEAN_REPL_HEADER_CMD` is unset and `LEAN_REPL_CMD` starts with a `lake`
executable, absolute or not, header validation reuses that executable without
the command's other arguments. A container or other wrapper must also set
`LEAN_REPL_HEADER_CMD` to a command that runs `servers/repl/parse_header.lean`
with the matching toolchain's `lean --run`, passing the source on standard
input. Like the limits above, `LEAN_REPL_CMD` and `LEAN_REPL_HEADER_CMD` are
node-wide and read only when the runtime starts; after changing either, run
`uv run autoform-lean-runtime stop`. The next process to start the runtime, an
MCP adapter serving a tool call or `uv run autoform-lean-runtime start`, must
have the new values in its environment, and
`uv run autoform-lean-runtime status` shows the resolved commands under
`config`.

`AUTOFORM_MAX_REPL_REQUEST_SECONDS` caps the REPL `timeout`; when
`AUTOFORM_REPL_REQUEST_TIMEOUT` is unset, the default `timeout` is the lower of
240 seconds and that cap. If `LEAN_REPL_CMD` does not start with a `lake`
executable and `LEAN_REPL_HEADER_CMD` is unset, if an explicit
`AUTOFORM_REPL_REQUEST_TIMEOUT` exceeds that cap, or if that cap does not fit
the response budget below, the runtime refuses to start, so the LSP tools are
unavailable as well.

`AUTOFORM_RUNTIME_RESPONSE_TIMEOUT` (default 15 minutes) bounds project
admission, the tool operation, and a safety margin. Failed cleanup transfers to
a retained background reaper and may continue past that response budget rather
than allowing an overlapping replacement. A `warm` REPL project means its REPL
slot pool is cached; it does not mean a REPL subprocess remains resident.
4 changes: 2 additions & 2 deletions servers/lean_client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Client and bootstrap logic for Autoform's node-local Lean runtime.

The two public MCP processes are intentionally short-lived stdio adapters. A
small detached process owns the expensive Lean REPL pools and LSP sessions and
is reached through a private Unix-domain socket.
small detached process owns the warm LSP sessions and the per-project REPL slot
pools, and is reached through a private Unix-domain socket.
"""

from __future__ import annotations
Expand Down
56 changes: 37 additions & 19 deletions servers/lean_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@
LeanLspSession,
format_lsp_diagnostics,
)
from servers.repl.core import DEFAULT_REPL_STARTUP_TIMEOUT, format_repl_response
from servers.repl.core import default_header_command, format_repl_response
from servers.repl.pool import (
DEFAULT_RAM_FRACTION,
DEFAULT_STARTUP_STAGGER_SECONDS,
LeanReplPool,
LeanReplPoolConfig,
LeanReplPoolStartupError,
Expand All @@ -62,7 +61,7 @@
DEFAULT_IDLE_SECONDS = 30 * 60
DEFAULT_LSP_TIMEOUT = 60.0
DEFAULT_MAX_LSP_REQUEST_SECONDS = 600.0
DEFAULT_REPL_REQUEST_TIMEOUT = 30.0
DEFAULT_REPL_REQUEST_TIMEOUT = 240.0
DEFAULT_MAX_REPL_REQUEST_SECONDS = 240.0
DEFAULT_RPC_READ_TIMEOUT = 10.0
DEFAULT_MAX_CONNECTIONS = 64
Expand All @@ -88,12 +87,8 @@ class _FailedResourceCreation:


def _repl_creation_budget(worker_count: int) -> float:
"""Bound victim cleanup, cold startup, and failed-start cleanup."""
return (
worker_count * DEFAULT_REPL_STARTUP_TIMEOUT
+ max(0, worker_count - 1) * DEFAULT_STARTUP_STAGGER_SECONDS
+ 2 * worker_count * REPL_WORKER_CLOSE_BUDGET
)
"""Bound cleanup of an evicted pool; cold slots spawn no subprocesses."""
return worker_count * REPL_WORKER_CLOSE_BUDGET


def _positive_int(name: str, default: int) -> int:
Expand Down Expand Up @@ -147,6 +142,7 @@ class LeanRuntimeConfig:
repl_workers_per_project: int
repl_project_limit: int
repl_command: tuple[str, ...]
repl_header_command: tuple[str, ...]
lsp_command: tuple[str, ...]
lsp_timeout: float
max_lsp_request_seconds: float
Expand Down Expand Up @@ -185,20 +181,33 @@ def from_environment(cls) -> "LeanRuntimeConfig":
"AUTOFORM_REPL_TOTAL_WORKERS"
)
repl_project_limit = min(max_projects, total_workers // workers_per_project)
repl_command = tuple(shlex.split(os.environ.get("LEAN_REPL_CMD", "lake exe repl")))
repl_command = tuple(shlex.split(os.environ.get("LEAN_REPL_CMD", "lake exe @repl/repl")))
raw_header_command = os.environ.get("LEAN_REPL_HEADER_CMD")
lsp_command = tuple(shlex.split(os.environ.get("LEAN_LSP_CMD", "lake serve")))
if not repl_command:
raise ValueError("LEAN_REPL_CMD must not be empty")
if raw_header_command is not None:
repl_header_command = tuple(shlex.split(raw_header_command))
if not repl_header_command:
raise ValueError("LEAN_REPL_HEADER_CMD must not be empty")
elif Path(repl_command[0]).name == "lake":
repl_header_command = tuple(default_header_command(repl_command[0]))
else:
raise ValueError(
"LEAN_REPL_HEADER_CMD must be set when LEAN_REPL_CMD does not "
"start with a `lake` executable"
)
if not lsp_command:
raise ValueError("LEAN_LSP_CMD must not be empty")
repl_request_timeout = _positive_float(
"AUTOFORM_REPL_REQUEST_TIMEOUT",
DEFAULT_REPL_REQUEST_TIMEOUT,
)
max_repl_request_seconds = _positive_float(
"AUTOFORM_MAX_REPL_REQUEST_SECONDS",
DEFAULT_MAX_REPL_REQUEST_SECONDS,
)
# The default follows a lowered maximum, so the maximum can be set alone.
repl_request_timeout = _positive_float(
"AUTOFORM_REPL_REQUEST_TIMEOUT",
min(DEFAULT_REPL_REQUEST_TIMEOUT, max_repl_request_seconds),
)
if repl_request_timeout > max_repl_request_seconds:
raise ValueError(
"AUTOFORM_REPL_REQUEST_TIMEOUT cannot exceed "
Expand Down Expand Up @@ -226,7 +235,7 @@ def from_environment(cls) -> "LeanRuntimeConfig":
):
raise ValueError(
"AUTOFORM_RUNTIME_RESPONSE_TIMEOUT is too small for the configured "
"REPL worker startup and request limits"
"REPL pool replacement and request limits"
)
if (
LSP_CLOSE_BUDGET
Expand All @@ -246,6 +255,7 @@ def from_environment(cls) -> "LeanRuntimeConfig":
repl_workers_per_project=workers_per_project,
repl_project_limit=max(1, repl_project_limit),
repl_command=repl_command,
repl_header_command=repl_header_command,
lsp_command=lsp_command,
lsp_timeout=lsp_timeout,
max_lsp_request_seconds=max_lsp_request_seconds,
Expand All @@ -270,6 +280,7 @@ def as_dict(self) -> dict[str, Any]:
"repl_workers_per_project": self.repl_workers_per_project,
"repl_project_limit": self.repl_project_limit,
"repl_command": list(self.repl_command),
"repl_header_command": list(self.repl_header_command),
"lsp_command": list(self.lsp_command),
"lsp_timeout": self.lsp_timeout,
"max_lsp_request_seconds": self.max_lsp_request_seconds,
Expand Down Expand Up @@ -818,8 +829,8 @@ def default_repl_factory(
LeanReplPoolConfig(
cwd=str(project_dir),
repl_command=list(self.config.repl_command),
header_command=list(self.config.repl_header_command),
num_repls=self.config.repl_workers_per_project,
max_retries=0,
)
)
except LeanReplPoolStartupError as error:
Expand All @@ -846,6 +857,7 @@ def default_lsp_factory(
lambda pool: pool.shutdown(),
max_entries=self.config.repl_project_limit,
idle_seconds=self.config.idle_seconds,
is_valid=lambda pool: not pool.requires_retirement,
start_sweeper=start_sweepers,
)
try:
Expand Down Expand Up @@ -886,13 +898,19 @@ def dispatch(self, method: str, params: dict[str, Any]) -> Any:
"timeout exceeds the node-wide limit of "
f"{self.config.max_repl_request_seconds:g} seconds"
)
with self.repl_projects.lease(
project_dir,
root = resolve_lean_project_dir(project_dir)
with self.repl_projects.lease_resolved(
root,
acquisition_timeout=self._acquisition_timeout(effective_timeout),
creation_budget=self.repl_creation_budget,
) as pool:
assert pool is not None
return format_repl_response(pool.run(code, timeout=effective_timeout))
try:
result = pool.run(code, timeout=effective_timeout)
finally:
if pool.requires_retirement:
self.repl_projects.invalidate_resolved(root, pool)
return format_repl_response(result)
if method == "repl.status":
project_dir = self._string_param(params, "project_dir")
with self.repl_projects.observe(project_dir) as (pool, state):
Expand Down
2 changes: 1 addition & 1 deletion servers/repl/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Lean REPL server and process pool."""
"""Lean REPL server and per-project slot pool."""

from .core import LeanRepl, LeanReplConfig
from .pool import LeanReplPool, LeanReplPoolConfig, LeanReplPoolStartupError
Expand Down
Loading
Loading