@nimbus-sh/worker 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/dist/_shared/session-router.d.ts +8 -0
  2. package/dist/_shared/session-router.d.ts.map +1 -1
  3. package/dist/_shared/session-router.js +8 -0
  4. package/dist/esbuild-wasm-bundle.generated.d.ts +1 -1
  5. package/dist/esbuild-wasm-bundle.generated.js +1 -1
  6. package/dist/facets/cirrus-real.js +1 -1
  7. package/dist/facets/manager.d.ts +43 -129
  8. package/dist/facets/manager.d.ts.map +1 -1
  9. package/dist/facets/manager.js +107 -318
  10. package/dist/facets/opencode-staging.d.ts +1 -1
  11. package/dist/facets/opencode-staging.d.ts.map +1 -1
  12. package/dist/facets/opencode-staging.js +3 -0
  13. package/dist/facets/real-vite-hmr.js +1 -1
  14. package/dist/facets/vite-dev-server.d.ts +4 -4
  15. package/dist/facets/vite-dev-server.js +5 -5
  16. package/dist/git/commands.d.ts +8 -0
  17. package/dist/git/commands.d.ts.map +1 -1
  18. package/dist/git/commands.js +41 -9
  19. package/dist/git/network-facet.d.ts +1 -1
  20. package/dist/git/network-facet.d.ts.map +1 -1
  21. package/dist/git/network-facet.js +39 -15
  22. package/dist/git-bundle.generated.d.ts +2 -2
  23. package/dist/git-bundle.generated.d.ts.map +1 -1
  24. package/dist/git-bundle.generated.js +3 -3
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +9 -1
  27. package/dist/loaders/child-process/spawn-facet.js +1 -1
  28. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  29. package/dist/loaders/child-process/spawn-pool.js +2 -2
  30. package/dist/loaders/generated-workers.d.ts +2 -2
  31. package/dist/loaders/generated-workers.d.ts.map +1 -1
  32. package/dist/loaders/generated-workers.js +3 -3
  33. package/dist/loaders/npm-resolve-preamble.d.ts +3 -3
  34. package/dist/loaders/npm-resolve-preamble.js +3 -3
  35. package/dist/loaders/pre-bundle-preamble.d.ts +5 -5
  36. package/dist/loaders/pre-bundle-preamble.js +6 -6
  37. package/dist/loaders/process-host.d.ts +6 -107
  38. package/dist/loaders/process-host.d.ts.map +1 -1
  39. package/dist/loaders/process-host.js +6 -405
  40. package/dist/npm/install-batch-facet.d.ts +1 -1
  41. package/dist/npm/install-batch-facet.js +1 -1
  42. package/dist/npm/installer.d.ts +6 -6
  43. package/dist/npm/installer.d.ts.map +1 -1
  44. package/dist/npm/installer.js +28 -27
  45. package/dist/npm/pre-bundle-facet.d.ts +2 -2
  46. package/dist/npm/pre-bundle-facet.js +5 -5
  47. package/dist/npm/r2-cache.d.ts +1 -1
  48. package/dist/npm/resolve-facet.d.ts +1 -1
  49. package/dist/npm/resolve-facet.js +1 -1
  50. package/dist/npm/resolve-one-facet.d.ts +5 -5
  51. package/dist/npm/resolve-one-facet.js +5 -5
  52. package/dist/router/index.js +2 -2
  53. package/dist/router/remote-api.js +9 -1
  54. package/dist/runtime/bun-repl.d.ts +1 -1
  55. package/dist/runtime/bun-repl.js +3 -3
  56. package/dist/runtime/esbuild-wasm-bytes.js +1 -1
  57. package/dist/runtime/facet-loader-host.d.ts +5 -8
  58. package/dist/runtime/facet-loader-host.d.ts.map +1 -1
  59. package/dist/runtime/facet-loader-host.js +7 -11
  60. package/dist/runtime/node-repl.js +2 -2
  61. package/dist/runtime/node-shims-artifact.js +1 -1
  62. package/dist/runtime/node-shims.js +2 -1
  63. package/dist/runtime/opencode-artifact.js +1 -1
  64. package/dist/runtime/opentui-wasm-bytes.js +1 -1
  65. package/dist/runtime/python-repl.js +3 -3
  66. package/dist/runtime/ruby-repl.js +2 -2
  67. package/dist/runtime/ruby-resident.js +1 -1
  68. package/dist/runtime/sqlite-wasm-bytes.js +1 -1
  69. package/dist/session/diag.js +1 -1
  70. package/dist/session/hibernation.d.ts +20 -53
  71. package/dist/session/hibernation.d.ts.map +1 -1
  72. package/dist/session/hibernation.js +57 -194
  73. package/dist/session/init-phases.d.ts +2 -3
  74. package/dist/session/init-phases.d.ts.map +1 -1
  75. package/dist/session/init-phases.js +3 -2
  76. package/dist/session/init.d.ts.map +1 -1
  77. package/dist/session/init.js +8 -4
  78. package/dist/session/keys.d.ts +14 -37
  79. package/dist/session/keys.d.ts.map +1 -1
  80. package/dist/session/keys.js +18 -37
  81. package/dist/session/nimbus-session.d.ts +10 -15
  82. package/dist/session/nimbus-session.d.ts.map +1 -1
  83. package/dist/session/nimbus-session.js +28 -31
  84. package/dist/session/port-capability.d.ts +43 -0
  85. package/dist/session/port-capability.d.ts.map +1 -0
  86. package/dist/session/port-capability.js +49 -0
  87. package/dist/session/programmatic.d.ts +39 -5
  88. package/dist/session/programmatic.d.ts.map +1 -1
  89. package/dist/session/programmatic.js +150 -19
  90. package/dist/session/routes.d.ts +2 -0
  91. package/dist/session/routes.d.ts.map +1 -1
  92. package/dist/session/routes.js +96 -20
  93. package/dist/session/rpc.d.ts +36 -9
  94. package/dist/session/rpc.d.ts.map +1 -1
  95. package/dist/session/rpc.js +84 -24
  96. package/dist/session/start-real-vite.d.ts.map +1 -1
  97. package/dist/session/start-real-vite.js +3 -1
  98. package/dist/session/state-store.d.ts +3 -2
  99. package/dist/session/state-store.d.ts.map +1 -1
  100. package/dist/session/state-store.js +3 -2
  101. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  102. package/dist/session/supervisor-rpc.js +7 -4
  103. package/dist/session/ws.d.ts +1 -3
  104. package/dist/session/ws.d.ts.map +1 -1
  105. package/dist/session/ws.js +4 -3
  106. package/dist/wrangler/nimbus-wrangler.js +1 -1
  107. package/package.json +4 -2
  108. package/scripts/bundle-esbuild-wasm.mjs +2 -2
  109. package/scripts/bundle-facet-workers.mjs +5 -7
  110. package/scripts/bundle-sqlite-wasm.mjs +1 -1
  111. package/dist/facets/inner-do-registry.d.ts +0 -41
  112. package/dist/facets/inner-do-registry.d.ts.map +0 -1
  113. package/dist/facets/inner-do-registry.js +0 -51
  114. package/dist/facets/launch-pacer.d.ts +0 -111
  115. package/dist/facets/launch-pacer.d.ts.map +0 -1
  116. package/dist/facets/launch-pacer.js +0 -130
  117. package/dist/loaders/fanout-pool.d.ts +0 -207
  118. package/dist/loaders/fanout-pool.d.ts.map +0 -1
  119. package/dist/loaders/fanout-pool.js +0 -363
  120. package/dist/loaders/loader-pool.d.ts +0 -294
  121. package/dist/loaders/loader-pool.d.ts.map +0 -1
  122. package/dist/loaders/loader-pool.js +0 -642
  123. package/dist/loaders/process-fabric.d.ts +0 -509
  124. package/dist/loaders/process-fabric.d.ts.map +0 -1
  125. package/dist/loaders/process-fabric.js +0 -360
  126. package/dist/loaders/vendor/errors.d.ts +0 -24
  127. package/dist/loaders/vendor/errors.d.ts.map +0 -1
  128. package/dist/loaders/vendor/errors.js +0 -46
  129. package/dist/loaders/vendor/serialize.d.ts +0 -3
  130. package/dist/loaders/vendor/serialize.d.ts.map +0 -1
  131. package/dist/loaders/vendor/serialize.js +0 -25
  132. package/dist/loaders/vendor/types.d.ts +0 -69
  133. package/dist/loaders/vendor/types.d.ts.map +0 -1
  134. package/dist/loaders/vendor/types.js +0 -4
  135. package/dist/loaders/workerd-facet-host.d.ts +0 -141
  136. package/dist/loaders/workerd-facet-host.d.ts.map +0 -1
  137. package/dist/loaders/workerd-facet-host.js +0 -284
  138. package/dist/session/bindings.d.ts +0 -209
  139. package/dist/session/bindings.d.ts.map +0 -1
  140. package/dist/session/bindings.js +0 -680
  141. package/dist/session/ctx-exports.d.ts +0 -16
  142. package/dist/session/ctx-exports.d.ts.map +0 -1
  143. package/dist/session/ctx-exports.js +0 -22
  144. package/dist/session/ws-hibernation-config.d.ts +0 -64
  145. package/dist/session/ws-hibernation-config.d.ts.map +0 -1
  146. package/dist/session/ws-hibernation-config.js +0 -89
@@ -22,26 +22,30 @@ import { FACET_RESIDENT_STORE_SOURCE } from '../vfs/facet-resident-store.js';
22
22
  import { VFS_CURSOR_SEED_SOURCE, serializeFacetVfsCursor, } from '@nimbus-sh/core/_shared/facet-vfs-cursor.js';
23
23
  import { VFS_WRITE_LEDGER_SOURCE } from '@nimbus-sh/core/_shared/vfs-write-ledger.js';
24
24
  import { vfsPathExtension } from '@nimbus-sh/core/vfs/path.js';
25
+ import { clearPortCapability } from '../session/port-capability.js';
25
26
  import { prefetchForRequire } from '@nimbus-sh/core/runtime/require-resolver.js';
26
27
  import { hasTopLevelModuleSyntax } from '@nimbus-sh/core/runtime/javascript-ast.js';
27
28
  import { bindImportMetaResolve, importMetaDefines } from '@nimbus-sh/core/runtime/import-meta-transform.js';
28
- import { recordFailure, getLastRpcFrame, getLastFacetId } from '@nimbus-sh/core/observability/oom-discriminator.js';
29
- import { classifyError } from '@nimbus-sh/core/observability/oom-classify.js';
30
- import { LaunchPacer, launchChunkMaxBytes } from './launch-pacer.js';
29
+ import { recordFailure, getLastRpcFrame, getLastFacetId } from '@nimbus-sh/platform/oom-discriminator.js';
30
+ import { classifyError } from '@nimbus-sh/platform/oom-classify.js';
31
+ import { TurnBudget, PacedWork, turnChunkMaxBytes } from '@nimbus-sh/fabric/turn-budget.js';
32
+ import { onColdStart } from '@nimbus-sh/fabric/generation.js';
33
+ import { FencedWork, } from '@nimbus-sh/fabric/fenced-work.js';
31
34
  import { EsbuildService } from '@nimbus-sh/core/runtime/esbuild-service.js';
32
35
  import { isExecDiagEnabled, recordExecTelemetry } from './exec-telemetry.js';
33
- import { disposeRpcResource, disposeRpcResources } from '@nimbus-sh/core/_shared/rpc-dispose.js';
36
+ import { disposeRpcResource, disposeRpcResources } from '@nimbus-sh/platform/rpc-dispose.js';
34
37
  import { sqliteWasmModuleEntry } from './opencode-staging.js';
35
- import { ProcessFabric, FACET_IMAGE_DIR, facetImageDigest, facetImagePath, } from '../loaders/process-fabric.js';
36
- import { createLoadedWorkerEntrypoint, getNimbusCtxExports, } from '../loaders/workerd-facet-host.js';
38
+ import { ImageStore, } from '@nimbus-sh/fabric/image-store.js';
39
+ import { ProcessFabric, } from '@nimbus-sh/fabric/process-fabric.js';
40
+ import { createLoadedWorkerEntrypoint, getNimbusCtxExports, } from '@nimbus-sh/fabric/workerd-facet-host.js';
37
41
  import { SQLITE_WASM_MODULE_NAME, } from '../runtime/opencode-facet-runner.js';
38
42
  import { parsePortFromArgv, resolveLongRunningPort } from '@nimbus-sh/core/runtime/long-running-handle.js';
39
43
  import { DEFAULT_FACET_BUNDLE_PROFILE, } from '@nimbus-sh/core/runtime/bundle-profile.js';
40
- import { BUNDLE_BUILD_DEADLINE_MS, CF_COMPAT_DATE, CHUNK_SIZE, FACET_TIMEOUT_MS, VFS_BUNDLE_MAX_FILES, VFS_BUNDLE_MAX_BYTES, CWD_SNAPSHOT_MAX_FILE_BYTES, BUNDLE_MAX_ENCODED_BYTES, MAX_RPC_SAFE_PAYLOAD_BYTES, MAX_TX_BLOB_BYTES, PREFETCH_CACHE_MAX_BYTES, } from '@nimbus-sh/core/constants.js';
41
- import { RESIDENT_LAUNCH_KEY_PREFIX } from '../session/keys.js';
44
+ import { BUNDLE_BUILD_DEADLINE_MS, CF_COMPAT_DATE, FACET_TIMEOUT_MS, VFS_BUNDLE_MAX_FILES, VFS_BUNDLE_MAX_BYTES, CWD_SNAPSHOT_MAX_FILE_BYTES, BUNDLE_MAX_ENCODED_BYTES, PREFETCH_CACHE_MAX_BYTES, } from '@nimbus-sh/core/constants.js';
45
+ import { MAX_RPC_SAFE_PAYLOAD_BYTES } from '@nimbus-sh/platform/limits.js';
42
46
  import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
43
- import { acquireSupervisorAllocation } from '@nimbus-sh/core/observability/heavy-alloc-coord.js';
44
- import { prefetchBundleStart, prefetchBundleEnd, setPrefetchCacheBytes, } from '@nimbus-sh/core/observability/diag-counters.js';
47
+ import { acquireSupervisorAllocation } from '@nimbus-sh/platform/heavy-alloc-coord.js';
48
+ import { prefetchBundleStart, prefetchBundleEnd, setPrefetchCacheBytes, } from '@nimbus-sh/platform/diag-counters.js';
45
49
  /**
46
50
  * Detect & restore a Uint8Array that's been JSON-mangled to a
47
51
  * {"0":n,"1":n,...} object during the result-envelope round-trip.
@@ -3147,19 +3151,11 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
3147
3151
  };
3148
3152
  }
3149
3153
  const ROUTEABLE_PORT_ATTACH_TIMEOUT_MS = 1_000;
3150
- /**
3151
- * Bytes of an image written in one storage transaction.
3152
- *
3153
- * The bound is the VFS's own, not a knob: a `writeRange` whose chunks fit
3154
- * inside one transaction is committed in place, and one that does not falls
3155
- * back to copy-on-write — which rewrites every chunk of the file, per slice,
3156
- * making a sliced write quadratic in its size. A whole number of chunks is
3157
- * the other half of that: a slice that ends mid-chunk makes the next one read
3158
- * the partial chunk back to complete it.
3159
- */
3160
- const FACET_IMAGE_WRITE_SLICE_BYTES = Math.floor(MAX_TX_BLOB_BYTES / CHUNK_SIZE) * CHUNK_SIZE;
3161
- /** A launch is re-driven once. A reset that recurs is not the transient one. */
3162
- const RESIDENT_LAUNCH_MAX_ATTEMPT = 1;
3154
+ /** 'running' is the measured common case: the platform's reset strikes
3155
+ * seconds after a launch settles, while the resident runs. */
3156
+ function residentLaunchDoing(record) {
3157
+ return record.phase === 'running' ? 'running' : 'starting';
3158
+ }
3163
3159
  export class FacetManager {
3164
3160
  ctx;
3165
3161
  env;
@@ -3183,29 +3179,25 @@ export class FacetManager {
3183
3179
  /** NIMBUS_DEBUG=1: placement diagnostics into the process log store. */
3184
3180
  debugEnabled = false;
3185
3181
  processRpcResources = new Map();
3186
- /** pid → the boot images its facet loads from; the image sweep's root set. */
3187
- residentImages = new Map();
3188
3182
  /**
3189
- * Launches suspended between chunks, waiting for a turn of their own.
3190
- *
3191
- * In-memory on purpose: a launch is only meaningful while the process table
3192
- * entry it is building for exists, and both are lost together if the isolate
3193
- * resets. What survives a reset is the journal, which names the launch's
3194
- * INPUTS rather than its position — a resumed queue would be resurrecting
3195
- * half-built work for pids that no longer exist, where re-driving a launch
3196
- * from its inputs is the same idempotent work again.
3183
+ * The content-addressed boot-image store (fabric's image-store.ts),
3184
+ * writing through this session's kernel-credentialed VFS and rooted off the
3185
+ * live process table.
3186
+ */
3187
+ imageStore = new ImageStore(() => this._imageBlobs(), (pid) => this.processes.get(pid)?.state === 'running');
3188
+ /**
3189
+ * The resident-launch journal (fabric's fenced-work.ts): the durable
3190
+ * record of every resident this session owes the user, and its recovery
3191
+ * after an instance reset. This manager supplies what a launch IS — the
3192
+ * inputs `_spawnResident` re-drives from — and how its loss is reported.
3197
3193
  */
3198
- launchWaiters = [];
3199
- /** Whether this instance has already read the journal a reset leaves behind. */
3200
- launchesRecovered = false;
3194
+ launchJournal;
3201
3195
  /**
3202
- * Pids THIS instance holds journal rows for. What keeps the terminal hook —
3203
- * which fires for every process, shells and one-shots included — from
3204
- * paying a storage delete for pids that never had a row. In-memory is
3205
- * correct: rows from a previous instance are recovery's to consume, never
3206
- * this hook's.
3196
+ * The granting side of the launch budget (fabric's turn-budget.ts). The
3197
+ * session's alarm re-enters the object through `pumpResidentLaunches`;
3198
+ * journal recovery rides the first pump.
3207
3199
  */
3208
- journalledPids = new Set();
3200
+ launchPump;
3209
3201
  timedOutProcessIds = new Set();
3210
3202
  // attach-pid → serve-pid: the resident serve facet a bare-`opencode` dual
3211
3203
  // spawn created as an OS-child of the attach TUI. When the attach process
@@ -3276,42 +3268,65 @@ export class FacetManager {
3276
3268
  ? Reflect.get(env, 'NIMBUS_DEBUG')
3277
3269
  : undefined;
3278
3270
  this.debugEnabled = debugVar === '1' || debugVar === 'true';
3271
+ this.launchJournal = new FencedWork(ctx.storage, {
3272
+ generationBase: () => this.processes.pidBase,
3273
+ waitUntil: (promise) => this.ctx.waitUntil(promise),
3274
+ redrive: (record, attempt) => this._spawnResident(record.code, record.opts, attempt),
3275
+ onRedrive: (record) => this.hooks.notify?.('\x1b[2m[nimbus: the session restarted while '
3276
+ + `"${record.command}" was ${residentLaunchDoing(record)} — restarting it]\x1b[0m\r\n`),
3277
+ onAbandoned: (record) => this.hooks.notify?.('\x1b[2m[nimbus: the session restarted again while '
3278
+ + `"${record.command}" was ${residentLaunchDoing(record)} — leaving it stopped]\x1b[0m\r\n`),
3279
+ onRedriveFailed: (record, e) => this.hooks.notify?.(`\x1b[2m[nimbus: "${record.command}" could not be restarted: `
3280
+ + `${errorMessage(e)}]\x1b[0m\r\n`),
3281
+ });
3282
+ this.launchPump = new PacedWork(ctx, {
3283
+ requestTurn: hooks.requestLaunchTurn?.bind(hooks),
3284
+ });
3285
+ // A reset that killed a resident launch left journal rows behind; the
3286
+ // first pump after the reset drains this reconciliation before any
3287
+ // waiter resumes, OFF the constructor's init gate.
3288
+ onColdStart(ctx, () => this.launchJournal.recoverInterrupted());
3279
3289
  // The journal row of a resident lives for the PROCESS's lifetime, so its
3280
3290
  // release belongs on the one seam every end-of-life passes through —
3281
3291
  // exit, kill, self-reported exit and timeout abort all mark the table.
3282
3292
  // Rooted on waitUntil: the hook fires synchronously inside whatever turn
3283
3293
  // ended the process, and the delete must not be a floating promise there.
3284
3294
  this.processes.setOnTerminal((pid) => {
3285
- this.ctx.waitUntil(this._releaseResidentJournal(pid));
3295
+ this.ctx.waitUntil(this.launchJournal.release(pid));
3286
3296
  });
3287
3297
  }
3288
3298
  setVfs(vfs) { this.vfs = vfs; }
3289
3299
  /**
3290
- * The image store's directory, created before the first filesystem view is
3291
- * built rather than on the first image write.
3292
- *
3293
- * Lazily created, it made the store perturb the very view every manifest is
3294
- * built from: the root listing gained an entry the moment an image landed,
3295
- * so the next spawn of an identical program generated different text and
3296
- * addressed a different image. Existing before the first walk makes it
3297
- * stable.
3298
- *
3299
- * Sited on the exec path and not in setVfs, because setVfs runs while the
3300
- * Durable Object is coming up — including on every wake — and a synchronous
3301
- * filesystem write there costs the session its startup. Measured: a
3302
- * throwaway built that way stopped accepting terminal connections at all,
3303
- * while the same build without it served them.
3300
+ * The env/ctx pair every loader-backed runtime builds its facet pools
3301
+ * from. A pool is constructed from exactly these two, so the manager
3302
+ * exposes them as one narrow accessor rather than every runtime reaching
3303
+ * into its private fields.
3304
3304
  */
3305
- _ensureImageStoreDir(vfs) {
3306
- if (this.imageStoreDirReady)
3307
- return;
3308
- this.imageStoreDirReady = true;
3309
- try {
3310
- vfs.as(CRED_KERNEL).mkdir(FACET_IMAGE_DIR, { recursive: true, mode: 0o755 });
3305
+ loaderHost() {
3306
+ return { env: this.env, ctx: this.ctx };
3307
+ }
3308
+ /**
3309
+ * The image store's disk: this session's VFS, as the kernel — the store is
3310
+ * written by the kernel and read by processes through supervisor bindings
3311
+ * that enforce their own credential. Mode 0644 at creation, as POSIX has
3312
+ * it, is what makes the read succeed for any process by construction; the
3313
+ * store itself decides nothing about modes.
3314
+ */
3315
+ _imageBlobs() {
3316
+ const vfs = this.vfs;
3317
+ if (!vfs) {
3318
+ throw new Error('Nimbus: a resident process needs a session filesystem to materialize its boot image');
3311
3319
  }
3312
- catch { /* a session whose disk is not writable has no images to store */ }
3320
+ const fs = vfs.as(CRED_KERNEL);
3321
+ return {
3322
+ mkdirp: (dir) => fs.mkdir(dir, { recursive: true, mode: 0o755 }),
3323
+ sizeOf: (path) => (fs.exists(path) ? fs.lstat(path).size : null),
3324
+ writeFile: (path, bytes) => fs.writeFile(path, bytes, { mode: 0o644 }),
3325
+ writeRange: (path, offset, bytes) => fs.writeRange(path, offset, bytes),
3326
+ list: (dir) => fs.readdir(dir).map((entry) => entry.name),
3327
+ unlink: (path) => fs.unlink(path),
3328
+ };
3313
3329
  }
3314
- imageStoreDirReady = false;
3315
3330
  /**
3316
3331
  * W3.5 Fix B: hand the FacetManager a pre-warmed EsbuildService for
3317
3332
  * the ESM→CJS bundle pre-pass. NimbusSession already lazy-creates one
@@ -3619,7 +3634,7 @@ export class FacetManager {
3619
3634
  const diagOn = isExecDiagEnabled();
3620
3635
  const __bundleStart = diagOn ? Date.now() : 0;
3621
3636
  if (this.vfs)
3622
- this._ensureImageStoreDir(this.vfs);
3637
+ this.imageStore.ensureDir();
3623
3638
  const processVfs = this.vfs?.as(entry.cred);
3624
3639
  const credKey = `${entry.cred.uid}:${entry.cred.gid}:${entry.cred.groups.join(',')}`;
3625
3640
  const vfsState = processVfs
@@ -3929,7 +3944,7 @@ export class FacetManager {
3929
3944
  // home dirs (~/.local/share/opencode, …) via fs.promises.mkdir; those and
3930
3945
  // other writes flush live through the SUPERVISOR RPC bridge.
3931
3946
  if (this.vfs)
3932
- this._ensureImageStoreDir(this.vfs);
3947
+ this.imageStore.ensureDir();
3933
3948
  const processVfs = this.vfs?.as(entry.cred);
3934
3949
  const vfsState = processVfs
3935
3950
  ? await buildPrefetchBundle(processVfs, undefined, opts.cwd, '', this.esbuild || undefined)
@@ -4138,6 +4153,7 @@ export class FacetManager {
4138
4153
  .finally(() => {
4139
4154
  this.releaseProcessRpcResources(pid);
4140
4155
  }));
4156
+ await clearPortCapability({ ctx: this.ctx, portRegistry: this.portRegistry }, port);
4141
4157
  this.portRegistry.register(port, pid);
4142
4158
  return { pid, exitCode: 0, stdout: '', stderr: '', vfsWrites: {} };
4143
4159
  }
@@ -4174,7 +4190,7 @@ export class FacetManager {
4174
4190
  * The reader the fabric completes a boot spec's by-path members with.
4175
4191
  *
4176
4192
  * Reads as CRED_KERNEL because that is who WROTE them: the generated images
4177
- * are kernel-owned (`_materializeFacetImages`) and the runtime wasm images
4193
+ * are kernel-owned (`_imageBlobs`) and the runtime wasm images
4178
4194
  * are installed by the kernel. Uncached because these are the session's
4179
4195
  * largest files — a ruby interpreter image is 34.3 MiB — and pinning one in
4180
4196
  * the VFS content LRU for the life of the session is what once crashed the
@@ -4188,82 +4204,11 @@ export class FacetManager {
4188
4204
  const fs = vfs.as(CRED_KERNEL);
4189
4205
  return { readFile: (path) => fs.readFileUncached(path) };
4190
4206
  }
4191
- /**
4192
- * Materialize generated module sources in the content-addressed image store
4193
- * and return the `vfsTextModules` map naming them.
4194
- *
4195
- * A resident process's module map is sized by the user's disk, so it does
4196
- * not ride inside the boot spec — see ResidentCodeSpec.vfsTextModules.
4197
- * Writing it here, once, is what lets the session stop holding it: after
4198
- * this returns, the only thing it keeps is a path.
4199
- *
4200
- * The store is written by the kernel and read by the process, so nothing
4201
- * here depends on which credential spawned what. Digest collisions are the
4202
- * hash's problem; everything else is idempotent — an image already present
4203
- * at its own digest is already the bytes we were about to write.
4204
- */
4205
- async _materializeFacetImages(pid, modules, pacer) {
4206
- const vfs = this.vfs;
4207
- if (!vfs) {
4208
- throw new Error('Nimbus: a resident process needs a session filesystem to materialize its boot image');
4209
- }
4210
- const fs = vfs.as(CRED_KERNEL);
4211
- const images = {};
4212
- const sources = new Map();
4213
- for (const [moduleName, source] of Object.entries(modules)) {
4214
- const path = facetImagePath(await facetImageDigest(source));
4215
- images[moduleName] = path;
4216
- sources.set(path, source);
4217
- await pacer.spend(source.length);
4218
- }
4219
- // Register the WHOLE root set here, in one synchronous step, before any
4220
- // byte of it exists on disk. That ordering is the entire protocol between
4221
- // a launch and the sweep: an image is rooted from before it is written, so
4222
- // a sweep can never observe a file this launch has written but not yet
4223
- // claimed. The old loop achieved it by not awaiting at all, which read as
4224
- // "the writes must not be interrupted" — they may be. What must not be
4225
- // interrupted is the gap between writing and rooting, and rooting first
4226
- // closes it for every write that follows, however many turns they span.
4227
- this.residentImages.set(pid, [...sources.keys()]);
4228
- fs.mkdir(FACET_IMAGE_DIR, { recursive: true, mode: 0o755 });
4229
- for (const [path, source] of sources) {
4230
- const stored = path.replace(/^\/+/, '');
4231
- const bytes = new TextEncoder().encode(source);
4232
- // An image at its full size is a COMPLETE one: a write only ever grows
4233
- // the file from offset zero, so a write cut short by a reset leaves a
4234
- // strictly shorter file and fails this test. Size is enough of a check
4235
- // because the reader verifies the digest before the loader sees it.
4236
- if (fs.exists(stored) && fs.lstat(stored).size === bytes.byteLength) {
4237
- await pacer.spend(bytes.byteLength);
4238
- continue;
4239
- }
4240
- // Sliced because the platform resets the object over what ONE TURN has
4241
- // outstanding, not over what it eventually writes — pi's 22.9 MB map
4242
- // went in as a single write and took the session down with it ~25% of
4243
- // the time. Spending between slices is what puts the rest of the image
4244
- // on later turns; the slice bound is what keeps any one of them small.
4245
- let offset = 0;
4246
- do {
4247
- const slice = bytes.subarray(offset, offset + FACET_IMAGE_WRITE_SLICE_BYTES);
4248
- // The first slice REPLACES the file, so an interrupted write's remains
4249
- // are truncated to a known length rather than left as a tail past this
4250
- // content — and the mode is chosen here, at creation, as POSIX has it.
4251
- if (offset === 0)
4252
- fs.writeFile(stored, slice, { mode: 0o644 });
4253
- else
4254
- fs.writeRange(stored, offset, slice);
4255
- offset += slice.byteLength;
4256
- await pacer.spend(slice.byteLength);
4257
- } while (offset < bytes.byteLength);
4258
- }
4259
- this._sweepFacetImages(fs);
4260
- return images;
4261
- }
4262
4207
  /**
4263
4208
  * Fail a paced launch whose process went away while it was suspended.
4264
4209
  *
4265
- * Between turns anything may happen to the process — a kill, a reap, a
4266
- * `_sweepFacetImages` that has already unrooted its images. Continuing would
4210
+ * Between turns anything may happen to the process — a kill, a reap, an
4211
+ * image sweep that has already unrooted its images. Continuing would
4267
4212
  * spend further turns building a facet for a pid nothing will ever attach
4268
4213
  * to, and would write image files the next sweep immediately collects.
4269
4214
  */
@@ -4272,44 +4217,6 @@ export class FacetManager {
4272
4217
  return;
4273
4218
  throw new Error(`Nimbus: resident launch for pid ${pid} was cancelled while it was suspended`);
4274
4219
  }
4275
- /**
4276
- * Drop every image no running process boots from.
4277
- *
4278
- * Content addressing means a changed program writes a NEW image rather than
4279
- * replacing one, so a watch loop — or simply a session that runs a few
4280
- * different programs — would otherwise leave one bundle-sized file behind
4281
- * per distinct version. The root set is the process table, which is exact:
4282
- * an image is live for precisely as long as the process that boots from it.
4283
- * Nothing is left for a TTL or an eviction heuristic to guess at, and after
4284
- * a DO reset the table is empty so every orphan goes.
4285
- */
4286
- _sweepFacetImages(fs) {
4287
- const live = new Set();
4288
- for (const [pid, paths] of this.residentImages) {
4289
- if (this.processes.get(pid)?.state === 'running') {
4290
- for (const path of paths)
4291
- live.add(path);
4292
- }
4293
- else {
4294
- this.residentImages.delete(pid);
4295
- }
4296
- }
4297
- let entries;
4298
- try {
4299
- entries = fs.readdir(FACET_IMAGE_DIR);
4300
- }
4301
- catch {
4302
- return;
4303
- }
4304
- for (const entry of entries) {
4305
- if (live.has(`/${FACET_IMAGE_DIR}/${entry.name}`))
4306
- continue;
4307
- try {
4308
- fs.unlink(`${FACET_IMAGE_DIR}/${entry.name}`);
4309
- }
4310
- catch { /* already gone */ }
4311
- }
4312
- }
4313
4220
  /**
4314
4221
  * The one way this manager boots a resident process. Every resident process
4315
4222
  * is a facet of this session; there is nothing to place and nothing here
@@ -4340,125 +4247,16 @@ export class FacetManager {
4340
4247
  this.vfs?.activateAppendWriter(pid, writerId);
4341
4248
  }
4342
4249
  /**
4343
- * How a paced launch asks for a fresh turn.
4344
- *
4345
- * The host grants one by calling `pumpResidentLaunches` from a context that
4346
- * is genuinely a new invocation — the session's alarm. Without such a host
4347
- * there is no fresh turn to be had, and the launch continues on this one
4348
- * rather than hanging: that is exactly the single-turn launch this path has
4349
- * always performed, so a harness or a runtime without alarms loses the
4350
- * responsiveness but keeps the behaviour.
4351
- */
4352
- launchScheduler = {
4353
- nextTurn: (chunkEnded) => new Promise((resume) => {
4354
- this.launchWaiters.push({ resume, chunkEnded });
4355
- if (this.hooks.requestLaunchTurn) {
4356
- this.hooks.requestLaunchTurn();
4357
- return;
4358
- }
4359
- setTimeout(() => { void this.pumpResidentLaunches(); }, 0);
4360
- }),
4361
- };
4362
- /**
4363
- * Run one chunk of every launch waiting for a turn.
4364
- *
4365
- * Awaits the chunk each resumed launch then performs, so the invocation that
4366
- * granted the turn is the invocation that pays for the work — rather than
4367
- * releasing it into a handler's microtask drain, where nothing owns it and
4368
- * the runtime may tear the context down mid-chunk.
4369
- */
4370
- async pumpResidentLaunches() {
4371
- await this._recoverInterruptedLaunches();
4372
- const waiting = this.launchWaiters;
4373
- if (waiting.length === 0)
4374
- return;
4375
- this.launchWaiters = [];
4376
- for (const waiter of waiting)
4377
- waiter.resume();
4378
- await Promise.all(waiting.map((waiter) => waiter.chunkEnded));
4379
- }
4380
- /**
4381
- * Re-drive the launches a previous instance was building when it was reset.
4382
- *
4383
- * The platform resets a session Durable Object over what one turn has
4384
- * outstanding in storage ("Internal error in Durable Object storage caused
4385
- * object to be reset"), and a launch is the largest writer this session has.
4386
- * Everything the launch held was in memory, so the process it was building
4387
- * and the terminal watching it both went with the instance, and until now
4388
- * the user was told nothing at all.
4389
- *
4390
- * Sited on the pump because the pump is what an alarm calls, and a launch
4391
- * that was suspended has an alarm armed for it — a reset during a chunk
4392
- * fails that alarm, and the platform re-delivers it to the instance that
4393
- * replaces this one. So the first turn after a reset is already this one.
4394
- *
4395
- * Runs once per instance: the journal only changes when a launch of THIS
4396
- * instance starts or settles, and those are rows this instance wrote.
4397
- */
4398
- async _recoverInterruptedLaunches() {
4399
- if (this.launchesRecovered)
4400
- return;
4401
- this.launchesRecovered = true;
4402
- const journal = await this.ctx.storage.list({
4403
- prefix: RESIDENT_LAUNCH_KEY_PREFIX,
4404
- });
4405
- for (const [key, record] of journal) {
4406
- // A pid at or below this instance's base was allocated by a PREVIOUS one
4407
- // (process-table.ts, PID_GEN_STRIDE), so its launch never finished; above
4408
- // the base is this instance's own, still running. Same predicate as
4409
- // `session/rpc.ts` uses to attribute a prior generation's pid.
4410
- if (!(record.pid > 0 && record.pid <= this.processes.pidBase))
4411
- continue;
4412
- await this.ctx.storage.delete(key);
4413
- // 'running' is the measured common case: the platform's reset strikes
4414
- // seconds after a launch settles, while the resident runs.
4415
- const doing = record.phase === 'running' ? 'running' : 'starting';
4416
- if (record.attempt >= RESIDENT_LAUNCH_MAX_ATTEMPT) {
4417
- this.hooks.notify?.('\x1b[2m[nimbus: the session restarted again while '
4418
- + `"${record.command}" was ${doing} — leaving it stopped]\x1b[0m\r\n`);
4419
- continue;
4420
- }
4421
- this.hooks.notify?.('\x1b[2m[nimbus: the session restarted while '
4422
- + `"${record.command}" was ${doing} — restarting it]\x1b[0m\r\n`);
4423
- // Not awaited: this call is running inside the alarm that granted the
4424
- // turn, and the launch it starts asks for turns of its own through that
4425
- // same alarm — awaiting it here would be waiting on an alarm that cannot
4426
- // be scheduled until this one returns.
4427
- this.ctx.waitUntil(this._spawnResident(record.code, record.opts, record.attempt + 1)
4428
- .catch((e) => {
4429
- this.hooks.notify?.(`\x1b[2m[nimbus: "${record.command}" could not be restarted: `
4430
- + `${errorMessage(e)}]\x1b[0m\r\n`);
4431
- }));
4432
- }
4433
- }
4434
- /**
4435
- * Record a launch as in flight, so an instance that replaces this one knows
4436
- * it never finished. Best-effort: a launch that cannot be journalled still
4437
- * runs, and a reset then costs exactly what it cost before the journal.
4438
- *
4439
- * Synced, not merely put: `await put()` resolves before durability, and the
4440
- * reset this journal exists for destroys every write its turn still had
4441
- * outstanding — measured live, a launch killed in its first chunks left NO
4442
- * row for the replacement instance to find, which is how the recovery this
4443
- * feeds sat inert while its own test stayed green. `sync()` is the storage
4444
- * layer's durability barrier: the row is on disk before the launch performs
4445
- * its first byte of real work. What remains is a reset between the put and
4446
- * the sync's completion — and a launch that dies there has not started, so
4447
- * losing its row costs a retype, not a recovery.
4250
+ * Grant every suspended launch a chunk of this turn — the session's alarm
4251
+ * calls this, and journal recovery rides the first pump. See fabric's
4252
+ * `PacedWork.pump` for the ownership argument.
4448
4253
  */
4449
- async _journalLaunch(record) {
4450
- try {
4451
- this.journalledPids.add(record.pid);
4452
- await this.ctx.storage.put(`${RESIDENT_LAUNCH_KEY_PREFIX}${record.pid}`, record);
4453
- await this.ctx.storage.sync();
4454
- }
4455
- catch (e) {
4456
- console.warn('[nimbus] resident launch journal write failed:', errorMessage(e));
4457
- }
4254
+ pumpResidentLaunches() {
4255
+ return this.launchPump.pump();
4458
4256
  }
4459
4257
  /** Whether any launch is suspended waiting for a turn. */
4460
4258
  get hasPendingLaunchTurns() {
4461
- return this.launchWaiters.length > 0;
4259
+ return this.launchPump.hasPending;
4462
4260
  }
4463
4261
  /** Allocate a free loopback port for a resident server facet (from 4096 up). */
4464
4262
  _allocateLoopbackPort() {
@@ -4695,7 +4493,7 @@ export class FacetManager {
4695
4493
  */
4696
4494
  async _runResidentLaunch(entry, code, command, opts, attempt) {
4697
4495
  const cwd = opts.cwd || '/home/user';
4698
- const pacer = new LaunchPacer(this.launchScheduler, launchChunkMaxBytes(this.env), () => this._assertLaunchStillOwned(entry.pid));
4496
+ const pacer = new TurnBudget(this.launchPump, turnChunkMaxBytes(this.env), () => this._assertLaunchStillOwned(entry.pid));
4699
4497
  // Journalled before the first byte of work. A launch that FAILS deletes
4700
4498
  // its row on the way out — its process has already been exited and the
4701
4499
  // user notified, so there is nothing left to owe. A launch that SETTLES
@@ -4713,12 +4511,12 @@ export class FacetManager {
4713
4511
  attempt,
4714
4512
  phase: 'starting',
4715
4513
  };
4716
- await this._journalLaunch(record);
4514
+ await this.launchJournal.journal(record);
4717
4515
  try {
4718
4516
  await this._residentLaunchBody(entry, code, command, cwd, opts, pacer);
4719
4517
  }
4720
4518
  catch (e) {
4721
- await this._releaseResidentJournal(entry.pid);
4519
+ await this.launchJournal.release(entry.pid);
4722
4520
  throw e;
4723
4521
  }
4724
4522
  finally {
@@ -4728,24 +4526,8 @@ export class FacetManager {
4728
4526
  // its running life with a fresh re-drive budget. If the process already
4729
4527
  // ended inside the launch body's own settlement, the terminal hook has
4730
4528
  // released the row — do not write it back.
4731
- if (this.journalledPids.has(entry.pid)) {
4732
- await this._journalLaunch({ ...record, attempt: 0, phase: 'running' });
4733
- }
4734
- }
4735
- /**
4736
- * The journal row's one release: the process is over, nothing is owed.
4737
- * Synced so an instance reset moments later cannot roll the delete back and
4738
- * resurrect a process the user watched end.
4739
- */
4740
- async _releaseResidentJournal(pid) {
4741
- if (!this.journalledPids.delete(pid))
4742
- return;
4743
- try {
4744
- await this.ctx.storage.delete(`${RESIDENT_LAUNCH_KEY_PREFIX}${pid}`);
4745
- await this.ctx.storage.sync();
4746
- }
4747
- catch (e) {
4748
- console.warn('[nimbus] resident launch journal delete failed:', errorMessage(e));
4529
+ if (this.launchJournal.has(entry.pid)) {
4530
+ await this.launchJournal.journal({ ...record, attempt: 0, phase: 'running' });
4749
4531
  }
4750
4532
  }
4751
4533
  async _residentLaunchBody(entry, code, command, cwd, opts, pacer) {
@@ -4758,7 +4540,7 @@ export class FacetManager {
4758
4540
  }
4759
4541
  }
4760
4542
  if (this.vfs)
4761
- this._ensureImageStoreDir(this.vfs);
4543
+ this.imageStore.ensureDir();
4762
4544
  const diagOn = isExecDiagEnabled();
4763
4545
  const __bundleStart = diagOn ? Date.now() : 0;
4764
4546
  const processVfs = this.vfs?.as(entry.cred);
@@ -4803,7 +4585,7 @@ export class FacetManager {
4803
4585
  let handle;
4804
4586
  let resourcesTracked = false;
4805
4587
  try {
4806
- const vfsTextModules = await this._materializeFacetImages(entry.pid, {
4588
+ const vfsTextModules = await this.imageStore.materialize(entry.pid, {
4807
4589
  'worker.js': generatedWorker.code,
4808
4590
  ...generatedWorker.modules,
4809
4591
  }, pacer);
@@ -4893,6 +4675,9 @@ export class FacetManager {
4893
4675
  await handle.booted();
4894
4676
  }
4895
4677
  if (opts.port && opts.port > 0 && opts.port < 65536) {
4678
+ // A new process on a port retires the previous occupant's preview
4679
+ // capability, so a URL handed out for that one cannot reach this one.
4680
+ await clearPortCapability({ ctx: this.ctx, portRegistry: this.portRegistry }, opts.port);
4896
4681
  this.portRegistry.register(opts.port, entry.pid);
4897
4682
  }
4898
4683
  }
@@ -4960,6 +4745,9 @@ export class FacetManager {
4960
4745
  resourcesTracked = true;
4961
4746
  this.portRegistry.bindFacetStub(entry.pid, handle.routeTarget);
4962
4747
  if (opts.port && opts.port > 0 && opts.port < 65536) {
4748
+ // A new process on a port retires the previous occupant's preview
4749
+ // capability, so a URL handed out for that one cannot reach this one.
4750
+ await clearPortCapability({ ctx: this.ctx, portRegistry: this.portRegistry }, opts.port);
4963
4751
  this.portRegistry.register(opts.port, entry.pid);
4964
4752
  }
4965
4753
  return { pid: entry.pid, boot: await handle.booted() };
@@ -4980,8 +4768,9 @@ export class FacetManager {
4980
4768
  throw e;
4981
4769
  }
4982
4770
  }
4983
- registerPort(pid, port) {
4771
+ async registerPort(pid, port) {
4984
4772
  if (port > 0 && port < 65536) {
4773
+ await clearPortCapability({ ctx: this.ctx, portRegistry: this.portRegistry }, port);
4985
4774
  this.portRegistry.register(port, pid);
4986
4775
  }
4987
4776
  }
@@ -23,7 +23,7 @@
23
23
  * place; the L2 asset cache makes refetches cheap).
24
24
  */
25
25
  import { z } from 'zod/v4';
26
- import type { WorkerCode } from '../loaders/vendor/types.js';
26
+ import type { WorkerCode } from '@nimbus-sh/fabric/vendor/types.js';
27
27
  export interface OpencodeAssetsEnv {
28
28
  ASSETS: {
29
29
  fetch(req: Request): Promise<Response>;
@@ -1 +1 @@
1
- {"version":3,"file":"opencode-staging.d.ts","sourceRoot":"","sources":["../../src/facets/opencode-staging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAwB3B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAG7D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE;QAAE,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAC;CACpD;AAED;;;;GAIG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;iBAwBlC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAoCxE,iFAAiF;AACjF,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,OAAO,CAAC,iBAAiB,CAAC,EAC/B,UAAU,EAAE,OAAO,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,CAAC,CAMhD;AAkFD;;;;GAIG;AACH,wBAAsB,2BAA2B,CAC/C,GAAG,EAAE,OAAO,CAAC,iBAAiB,CAAC,EAC/B,SAAS,EAAE,OAAO,GACjB,OAAO,CAAC,UAAU,CAAC,CAyDrB"}
1
+ {"version":3,"file":"opencode-staging.d.ts","sourceRoot":"","sources":["../../src/facets/opencode-staging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAwB3B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAGpE,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE;QAAE,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAC;CACpD;AAED;;;;GAIG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;iBAwBlC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAoCxE,iFAAiF;AACjF,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,OAAO,CAAC,iBAAiB,CAAC,EAC/B,UAAU,EAAE,OAAO,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,CAAC,CAMhD;AAsFD;;;;GAIG;AACH,wBAAsB,2BAA2B,CAC/C,GAAG,EAAE,OAAO,CAAC,iBAAiB,CAAC,EAC/B,SAAS,EAAE,OAAO,GACjB,OAAO,CAAC,UAAU,CAAC,CAyDrB"}
@@ -145,6 +145,9 @@ function chunkModuleEntries(env) {
145
145
  }
146
146
  return chunkEntriesInflight;
147
147
  }
148
+ // A 'staged' boot spec reaching the fabric assembles through
149
+ // assembleOpencodeFacetConfig below; the worker's composition root
150
+ // (src/index.ts) hands it to the fabric with composeFabric.
148
151
  /**
149
152
  * Assemble the full Worker Loader config for an opencode facet from a stage
150
153
  * spec. Returns the config WITHOUT the SUPERVISOR env binding — the caller
@@ -41,7 +41,7 @@
41
41
  // 3. Facet-side: ESM source strings for the ws shim + chokidar shim.
42
42
  // All are emitted by the generators below.
43
43
  import { WorkerEntrypoint } from 'cloudflare:workers';
44
- import { disposeRpcResource, useRpcResource } from '@nimbus-sh/core/_shared/rpc-dispose.js';
44
+ import { disposeRpcResource, useRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
45
45
  // CLN-3 (2026-05-11): supervisor-side debug gate. Mirrors the facet-side
46
46
  // `globalThis.__cirrusDebug` flag declared at cirrus-real.ts:160. When
47
47
  // `false` (default), the hot-path console.log calls in `HmrBridge`