smolmachines 1.22.1 → 1.24.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.
package/dist/binding.d.ts CHANGED
@@ -265,12 +265,12 @@ export declare class NapiMachine {
265
265
  * live RAM + disks (same host). `ports` are `{ host, guest }` inbound
266
266
  * forwards for the clone. Returns a handle to the running clone.
267
267
  */
268
- fork(name: string, ports?: Array<PortMappingConfig> | undefined | null, checkpointable?: boolean | undefined | null): Promise<NapiMachine>
268
+ fork(name: string, ports?: Array<PortMappingConfig> | undefined | null, checkpointable?: boolean | undefined | null, freezeSource?: boolean | undefined | null): Promise<NapiMachine>
269
269
  /**
270
270
  * Fork many clones from one retained snapshot and boot them in bounded
271
271
  * parallel waves. Transactional: an error removes every clone in this call.
272
272
  */
273
- forkBatch(names: Array<string>, ports?: Array<PortMappingConfig> | undefined | null, parallel?: number | undefined | null): Promise<Array<NapiMachine>>
273
+ forkBatch(names: Array<string>, ports?: Array<PortMappingConfig> | undefined | null, parallel?: number | undefined | null, freezeSource?: boolean | undefined | null): Promise<Array<NapiMachine>>
274
274
  /** Execute a command directly in the VM (not in a container). */
275
275
  exec(command: Array<string>, options?: ExecOptions | undefined | null): Promise<ExecResult>
276
276
  /**
package/dist/index.d.ts CHANGED
@@ -20,5 +20,6 @@ export { Machine, Episode } from './machine';
20
20
  export { RolloutClient, RolloutError, adapterSha256 } from './rollout';
21
21
  export type { BuildRolloutJobOptions, EnsureRolloutExecutorOptions, RolloutBatchItem, RolloutClientOptions, RolloutCompletion, RolloutExecutorInfo, RolloutGenerateResponse, RolloutJob, RolloutPolicyInfo, RolloutPrompt, RolloutSampling, } from './rollout';
22
22
  export { SmolError, NotSupportedError, InvalidConfigError, ExecutionError } from './errors';
23
+ export type { CloudCheckpointInfo } from './transport';
23
24
  export type { LocalAvailability, LocalUnavailableCode } from './availability';
24
25
  export type { MachineConfig, NetworkPolicy, RestoreCheckpointOptions, MachineState, ResourceSpec, MountSpec, PortSpec, BranchOptions, BranchBatchOptions, CheckpointOptions, ResizeOptions, MachineResources, ForkOptions, ForkBatchOptions, AssignOptions, ExecOptions, ExecResult, MachineUsageReport, ShareLink, PortableCheckpointInfo, MachineUsageTotals, MachineCostBreakdown, ImageInfo, MachineSummary, ListOptions, ExecEvent, ConnectOptions, EgressInterceptor, StartOptions, WaitReadyOptions, PortEndpoint, } from './types';
package/dist/machine.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * a given backend lacks throw `NotSupportedError`.
9
9
  */
10
10
  import { type LocalAvailability } from "./availability";
11
- import { type Transport } from "./transport";
11
+ import { type CloudCheckpointInfo, type Transport } from "./transport";
12
12
  import type { MachineResources, ResizeOptions, AssignOptions, BranchBatchOptions, BranchOptions, CheckpointOptions, ConnectOptions, ExecEvent, ExecOptions, ExecResult, ForkBatchOptions, ForkOptions, ImageInfo, ListOptions, MachineConfig, MachineSummary, NetworkPolicy, RestoreCheckpointOptions, StartOptions, MachineUsageReport, ShareLink, PortableCheckpointInfo, PortEndpoint, PortSpec, WaitReadyOptions } from "./types";
13
13
  export declare class Machine {
14
14
  private readonly transport;
@@ -48,8 +48,13 @@ export declare class Machine {
48
48
  * @param options `labels`: keep machines carrying every one of these
49
49
  */
50
50
  static list(conn?: ConnectOptions, options?: ListOptions): Promise<MachineSummary[]>;
51
+ /** Upload a `.checkpoint` file taken on this computer to the cloud, so it
52
+ * can be restored there by the returned id. Restoring the file with a cloud
53
+ * target uploads it for you. */
54
+ static uploadCheckpoint(path: string, conn?: ConnectOptions, onProgress?: (sent: number, total: number) => void): Promise<CloudCheckpointInfo>;
51
55
  /** Restore a local `.smolcheckpoint` path or durable cloud checkpoint id into
52
- * a ready, immediately forkable machine. */
56
+ * a ready, immediately forkable machine. With a cloud target, a local file is
57
+ * uploaded first, so a machine checkpointed here resumes in the cloud. */
53
58
  static restoreCheckpoint(checkpointId: string, name: string, conn?: ConnectOptions, options?: RestoreCheckpointOptions): Promise<Machine>;
54
59
  /** Export a local stored checkpoint directory as one portable file. */
55
60
  static exportCheckpoint(source: string, output: string): number;
package/dist/machine.js CHANGED
@@ -100,8 +100,15 @@ class Machine {
100
100
  static async list(conn = {}, options = {}) {
101
101
  return (0, transport_1.listMachines)(conn, options);
102
102
  }
103
+ /** Upload a `.checkpoint` file taken on this computer to the cloud, so it
104
+ * can be restored there by the returned id. Restoring the file with a cloud
105
+ * target uploads it for you. */
106
+ static async uploadCheckpoint(path, conn = {}, onProgress) {
107
+ return (0, transport_1.uploadCheckpoint)(path, conn, onProgress);
108
+ }
103
109
  /** Restore a local `.smolcheckpoint` path or durable cloud checkpoint id into
104
- * a ready, immediately forkable machine. */
110
+ * a ready, immediately forkable machine. With a cloud target, a local file is
111
+ * uploaded first, so a machine checkpointed here resumes in the cloud. */
105
112
  static async restoreCheckpoint(checkpointId, name, conn, options) {
106
113
  return new Machine(await (0, transport_1.restoreCheckpointTransport)(checkpointId, name, conn, options));
107
114
  }
package/dist/native.d.ts CHANGED
@@ -118,8 +118,8 @@ export interface NapiMachine {
118
118
  startForkable(): Promise<void>;
119
119
  checkpoint(output: string, store?: string): Promise<NativeCheckpointResult>;
120
120
  resize(spec: NativeResizeConfig): Promise<NativeMachineResources>;
121
- fork(name: string, ports?: NativePortMapping[], checkpointable?: boolean): Promise<NapiMachine>;
122
- forkBatch(names: string[], ports?: NativePortMapping[], parallel?: number): Promise<NapiMachine[]>;
121
+ fork(name: string, ports?: NativePortMapping[], checkpointable?: boolean, freezeSource?: boolean): Promise<NapiMachine>;
122
+ forkBatch(names: string[], ports?: NativePortMapping[], parallel?: number, freezeSource?: boolean): Promise<NapiMachine[]>;
123
123
  exec(command: string[], options?: NativeExecOptions): Promise<NativeExecResult>;
124
124
  run(image: string, command: string[], options?: NativeExecOptions): Promise<NativeExecResult>;
125
125
  pullImage(image: string): Promise<NativeImageInfo>;
@@ -90,6 +90,12 @@ export declare function networkMode(network: boolean | undefined): {
90
90
  mode: "open" | "blocked";
91
91
  };
92
92
  } | Record<string, never>;
93
+ /** The cloud API's form of a {@link NetworkPolicy}. */
94
+ export declare function cloudNetwork(policy: NetworkPolicy): {
95
+ mode: "open" | "blocked" | "allowCidrs";
96
+ cidrs?: string[];
97
+ hosts?: string[];
98
+ };
93
99
  /** The engine's form of a {@link NetworkPolicy}. */
94
100
  export declare function egressPolicy(policy: NetworkPolicy): {
95
101
  network: boolean;
@@ -99,6 +105,16 @@ export declare function egressPolicy(policy: NetworkPolicy): {
99
105
  /** Resolve the primary branch lifecycle name and its compatibility aliases. */
100
106
  export declare function resolveBranchable(config: MachineConfig): boolean | undefined;
101
107
  export declare function toNativeConfig(name: string, config: MachineConfig): NativeMachineConfig;
108
+ interface CloudConn {
109
+ baseUrl: string;
110
+ apiKey: string;
111
+ }
112
+ /** The sentence a person should read from a failed response's body. The
113
+ * control plane answers errors as RFC 9457 problem+json, whose `detail` is the
114
+ * sentence; older builds sent `{"error": …}` or plain text. A JSON object
115
+ * yields its first string among detail, error, message and title, so a message
116
+ * never pastes JSON; anything else is returned trimmed, as before. */
117
+ export declare function errorSentence(body: string): string;
102
118
  /** Percent-encode each path segment but keep the `/` separators — the smolfleet
103
119
  * files route is a wildcard (`/files/<path>`), so slashes are meaningful while
104
120
  * spaces/?/#/% in a filename must be escaped. */
@@ -130,3 +146,18 @@ export declare function connectTransport(id: string, conn: ConnectOptions): Prom
130
146
  /** Restore a durable cloud checkpoint into a new machine, then return only once
131
147
  * the restored guest is ready. */
132
148
  export declare function restoreCheckpointTransport(checkpointId: string, name: string, conn?: ConnectOptions, options?: RestoreCheckpointOptions): Promise<Transport>;
149
+ /** Upload a checkpoint file to the cloud; see {@link uploadCheckpointFile}. */
150
+ export declare function uploadCheckpoint(path: string, conn?: ConnectOptions, onProgress?: (sent: number, total: number) => void): Promise<CloudCheckpointInfo>;
151
+ /** A checkpoint as the cloud describes it. */
152
+ export interface CloudCheckpointInfo {
153
+ id: string;
154
+ status: string;
155
+ sizeBytes: number;
156
+ arch: string;
157
+ createdAt: string;
158
+ }
159
+ /** Upload a `.checkpoint` file captured on this computer so it can be
160
+ * restored in the cloud. The bytes go straight to object storage in parallel
161
+ * parts; the cloud then reads the checkpoint's own manifest. */
162
+ export declare function uploadCheckpointFile(conn: CloudConn, path: string, onProgress?: (sent: number, total: number) => void): Promise<CloudCheckpointInfo>;
163
+ export {};
package/dist/transport.js CHANGED
@@ -11,16 +11,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.cliConfigApiKey = cliConfigApiKey;
12
12
  exports.resolveNetwork = resolveNetwork;
13
13
  exports.networkMode = networkMode;
14
+ exports.cloudNetwork = cloudNetwork;
14
15
  exports.egressPolicy = egressPolicy;
15
16
  exports.resolveBranchable = resolveBranchable;
16
17
  exports.toNativeConfig = toNativeConfig;
18
+ exports.errorSentence = errorSentence;
17
19
  exports.encodePath = encodePath;
18
20
  exports.selectsCloud = selectsCloud;
19
21
  exports.makeTransport = makeTransport;
20
22
  exports.listMachines = listMachines;
21
23
  exports.connectTransport = connectTransport;
22
24
  exports.restoreCheckpointTransport = restoreCheckpointTransport;
25
+ exports.uploadCheckpoint = uploadCheckpoint;
26
+ exports.uploadCheckpointFile = uploadCheckpointFile;
23
27
  const node_fs_1 = require("node:fs");
28
+ const node_http_1 = require("node:http");
29
+ const node_https_1 = require("node:https");
24
30
  const node_net_1 = require("node:net");
25
31
  const node_os_1 = require("node:os");
26
32
  const node_path_1 = require("node:path");
@@ -176,6 +182,15 @@ function networkMode(network) {
176
182
  return { network: { mode: "blocked" } };
177
183
  return {};
178
184
  }
185
+ /** The cloud API's form of a {@link NetworkPolicy}. */
186
+ function cloudNetwork(policy) {
187
+ if (policy === "allow-all")
188
+ return { mode: "open" };
189
+ if (policy === "deny-all")
190
+ return { mode: "blocked" };
191
+ const { cidrs, hosts } = egressPolicy(policy);
192
+ return { mode: "allowCidrs", cidrs, hosts };
193
+ }
179
194
  /** The engine's form of a {@link NetworkPolicy}. */
180
195
  function egressPolicy(policy) {
181
196
  if (policy === "allow-all")
@@ -585,7 +600,7 @@ class LocalTransport {
585
600
  }));
586
601
  let clone;
587
602
  try {
588
- clone = new LocalTransport(await this.inner.fork(name, nativePorts, opts.branchable ?? opts.checkpointable ?? false), this.handleSignals, undefined, this.handleSignals);
603
+ clone = new LocalTransport(await this.inner.fork(name, nativePorts, opts.branchable ?? opts.checkpointable ?? false, opts.freezeSource ?? false), this.handleSignals, undefined, this.handleSignals);
589
604
  await clone.waitUntilReady();
590
605
  return clone; // ctor registers for cleanup
591
606
  }
@@ -602,7 +617,7 @@ class LocalTransport {
602
617
  }));
603
618
  const branches = [];
604
619
  try {
605
- const inners = await this.inner.forkBatch(names, nativePorts, Math.min(8, names.length));
620
+ const inners = await this.inner.forkBatch(names, nativePorts, Math.min(8, names.length), opts.freezeSource ?? false);
606
621
  branches.push(...inners.map((inner) => new LocalTransport(inner, this.handleSignals, undefined, this.handleSignals)));
607
622
  for (const branch of branches) {
608
623
  await branch.waitUntilReady();
@@ -644,6 +659,29 @@ const NO_PORT_READY_PROBE_GRACE_MS = 2_000;
644
659
  * request timeout (ms), covering network round-trip and server-side overhead so
645
660
  * the client never aborts before the server has had a chance to finish. */
646
661
  const CLOUD_EXEC_TIMEOUT_HEADROOM_MS = 30_000;
662
+ /** The sentence a person should read from a failed response's body. The
663
+ * control plane answers errors as RFC 9457 problem+json, whose `detail` is the
664
+ * sentence; older builds sent `{"error": …}` or plain text. A JSON object
665
+ * yields its first string among detail, error, message and title, so a message
666
+ * never pastes JSON; anything else is returned trimmed, as before. */
667
+ function errorSentence(body) {
668
+ const trimmed = body.trim();
669
+ try {
670
+ const parsed = JSON.parse(trimmed);
671
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
672
+ const fields = parsed;
673
+ for (const key of ["detail", "error", "message", "title"]) {
674
+ const value = fields[key];
675
+ if (typeof value === "string")
676
+ return value.trim();
677
+ }
678
+ }
679
+ }
680
+ catch {
681
+ // Not JSON: the text is the message.
682
+ }
683
+ return trimmed;
684
+ }
647
685
  /** Percent-encode each path segment but keep the `/` separators — the smolfleet
648
686
  * files route is a wildcard (`/files/<path>`), so slashes are meaningful while
649
687
  * spaces/?/#/% in a filename must be escaped. */
@@ -691,7 +729,7 @@ async function cloudFetch(conn, method, path, opts = {}) {
691
729
  opts.signal?.removeEventListener("abort", onAbort);
692
730
  }
693
731
  if (!res.ok) {
694
- const text = await res.text().catch(() => "");
732
+ const text = errorSentence(await res.text().catch(() => ""));
695
733
  // Surface the server's correlation id (every response carries `x-request-id`)
696
734
  // in the error message — clients see the error body but not headers, so
697
735
  // without this the id is invisible and support can't correlate the call.
@@ -940,7 +978,7 @@ class CloudTransport {
940
978
  throw new errors_1.SmolError("CONNECTION", `cloud exec/stream failed: ${e.message}`);
941
979
  }
942
980
  if (!res.ok) {
943
- const text = await res.text().catch(() => "");
981
+ const text = errorSentence(await res.text().catch(() => ""));
944
982
  const rid = res.headers.get("x-request-id");
945
983
  throw new errors_1.SmolError(res.status === 404
946
984
  ? "NOT_FOUND"
@@ -1123,6 +1161,7 @@ class CloudTransport {
1123
1161
  name,
1124
1162
  ports: portBody,
1125
1163
  ...(branchable ? { branchable: true } : {}),
1164
+ ...(opts.freezeSource ? { freezeSource: true } : {}),
1126
1165
  },
1127
1166
  });
1128
1167
  }
@@ -1136,6 +1175,7 @@ class CloudTransport {
1136
1175
  name,
1137
1176
  ports: portBody,
1138
1177
  ...(branchable ? { forkable: true } : {}),
1178
+ ...(opts.freezeSource ? { freezeSource: true } : {}),
1139
1179
  },
1140
1180
  });
1141
1181
  }
@@ -1158,6 +1198,8 @@ class CloudTransport {
1158
1198
  body.count = opts.count;
1159
1199
  if (opts.namePrefix !== undefined)
1160
1200
  body.namePrefix = opts.namePrefix;
1201
+ if (opts.freezeSource)
1202
+ body.freezeSource = true;
1161
1203
  let resp;
1162
1204
  try {
1163
1205
  resp = await cloudFetch(this.conn, "POST", `/v1/machines/${this.id}/branches/batch`, { json: body });
@@ -1605,18 +1647,19 @@ async function restoreCheckpointTransport(checkpointId, name, conn, options) {
1605
1647
  if (options?.keepIdentity) {
1606
1648
  throw new errors_1.InvalidConfigError("keepIdentity is local-only: a cloud restore always gives the new machine its own identity.");
1607
1649
  }
1608
- const explicitKey = conn.apiKey ?? process.env.SMOL_CLOUD_TOKEN;
1609
- const { apiKey: cliKey, endpoint: cliUrl } = cliSession(conn.target);
1610
- const key = explicitKey ?? cliKey ?? cliConfigApiKey();
1611
- if (!key) {
1612
- throw new errors_1.InvalidConfigError(`restoreCheckpoint requires an API key — ${NO_KEY_HINT}.`);
1613
- }
1614
- const baseUrl = (conn.baseUrl ??
1615
- process.env.SMOL_CLOUD_URL ??
1616
- cliUrl ??
1617
- DEFAULT_CLOUD_URL).replace(/\/+$/, "");
1618
- const cloudConn = { baseUrl, apiKey: key };
1619
- const created = await cloudFetch(cloudConn, "POST", `/v1/checkpoints/${encodeURIComponent(checkpointId)}/restore`, { json: { name } });
1650
+ const cloudConn = cloudConnFor(conn, "restoreCheckpoint");
1651
+ // A checkpoint file taken on this computer is uploaded first.
1652
+ const checkpoint = isCheckpointFile(checkpointId)
1653
+ ? (await uploadCheckpointFile(cloudConn, (0, node_path_1.resolve)(checkpointId))).id
1654
+ : checkpointId;
1655
+ const created = await cloudFetch(cloudConn, "POST", `/v1/checkpoints/${encodeURIComponent(checkpoint)}/restore`, {
1656
+ json: {
1657
+ name,
1658
+ ...(options?.networkPolicy !== undefined
1659
+ ? { network: cloudNetwork(options.networkPolicy) }
1660
+ : {}),
1661
+ },
1662
+ });
1620
1663
  const id = created.id;
1621
1664
  try {
1622
1665
  await cloudFetch(cloudConn, "POST", `/v1/machines/${id}/start`, {
@@ -1630,6 +1673,105 @@ async function restoreCheckpointTransport(checkpointId, name, conn, options) {
1630
1673
  }
1631
1674
  return new CloudTransport(cloudConn, created.name ?? name, id);
1632
1675
  }
1676
+ /** The control plane and key a cloud operation uses: explicit options, then
1677
+ * the environment, then the CLI session. */
1678
+ function cloudConnFor(conn, operation) {
1679
+ const explicitKey = conn.apiKey ?? process.env.SMOL_CLOUD_TOKEN;
1680
+ const { apiKey: cliKey, endpoint: cliUrl } = cliSession(conn.target);
1681
+ const key = explicitKey ?? cliKey ?? cliConfigApiKey();
1682
+ if (!key) {
1683
+ throw new errors_1.InvalidConfigError(`${operation} requires an API key — ${NO_KEY_HINT}.`);
1684
+ }
1685
+ const baseUrl = (conn.baseUrl ??
1686
+ process.env.SMOL_CLOUD_URL ??
1687
+ cliUrl ??
1688
+ DEFAULT_CLOUD_URL).replace(/\/+$/, "");
1689
+ return { baseUrl, apiKey: key };
1690
+ }
1691
+ /** Upload a checkpoint file to the cloud; see {@link uploadCheckpointFile}. */
1692
+ function uploadCheckpoint(path, conn = {}, onProgress) {
1693
+ return uploadCheckpointFile(cloudConnFor(conn, "uploadCheckpoint"), (0, node_path_1.resolve)(path), onProgress);
1694
+ }
1695
+ function isCheckpointFile(checkpoint) {
1696
+ try {
1697
+ return (0, node_fs_1.statSync)(checkpoint).isFile();
1698
+ }
1699
+ catch {
1700
+ return false;
1701
+ }
1702
+ }
1703
+ const UPLOAD_PARALLELISM = 4;
1704
+ const UPLOAD_PART_ATTEMPTS = 4;
1705
+ /** Upload a `.checkpoint` file captured on this computer so it can be
1706
+ * restored in the cloud. The bytes go straight to object storage in parallel
1707
+ * parts; the cloud then reads the checkpoint's own manifest. */
1708
+ async function uploadCheckpointFile(conn, path, onProgress) {
1709
+ const size = (0, node_fs_1.statSync)(path).size;
1710
+ const upload = await cloudFetch(conn, "POST", "/v1/checkpoints", { json: { sizeBytes: size } });
1711
+ const sent = new Array(upload.uploadUrls.length).fill(0);
1712
+ const report = () => onProgress?.(sent.reduce((a, b) => a + b, 0), size);
1713
+ let next = 0;
1714
+ const worker = async () => {
1715
+ for (;;) {
1716
+ const part = next++;
1717
+ if (part >= upload.uploadUrls.length)
1718
+ return;
1719
+ const start = part * upload.partSizeBytes;
1720
+ const length = Math.min(upload.partSizeBytes, size - start);
1721
+ for (let attempt = 0;; attempt++) {
1722
+ try {
1723
+ await putPart(upload.uploadUrls[part], path, start, length, (n) => {
1724
+ sent[part] = n;
1725
+ report();
1726
+ });
1727
+ break;
1728
+ }
1729
+ catch (error) {
1730
+ sent[part] = 0;
1731
+ if (attempt + 1 >= UPLOAD_PART_ATTEMPTS)
1732
+ throw error;
1733
+ await new Promise((r) => setTimeout(r, 2_000 * 2 ** attempt));
1734
+ }
1735
+ }
1736
+ }
1737
+ };
1738
+ await Promise.all(Array.from({ length: Math.min(UPLOAD_PARALLELISM, upload.uploadUrls.length) }, worker));
1739
+ return cloudFetch(conn, "POST", `/v1/checkpoints/${encodeURIComponent(upload.checkpoint.id)}/complete`, { timeoutMs: 30 * 60 * 1_000 });
1740
+ }
1741
+ /** PUT one slice of a file to a pre-signed URL. */
1742
+ function putPart(url, path, start, length, onSent) {
1743
+ return new Promise((resolve, reject) => {
1744
+ const send = url.startsWith("http://") ? node_http_1.request : node_https_1.request;
1745
+ const request = send(url, {
1746
+ method: "PUT",
1747
+ headers: { "content-length": String(length) },
1748
+ });
1749
+ request.on("response", (response) => {
1750
+ const chunks = [];
1751
+ response.on("data", (chunk) => chunks.push(chunk));
1752
+ response.on("end", () => {
1753
+ const status = response.statusCode ?? 0;
1754
+ if (status >= 200 && status < 300)
1755
+ resolve();
1756
+ else
1757
+ reject(new errors_1.SmolError("CONNECTION", `upload part at byte ${start} failed: ${status} ${Buffer.concat(chunks).toString().slice(0, 300)}`));
1758
+ });
1759
+ });
1760
+ request.on("error", reject);
1761
+ if (length === 0) {
1762
+ request.end();
1763
+ return;
1764
+ }
1765
+ let sentBytes = 0;
1766
+ const stream = (0, node_fs_1.createReadStream)(path, { start, end: start + length - 1 });
1767
+ stream.on("data", (chunk) => {
1768
+ sentBytes += chunk.length;
1769
+ onSent(sentBytes);
1770
+ });
1771
+ stream.on("error", reject);
1772
+ stream.pipe(request);
1773
+ });
1774
+ }
1633
1775
  function looksLikeLocalCheckpoint(checkpoint) {
1634
1776
  return ((0, node_fs_1.existsSync)(checkpoint) ||
1635
1777
  checkpoint.endsWith(".smolcheckpoint") ||
package/dist/types.d.ts CHANGED
@@ -88,6 +88,11 @@ export interface BranchOptions {
88
88
  branchable?: boolean;
89
89
  /** @deprecated Use `branchable`. */
90
90
  checkpointable?: boolean;
91
+ /** Keep the source paused as a reusable branch base instead of resuming it.
92
+ * Later branches start from that same state, and a frozen source no longer
93
+ * counts toward the concurrency cap. The source cannot run commands again;
94
+ * work in its branches, or stop it. */
95
+ freezeSource?: boolean;
91
96
  }
92
97
  /** Backwards-compatible name for {@link BranchOptions}. */
93
98
  export type ForkOptions = BranchOptions;
@@ -104,6 +109,11 @@ export interface BranchBatchOptions {
104
109
  /** Inbound port forwards applied to every child. Empty = each child gets fresh
105
110
  * host ports so children don't collide. */
106
111
  ports?: PortSpec[];
112
+ /** Keep the source paused as a reusable branch base instead of resuming it.
113
+ * Later branches start from that same state, and a frozen source no longer
114
+ * counts toward the concurrency cap. The source cannot run commands again;
115
+ * work in its branches, or stop it. */
116
+ freezeSource?: boolean;
107
117
  }
108
118
  /** Backwards-compatible name for {@link BranchBatchOptions}. */
109
119
  export type ForkBatchOptions = BranchBatchOptions;
@@ -264,8 +274,9 @@ export type NetworkPolicy = "allow-all" | "deny-all" | {
264
274
  /** Options for `Machine.restoreCheckpoint`. */
265
275
  export interface RestoreCheckpointOptions {
266
276
  /** Apply this network policy to the restored machine before it boots.
267
- * The checkpoint must have been taken on the `"virtio-net"` backend for an
268
- * allow list or `deny-all`. Local target only. */
277
+ * Locally, the checkpoint must have been taken on the `"virtio-net"`
278
+ * backend for an allow list or `deny-all`. A cloud restore without one has
279
+ * networking blocked. */
269
280
  networkPolicy?: NetworkPolicy;
270
281
  /** Restore the machine as itself going back in time rather than as a clone:
271
282
  * it keeps the hostname and machine ID it was saved with, which skips about
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smolmachines",
3
- "version": "1.22.1",
3
+ "version": "1.24.0",
4
4
  "description": "Embed isolated microVM sandboxes directly in your code — no server to run.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "smol-machines",
@@ -62,8 +62,8 @@
62
62
  "typescript": "^5.6.0"
63
63
  },
64
64
  "optionalDependencies": {
65
- "smolmachines-darwin-arm64": "1.22.1",
66
- "smolmachines-linux-x64-gnu": "1.22.1",
67
- "smolmachines-linux-arm64-gnu": "1.22.1"
65
+ "smolmachines-darwin-arm64": "1.24.0",
66
+ "smolmachines-linux-x64-gnu": "1.24.0",
67
+ "smolmachines-linux-arm64-gnu": "1.24.0"
68
68
  }
69
69
  }