|
| 1 | +# ADR-0003: One Extension Runner process per project execution environment |
| 2 | + |
| 3 | +- **Status:** accepted |
| 4 | +- **Date:** 2026-03-19 |
| 5 | +- **Deciders:** @Aksem |
| 6 | +- **Tags:** architecture, extension-runner |
| 7 | + |
| 8 | +## Context |
| 9 | + |
| 10 | +FineCode executes action handlers contributed by extensions. Each handler |
| 11 | +declares the **execution environment** (`env`) it runs in and its own set of |
| 12 | +dependencies. An execution environment is a named, isolated context serving a |
| 13 | +specific purpose (e.g. `runtime` for the project's own runtime code, |
| 14 | +`dev_workspace` for workspace tooling, `dev_no_runtime` for dev tools without |
| 15 | +runtime deps). In Python, each execution environment is materialized as a |
| 16 | +project-local virtual environment. |
| 17 | + |
| 18 | +The **Extension Runner (ER)** is an inter-language concept — a process that |
| 19 | +executes handler code inside a specific execution environment. The current |
| 20 | +implementation, `finecode_extension_runner`, is Python-specific. Future |
| 21 | +implementations for other languages (e.g. JavaScript, Rust) would follow the |
| 22 | +same one-process-per-execution-environment model. |
| 23 | + |
| 24 | +The primary requirement is to separate dependencies needed by the project's |
| 25 | +own runtime from dependencies needed only by tooling. FineCode must be able to |
| 26 | +run project code in one execution environment and run development tooling in |
| 27 | +other execution environments without forcing them into a single shared |
| 28 | +dependency set. |
| 29 | + |
| 30 | +Once execution environments are isolated, they can also be made more |
| 31 | +fine-grained by purpose. This allows tooling dependencies to be grouped |
| 32 | +according to their role and makes it possible to move tools with incompatible |
| 33 | +dependency requirements into separate execution environments when needed. |
| 34 | + |
| 35 | +The Workspace Manager (WM) is a long-running server that must stay stable |
| 36 | +across the full user session. A handler bug, crash, or blocking call in one |
| 37 | +execution environment must not take down the WM or interfere with other |
| 38 | +execution environments. |
| 39 | + |
| 40 | +## Related ADRs Considered |
| 41 | + |
| 42 | +None — process isolation model has no overlap with other ADRs at the time of writing. |
| 43 | + |
| 44 | +## Decision |
| 45 | + |
| 46 | +Each execution environment in a project runs as an independent |
| 47 | +**Extension Runner (ER)** subprocess. In the Python implementation, the ER is |
| 48 | +launched using the interpreter from the corresponding project-local virtual |
| 49 | +environment, so each ER has a fully isolated dependency set. |
| 50 | + |
| 51 | +Key properties of this design: |
| 52 | + |
| 53 | +- **One ER per (project, execution environment) pair.** ERs are keyed by |
| 54 | + `(project_dir_path, env_name)` in the WM's workspace context. |
| 55 | +- **Lazy startup with bootstrap exception.** An ER is started only when the |
| 56 | + first action request requiring its execution environment arrives, then cached |
| 57 | + and reused for subsequent requests. The `dev_workspace` execution |
| 58 | + environment is the exception because it must be started first to resolve |
| 59 | + presets for other execution environments. |
| 60 | +- **JSON-RPC over TCP.** Each ER binds to a random loopback port on startup |
| 61 | + and advertises it to the WM. The WM connects via TCP and communicates using |
| 62 | + JSON-RPC with Content-Length framing (the same wire format as LSP). |
| 63 | +- **Independent lifecycle.** An ER can crash and be restarted without |
| 64 | + affecting the WM or ERs for other execution environments. Shutdown is |
| 65 | + cooperative: the WM sends `shutdown` + `exit` JSON-RPC calls; the ER exits |
| 66 | + cleanly. |
| 67 | +- **`dev_workspace` bootstrap execution environment.** The `dev_workspace` |
| 68 | + execution environment is always started first; it resolves presets for all |
| 69 | + other execution environments before they are configured or started. |
| 70 | + |
| 71 | +## Consequences |
| 72 | + |
| 73 | +- **Dependency isolation**: project runtime dependencies and tooling |
| 74 | + dependencies are kept separate, and tooling can be split further into |
| 75 | + purpose-specific execution environments when conflicts or different |
| 76 | + dependency sets require it. |
| 77 | +- **Fault isolation**: a crash or hang in one ER does not affect the WM or |
| 78 | + other ERs. The WM can restart a failed ER independently. |
| 79 | +- **Startup cost**: launching a Python subprocess and importing handler modules |
| 80 | + takes time. Mitigated by lazy startup and long-lived reuse. |
| 81 | +- **Higher memory usage**: running multiple ER processes per project uses more |
| 82 | + RAM than a single shared process. The overhead is expected to be acceptable |
| 83 | + relative to the benefits of dependency isolation, fault isolation, and |
| 84 | + long-lived per-environment state. |
| 85 | +- **One virtual environment per execution environment per project**: |
| 86 | + `prepare-envs` must create and populate the project-local virtual |
| 87 | + environment for each declared execution environment before the ER can start. |
| 88 | + Missing virtual environments result in `RunnerStatus.NO_VENV` rather than a |
| 89 | + crash. |
| 90 | +- **`dev_workspace` is a prerequisite**: preset resolution depends on the |
| 91 | + `dev_workspace` ER being available. Actions in other execution environments |
| 92 | + cannot be configured until `dev_workspace` is initialized. |
| 93 | + |
| 94 | +### Alternatives Considered |
| 95 | + |
| 96 | +- **Single shared process for all handlers**: eliminates subprocess overhead |
| 97 | + but forces runtime code and tooling into one shared dependency set, makes |
| 98 | + fine-grained environment separation impractical, and means one handler crash |
| 99 | + can corrupt or kill the entire tool. |
| 100 | +- **Thread per handler invocation**: handlers run in the same process and |
| 101 | + virtual environment. No dependency isolation; a blocking or crashing handler |
| 102 | + affects all others. |
| 103 | +- **In-process plugin loading**: simplest architecture but handlers can import |
| 104 | + conflicting packages and accidentally mutate shared WM state. |
| 105 | +- **New subprocess per handler invocation**: full isolation per call, but |
| 106 | + Python startup cost makes interactive use (e.g. format-on-save) too slow. |
| 107 | + It also prevents effective in-process caching between calls because each |
| 108 | + invocation starts with cold process state. The long-lived ER model amortizes |
| 109 | + startup cost across many invocations and allows caches to be retained in |
| 110 | + process when appropriate. |
0 commit comments