Runtime bridge reads the decorator registry before any construction - all lifecycle/tool/run registrations appear empty on published 0.1.0
Summary
The SDK registers @tool / @interceptor / @command / @install / @upgrade / @run methods via TC39 context.addInitializer(...) (packages/astrid-sdk/src/tool.ts, src/capsule.ts). Per the decorators proposal, initializers added by non-static method decorators run during instance construction — i.e. only when new CapsuleClass() first executes.
createBridge() in packages/astrid-sdk/src/runtime/bridge.ts, however, reads the registration maps before constructing anything:
astridInstall()checksr.installMethod === undefined→ returns (the instance that would have populated it is only constructed after this check);run()checksr.runMethod === undefined→ returns immediately, so the kernel sees "run loop exited before signaling ready" and health-restarts the capsule forever;tool_describe/ hook dispatch inastridHookTriggersee emptytools/interceptors/commandsmaps.
The comment at the top of src/runtime/registry.ts states the assumption that breaks:
Decorators run during class evaluation, which happens at module init. The bridge is constructed AFTER all decorators have populated the registry, so reads are safe.
That holds for the @capsule class decorator (registerCapsule runs at class evaluation), but not for the method decorators — their addInitializer callbacks only fire at first construction, which the bridge never triggers before reading.
Net effect on a stock capsule built with the published 0.1.0 packages and run on released kernels (verified on astrid 0.9.0/0.9.1, Ubuntu 24.04): @install hooks silently no-op, @run loops never start, and bus-routed tool dispatch never reaches the capsule. Because @capsule does register the constructor, the failure looks like "the capsule is fine but empty".
Reproduction
npm i @unicity-astrid/[email protected] @unicity-astrid/[email protected]- Any capsule with
@installthat logs; build;astrid capsule install . - Observe:
Lifecycle hook completed successfullyin <2 ms with no guest log output. Add@runwithruntime.signalReady(): observerun loop exited before signaling ready+ health-restart loop.
Fix that worked for me
Warm the registry with one throwaway construction before the first read, and make duplicate records idempotent (the warm-up construction plus the bridge's own later new r.ctor() calls would otherwise trip the duplicate guards in registry.ts):
// bridge.js — inside createBridge()
let warmed = false;
function reg() {
const r = getRegistration();
if (r !== undefined && !warmed) {
warmed = true;
try { new r.ctor(); } catch { /* state-free constructor */ }
}
// ... existing undefined check ...
}// registry.js — recordTool/recordInterceptor/recordCommand/recordInstall/
// recordUpgrade/recordRun: replace the duplicate-throw with an early return.A cleaner long-term fix: record method metadata from the decorator call itself rather than addInitializer — the method name and options are already known at decoration time, so no construction would be needed at all. That would also resolve the re-registration-per-construction concern the automated audit flagged in #18.
With this patch applied on top of the published package, my capsule's @install hook executed a full HTTP session inside the kernel sandbox, and @run + runtime.signalReady() kept the daemon healthy on astrid 0.9.0. The exact postinstall patch I apply is here: patch-sdk.mjs.
Happy to turn this into a PR against packages/astrid-sdk if you'd take one — just tell me which direction you prefer (warm-up vs. decoration-time registration).
Environment
@unicity-astrid/sdk0.1.0,@unicity-astrid/build0.1.0 (npm)- astrid 0.9.0 / 0.9.1 release binaries, x86_64-unknown-linux-gnu
- Ubuntu 24.04 (WSL2), Node 22
- Capsule source: TypeScript, standard TC39 decorators (
experimentalDecorators: false, target ES2022), built viaastrid-js-build→ ComponentizeJS → wasm32-wasip2
Related observation (separate, minor)
astrid 0.9.1 fails to pre-instantiate JS capsules with component imports instance astrid:process/[email protected], but a matching implementation was not found in the linker — the JS bundle always links every SDK module (esbuild keeps the side-effectful WIT imports), so kernels that gate host interfaces on capabilities can no longer load stock JS capsules. 0.9.0 links it unconditionally and works.
Source: astrid-runtime/sdk-js