| 1 | # Extension Runtime v2 (Spatiotemporal Composability) |
| 2 | |
| 3 | English overview of the Reasonix plugin/runtime v2 model. Chinese: [EXTENSION_RUNTIME_V2.zh-CN.md](./EXTENSION_RUNTIME_V2.zh-CN.md). |
| 4 | |
| 5 | ## Protocol and manifest |
| 6 | |
| 7 | - Plugin manifest: exact `apiVersion: reasonix.io/plugin/v2` only (no `v2.0`/`v2.1` aliases; v1 and legacy native rejected). |
| 8 | - Compatibility boundary: no v1 dual-read or automatic migration. Extension |
| 9 | manifests were not publicly released on v1, so v2 is the first supported |
| 10 | runtime manifest. |
| 11 | - Extension wire protocol: `reasonix.extension.v2` (major 2). |
| 12 | - Handshake `provides` must be a subset of the manifest provides ceiling. |
| 13 | |
| 14 | ## Runtime model |
| 15 | |
| 16 | ```text |
| 17 | RuntimeSnapshot = immutable config + dependency view |
| 18 | RuntimeSet / EffectScope = live resources for one generation |
| 19 | RuntimePlan = old → new transition (subgraph classification) |
| 20 | RuntimeOwner = one session lineage's gate + receipts + stream/file evidence |
| 21 | Controller = published generation consumer (admission bound to RuntimeOwner) |
| 22 | ``` |
| 23 | |
| 24 | Component states: `Inactive → Preparing → Active → Draining → Inactive` (or `Failed`). |
| 25 | |
| 26 | ## Rebuild |
| 27 | |
| 28 | - Prefer `boot.RebuildFrom(previousBuildResult, opts)`. |
| 29 | - No-op / interceptor / UI / provider / MCP-only plans use **true subgraph patch** (no `BuildRuntime`); set `ReusedController` so callers must not `Close` the old pointer. |
| 30 | - Provider/MCP subgraph: live sidecar contributions refresh interceptor/provider/UI catalog via `WithLiveContributions`; **system prompt + tool schemas + CacheHash stay stable** (backend roll only). Tool schema renames still require full rebuild. |
| 31 | - Narrow path is **stage → ready → commit** (fail-atomic): |
| 32 | - **Stage**: start/adopt sidecars, build next dispatcher/resolver; does **not** install stream routers or `BindGeneration` on the UI hub. |
| 33 | - **Ready**: await sidecar readiness. |
| 34 | - **Commit**: install stream routers, bind UI generation, replace controller bindings, then publish. |
| 35 | - On any stage/ready/commit failure: `RollbackPlanStart` reattaches Unchanged clients; pre-stage stream routers keep consuming `stream/chunk` / `stream/end`. |
| 36 | - **UI during stage**: the previous UI hub generation stays bound until commit. Sidecars that emit `host/ui/publish` or `host/ui/request` with the staged (next) generation during handshake/ready are **dropped as stale**. Protocol policy: do not rely on UI visibility before the runtime generation is published. |
| 37 | - After successful migration: **publish** new generation, then **drain** old sidecars. |
| 38 | - Drain timeout cancels registered work (controller when replaced, **host provider streams** via `HostStreamRegistry`, extension provider streams, StableProxy/MCP in-flight) then writes `drain-timeout` receipts. |
| 39 | - Draining controllers reject new turns (`turnDroppedDraining`). |
| 40 | |
| 41 | ## Diagnostics |
| 42 | |
| 43 | ```bash |
| 44 | reasonix doctor runtime |
| 45 | reasonix doctor runtime --json |
| 46 | reasonix plugin doctor <name> |
| 47 | ``` |
| 48 | |
| 49 | Reports component status, plan, effect receipts, recoverability, lifecycle |
| 50 | metrics, and the process-local `runtimeOwnerFallbacks` count. Product boot binds |
| 51 | an isolated owner; a non-zero fallback count identifies compatibility code that |
| 52 | reached the shared default owner and needs explicit wiring. |
| 53 | Plan diagnostics separate two facts: `prefixChanged` is computed after build by |
| 54 | comparing the previous and current snapshot `CacheHash`; `providerChanged` |
| 55 | reports provider capability additions, removals, or reloads. A provider-only |
| 56 | backend roll therefore reports `prefixChanged=false, providerChanged=true` when |
| 57 | the provider-visible system prompt and tool schemas remain byte-identical. |
| 58 | |
| 59 | ## Effect receipts |
| 60 | |
| 61 | Irreversible external work is recorded in the current `RuntimeOwner`'s receipt |
| 62 | store. Independent sessions never share publish/drain state or recovery |
| 63 | evidence. Recovery never claims successful rollback for irreversible effects; |
| 64 | use the owner-scoped `AssessRecoverability(generation)` / |
| 65 | `DecideResume(generation)` methods. |
| 66 | |
| 67 | The receipt ledger supports **in-process rebuild/resume only**. It is not |
| 68 | persisted, so recovery after a process crash is outside this runtime-v2 scope. |
| 69 | Memory is bounded to the latest 32 generations and 256 receipts per generation. |
| 70 | Eviction also drops associated file-prior bytes and marks the affected |
| 71 | generation's evidence as truncated, so diagnostics refuse to claim a clean |
| 72 | rollback when complete evidence is no longer available. |
| 73 | Message-send deduplication follows the same receipt retention: evicting a |
| 74 | message receipt releases its `(generation, messageID)` key instead of growing a |
| 75 | second unbounded ledger. File priors are capped at 8 MiB per entry and 32 MiB |
| 76 | per `RuntimeOwner`; writes beyond either limit record `prior_truncated`, and |
| 77 | recovery remains conservative rather than claiming a clean rollback. |
| 78 | |
| 79 | - Provider stream open records `provider-submit:<id>` (irreversible). |
| 80 | - Completed provider streams unregister their drain callback; only streams still |
| 81 | in flight remain retained by the generation gate. |
| 82 | - Drain timeout force-expire records `drain-timeout:<gen>`. `ScheduleDrainWatch` |
| 83 | starts only when a generation is actually draining and coalesces rapid |
| 84 | publishes into one watcher per owner; the doctor sweep remains a fallback. |
| 85 | - Late-cancel expiry markers retain the latest 256 generations per owner. |
| 86 | |
| 87 | ## EffectScope ownership |
| 88 | |
| 89 | Live resources for one generation are tracked on `RuntimeSet` / `EffectScope`: |
| 90 | |
| 91 | | Resource | Tracker | |
| 92 | | --- | --- | |
| 93 | | Sidecar manager | Cancelable effect in activator | |
| 94 | | UI hub binding | `TrackUIHub` | |
| 95 | | MCP plugin host | Inventory + `session-resources` dispose | |
| 96 | | LSP manager | `TrackWatcher` inventory | |
| 97 | | Session cleanup chain | `TrackControllerCleanup` | |
| 98 | | Provider submit | `RecordProviderSubmit` receipt | |
| 99 | |
| 100 | ## Acceptance mapping |
| 101 | |
| 102 | | Spec acceptance | Status | |
| 103 | | --- | --- | |
| 104 | | Clear owner per resource and session lineage | Done (`RuntimeOwner` + EffectScope wiring for sidecar/MCP/UI/LSP/stream/file receipts) | |
| 105 | | Activation failure never leaks / never publishes | Done | |
| 106 | | Missing deps → Inactive diagnostics | Done (structured missing requirement + Unavailable) | |
| 107 | | Subgraph-only rebuild (no full BuildRuntime) | Done for None/Interceptor/UI/Provider/MCP | |
| 108 | | Publish/drain order + drain cancel | Done | |
| 109 | | Irreversible never rollback-success | Done (recovery `AssessRuntimeResume`) | |
| 110 | | Strict v2-only native runtime manifest | Done (no v1 dual-read / auto-migration) | |
| 111 | | Cache stability guards | Done | |
| 112 | | Doctor explains inactive + resume | Done (CLI + desktop RuntimeDoctor UI) | |
| 113 | | No external runtime dependency | Done | |
| 114 | |
| 115 | ## Phase 5 decisions |
| 116 | |
| 117 | - **Compatibility**: v2 is the first supported extension runtime manifest; |
| 118 | install/doctor/boot do not dual-read v1 and do not auto-migrate it. |
| 119 | - **Performance**: see [EXTENSION_RUNTIME_V2_PERF.md](./EXTENSION_RUNTIME_V2_PERF.md). |
| 120 | - **README / SDK**: v2 manifest examples under `sdk/go/examples/*`; protocol gen remains the source of truth for wire types. |
| 121 |