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() checks r.installMethod === undefined → returns (the instance that would have populated it is only constructed after this check);
run() checks r.runMethod === undefined → returns immediately, so the kernel sees "run loop exited before signaling ready" and health-restarts the capsule forever;
tool_describe / hook dispatch in astridHookTrigger see empty tools / interceptors / commands maps.
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/sdk@0.1.0 @unicity-astrid/build@0.1.0
- Any capsule with
@install that logs; build; astrid capsule install .
- Observe:
Lifecycle hook completed successfully in <2 ms with no guest log output. Add @run with runtime.signalReady(): observe run 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/sdk 0.1.0, @unicity-astrid/build 0.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 via astrid-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/host@1.0.0, 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.
Summary
The SDK registers
@tool/@interceptor/@command/@install/@upgrade/@runmethods via TC39context.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 whennew CapsuleClass()first executes.createBridge()inpackages/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.tsstates the assumption that breaks:That holds for the
@capsuleclass decorator (registerCapsuleruns at class evaluation), but not for the method decorators — theiraddInitializercallbacks 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):
@installhooks silently no-op,@runloops never start, and bus-routed tool dispatch never reaches the capsule. Because@capsuledoes register the constructor, the failure looks like "the capsule is fine but empty".Reproduction
npm i @unicity-astrid/sdk@0.1.0 @unicity-astrid/build@0.1.0@installthat logs; build;astrid capsule install .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 inregistry.ts):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
@installhook 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-sdkif 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)experimentalDecorators: false, target ES2022), built viaastrid-js-build→ ComponentizeJS → wasm32-wasip2Related observation (separate, minor)
astrid 0.9.1 fails to pre-instantiate JS capsules with
component imports instance astrid:process/host@1.0.0, 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.