@mastra/platform-workspace 1.1.0 → 1.2.0-alpha.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # @mastra/platform
2
2
 
3
+ ## 1.2.0-alpha.1
4
+
5
+ ### Minor Changes
6
+
7
+ - Split `PlatformSandbox.stop()` from `PlatformSandbox.destroy()` so the two lifecycle exits mirror `@mastra/railway` `RailwaySandbox` ([#20956](https://github.com/mastra-ai/mastra/pull/20956))
8
+
9
+ **Before:** `stop()` was an alias for `destroy()`, and `destroy()` only released the sandbox VM — the on-provider recovery checkpoint was never actively deleted. There was no way to end a hosted sandbox while preserving its checkpoint for a later resume, and destroyed sandboxes accumulated stray checkpoints until the upstream provider's own GC.
10
+
11
+ **After:**
12
+
13
+ - **`stop()`** — releases the VM but **preserves the recovery checkpoint**. Any in-flight capture is awaited first so the preserved checkpoint reflects the caller's latest state. Corresponds to `DELETE /v1/projects/:pid/sandbox/:sandboxId` on workspace-proxy, which by contract does not touch the checkpoint.
14
+ - **`destroy()`** — releases the VM **and deletes the recovery checkpoint**. Cancels any in-flight capture (no reason to burn a capture on state we're releasing), asks the proxy to delete the checkpoint via `DELETE /v1/projects/:pid/sandbox/:sandboxId/checkpoint`, then releases the VM. Both remote operations are best-effort — an already-absent checkpoint or a transient checkpoint-delete failure does not block the VM teardown, since a half-torn-down sandbox is worse than a lingering checkpoint alone.
15
+
16
+ Callers constructed without a recovery `id` skip the checkpoint DELETE and behave identically to `stop()`, because they have no on-provider checkpoint to release.
17
+
18
+ This restores the "providers move in lockstep" invariant that broke after `@mastra/railway` gained its own `stop()`/`destroy()` split.
19
+
20
+ **Requires** a matching workspace-proxy release that exposes `DELETE /v1/projects/:pid/sandbox/:sandboxId/checkpoint`. Callers on older workspace-proxy versions will see the checkpoint DELETE 404 and fall through to the VM DELETE — same net effect as the pre-split behavior.
21
+
22
+ ### Patch Changes
23
+
24
+ - Add public `captureCheckpoint()` method to `PlatformSandbox` — mirrors `@mastra/railway`'s `RailwaySandbox.captureCheckpoint()` so callers (e.g. a factory-side scheduler) can capture the recovery checkpoint on demand at semantic moments (turn end, session-idle, pre-teardown) without having to know which provider is underneath. ([#20882](https://github.com/mastra-ai/mastra/pull/20882))
25
+
26
+ ```ts
27
+ const result = await sandbox.captureCheckpoint();
28
+ switch (result.status) {
29
+ case 'captured':
30
+ case 'coalesced':
31
+ await persistBinding({ sessionId, checkpointName: result.checkpointName });
32
+ break;
33
+ case 'skipped':
34
+ // result.reason: 'no-checkpoint-name-configured' | 'sandbox-not-running'
35
+ break;
36
+ }
37
+ ```
38
+
39
+ - POSTs to `/v1/projects/:projectId/sandbox/:sandboxId/checkpoint` with the caller-supplied recovery key (the `id` the sandbox was constructed with) as the body, matching the shape the workspace-proxy expects.
40
+ - Coalesces concurrent callers on the same instance onto a single upstream request, so N simultaneous turn-end fires do not each round-trip the proxy.
41
+ - Returns `{ status: 'skipped', reason: 'no-checkpoint-name-configured' }` when the sandbox was constructed without a caller-supplied `id` (an auto-generated random id is never a meaningful recovery key), and `{ status: 'skipped', reason: 'sandbox-not-running' }` when the sandbox has not been started yet.
42
+ - Normalizes upstream "sandbox destroyed" outcomes (a 410 from the proxy, or the proxy's own `skipped` status) to `{ status: 'skipped', reason: 'sandbox-not-running' }` — the discriminant matches the pre-flight case so callers branch uniformly, and the sandbox's local state is cleared as a side effect so the next `start()` provisions fresh instead of reattaching to a dead id.
43
+ - Transport failures other than 410 (5xx, 429) propagate as `PlatformApiError` for the caller to handle.
44
+
45
+ - Coalesce concurrent `PlatformSandbox.start()` callers onto a single in-flight attempt ([#20960](https://github.com/mastra-ai/mastra/pull/20960))
46
+
47
+ Two callers hitting `start()` on the same instance before the first one resolves used to both race to `POST /v1/projects/:pid/sandbox` (or `GET /sandbox/:id` on the reattach path), burning N proxy provisions and leaving `N-1` stray sandboxes behind. Fleet-level coalescing on the caller side masked most of this, but the underlying invariant "providers move in lockstep" was false — `@mastra/railway` `RailwaySandbox` has always had `_startInFlight` coalescing.
48
+
49
+ `start()` now publishes a single shared promise via `??=` **before** the first `await`, so a second caller entering `start()` while the first is mid-round-trip joins the existing promise instead of racing past the null check. The slot is cleared in `.finally()` on both success and failure paths so a failed attempt isn't a permanent latch — the next call starts fresh. Failures propagate to every joined caller.
50
+
51
+ Bug fix; no public API surface change. Callers already awaiting `start()` see the same success/failure semantics; the only observable difference is one upstream call instead of N.
52
+
53
+ - Updated dependencies [[`cdd5c33`](https://github.com/mastra-ai/mastra/commit/cdd5c33ac6c7118a9f139e6dc0e14e6a8ae31658), [`d7cf7fa`](https://github.com/mastra-ai/mastra/commit/d7cf7fafc1ae1b50bd8462dd0e6c671a8606db93), [`0f9a448`](https://github.com/mastra-ai/mastra/commit/0f9a448502157e59f7b76f24360ad497168f5ef8), [`289f4ce`](https://github.com/mastra-ai/mastra/commit/289f4ce16e3293370440172132c52ee787cbc09f), [`4f16ff8`](https://github.com/mastra-ai/mastra/commit/4f16ff824bf2f9b0ddc93f210477c10c8a4fb1ab), [`1c67d85`](https://github.com/mastra-ai/mastra/commit/1c67d85e9da8285662f4dbbf47e0378c3fee0747), [`ba24be6`](https://github.com/mastra-ai/mastra/commit/ba24be662439c331ab23a600041f93803c89eca8), [`842b5fe`](https://github.com/mastra-ai/mastra/commit/842b5fe22b6a7fa811bd14e48eb9af523ac989f2), [`80bdf3a`](https://github.com/mastra-ai/mastra/commit/80bdf3ae16ade6ff63bde0cb16fa2df8ab7dd4dd), [`9ba1247`](https://github.com/mastra-ai/mastra/commit/9ba12470c77f1c03642d720ce67e517e878f666e), [`fd96298`](https://github.com/mastra-ai/mastra/commit/fd96298a8367622f4ebfcaa97b5b6c1fbbd14564), [`6a84954`](https://github.com/mastra-ai/mastra/commit/6a84954a2667f85b6d59da652dab1bbff007ccb0), [`52d8ef0`](https://github.com/mastra-ai/mastra/commit/52d8ef03801f1deb7ee48532fc4190dd4a33916c), [`cdd5c33`](https://github.com/mastra-ai/mastra/commit/cdd5c33ac6c7118a9f139e6dc0e14e6a8ae31658), [`efd5c81`](https://github.com/mastra-ai/mastra/commit/efd5c81cc25fde3c2ddd86fc1178deb4ec176e19), [`0976933`](https://github.com/mastra-ai/mastra/commit/0976933142333ec78451feef265b68bcb45aa5e7), [`242b945`](https://github.com/mastra-ai/mastra/commit/242b94558777bfbdeb42cbfea84afff0b6ad0633), [`fea5cae`](https://github.com/mastra-ai/mastra/commit/fea5caedc7e2cfea51784a15e015952692027abf), [`4b59f78`](https://github.com/mastra-ai/mastra/commit/4b59f786cbc9a7d1ef07a07517dbd4b96865e99d), [`7010c5d`](https://github.com/mastra-ai/mastra/commit/7010c5d15728bf9c5dfe4fb6b1bf80ce23bf143a)]:
54
+ - @mastra/core@1.58.0-alpha.3
55
+
56
+ ## 1.1.1-alpha.0
57
+
58
+ ### Patch Changes
59
+
60
+ - Improved `PlatformSandbox.getInfo()` to return cached sandbox information when the sandbox is known to be directly reachable, removing a redundant network round-trip on every workspace status poll. When no cached address is available, `getInfo()` behaves exactly as before. ([#20855](https://github.com/mastra-ai/mastra/pull/20855))
61
+
62
+ - Updated dependencies [[`e7109ee`](https://github.com/mastra-ai/mastra/commit/e7109ee6f731bacc79c885906f3c7dca8d8f013a), [`772c0c8`](https://github.com/mastra-ai/mastra/commit/772c0c897cec383258de2e6178147f8014767c7b), [`578bf2e`](https://github.com/mastra-ai/mastra/commit/578bf2e6a88e9d5b8bf502204e15a95dfbb679ae), [`06b2d87`](https://github.com/mastra-ai/mastra/commit/06b2d87e63bcdd0ed59215c6789692b9b12de376), [`ac01d63`](https://github.com/mastra-ai/mastra/commit/ac01d6355974aec73fdb8781449ed12bac582094), [`a810a05`](https://github.com/mastra-ai/mastra/commit/a810a058f62ad407cfc1701e0be36ae91145d7cf), [`f8da216`](https://github.com/mastra-ai/mastra/commit/f8da21633e7eb0e31c9ce0fc30567870d19416d3), [`6104347`](https://github.com/mastra-ai/mastra/commit/61043473ba6bfd0a25156824e853e13165562e6c), [`45bfb88`](https://github.com/mastra-ai/mastra/commit/45bfb88fd52f1dd3be20e2a38905777c96499c90), [`e3b9307`](https://github.com/mastra-ai/mastra/commit/e3b9307098daefbfae2a52ae2ef51bc9fc701190), [`d6834c5`](https://github.com/mastra-ai/mastra/commit/d6834c5a7866b16734d23900163c2414ed70d791), [`c52d346`](https://github.com/mastra-ai/mastra/commit/c52d3462ec831a5d95926ecd3d3373f5928ad2e5), [`0023e79`](https://github.com/mastra-ai/mastra/commit/0023e7919431078280abd11c89d1edeae35fcc69), [`c2ad51e`](https://github.com/mastra-ai/mastra/commit/c2ad51e2467f901eecba8c9f4a45e22a50bd7c18), [`3dc97ea`](https://github.com/mastra-ai/mastra/commit/3dc97ea415fad353b48a13095fad1835933cc12a), [`3d01cd3`](https://github.com/mastra-ai/mastra/commit/3d01cd387321b6f9c5cac31d487c84bf51b19c78), [`7bf3086`](https://github.com/mastra-ai/mastra/commit/7bf308663f0115ca74ad20554ade740f06640859), [`a8dd139`](https://github.com/mastra-ai/mastra/commit/a8dd1391a9fe9a6632c25809ef236980afa9a020), [`e5786be`](https://github.com/mastra-ai/mastra/commit/e5786be02bb903073082bd9d6da880ebaacc343f), [`2093fbd`](https://github.com/mastra-ai/mastra/commit/2093fbd53bb744bae19ec89f6d73db9a66fbe8a7), [`e7a5da4`](https://github.com/mastra-ai/mastra/commit/e7a5da4ef8e4dd452d2f232961b4e682a85ffe43), [`7b4393d`](https://github.com/mastra-ai/mastra/commit/7b4393d557411fdcf07b0e30e5acaf7cc85154ae)]:
63
+ - @mastra/core@1.58.0-alpha.1
64
+
3
65
  ## 1.1.0
4
66
 
5
67
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -858,12 +858,42 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
858
858
  * Cleared (regardless of success or failure) when the request settles.
859
859
  */
860
860
  _leaseInFlight = null;
861
+ /**
862
+ * True when this sandbox was constructed with a caller-supplied `id` (the
863
+ * recovery key the proxy hashes into an on-provider checkpoint name).
864
+ * `captureCheckpoint()` needs this to distinguish "no checkpoint intent"
865
+ * (auto-generated random id — capture would land under a name no future
866
+ * boot would look for) from "capture on demand". Cloned sandboxes route
867
+ * `checkpointName` through `id`, so both entry points set this the same
868
+ * way.
869
+ */
870
+ _hasRecoveryKey;
871
+ /**
872
+ * In-flight `captureCheckpoint()` request. Concurrent callers on the same
873
+ * instance coalesce onto this single promise so we don't burn N `POST
874
+ * /checkpoint` round-trips when the fleet fires several turn-end captures
875
+ * before the first one resolves. Cleared when the request settles.
876
+ */
877
+ _captureInFlight = null;
878
+ /**
879
+ * In-flight `start()` attempt. Concurrent callers on a fresh instance
880
+ * coalesce onto this single promise so a `POST /sandbox` is not fired
881
+ * N times when N fleet callers race to bring the same logical sandbox
882
+ * up. Published **synchronously** with `??=` before the first `await`
883
+ * so a later caller cannot slip through the null check while the
884
+ * originator is mid-round-trip. Cleared when the shared attempt
885
+ * settles (success or failure) so the next call sees a clean slot.
886
+ *
887
+ * Mirrors OSS `@mastra/railway` `RailwaySandbox._startInFlight`.
888
+ */
889
+ _startInFlight = null;
861
890
  constructor(options = {}) {
862
891
  super({
863
892
  ...options,
864
893
  name: "PlatformSandbox",
865
894
  processes: new PlatformProcessManager()
866
895
  });
896
+ this._hasRecoveryKey = options.id !== void 0;
867
897
  this.id = options.id ?? this.generateId();
868
898
  this._client = new PlatformClient(options);
869
899
  this._environmentId = options.environmentId ?? process.env.MASTRA_ENVIRONMENT_ID ?? "";
@@ -913,6 +943,21 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
913
943
  });
914
944
  }
915
945
  async start() {
946
+ this._startInFlight ??= this._doStart().finally(() => {
947
+ this._startInFlight = null;
948
+ });
949
+ return this._startInFlight;
950
+ }
951
+ /**
952
+ * The single `start` attempt behind {@link start}'s coalescing wrapper.
953
+ *
954
+ * Split out so the wrapper can install a shared in-flight promise
955
+ * synchronously (before the first `await`) without inlining the reattach
956
+ * / retry logic. Joined callers observe whatever outcome this method
957
+ * produces — success returns normally, failures propagate to every
958
+ * awaiter.
959
+ */
960
+ async _doStart() {
916
961
  if (this._sandboxId) try {
917
962
  const json = await (await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`)).json();
918
963
  if (!json.destroyedAt) {
@@ -971,10 +1016,67 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
971
1016
  if (!json.instanceUrl) return;
972
1017
  this._addressRegistry.set(json.id, json.instanceUrl);
973
1018
  }
1019
+ /**
1020
+ * Stop the sandbox while **preserving its recovery checkpoint**.
1021
+ *
1022
+ * Semantic parity with `@mastra/railway` `RailwaySandbox.stop()`: the VM
1023
+ * is released but the on-provider checkpoint survives, so a subsequent
1024
+ * `start()` on a sandbox constructed with the same `id` can restore from
1025
+ * it. Any in-flight capture is awaited first so the preserved checkpoint
1026
+ * reflects the latest disk state we asked for.
1027
+ *
1028
+ * Corresponds to `DELETE /v1/projects/:pid/sandbox/:sandboxId` on
1029
+ * workspace-proxy, which by contract does not touch the checkpoint. Use
1030
+ * {@link destroy} when you want the checkpoint released too.
1031
+ */
974
1032
  async stop() {
975
- await this.destroy();
1033
+ if (this._captureInFlight) await this._captureInFlight.catch((error) => {
1034
+ this.logger.warn(`stop(): failed to flush in-flight capture before teardown:`, error);
1035
+ });
1036
+ await this._teardownSandbox();
976
1037
  }
1038
+ /**
1039
+ * Destroy the sandbox **and release its recovery checkpoint**.
1040
+ *
1041
+ * Semantic parity with `@mastra/railway` `RailwaySandbox.destroy()`:
1042
+ * cancels any in-flight capture (the checkpoint is about to be deleted
1043
+ * — no reason to burn a capture on state we're releasing), asks the
1044
+ * proxy to delete the checkpoint, then releases the VM. Both remote
1045
+ * operations are best-effort logged failures — a stray checkpoint or a
1046
+ * transient proxy error must not leave the caller with a half-torn-down
1047
+ * sandbox they can't safely retry.
1048
+ *
1049
+ * Requires the caller to have constructed with a recovery `id` (there is
1050
+ * no checkpoint to delete otherwise); callers without one skip the
1051
+ * checkpoint DELETE and behave identically to {@link stop}.
1052
+ */
977
1053
  async destroy() {
1054
+ if (!this._sandboxId) return;
1055
+ const destroyedSandboxId = this._sandboxId;
1056
+ this._captureInFlight = null;
1057
+ if (this._hasRecoveryKey) try {
1058
+ await this._client.request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}/checkpoint`, {
1059
+ method: "DELETE",
1060
+ headers: { "content-type": "application/json" },
1061
+ body: JSON.stringify({ id: this.id })
1062
+ });
1063
+ } catch (error) {
1064
+ if (error instanceof PlatformApiError && (error.status === 404 || error.status === 410)) this.logger.debug(`destroy(): checkpoint already absent upstream (status=${error.status})`);
1065
+ else this.logger.warn(`destroy(): failed to delete checkpoint upstream:`, error);
1066
+ }
1067
+ await this._teardownSandbox();
1068
+ }
1069
+ /**
1070
+ * Release the remote sandbox VM and clear the local state pointing at it.
1071
+ *
1072
+ * Shared body of {@link stop} and {@link destroy} — both funnel through
1073
+ * here after they've dealt with the checkpoint (preserve vs release).
1074
+ * The VM DELETE is safe to issue in either mode: the proxy's DELETE
1075
+ * route does not touch the checkpoint on its own, so `stop()` correctly
1076
+ * leaves the checkpoint intact and `destroy()` has already removed it
1077
+ * before this call.
1078
+ */
1079
+ async _teardownSandbox() {
978
1080
  if (!this._sandboxId) return;
979
1081
  const destroyedSandboxId = this._sandboxId;
980
1082
  await this._client.request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}`, { method: "DELETE" });
@@ -984,6 +1086,131 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
984
1086
  this._addressRegistry?.delete(destroyedSandboxId);
985
1087
  }
986
1088
  /**
1089
+ * Capture the sandbox's checkpoint on demand, outside any refresh timer the
1090
+ * workspace-proxy owns internally.
1091
+ *
1092
+ * Intended for callers (e.g. a factory-side scheduler) that want to refresh
1093
+ * the recovery checkpoint at semantic moments — turn end, session-idle,
1094
+ * pre-teardown — rather than only just before the upstream's idle destroy.
1095
+ *
1096
+ * Mirrors the OSS `@mastra/railway` `RailwaySandbox.captureCheckpoint()`
1097
+ * shape so factory can call `sandbox.captureCheckpoint()` uniformly and
1098
+ * branch on `status`/`reason` without knowing which provider is underneath.
1099
+ * Both `captured` and `coalesced` carry the checkpoint name inline so the
1100
+ * caller can persist a session→checkpoint binding atomically with the
1101
+ * awaited capture.
1102
+ *
1103
+ * Skip semantics:
1104
+ * - No caller-supplied `id`: returns `{ status: 'skipped', reason:
1105
+ * 'no-checkpoint-name-configured' }`. An auto-generated random id is
1106
+ * never a meaningful recovery key (no future boot would look for a
1107
+ * checkpoint under it), so capturing would silently produce dead data.
1108
+ * - Not started (no `_sandboxId`): returns `{ status: 'skipped', reason:
1109
+ * 'sandbox-not-running' }` without a round-trip.
1110
+ * - Upstream 410 (workspace-proxy or Railway reports the sandbox is
1111
+ * already destroyed): returns the same `sandbox-not-running` skip so
1112
+ * the discriminant matches the pre-flight case. Local state
1113
+ * (`_sandboxId`, `_lease`, sidecar address) is cleared as a side
1114
+ * effect so the next `start()` provisions fresh instead of reattaching
1115
+ * to a dead id. The diagnostic distinction (pre-flight vs post-hoc)
1116
+ * is preserved in log level: debug for the expected pre-flight skip,
1117
+ * warn for the surprise upstream destroy.
1118
+ *
1119
+ * Concurrent callers on the same instance coalesce onto a single in-flight
1120
+ * `POST /checkpoint` so N simultaneous turn-end fires (e.g. several tabs)
1121
+ * do not each round-trip the proxy. Both the originator and joiners
1122
+ * receive `{ status: 'coalesced', ... }` for the joined result — the
1123
+ * outer contract does not distinguish who started the request, only that
1124
+ * one upstream capture was made.
1125
+ *
1126
+ * Never throws for expected outcomes. Transport failures (5xx, 4xx other
1127
+ * than 410) propagate as {@link PlatformApiError}; a 410 is normalized
1128
+ * to a skip as described above.
1129
+ */
1130
+ async captureCheckpoint() {
1131
+ if (!this._hasRecoveryKey) {
1132
+ this.logger.debug(`captureCheckpoint skipped: no recovery key configured for sandbox ${this._sandboxId ?? "(unstarted)"}`);
1133
+ return {
1134
+ status: "skipped",
1135
+ reason: "no-checkpoint-name-configured"
1136
+ };
1137
+ }
1138
+ if (!this._sandboxId) {
1139
+ this.logger.debug(`captureCheckpoint skipped: sandbox not running (local pre-flight, id=${this.id})`);
1140
+ return {
1141
+ status: "skipped",
1142
+ reason: "sandbox-not-running"
1143
+ };
1144
+ }
1145
+ if (this._captureInFlight) return this._captureInFlight;
1146
+ const sandboxId = this._sandboxId;
1147
+ const capture = this._doCaptureCheckpoint(sandboxId).finally(() => {
1148
+ if (this._captureInFlight === capture) this._captureInFlight = null;
1149
+ });
1150
+ this._captureInFlight = capture;
1151
+ return capture;
1152
+ }
1153
+ /**
1154
+ * The single `POST /checkpoint` attempt behind {@link captureCheckpoint}.
1155
+ *
1156
+ * Split out so the coalescing wrapper can install a shared in-flight
1157
+ * promise without inlining the transport + response-mapping logic.
1158
+ * Joined callers observe `{ status: 'coalesced', ... }` — the initiator
1159
+ * sees the underlying `captured` / `coalesced` / `skipped` result the
1160
+ * proxy returned. Both are legitimate: the OSS mirror uses the same
1161
+ * "initiator sees the truth, joiners see coalesced" split.
1162
+ */
1163
+ async _doCaptureCheckpoint(sandboxId) {
1164
+ let response;
1165
+ try {
1166
+ response = await this._client.request(`/sandbox/${encodeURIComponent(sandboxId)}/checkpoint`, {
1167
+ method: "POST",
1168
+ headers: { "content-type": "application/json" },
1169
+ body: JSON.stringify({ id: this.id })
1170
+ });
1171
+ } catch (error) {
1172
+ if (error instanceof PlatformApiError && error.status === 410) {
1173
+ this.logger.warn(`captureCheckpoint skipped: sandbox destroyed upstream (proxy 410, sandboxId=${sandboxId})`);
1174
+ this._clearDestroyedState(sandboxId);
1175
+ return {
1176
+ status: "skipped",
1177
+ reason: "sandbox-not-running"
1178
+ };
1179
+ }
1180
+ throw error;
1181
+ }
1182
+ const json = await response.json();
1183
+ if (json.status === "skipped") {
1184
+ this.logger.warn(`captureCheckpoint skipped: sandbox destroyed upstream (proxy reported skipped, sandboxId=${sandboxId})`);
1185
+ this._clearDestroyedState(sandboxId);
1186
+ return {
1187
+ status: "skipped",
1188
+ reason: "sandbox-not-running"
1189
+ };
1190
+ }
1191
+ return {
1192
+ status: json.status,
1193
+ checkpointName: json.checkpointName
1194
+ };
1195
+ }
1196
+ /**
1197
+ * Clear local state that would otherwise let the caller keep exec'ing
1198
+ * against a sandbox the upstream has already destroyed. Mirrors what
1199
+ * `destroy()` does minus the outbound DELETE — the sandbox is already
1200
+ * gone, so all that remains is to stop pointing at it.
1201
+ *
1202
+ * Also resets `status` to `'pending'` so a subsequent `_start()` on this
1203
+ * reused instance re-runs provisioning instead of short-circuiting on
1204
+ * the cached `'running'` state (see `MastraSandbox._start`).
1205
+ */
1206
+ _clearDestroyedState(destroyedSandboxId) {
1207
+ this._sandboxId = void 0;
1208
+ this._createdAt = null;
1209
+ this._lease = null;
1210
+ this._addressRegistry?.delete(destroyedSandboxId);
1211
+ this.status = "pending";
1212
+ }
1213
+ /**
987
1214
  * Execute a command on the remote sandbox.
988
1215
  *
989
1216
  * `command` is a **shell string**: it is concatenated verbatim into the
@@ -1193,6 +1420,14 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1193
1420
  status: this.status,
1194
1421
  createdAt: this._createdAt ?? /* @__PURE__ */ new Date()
1195
1422
  };
1423
+ if (this._addressRegistry?.get(this._sandboxId)) return {
1424
+ id: this._sandboxId,
1425
+ name: this.name,
1426
+ provider: this.provider,
1427
+ status: this.status,
1428
+ createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
1429
+ metadata: { sandboxId: this._sandboxId }
1430
+ };
1196
1431
  const json = await (await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`)).json();
1197
1432
  return {
1198
1433
  id: json.id,