@lunora/container 1.0.0-alpha.9 → 1.0.0-alpha.90

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 (37) hide show
  1. package/README.md +24 -5
  2. package/dist/bridge.d.mts +43 -33
  3. package/dist/bridge.d.ts +43 -33
  4. package/dist/bridge.mjs +1 -79
  5. package/dist/do/index.d.mts +354 -78
  6. package/dist/do/index.d.ts +354 -78
  7. package/dist/do/index.mjs +1 -321
  8. package/dist/index.d.mts +585 -148
  9. package/dist/index.d.ts +585 -148
  10. package/dist/index.mjs +1 -2
  11. package/dist/otel.d.mts +217 -0
  12. package/dist/otel.d.ts +217 -0
  13. package/dist/otel.mjs +1 -0
  14. package/dist/packem_shared/CONTAINER_EXEC_PATH-7Hcd6-zs.mjs +1 -0
  15. package/dist/packem_shared/CUSTOM_INSTANCE_MIN_MEMORY_MIB_PER_VCPU-D8Hy9Rxv.mjs +1 -0
  16. package/dist/packem_shared/ContainerProxy-CfXjS6ZI.mjs +27 -0
  17. package/dist/packem_shared/DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-gYi8RBUO.mjs +1 -0
  18. package/dist/packem_shared/LunoraSandboxContainer-DPnhJUu1.mjs +1 -0
  19. package/dist/packem_shared/abort-deadline-jwbW9Ala.mjs +1 -0
  20. package/dist/packem_shared/createContainerContext-p6QxWjsm.mjs +1 -0
  21. package/dist/packem_shared/exec-BYLW70sR.mjs +1 -0
  22. package/dist/packem_shared/jurisdiction-DtE9s70w.mjs +1 -0
  23. package/dist/packem_shared/jurisdiction.d-BwpauCoI.d.mts +471 -0
  24. package/dist/packem_shared/jurisdiction.d-BwpauCoI.d.ts +471 -0
  25. package/dist/packem_shared/sampling-D21rpu_s.mjs +1 -0
  26. package/dist/packem_shared/sandbox-types.d-DXlHnsEg.d.mts +110 -0
  27. package/dist/packem_shared/sandbox-types.d-DXlHnsEg.d.ts +110 -0
  28. package/dist/sandbox/index.d.mts +66 -0
  29. package/dist/sandbox/index.d.ts +66 -0
  30. package/dist/sandbox/index.mjs +1 -0
  31. package/package.json +12 -2
  32. package/dist/packem_shared/ContainerProxy-DWqUX_re.mjs +0 -1474
  33. package/dist/packem_shared/containerBindingName-BiTrAF1J.mjs +0 -224
  34. package/dist/packem_shared/createContainerContext-CIVzsY5m.mjs +0 -220
  35. package/dist/packem_shared/jurisdiction-CuPNcLDt.mjs +0 -13
  36. package/dist/packem_shared/jurisdiction.d-TwTGkgTg.d.mts +0 -266
  37. package/dist/packem_shared/jurisdiction.d-TwTGkgTg.d.ts +0 -266
@@ -1,5 +1,5 @@
1
- import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
2
- import { a as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-TwTGkgTg.js";
1
+ import { d as ContainerRuntimeInstanceType, C as ContainerDefinition, D as DurableObjectJurisdiction, b as ContainerSnapshot, e as ContainerSpawnRequest } from "../packem_shared/jurisdiction.d-BwpauCoI.js";
2
+ import { DurableObject, WorkerEntrypoint, RpcTarget } from 'cloudflare:workers';
3
3
  /**
4
4
  * ContainerStartOptions as they come from worker types
5
5
  */
@@ -119,7 +119,7 @@ type OutboundHandler<E = Cloudflare.Env, P = unknown> = {
119
119
  type OutboundHandlerParams = Record<string, unknown>;
120
120
  type OutboundHandlerParamsOf<THandler> = THandler extends ((req: Request, env: unknown, ctx: OutboundHandlerContext<infer Params>) => Promise<Response> | Response) ? Params : never;
121
121
  declare function outboundParams<THandler extends OutboundHandler<unknown, unknown>>(_handler: THandler, params: OutboundHandlerParamsOf<THandler>): OutboundHandlerParamsOf<THandler>;
122
- type OutboundHandlers<ParamsByMethod extends OutboundHandlerParams, E = Cloudflare.Env> = { [Method in keyof ParamsByMethod]?: OutboundHandler<E, ParamsByMethod[Method]> };
122
+ type OutboundHandlers<ParamsByMethod extends OutboundHandlerParams, E = Cloudflare.Env> = { [Method in keyof ParamsByMethod]?: OutboundHandler<E, ParamsByMethod[Method]>; };
123
123
  type OutboundHandlerOverride<Params = unknown> = {
124
124
  method: string;
125
125
  } & ([Params] extends [undefined] ? {
@@ -345,7 +345,7 @@ declare class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
345
345
  * When the counter transitions to 0, renew the activity timeout so the
346
346
  * inactivity window starts fresh from the moment the last request completes.
347
347
  */
348
- private decrementInflight;
348
+ protected decrementInflight(): void;
349
349
  /**
350
350
  * Schedule a task to be executed in the future.
351
351
  *
@@ -387,10 +387,17 @@ declare class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
387
387
  private onStopCalled;
388
388
  private state;
389
389
  private monitor;
390
- private startInFlight;
390
+ protected startInFlight?: Promise<unknown>;
391
391
  private monitoredPromise;
392
392
  private sleepAfterMs;
393
- private inflightRequests;
393
+ protected inflightRequests: number;
394
+ /** Patched (Lunora): the `durable_object`-policy image reference and size `start()` forwards. */
395
+ protected image?: string;
396
+ protected instance?: string | {
397
+ vcpu: number;
398
+ memoryMib: number;
399
+ diskMb: number;
400
+ };
394
401
  private outboundByHostOverrides;
395
402
  private outboundHandlerOverride?;
396
403
  private allowedHostsOverride?;
@@ -483,115 +490,384 @@ declare class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
483
490
  getSchedule<T = string>(id: string): Promise<Schedule<T> | undefined>;
484
491
  private isActivityExpired;
485
492
  }
493
+ /** The runtime's `ExecProcess`, structurally — only what a spawned process uses. */
494
+ interface NativeProcess {
495
+ exitCode: Promise<number>;
496
+ isPty?: boolean;
497
+ kill: (signal?: number) => void;
498
+ pid?: number;
499
+ resize?: (cols: number, rows: number) => void;
500
+ stderr?: ReadableStream | null;
501
+ stdin?: WritableStream | null;
502
+ stdout?: ReadableStream | null;
503
+ }
504
+ /**
505
+ * Control half of a spawned process, returned to the caller as an RPC stub.
506
+ * The streams travel next to it in {@link SpawnedProcess}: a stream can cross
507
+ * RPC only once, so it is handed over directly rather than exposed through a
508
+ * property that could be read twice.
509
+ */
510
+ declare class ContainerProcessControl extends RpcTarget {
511
+ #private;
512
+ constructor(process: NativeProcess, exit: Promise<number>);
513
+ /** Resolves with the exit code once the process ends. */
514
+ exitCode(): Promise<number>;
515
+ /** Send `signal` (default `SIGTERM`) to the process. A no-op once it has exited. */
516
+ kill(signal?: number): void;
517
+ /** Resize a PTY process's terminal. Throws for a process spawned without `pty`. */
518
+ resize(cols: number, rows: number): void;
519
+ }
520
+ /** What `lunoraSpawn` returns over RPC. */
521
+ interface SpawnedProcess {
522
+ control: ContainerProcessControl;
523
+ isPty: boolean;
524
+ pid: number;
525
+ /** `null` under `pty`, where stderr is merged into the terminal output. */
526
+ stderr: ReadableStream<Uint8Array> | null;
527
+ /** A writable stdin when the request asked for `stdin: true`, else `null`. */
528
+ stdin: WritableStream<Uint8Array> | null;
529
+ stdout: ReadableStream<Uint8Array> | null;
530
+ }
531
+ /** What an explicit `start()` chose that later restarts must reuse. */
532
+ interface StartOverride {
533
+ /** Replaces the declared env, `secrets` and `secretsStore` wholesale. */
534
+ envVars?: Record<string, string>;
535
+ /** A key of the definition's `images`, or a managed `cloudflare/…` image. */
536
+ image?: string;
537
+ instanceType?: ContainerRuntimeInstanceType;
538
+ }
486
539
  type DurableObjectContext = ConstructorParameters<typeof Container>[0];
540
+ /** The runtime's `ctx.container`, once known to be present. */
541
+ type ContainerRuntime = NonNullable<DurableObjectContext["container"]>;
542
+ /** A running container plus the release for the in-flight count taken on it. See `LunoraContainer.lunoraAcquire`. */
543
+ interface AcquiredContainer {
544
+ container: ContainerRuntime;
545
+ /** Drop the in-flight count. Idempotent. */
546
+ release: () => void;
547
+ }
548
+ /**
549
+ * The `start()` options `LunoraContainer` accepts on top of the base's: the
550
+ * `durable_object`-policy image, size and snapshot to restore.
551
+ */
552
+ type LunoraStartOptions = NonNullable<Parameters<Container["start"]>[0]> & Pick<StartOverride, "image" | "instanceType"> & {
553
+ snapshot?: ContainerSnapshot;
554
+ };
487
555
  /**
488
- * Base class for the generated Container DO classes. Applies a
489
- * `defineContainer` definition onto `@cloudflare/containers`' `Container`:
490
- * port, sleep timeout, internet access, and the container environment (static
491
- * `env` merged with the declared Worker secrets — a declared-but-unset secret
492
- * fails fast here rather than starting a container without its credential).
493
- *
494
- * Generated subclasses stay one line of behavior:
495
- *
496
- * ```ts
497
- * export class TranscoderContainer extends LunoraContainer {
498
- * constructor(ctx: DurableObjectState, env: Env) {
499
- * super(ctx, env, transcoder, "transcoder");
500
- * }
501
- * }
502
- * ```
503
- */
556
+ * Base class for the generated Container DO classes. Applies a
557
+ * `defineContainer` definition onto `@cloudflare/containers`' `Container`:
558
+ * port, sleep timeout, internet access, and the container environment (static
559
+ * `env` merged with the declared Worker secrets — a declared-but-unset secret
560
+ * fails fast here rather than starting a container without its credential).
561
+ *
562
+ * Generated subclasses stay one line of behavior:
563
+ *
564
+ * ```ts
565
+ * export class TranscoderContainer extends LunoraContainer {
566
+ * constructor(ctx: DurableObjectState, env: Env) {
567
+ * super(ctx, env, transcoder, "transcoder");
568
+ * }
569
+ * }
570
+ * ```
571
+ */
504
572
  declare class LunoraContainer<Env = unknown> extends Container<Env> {
573
+ /** The `lunora/containers.ts` export name, for lifecycle log correlation and error messages. */
574
+ protected readonly lunoraName: string;
505
575
  /**
506
- * Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
507
- * schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
508
- * to the same region as the root shard. `undefined` ⇒ un-pinned.
509
- */
576
+ * Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
577
+ * schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
578
+ * to the same region as the root shard. `undefined` ⇒ un-pinned.
579
+ */
510
580
  private readonly lunoraJurisdiction?;
511
- /** The `lunora/containers.ts` export name, for lifecycle log correlation. */
512
- private readonly lunoraName;
513
581
  /** Default port the readiness probes target when a check omits its own `port`. */
514
582
  private readonly lunoraDefaultPort?;
515
583
  /** Hard-cap lifetime in whole seconds (from the `hardTimeout` config), or `undefined`. */
516
584
  private readonly lunoraHardTimeoutSeconds?;
585
+ /** In-flight `readyOn` gate for the current start; cleared when it fails. See `awaitReadinessGate`. */
586
+ private lunoraReadiness?;
517
587
  /** Declarative readiness probes that gate request proxying (from the `readyOn` config). */
518
588
  private readonly lunoraReadyOn;
519
589
  /** Map of container env-var name → Worker Secrets Store binding name (from the `secretsStore` config). */
520
590
  private readonly lunoraSecretsStore?;
521
- /** Memoised Secrets Store resolution: run once, then merged into `envVars` before the first start. */
591
+ /** The definition's `env` + `secrets`, as resolved at construction — the env a start uses absent an override. */
592
+ private readonly lunoraDeclaredEnv;
593
+ /** The persisted start override, once loaded. See {@link START_OVERRIDE_KEY}. */
594
+ private lunoraOverride?;
595
+ /** Whether the override field reflects storage (read once per DO instance, then kept in step). */
596
+ private lunoraOverrideLoaded;
597
+ /** Memoised Secrets Store resolution: the resolved `name → value` map, fetched once. */
522
598
  private lunoraSecretsStoreResolved?;
599
+ /** Whether the definition uses the `durable_object` scheduling policy. */
600
+ private readonly lunoraDurableObjectScheduled;
601
+ /** The definition's default image name and instance size (`durable_object` policy only). */
602
+ private readonly lunoraDefaultSelection;
603
+ /**
604
+ * Count of runs observed to have ENDED, bumped by the `onStop` hook. Read
605
+ * across a start: a bump means the run that start snapshotted is over,
606
+ * whatever the `running` flag said at snapshot time. See `beginStart`.
607
+ */
608
+ private lunoraStops;
523
609
  constructor(context: DurableObjectContext, env: Env, definition: ContainerDefinition, exportName?: string, jurisdiction?: DurableObjectJurisdiction);
524
610
  /**
525
- * Proxy entry for every `ctx.containers.&lt;name>` fetch. Resolves the
526
- * `secretsStore` bindings into `envVars` before delegating, so the values
527
- * are present when the base implicitly starts the container for this
528
- * request — a no-op when `secretsStore` is unset.
529
- */
611
+ * Save the running container's filesystem (`durable_object` policy only).
612
+ * The handle is plain data: store it and pass it to `start({ snapshot })` to
613
+ * restore it here or in another instance. Memory and processes are not
614
+ * captured — a restored container runs its entrypoint again.
615
+ */
616
+ lunoraSnapshot(options?: {
617
+ name?: string;
618
+ }): Promise<ContainerSnapshot>;
619
+ /**
620
+ * HTTP entry for every request to this Durable Object. Refuses the reserved
621
+ * `/__lunora/*` namespace unconditionally: `exec` reaches it only through
622
+ * the {@link lunoraExec} RPC, so no header, path spelling, or inbound request
623
+ * an app forwards as-is (`env.CONTAINER_X.get(id).fetch(request)`) opens it.
624
+ * The client-side path guard is the first line; this is the second.
625
+ */
626
+ override fetch(request: Request): Promise<Response>;
627
+ /**
628
+ * Proxy entry for every `ctx.containers.<name>` fetch — and a public RPC on
629
+ * the stub in its own right, so it applies the same `/__lunora/*` refusal
630
+ * as {@link fetch} rather than trusting that every caller came through it.
631
+ * The base starts the container for this request through
632
+ * {@link startAndWaitForPorts}, which is where the `secretsStore` resolution
633
+ * lives — a request that finds the container already healthy needs no
634
+ * resolution at all.
635
+ */
530
636
  override containerFetch(...args: Parameters<Container<Env>["containerFetch"]>): Promise<Response>;
531
637
  /**
532
- * Explicit start (`ctx.containers.&lt;name>.get(id).start()`). Resolves the
533
- * `secretsStore` bindings into `envVars` first, mirroring
534
- * {@link containerFetch}. A per-instance `start({ envVars })` replaces the
535
- * env set wholesale (base behavior), so the injected values only apply to a
536
- * bare `start()` — same as the static `env`/`secrets`. When the caller
537
- * supplies its own `envVars` we skip resolution entirely: those values would
538
- * be discarded anyway, so a missing/unreadable binding shouldn't fail a start
539
- * that never uses them.
540
- */
541
- override start(...args: Parameters<Container<Env>["start"]>): Promise<void>;
638
+ * The exec entry: `handle.exec` delivers its `POST /__lunora/exec` here, as
639
+ * an RPC, never over {@link fetch}. An RPC can only be invoked by code that
640
+ * holds the Durable Object binding, which is what makes the exec route
641
+ * unreachable from request content. Accepts exactly the exec route and
642
+ * nothing else under the reserved namespace, and honours a `.port(n)`
643
+ * handle's target-port header the way the base `fetch` does.
644
+ */
645
+ lunoraExec(request: Request): Promise<Response>;
646
+ /**
647
+ * Start a process and stream it (`handle.spawn()`), through the runtime's
648
+ * native `ctx.container.exec()`. Starts the container first when it is not
649
+ * running, and counts the process in flight until it exits, so `sleepAfter`
650
+ * cannot stop the container under it. The process's streams and a control
651
+ * stub (`exitCode` / `kill` / `resize`) cross back over RPC.
652
+ */
653
+ lunoraSpawn(request: ContainerSpawnRequest): Promise<SpawnedProcess>;
654
+ /**
655
+ * The start path `containerFetch` takes (and the one an app can call itself).
656
+ * Resolves the `secretsStore` bindings into `envVars` first — `doStartContainer`
657
+ * reads `this.envVars`, so a container started this way would otherwise boot
658
+ * without its Secrets Store values.
659
+ *
660
+ * The base's last act is `blockConcurrencyWhile(… onStart())`, so this
661
+ * override resumes on the far side of that gate — which is where
662
+ * {@link afterContainerStart} has to run. See its docblock.
663
+ */
664
+ override startAndWaitForPorts(...args: Parameters<Container<Env>["startAndWaitForPorts"]>): Promise<void>;
665
+ /**
666
+ * Explicit start (`ctx.containers.<name>.get(id).start()`).
667
+ *
668
+ * A per-instance `start({ envVars })` REPLACES the declared env (`env`,
669
+ * `secrets`, `secretsStore` — the Secrets Store is not even read) and is
670
+ * PERSISTED: every later start of this instance, explicit or implicit (a
671
+ * `fetch`/`exec` after the container slept, crashed or hit `hardTimeout`),
672
+ * boots with the same variables until `destroy()`. That is what makes
673
+ * `start({ envVars: {} })` a credential-free sandbox rather than one that
674
+ * gets its credentials back on the next restart.
675
+ *
676
+ * A start with `envVars` that differ from what the instance is running (or
677
+ * starting) with is REJECTED rather than silently joined: the base would
678
+ * return early on a live container, reporting a start whose env never took
679
+ * effect. `stop()` it first, or `destroy()` it to drop the override.
680
+ */
681
+ override start(options?: LunoraStartOptions, waitOptions?: Parameters<Container<Env>["start"]>[1]): Promise<void>;
682
+ /**
683
+ * Stop the container and forget this instance's start override, so the next
684
+ * start uses the declared env, image and size again.
685
+ */
686
+ override destroy(): Promise<void>;
542
687
  override onActivityExpired(): Promise<void>;
543
688
  override onError(error: unknown): unknown;
544
689
  override onStart(): Promise<void>;
545
690
  /**
546
- * Hook run when the container's `hardTimeout` elapses (dispatched by the base
547
- * scheduler via the run-generation-stamped schedule armed in
548
- * {@link onStart}). Default: stop the instance. Override to drain/checkpoint
549
- * first. A stale schedule from a previous run, or an already-stopped
550
- * instance, is ignored (upstream cloudflare/containers#85).
551
- */
691
+ * Hook run when the container's `hardTimeout` elapses (dispatched by the base
692
+ * scheduler via the run-generation-stamped schedule armed in
693
+ * {@link onStart}). Default: stop the instance. Override to drain/checkpoint
694
+ * first. A stale schedule from a previous run, or an already-stopped
695
+ * instance, is ignored (upstream cloudflare/containers#85).
696
+ *
697
+ * `stop()` sends SIGTERM and returns; nothing escalates to `destroy()`. The
698
+ * cap is therefore a signal, not a bound — a container that traps or ignores
699
+ * SIGTERM keeps running, and the schedule is ONE-SHOT (the base deletes the
700
+ * row once it fires), so nothing signals it a second time either: an ignored
701
+ * SIGTERM means the cap is spent, not retried. Override this hook and follow
702
+ * the stop with a `destroy()` after a grace period if your workload needs a
703
+ * real ceiling.
704
+ */
552
705
  onHardTimeoutExpired(payload?: {
553
706
  generation?: number;
554
707
  }): Promise<void>;
555
708
  override onStop(parameters: StopParams): Promise<void>;
556
709
  /**
557
- * Arm the hard-timeout kill via the base scheduler (so it integrates with
558
- * the container's own alarm machinery instead of fighting it). Bumps the run
559
- * generation and stamps the schedule with it, so {@link onHardTimeoutExpired}
560
- * can tell a fresh schedule from a stale one. No-op without a `hardTimeout`.
561
- */
710
+ * Get the container running for an operation that talks to it through
711
+ * `ctx.container` rather than over HTTP (`spawn`, and the sandbox helpers),
712
+ * and count the operation in flight so `sleepAfter` cannot stop the
713
+ * container underneath it. Goes through the same readiness gate a proxied
714
+ * request does, and through {@link start} when the container is stopped —
715
+ * `start()` returns once the container runs, so a container that serves no
716
+ * port starts here too. The caller must call `release` when the operation
717
+ * ends, including when the result is a stream that outlives this call.
718
+ */
719
+ protected lunoraAcquire(operation: string): Promise<AcquiredContainer>;
720
+ /**
721
+ * Runs on every start path after the env and image are settled and before
722
+ * the base starts the container — which is also before the base installs
723
+ * its outbound interception. A subclass that has to register an outbound
724
+ * intercept ahead of the base's catch-all does it here. No-op by default.
725
+ */
726
+ protected beforeContainerStart(): Promise<void>;
727
+ /** A 403 for a path under the reserved namespace, else `undefined`. */
728
+ private refuseReserved;
729
+ /**
730
+ * Arm the hard timeout and block on the `readyOn` probes — the work that has
731
+ * to happen once per real start, **outside** the base's start gate.
732
+ *
733
+ * It cannot live in `onStart`, which is the obvious home for it: the base
734
+ * invokes that hook as `blockConcurrencyWhile(async () => { … onStart() })`
735
+ * (`@cloudflare/containers`, both `start()` and `startAndWaitForPorts()`),
736
+ * and workerd treats a *rejecting* `blockConcurrencyWhile` closure as
737
+ * unrecoverable — it aborts the Durable Object, discards its in-memory state
738
+ * and every hibernating socket on it, and flattens the error to a plain
739
+ * `Error`. A readiness timeout is an ordinary, diagnosable failure: it must
740
+ * surface as the `LunoraError` naming the check, the port and the budget,
741
+ * not cost the object its life and arrive as an opaque message. (The same
742
+ * reasoning, and the same settle-outside-the-gate remedy, is written up on
743
+ * `ShardHost.runSerialized` in `@lunora/platform-cloudflare`.) A 30-second
744
+ * wait also has no business inside a gate that blocks every other dispatch
745
+ * to the object.
746
+ *
747
+ * Both start entry points call this immediately after `super`, so it runs
748
+ * once per start. It does NOT gate proxying on its own: the base commits the
749
+ * healthy state inside the start gate, before this runs, so a concurrent
750
+ * request would sail past `containerFetch`'s status check. That is what
751
+ * `awaitReadinessGate` is for, and it is the seam the move cost us —
752
+ * the in-gate placement got this for free from `blockConcurrencyWhile`.
753
+ */
754
+ private afterContainerStart;
755
+ /**
756
+ * Decide — synchronously, before anything is started — whether this start
757
+ * begins a NEW container run, and drop the previous run's readiness gate when
758
+ * it does. Returns whether the container was already running.
759
+ *
760
+ * The gate has to be keyed on the run, and `onStop` alone cannot key it:
761
+ * `@cloudflare/containers` reaches `onStop` only through
762
+ * `syncPendingStoppedEvents`, which `start()` never calls (only
763
+ * `startAndWaitForPorts`, `stop()` and the alarm loop do), while the monitor
764
+ * callback that observes a container exit merely records the state. So a
765
+ * `start()` in the up-to-three-minute window before the next alarm found the
766
+ * finished run's settled gate and skipped BOTH `armHardTimeout` and the
767
+ * `readyOn` probes — run 2 ran uncapped and was proxied to before it reported
768
+ * ready. A `hardTimeout` firing its own SIGTERM lands in exactly that window.
769
+ *
770
+ * The mirror case is why the answer is not "always re-arm": a start that finds
771
+ * the container already up begins no run, and re-arming there moves the cap
772
+ * (see {@link afterContainerStart}).
773
+ *
774
+ * Read with no `await` between it and `super`, so two concurrent starts of a
775
+ * stopped container both observe `false` and the second joins the first's
776
+ * gate instead of arming a second schedule — and so the flag is as fresh as
777
+ * it can be. It is a snapshot either way, and the run can end after it: the
778
+ * base's own pre-start work (`getPortsToCheck`, `syncPendingStoppedEvents`)
779
+ * still runs on the far side of it, and a start that came in on a live
780
+ * container would then arm nothing for the run the base goes on to start.
781
+ * Hence the second half of the answer, in the callers: an `onStop` observed
782
+ * ACROSS the base call ({@link lunoraStops}) demotes the snapshot, because
783
+ * the run it described is over. What that does not cover is an exit inside
784
+ * `start()`'s own base call — that path never syncs pending stop events, so
785
+ * nothing reports the exit until the next alarm or `startAndWaitForPorts`,
786
+ * and the cap for such a run is armed only when one of those arrives.
787
+ */
788
+ private beginStart;
789
+ /**
790
+ * Block until the `readyOn` probes for the current start have passed.
791
+ *
792
+ * Necessary because the base marks the container healthy *inside* the start
793
+ * gate — `startAndWaitForPorts` runs `setHealthy()` immediately before
794
+ * `onStart()` — while our probes run after it returns. `containerFetch`
795
+ * skips the start path entirely once it observes
796
+ * `container.running && status === "healthy"`, so without this a request
797
+ * arriving mid-probe would proxy to a container that never reported ready,
798
+ * and a request arriving after a *failed* probe would do so permanently.
799
+ * Holding `setHealthy` and the probes together the way the base does would
800
+ * mean putting the probes back inside the gate, which is the defect this
801
+ * whole path exists to avoid.
802
+ */
803
+ private awaitReadinessGate;
804
+ /**
805
+ * Arm the hard-timeout kill via the base scheduler (so it integrates with
806
+ * the container's own alarm machinery instead of fighting it). Bumps the run
807
+ * generation and stamps the schedule with it, so {@link onHardTimeoutExpired}
808
+ * can tell a fresh schedule from a stale one. No-op without a `hardTimeout`.
809
+ */
562
810
  private armHardTimeout;
563
811
  /**
564
- * Resolve the `secretsStore` bindings (async `.get()`) once and merge the
565
- * values into `envVars`, so they're present when the base starts the
566
- * container. Memoised on the first call — every later start reuses the
567
- * resolved promise. A missing binding or a non-string value fails fast (the
568
- * start surfaces the error), the same fail-closed stance the static
569
- * `secrets` resolution takes for a missing Worker secret. No-op without
570
- * `secretsStore`.
571
- */
812
+ * Resolve the `secretsStore` bindings (async `.get()`) once and merge the
813
+ * values into `envVars`, so they're present when the base starts the
814
+ * container. Memoised on the first call — every later start reuses the
815
+ * resolved promise. A missing binding or a non-string value fails fast (the
816
+ * start surfaces the error), the same fail-closed stance the static
817
+ * `secrets` resolution takes for a missing Worker secret. No-op without
818
+ * `secretsStore`.
819
+ */
572
820
  private resolveSecretsStoreEnv;
821
+ /** The persisted start override, read from storage once per instance lifetime. */
822
+ private readOverride;
823
+ /**
824
+ * Validate an explicit start's choices and persist them — merged over the
825
+ * stored override, so `start({ instanceType })` alone keeps a chosen image.
826
+ * Everything is checked before the one write, so a rejected start stores
827
+ * nothing. A start whose choices differ from what a running (or starting)
828
+ * container has is REJECTED rather than silently joined: the base would
829
+ * return early on a live container, reporting a start that never took
830
+ * effect. A snapshot restore only applies to a fresh start, so it is refused
831
+ * on a busy one.
832
+ */
833
+ private recordStartOverride;
834
+ /**
835
+ * Set `envVars` for the start about to happen: the persisted override when
836
+ * there is one (the Secrets Store is not read), else the declared env plus
837
+ * the resolved Secrets Store values.
838
+ */
839
+ private applyStartEnv;
573
840
  /**
574
- * Block until every `readyOn` probe responds with its expected status, or
575
- * throw once the readiness budget is spent. Probes run in parallel and hit
576
- * the container's TCP port directly (NOT `containerFetch`, which would
577
- * recurse back into the start path). No-op without `readyOn`.
578
- */
841
+ * Resolve the image reference and instance size for the start about to
842
+ * happen (`durable_object` policy only): the persisted explicit choice, else
843
+ * the definition default. A fresh boot with neither an image nor a snapshot
844
+ * would be rejected by the runtime, so it fails here with the fix instead.
845
+ */
846
+ private applyStartSelection;
847
+ /** Map an image name to the reference `ctx.container.start()` takes, via `ctx.container.images`. */
848
+ private resolveImage;
849
+ /**
850
+ * Block until every `readyOn` probe responds with its expected status, or
851
+ * throw once the readiness budget is spent. Probes run in parallel and hit
852
+ * the container's TCP port directly (NOT `containerFetch`, which would
853
+ * recurse back into the start path). No-op without `readyOn`.
854
+ */
579
855
  private awaitContainerReadiness;
580
856
  /** Poll one readiness probe until it returns its expected status or the shared deadline passes. */
581
857
  private awaitReadinessCheck;
582
858
  /**
583
- * Best-effort push of `envelope` into the root ShardDO's log buffer so it
584
- * also appears in the Studio Logs panel (the terminal already has it via
585
- * `emitContainerLifecycle`). Fire-and-forget and fully swallowed: a missing
586
- * `SHARD` binding, a missing admin token, or a fetch failure NEVER throws
587
- * out of a lifecycle hook — the `console` path stays the source of truth.
588
- */
859
+ * Best-effort push of `envelope` into the root ShardDO's log buffer so it
860
+ * also appears in the Studio Logs panel (the terminal already has it via
861
+ * `emitContainerLifecycle`). Fire-and-forget and fully swallowed: a missing
862
+ * `SHARD` binding, a missing admin token, or a fetch failure NEVER throws
863
+ * out of a lifecycle hook — the `console` path stays the source of truth.
864
+ */
589
865
  private surfaceInStudioLogs;
590
866
  /**
591
- * Per-instance correlation id: the Durable Object id, which Cloudflare also
592
- * injects into the container as `CLOUDFLARE_DURABLE_OBJECT_ID`. Read
593
- * defensively — the id shape varies and isn't worth crashing a hook over.
594
- */
867
+ * Per-instance correlation id: the Durable Object id, which Cloudflare also
868
+ * injects into the container as `CLOUDFLARE_DURABLE_OBJECT_ID`. Read
869
+ * defensively — the id shape varies and isn't worth crashing a hook over.
870
+ */
595
871
  private instanceId;
596
872
  }
597
873
  export { ContainerProxy, LunoraContainer, type OutboundHandler, type OutboundHandlerContext, type OutboundHandlerParams, type OutboundHandlerParamsOf, type OutboundHandlers, outboundParams };