refactor(host): enter one runtime store once per daemon runtime (#3794)
Merged
TH
10h ago+221+221+012345678901234567890123456789-263-263-012345678901234567890123456789
Closes #3794
Part of #3747. Second PR: #3798 landed part 1 (configuration reads take the host explicitly). This one is part 2, one entered runtime store.
What changes
Every module that kept per-runtime state declared its own defineRuntimeSlots() store and exposed a createXxxRuntimeState() handle; startDaemonRuntime collected nine of those handles into one context — six constructed by the daemon plus two handed back through DaemonRuntimeDeps.runtimeStates — nested their run calls, and folded the last two in later through RuntimeContext.extend + enter(). That aggregation is now gone: one store per process, one handle entered once per runtime.
packages/host-kit/src/runtime-slots.ts—defineRuntimeSlotsis replaced byruntimeSlot(key, create, dispose?)andcreateRuntimeStateHandle(). Modules declare slot keys, they no longer own a store. The entered-store-else-standalone rule is unchanged, so a client process or a unit test still reads its own state without composing a runtime.- Six modules lose their handle factory and their per-module store:
request-cancel(host-kit),interactor-resolution,provider-device-admission,artifact-tracking,resumable-upload,materialized-path-registry. Their slot keys and disposers stay exactly where they were. startDaemonRuntimecreates one handle and runs composition, request dispatch and close inside it, so every read resolves from that runtime's state.DaemonRuntimeDeps.runtimeStates,RuntimeContext,createRuntimeContextandextendare deleted;src/daemon-composition.tsno longer returns platform handles — it callssetAppleRunnerHostEnvironment(host)where it stands, inside the entered handle, which is wherecreateAppleRunnerRuntimeStateused to bind it.- The Android helper (
adb-host) and the Apple runner (runner-owner-state) keep their slot keys and lose their stores. The runner still reaches the store'sruntimeSlotthrough its host port, so norunner/**file imports host-kit (R77). Creating a handle is not a production port member: the only callers were tests, so test-kit gainedcreateTestRuntimeStateHandle(runtime-state.ts), and the runner tests enter their handles through it.runnerRuntimeSlotandhelperRuntimeSlotnow nameruntimeSlot.
Done-when from the issue
- No module-level handle or
runtimeStatesaggregation remains.defineRuntimeSlots,createRuntimeContext,RuntimeContext,runtimeStatesand.enter()have zero hits insrc/,packages/andtest/; the onlyruntime-slotsimporters are the modules that declare slots plus the daemon that enters the handle. - Two-runtime isolation still passes. The existing tests keep their claims with the handle created explicitly instead of per module:
runner-owner-state.test.ts,runner-session-runtime-isolation.test.ts,runner-lease-runtime-host.test.ts(Apple),snapshot-helper-session-runtime-isolation.test.ts(Android helper),daemon-runtime-isolation.test.ts(two real daemons: per-runtime admission, interactor resolution and artifact tracking, and closing one leaves the other serving), plusdaemon-composition.test.ts, which now asserts each composition's writes land in the handle it ran inside and that one runtime's host is not the other's. The PNG worker has no slot state to collapse — it is a process-shared worker with holder counting (retainPngWorker), and its holder test still passes;check:affectedran it. - Bounded close still refuses to drop state while work is unfinished, observed honestly now:
daemon-runtime-bounded-close.test.tsdeclares a slot on the runtime's own store and asserts its disposer has not run whenplatformOwnersispending.
Scope notes
- No gate or lint-rule changes — #3753 owns the process-globals gate, and nothing here touches it.
- Overlap with the coordinator's parallel work (#3806, #3797): both merged to main while this PR was in review; the PR is rebased onto
b5f47e7542with their changes intact. The single predicted conflict, the close path inpackages/daemon/src/server/http/daemon-runtime.ts, was resolved by keeping main'sfinishDaemonRegistrationblock with theruntimeContext→runtimeStaterename. No other file overlapped.
Evidence
pnpm check:affected --run --head ad357823d9on the exact head,exactCommit=true(receiptrun-hpQD6R): format, lint, typecheck, layering, fallow (production-unused exports included), build, vitest-related and daemon-wire-compat all pass. Layering is the gate that proves the port still fronts host-kit for the runner subtree, and the eager-closure budgets were untouched.- Live device evidence at this head (round-3 comment has the commands and output): iOS simulator open + snapshot through the Apple runner (
backends: {"xctest": 2}, zeroapple_runner_host_unbound), and Android snapshot through the helper session (backend: android-helper,helperVersionmatchespackage.json). - Review rounds: round 1 removed
createRuntimeStateHandlefrom the runner host port (test-kit owns test support, R83); round 2 removed thegetAppleRunnerHostEnvironmentfacade re-export after the production-unused-exports gate caught it as test-only — the composition test now observes the host binding throughresolveRunnerAppBundleId(), a production read the runner's resource cleanup already uses; round 3 added the live runs above, the clean-close disposer assertion, trimmed the narration comments, and answered the provider-bundling scope question. See the round comments below for the full mapping. - Focused suites before the gate: platform-apple core+runner (124 files / 1291 tests), packages/daemon + host-kit +
src/daemon(551 files / 4087 tests), scripts (55 files / 1285 tests, includes the layering and wire-compat policy tests). - Diff is net −42 lines (221 added, 263 removed) across 29 files, in four commits by layer: host-kit store, platform slot declarations, daemon entry, root composition.