@mastra/platform-workspace 1.1.1-alpha.0 → 1.2.0-alpha.2
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 +83 -0
- package/dist/client.d.ts +19 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/index.cjs +377 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +377 -3
- package/dist/index.js.map +1 -1
- package/dist/sandbox.d.ts +228 -0
- package/dist/sandbox.d.ts.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,88 @@
|
|
|
1
1
|
# @mastra/platform
|
|
2
2
|
|
|
3
|
+
## 1.2.0-alpha.2
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Added startup observability to `PlatformSandbox`. New optional `sessionId` and `threadId` options let you correlate all sandbox startup activity with the session that triggered it, and the sandbox now logs how long startup took and whether it became reachable. ([#21189](https://github.com/mastra-ai/mastra/pull/21189))
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { PlatformSandbox } from '@mastra/platform-workspace';
|
|
11
|
+
|
|
12
|
+
const sandbox = new PlatformSandbox({
|
|
13
|
+
projectId: 'proj_123',
|
|
14
|
+
environmentId: 'env_123',
|
|
15
|
+
sessionId: 'session_abc', // correlate startup logs with your session
|
|
16
|
+
threadId: 'thread_xyz', // optional finer-grained correlation
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- Added `PlatformSandbox.snapshot()` to capture the configured recovery checkpoint. ([#21221](https://github.com/mastra-ai/mastra/pull/21221))
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
await sandbox.snapshot();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- Fixed Platform Sandbox startup so commands use a reliable connection while a new sandbox is starting. ([#21028](https://github.com/mastra-ai/mastra/pull/21028))
|
|
29
|
+
|
|
30
|
+
- Updated dependencies [[`9571e3a`](https://github.com/mastra-ai/mastra/commit/9571e3a06ed2c5220196460bf82a2129255c3a8b), [`d6c56f9`](https://github.com/mastra-ai/mastra/commit/d6c56f951db3213330b98b0abafa9778c8770e58), [`9571e3a`](https://github.com/mastra-ai/mastra/commit/9571e3a06ed2c5220196460bf82a2129255c3a8b), [`acc3513`](https://github.com/mastra-ai/mastra/commit/acc3513b19f79bf0a7ec2998694580edca54086c), [`94e7ae9`](https://github.com/mastra-ai/mastra/commit/94e7ae970b37c888cd1244ef013292639a2fe6d1), [`6a667b4`](https://github.com/mastra-ai/mastra/commit/6a667b4b7cd6a93fe41fcdd357b08c5a8c09b9ab), [`2440e09`](https://github.com/mastra-ai/mastra/commit/2440e096ea6c2def1ccc1eb2d0f3f5b88c4af940), [`a59049b`](https://github.com/mastra-ai/mastra/commit/a59049b1652a13efff66ac826326b5ed9a550342)]:
|
|
31
|
+
- @mastra/core@1.58.0-alpha.13
|
|
32
|
+
|
|
33
|
+
## 1.2.0-alpha.1
|
|
34
|
+
|
|
35
|
+
### Minor Changes
|
|
36
|
+
|
|
37
|
+
- Split `PlatformSandbox.stop()` from `PlatformSandbox.destroy()` so the two lifecycle exits mirror `@mastra/railway` `RailwaySandbox` ([#20956](https://github.com/mastra-ai/mastra/pull/20956))
|
|
38
|
+
|
|
39
|
+
**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.
|
|
40
|
+
|
|
41
|
+
**After:**
|
|
42
|
+
|
|
43
|
+
- **`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.
|
|
44
|
+
- **`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.
|
|
45
|
+
|
|
46
|
+
Callers constructed without a recovery `id` skip the checkpoint DELETE and behave identically to `stop()`, because they have no on-provider checkpoint to release.
|
|
47
|
+
|
|
48
|
+
This restores the "providers move in lockstep" invariant that broke after `@mastra/railway` gained its own `stop()`/`destroy()` split.
|
|
49
|
+
|
|
50
|
+
**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.
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- 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))
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const result = await sandbox.captureCheckpoint();
|
|
58
|
+
switch (result.status) {
|
|
59
|
+
case 'captured':
|
|
60
|
+
case 'coalesced':
|
|
61
|
+
await persistBinding({ sessionId, checkpointName: result.checkpointName });
|
|
62
|
+
break;
|
|
63
|
+
case 'skipped':
|
|
64
|
+
// result.reason: 'no-checkpoint-name-configured' | 'sandbox-not-running'
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- 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.
|
|
70
|
+
- 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.
|
|
71
|
+
- 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.
|
|
72
|
+
- 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.
|
|
73
|
+
- Transport failures other than 410 (5xx, 429) propagate as `PlatformApiError` for the caller to handle.
|
|
74
|
+
|
|
75
|
+
- Coalesce concurrent `PlatformSandbox.start()` callers onto a single in-flight attempt ([#20960](https://github.com/mastra-ai/mastra/pull/20960))
|
|
76
|
+
|
|
77
|
+
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.
|
|
78
|
+
|
|
79
|
+
`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.
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
- 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)]:
|
|
84
|
+
- @mastra/core@1.58.0-alpha.3
|
|
85
|
+
|
|
3
86
|
## 1.1.1-alpha.0
|
|
4
87
|
|
|
5
88
|
### Patch Changes
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
export interface PlatformClientOptions {
|
|
2
2
|
accessToken?: string;
|
|
3
3
|
projectId?: string;
|
|
4
|
+
/**
|
|
5
|
+
* Advisory correlation id for the factory session driving this client.
|
|
6
|
+
* Sent as `x-mastra-session-id` on every proxy request so proxy-side logs
|
|
7
|
+
* can be joined back to the calling session without a multi-store hand-join
|
|
8
|
+
* (`threadId → sessionId → sandboxId → providerResourceId`). Never used for
|
|
9
|
+
* authorization — the Bearer token remains the only credential.
|
|
10
|
+
*/
|
|
11
|
+
sessionId?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Advisory correlation id for the factory thread, sent as
|
|
14
|
+
* `x-mastra-thread-id` when present. See {@link PlatformClientOptions.sessionId}.
|
|
15
|
+
*/
|
|
16
|
+
threadId?: string;
|
|
4
17
|
fetch?: typeof fetch;
|
|
5
18
|
}
|
|
6
19
|
export interface PlatformRequestOptions extends RequestInit {
|
|
@@ -11,6 +24,8 @@ export declare function resolvePlatformOptions(options: PlatformClientOptions):
|
|
|
11
24
|
accessToken: string;
|
|
12
25
|
projectId: string;
|
|
13
26
|
proxyUrl: string;
|
|
27
|
+
sessionId: string | undefined;
|
|
28
|
+
threadId: string | undefined;
|
|
14
29
|
fetch: typeof fetch;
|
|
15
30
|
};
|
|
16
31
|
/**
|
|
@@ -37,6 +52,10 @@ export declare class PlatformClient {
|
|
|
37
52
|
readonly accessToken: string;
|
|
38
53
|
readonly projectId: string;
|
|
39
54
|
readonly proxyUrl: string;
|
|
55
|
+
/** Advisory session correlation id — see {@link PlatformClientOptions.sessionId}. */
|
|
56
|
+
readonly sessionId: string | undefined;
|
|
57
|
+
/** Advisory thread correlation id — see {@link PlatformClientOptions.threadId}. */
|
|
58
|
+
readonly threadId: string | undefined;
|
|
40
59
|
readonly fetch: typeof fetch;
|
|
41
60
|
constructor(options: PlatformClientOptions);
|
|
42
61
|
request(path: string, options?: PlatformRequestOptions): Promise<Response>;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,sBAAuB,SAAQ,WAAW;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;CAC/D;AAWD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7E;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,qBAAqB
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,sBAAuB,SAAQ,WAAW;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;CAC/D;AAWD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7E;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,qBAAqB;;;;;;;EASpE;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,gGAAgG;IAChG,IAAI,EAAE,MAAM,CAAC;CACd;AAkBD,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2HAA2H;IAC3H,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,yGAAyG;IACzG,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;gBAE9B,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAUzC;AAED,qBAAa,cAAc;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qFAAqF;IACrF,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC;gBAEjB,OAAO,EAAE,qBAAqB;IAUpC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,QAAQ,CAAC;CA0BrF"}
|
package/dist/index.cjs
CHANGED
|
@@ -42,6 +42,8 @@ function resolvePlatformOptions(options) {
|
|
|
42
42
|
accessToken: requireOption(options.accessToken ?? process.env.MASTRA_PLATFORM_ACCESS_TOKEN, "accessToken"),
|
|
43
43
|
projectId: requireOption(options.projectId ?? process.env.MASTRA_PROJECT_ID, "projectId"),
|
|
44
44
|
proxyUrl: (process.env.MASTRA_WORKSPACE_PROXY_URL ?? DEFAULT_PROXY_URL).replace(/\/$/, ""),
|
|
45
|
+
sessionId: options.sessionId,
|
|
46
|
+
threadId: options.threadId,
|
|
45
47
|
fetch: options.fetch ?? fetch
|
|
46
48
|
};
|
|
47
49
|
}
|
|
@@ -85,12 +87,18 @@ var PlatformClient = class {
|
|
|
85
87
|
accessToken;
|
|
86
88
|
projectId;
|
|
87
89
|
proxyUrl;
|
|
90
|
+
/** Advisory session correlation id — see {@link PlatformClientOptions.sessionId}. */
|
|
91
|
+
sessionId;
|
|
92
|
+
/** Advisory thread correlation id — see {@link PlatformClientOptions.threadId}. */
|
|
93
|
+
threadId;
|
|
88
94
|
fetch;
|
|
89
95
|
constructor(options) {
|
|
90
96
|
const resolved = resolvePlatformOptions(options);
|
|
91
97
|
this.accessToken = resolved.accessToken;
|
|
92
98
|
this.projectId = resolved.projectId;
|
|
93
99
|
this.proxyUrl = resolved.proxyUrl;
|
|
100
|
+
this.sessionId = resolved.sessionId;
|
|
101
|
+
this.threadId = resolved.threadId;
|
|
94
102
|
this.fetch = resolved.fetch;
|
|
95
103
|
}
|
|
96
104
|
async request(path, options = {}) {
|
|
@@ -98,6 +106,8 @@ var PlatformClient = class {
|
|
|
98
106
|
for (const [key, value] of Object.entries(options.query ?? {})) if (value !== void 0) url.searchParams.set(key, String(value));
|
|
99
107
|
const headers = new Headers(options.headers);
|
|
100
108
|
headers.set("authorization", `Bearer ${this.accessToken}`);
|
|
109
|
+
if (this.sessionId) headers.set("x-mastra-session-id", this.sessionId);
|
|
110
|
+
if (this.threadId) headers.set("x-mastra-thread-id", this.threadId);
|
|
101
111
|
const { query: _query, ...fetchOptions } = options;
|
|
102
112
|
const signal = fetchOptions.signal ?? AbortSignal.timeout(DEFAULT_REQUEST_TIMEOUT_MS);
|
|
103
113
|
const response = await this.fetch(url, {
|
|
@@ -693,6 +703,24 @@ const CREATE_MAX_ATTEMPTS = 3;
|
|
|
693
703
|
/** Base delay between create retries; multiplied by the attempt number. */
|
|
694
704
|
const CREATE_RETRY_BASE_DELAY_MS = 2e3;
|
|
695
705
|
/**
|
|
706
|
+
* How long to wait for the in-sandbox sidecar's `/health` endpoint to respond
|
|
707
|
+
* before giving up and leaving the address registry unpopulated (execs fall
|
|
708
|
+
* back to the lease path). This bounds the fire-and-forget probe that runs
|
|
709
|
+
* after `start()` resolves; the sandbox is usable immediately — the probe
|
|
710
|
+
* only controls whether early execs go via private-net or lease.
|
|
711
|
+
*/
|
|
712
|
+
const SIDECAR_PROBE_TIMEOUT_MS = 3e4;
|
|
713
|
+
/** Delay between sidecar probe attempts. */
|
|
714
|
+
const SIDECAR_PROBE_INTERVAL_MS = 250;
|
|
715
|
+
/**
|
|
716
|
+
* How long `executeCommand` waits for the transport to become ready before
|
|
717
|
+
* falling back to the lease path. This is much shorter than
|
|
718
|
+
* `SIDECAR_PROBE_TIMEOUT_MS` because we want execs to proceed quickly if
|
|
719
|
+
* the sidecar is slow to boot — the probe continues in the background and
|
|
720
|
+
* later execs will use private-net once it succeeds.
|
|
721
|
+
*/
|
|
722
|
+
const TRANSPORT_READY_WAIT_MS = 5e3;
|
|
723
|
+
/**
|
|
696
724
|
* Diagnostic error thrown when the direct-exec WebSocket transport fails
|
|
697
725
|
* twice in a row (opening handshake refused or socket closed mid-stream
|
|
698
726
|
* without an `exit` frame). Distinguishes "the sandbox transport is broken"
|
|
@@ -858,12 +886,58 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
858
886
|
* Cleared (regardless of success or failure) when the request settles.
|
|
859
887
|
*/
|
|
860
888
|
_leaseInFlight = null;
|
|
889
|
+
/**
|
|
890
|
+
* True when this sandbox was constructed with a caller-supplied `id` (the
|
|
891
|
+
* recovery key the proxy hashes into an on-provider checkpoint name).
|
|
892
|
+
* `captureCheckpoint()` needs this to distinguish "no checkpoint intent"
|
|
893
|
+
* (auto-generated random id — capture would land under a name no future
|
|
894
|
+
* boot would look for) from "capture on demand". Cloned sandboxes route
|
|
895
|
+
* `checkpointName` through `id`, so both entry points set this the same
|
|
896
|
+
* way.
|
|
897
|
+
*/
|
|
898
|
+
_hasRecoveryKey;
|
|
899
|
+
/**
|
|
900
|
+
* In-flight `captureCheckpoint()` request. Concurrent callers on the same
|
|
901
|
+
* instance coalesce onto this single promise so we don't burn N `POST
|
|
902
|
+
* /checkpoint` round-trips when the fleet fires several turn-end captures
|
|
903
|
+
* before the first one resolves. Cleared when the request settles.
|
|
904
|
+
*/
|
|
905
|
+
_captureInFlight = null;
|
|
906
|
+
/**
|
|
907
|
+
* In-flight `start()` attempt. Concurrent callers on a fresh instance
|
|
908
|
+
* coalesce onto this single promise so a `POST /sandbox` is not fired
|
|
909
|
+
* N times when N fleet callers race to bring the same logical sandbox
|
|
910
|
+
* up. Published **synchronously** with `??=` before the first `await`
|
|
911
|
+
* so a later caller cannot slip through the null check while the
|
|
912
|
+
* originator is mid-round-trip. Cleared when the shared attempt
|
|
913
|
+
* settles (success or failure) so the next call sees a clean slot.
|
|
914
|
+
*
|
|
915
|
+
* Mirrors OSS `@mastra/railway` `RailwaySandbox._startInFlight`.
|
|
916
|
+
*/
|
|
917
|
+
_startInFlight = null;
|
|
918
|
+
/**
|
|
919
|
+
* Generation token for the sidecar probe. Incremented on every `start()`
|
|
920
|
+
* and on teardown. The probe captures this value when it begins; if the
|
|
921
|
+
* generation has changed by the time the probe succeeds, the probe skips
|
|
922
|
+
* the `set()` to avoid re-populating a deleted or superseded sandbox entry.
|
|
923
|
+
*/
|
|
924
|
+
_probeGeneration = 0;
|
|
925
|
+
/**
|
|
926
|
+
* In-flight sidecar probe promise. Concurrent `executeCommand` callers that
|
|
927
|
+
* arrive before the registry is populated all await this single promise so
|
|
928
|
+
* we don't fire N independent lease requests during the sidecar boot window.
|
|
929
|
+
* Once the probe resolves (success or timeout), callers check the registry
|
|
930
|
+
* and proceed — either via private-net (probe succeeded) or via lease (probe
|
|
931
|
+
* failed/timed out, but now coalesced via `_leaseInFlight`).
|
|
932
|
+
*/
|
|
933
|
+
_transportReadyPromise = null;
|
|
861
934
|
constructor(options = {}) {
|
|
862
935
|
super({
|
|
863
936
|
...options,
|
|
864
937
|
name: "PlatformSandbox",
|
|
865
938
|
processes: new PlatformProcessManager()
|
|
866
939
|
});
|
|
940
|
+
this._hasRecoveryKey = options.id !== void 0;
|
|
867
941
|
this.id = options.id ?? this.generateId();
|
|
868
942
|
this._client = new PlatformClient(options);
|
|
869
943
|
this._environmentId = options.environmentId ?? process.env.MASTRA_ENVIRONMENT_ID ?? "";
|
|
@@ -899,6 +973,8 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
899
973
|
...id !== void 0 && { id },
|
|
900
974
|
accessToken: this._client.accessToken,
|
|
901
975
|
projectId: this._client.projectId,
|
|
976
|
+
...this._client.sessionId !== void 0 && { sessionId: this._client.sessionId },
|
|
977
|
+
...this._client.threadId !== void 0 && { threadId: this._client.threadId },
|
|
902
978
|
fetch: this._client.fetch,
|
|
903
979
|
environmentId: this._environmentId,
|
|
904
980
|
...options.sandboxId !== void 0 && { sandboxId: options.sandboxId },
|
|
@@ -913,11 +989,31 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
913
989
|
});
|
|
914
990
|
}
|
|
915
991
|
async start() {
|
|
992
|
+
this._startInFlight ??= this._doStart().finally(() => {
|
|
993
|
+
this._startInFlight = null;
|
|
994
|
+
});
|
|
995
|
+
return this._startInFlight;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* The single `start` attempt behind {@link start}'s coalescing wrapper.
|
|
999
|
+
*
|
|
1000
|
+
* Split out so the wrapper can install a shared in-flight promise
|
|
1001
|
+
* synchronously (before the first `await`) without inlining the reattach
|
|
1002
|
+
* / retry logic. Joined callers observe whatever outcome this method
|
|
1003
|
+
* produces — success returns normally, failures propagate to every
|
|
1004
|
+
* awaiter.
|
|
1005
|
+
*/
|
|
1006
|
+
async _doStart() {
|
|
1007
|
+
const startedAt = Date.now();
|
|
916
1008
|
if (this._sandboxId) try {
|
|
917
|
-
const
|
|
1009
|
+
const requestStartedAt = Date.now();
|
|
1010
|
+
const response = await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`);
|
|
1011
|
+
const requestMs = Date.now() - requestStartedAt;
|
|
1012
|
+
const json = await response.json();
|
|
918
1013
|
if (!json.destroyedAt) {
|
|
919
1014
|
this._createdAt = json.createdAt ? new Date(json.createdAt) : /* @__PURE__ */ new Date();
|
|
920
1015
|
this._populateAddressFromResponse(json);
|
|
1016
|
+
this._logStartComplete(json.id, startedAt, requestMs, "reattach");
|
|
921
1017
|
return;
|
|
922
1018
|
}
|
|
923
1019
|
this._sandboxId = void 0;
|
|
@@ -934,6 +1030,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
934
1030
|
env: this._env
|
|
935
1031
|
});
|
|
936
1032
|
let response;
|
|
1033
|
+
const requestStartedAt = Date.now();
|
|
937
1034
|
for (let attempt = 1;; attempt++) try {
|
|
938
1035
|
response = await this._client.request("/sandbox", {
|
|
939
1036
|
method: "POST",
|
|
@@ -945,10 +1042,32 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
945
1042
|
if (!(error instanceof PlatformApiError && error.status >= 500) || attempt >= CREATE_MAX_ATTEMPTS) throw error;
|
|
946
1043
|
await new Promise((resolve) => setTimeout(resolve, CREATE_RETRY_BASE_DELAY_MS * attempt));
|
|
947
1044
|
}
|
|
1045
|
+
const requestMs = Date.now() - requestStartedAt;
|
|
948
1046
|
const json = await response.json();
|
|
949
1047
|
this._sandboxId = json.id;
|
|
950
1048
|
this._createdAt = json.createdAt ? new Date(json.createdAt) : /* @__PURE__ */ new Date();
|
|
951
1049
|
this._populateAddressFromResponse(json);
|
|
1050
|
+
this._logStartComplete(json.id, startedAt, requestMs, "provision");
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* One timing summary per completed `start()` — the whole
|
|
1054
|
+
* `PlatformSandbox`-visible boot in a single greppable line.
|
|
1055
|
+
*
|
|
1056
|
+
* `requestMs` is the proxy round-trip (`GET /sandbox/:id` on reattach,
|
|
1057
|
+
* `POST /sandbox` including transient-5xx retries on provision) — a black
|
|
1058
|
+
* box from this side that rolls up Railway RPC, sidecar launch, and the
|
|
1059
|
+
* proxy's discovery exec. Sidecar probe cost is intentionally NOT here: the
|
|
1060
|
+
* probe is fire-and-forget and outlives `start()` by design, so its
|
|
1061
|
+
* duration lands on the `platform-workspace probe ok` line instead.
|
|
1062
|
+
*/
|
|
1063
|
+
_logStartComplete(sandboxId, startedAt, requestMs, mode) {
|
|
1064
|
+
this.logger.info("platform-workspace start complete", {
|
|
1065
|
+
sandboxId,
|
|
1066
|
+
sessionId: this._client.sessionId,
|
|
1067
|
+
mode,
|
|
1068
|
+
totalMs: Date.now() - startedAt,
|
|
1069
|
+
requestMs
|
|
1070
|
+
});
|
|
952
1071
|
}
|
|
953
1072
|
/**
|
|
954
1073
|
* Copy `response.instanceUrl` into the injected {@link SandboxAddressRegistry}
|
|
@@ -969,20 +1088,274 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
969
1088
|
_populateAddressFromResponse(json) {
|
|
970
1089
|
if (!this._addressRegistry) return;
|
|
971
1090
|
if (!json.instanceUrl) return;
|
|
972
|
-
this._addressRegistry.
|
|
1091
|
+
this._addressRegistry.delete(json.id);
|
|
1092
|
+
const generation = ++this._probeGeneration;
|
|
1093
|
+
this._transportReadyPromise = this._probeSidecarThenRegister(json.id, json.instanceUrl, generation);
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Fire-and-forget probe that polls the sidecar's `/health` endpoint until
|
|
1097
|
+
* it responds, then populates the address registry. Runs detached from
|
|
1098
|
+
* `start()` so sandbox provision latency is unchanged; early execs simply
|
|
1099
|
+
* fall back to the lease path until the probe succeeds.
|
|
1100
|
+
*
|
|
1101
|
+
* If the sidecar never comes up within {@link SIDECAR_PROBE_TIMEOUT_MS},
|
|
1102
|
+
* the registry stays unpopulated and all execs go via lease for this
|
|
1103
|
+
* sandbox's lifetime (or until a future `start()` re-runs the probe).
|
|
1104
|
+
*
|
|
1105
|
+
* @param generation - The probe generation captured at call time. If this
|
|
1106
|
+
* no longer matches `_probeGeneration` when the probe succeeds, the probe
|
|
1107
|
+
* was superseded by a teardown or a new `start()`, so we skip the `set()`.
|
|
1108
|
+
*/
|
|
1109
|
+
async _probeSidecarThenRegister(sandboxId, instanceUrl, generation) {
|
|
1110
|
+
const probeStartedAt = Date.now();
|
|
1111
|
+
const deadline = probeStartedAt + SIDECAR_PROBE_TIMEOUT_MS;
|
|
1112
|
+
const fetchFn = this._privateNetFetch ?? fetch;
|
|
1113
|
+
let attempts = 0;
|
|
1114
|
+
while (Date.now() < deadline) {
|
|
1115
|
+
if (generation !== this._probeGeneration) return;
|
|
1116
|
+
attempts++;
|
|
1117
|
+
try {
|
|
1118
|
+
const res = await fetchFn(`${instanceUrl}/health`, {
|
|
1119
|
+
method: "GET",
|
|
1120
|
+
signal: AbortSignal.timeout(1e3)
|
|
1121
|
+
});
|
|
1122
|
+
const ok = res.ok;
|
|
1123
|
+
await res.body?.cancel().catch(() => {});
|
|
1124
|
+
if (ok) {
|
|
1125
|
+
this.logger.info("platform-workspace probe ok", {
|
|
1126
|
+
sandboxId,
|
|
1127
|
+
sessionId: this._client.sessionId,
|
|
1128
|
+
probeDurationMs: Date.now() - probeStartedAt,
|
|
1129
|
+
attempts
|
|
1130
|
+
});
|
|
1131
|
+
if (generation === this._probeGeneration && this._sandboxId === sandboxId) this._addressRegistry?.set(sandboxId, instanceUrl);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
} catch {}
|
|
1135
|
+
await new Promise((r) => setTimeout(r, SIDECAR_PROBE_INTERVAL_MS));
|
|
1136
|
+
}
|
|
1137
|
+
this.logger.warn("platform-workspace probe timed out", {
|
|
1138
|
+
sandboxId,
|
|
1139
|
+
sessionId: this._client.sessionId,
|
|
1140
|
+
timeoutMs: SIDECAR_PROBE_TIMEOUT_MS,
|
|
1141
|
+
attempts
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Wait for the transport to become ready (sidecar probe succeeds) or time
|
|
1146
|
+
* out. Concurrent callers all await the same probe promise, coalescing the
|
|
1147
|
+
* cold-start storm into a single warmup attempt.
|
|
1148
|
+
*
|
|
1149
|
+
* If no probe is in flight (no registry, or registry already populated),
|
|
1150
|
+
* this returns immediately. After the wait (success or timeout), callers
|
|
1151
|
+
* check the registry and proceed — either via private-net or lease. The
|
|
1152
|
+
* lease path is still coalesced via `_leaseInFlight`, so even if the probe
|
|
1153
|
+
* times out, we only mint one lease for all concurrent execs.
|
|
1154
|
+
*/
|
|
1155
|
+
async _awaitTransportReady() {
|
|
1156
|
+
if (this._sandboxId && this._addressRegistry?.get(this._sandboxId)) return;
|
|
1157
|
+
if (!this._transportReadyPromise) return;
|
|
1158
|
+
await Promise.race([this._transportReadyPromise, new Promise((r) => setTimeout(r, TRANSPORT_READY_WAIT_MS))]);
|
|
973
1159
|
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Stop the sandbox while **preserving its recovery checkpoint**.
|
|
1162
|
+
*
|
|
1163
|
+
* Semantic parity with `@mastra/railway` `RailwaySandbox.stop()`: the VM
|
|
1164
|
+
* is released but the on-provider checkpoint survives, so a subsequent
|
|
1165
|
+
* `start()` on a sandbox constructed with the same `id` can restore from
|
|
1166
|
+
* it. Any in-flight capture is awaited first so the preserved checkpoint
|
|
1167
|
+
* reflects the latest disk state we asked for.
|
|
1168
|
+
*
|
|
1169
|
+
* Corresponds to `DELETE /v1/projects/:pid/sandbox/:sandboxId` on
|
|
1170
|
+
* workspace-proxy, which by contract does not touch the checkpoint. Use
|
|
1171
|
+
* {@link destroy} when you want the checkpoint released too.
|
|
1172
|
+
*/
|
|
974
1173
|
async stop() {
|
|
975
|
-
await this.
|
|
1174
|
+
if (this._captureInFlight) await this._captureInFlight.catch((error) => {
|
|
1175
|
+
this.logger.warn(`stop(): failed to flush in-flight capture before teardown:`, error);
|
|
1176
|
+
});
|
|
1177
|
+
await this._teardownSandbox();
|
|
976
1178
|
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Destroy the sandbox **and release its recovery checkpoint**.
|
|
1181
|
+
*
|
|
1182
|
+
* Semantic parity with `@mastra/railway` `RailwaySandbox.destroy()`:
|
|
1183
|
+
* cancels any in-flight capture (the checkpoint is about to be deleted
|
|
1184
|
+
* — no reason to burn a capture on state we're releasing), asks the
|
|
1185
|
+
* proxy to delete the checkpoint, then releases the VM. Both remote
|
|
1186
|
+
* operations are best-effort logged failures — a stray checkpoint or a
|
|
1187
|
+
* transient proxy error must not leave the caller with a half-torn-down
|
|
1188
|
+
* sandbox they can't safely retry.
|
|
1189
|
+
*
|
|
1190
|
+
* Requires the caller to have constructed with a recovery `id` (there is
|
|
1191
|
+
* no checkpoint to delete otherwise); callers without one skip the
|
|
1192
|
+
* checkpoint DELETE and behave identically to {@link stop}.
|
|
1193
|
+
*/
|
|
977
1194
|
async destroy() {
|
|
978
1195
|
if (!this._sandboxId) return;
|
|
979
1196
|
const destroyedSandboxId = this._sandboxId;
|
|
1197
|
+
this._captureInFlight = null;
|
|
1198
|
+
if (this._hasRecoveryKey) try {
|
|
1199
|
+
await this._client.request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}/checkpoint`, {
|
|
1200
|
+
method: "DELETE",
|
|
1201
|
+
headers: { "content-type": "application/json" },
|
|
1202
|
+
body: JSON.stringify({ id: this.id })
|
|
1203
|
+
});
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
if (error instanceof PlatformApiError && (error.status === 404 || error.status === 410)) this.logger.debug(`destroy(): checkpoint already absent upstream (status=${error.status})`);
|
|
1206
|
+
else this.logger.warn(`destroy(): failed to delete checkpoint upstream:`, error);
|
|
1207
|
+
}
|
|
1208
|
+
await this._teardownSandbox();
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Release the remote sandbox VM and clear the local state pointing at it.
|
|
1212
|
+
*
|
|
1213
|
+
* Shared body of {@link stop} and {@link destroy} — both funnel through
|
|
1214
|
+
* here after they've dealt with the checkpoint (preserve vs release).
|
|
1215
|
+
* The VM DELETE is safe to issue in either mode: the proxy's DELETE
|
|
1216
|
+
* route does not touch the checkpoint on its own, so `stop()` correctly
|
|
1217
|
+
* leaves the checkpoint intact and `destroy()` has already removed it
|
|
1218
|
+
* before this call.
|
|
1219
|
+
*/
|
|
1220
|
+
async _teardownSandbox() {
|
|
1221
|
+
if (!this._sandboxId) return;
|
|
1222
|
+
const destroyedSandboxId = this._sandboxId;
|
|
1223
|
+
this._probeGeneration++;
|
|
980
1224
|
await this._client.request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}`, { method: "DELETE" });
|
|
981
1225
|
this._sandboxId = void 0;
|
|
982
1226
|
this._createdAt = null;
|
|
983
1227
|
this._lease = null;
|
|
984
1228
|
this._addressRegistry?.delete(destroyedSandboxId);
|
|
985
1229
|
}
|
|
1230
|
+
/** Persist the configured recovery checkpoint when available. */
|
|
1231
|
+
async snapshot() {
|
|
1232
|
+
await this.captureCheckpoint();
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Capture the sandbox's checkpoint on demand, outside any refresh timer the
|
|
1236
|
+
* workspace-proxy owns internally.
|
|
1237
|
+
*
|
|
1238
|
+
* Intended for callers (e.g. a factory-side scheduler) that want to refresh
|
|
1239
|
+
* the recovery checkpoint at semantic moments — turn end, session-idle,
|
|
1240
|
+
* pre-teardown — rather than only just before the upstream's idle destroy.
|
|
1241
|
+
*
|
|
1242
|
+
* Mirrors the OSS `@mastra/railway` `RailwaySandbox.captureCheckpoint()`
|
|
1243
|
+
* shape so factory can call `sandbox.captureCheckpoint()` uniformly and
|
|
1244
|
+
* branch on `status`/`reason` without knowing which provider is underneath.
|
|
1245
|
+
* Both `captured` and `coalesced` carry the checkpoint name inline so the
|
|
1246
|
+
* caller can persist a session→checkpoint binding atomically with the
|
|
1247
|
+
* awaited capture.
|
|
1248
|
+
*
|
|
1249
|
+
* Skip semantics:
|
|
1250
|
+
* - No caller-supplied `id`: returns `{ status: 'skipped', reason:
|
|
1251
|
+
* 'no-checkpoint-name-configured' }`. An auto-generated random id is
|
|
1252
|
+
* never a meaningful recovery key (no future boot would look for a
|
|
1253
|
+
* checkpoint under it), so capturing would silently produce dead data.
|
|
1254
|
+
* - Not started (no `_sandboxId`): returns `{ status: 'skipped', reason:
|
|
1255
|
+
* 'sandbox-not-running' }` without a round-trip.
|
|
1256
|
+
* - Upstream 410 (workspace-proxy or Railway reports the sandbox is
|
|
1257
|
+
* already destroyed): returns the same `sandbox-not-running` skip so
|
|
1258
|
+
* the discriminant matches the pre-flight case. Local state
|
|
1259
|
+
* (`_sandboxId`, `_lease`, sidecar address) is cleared as a side
|
|
1260
|
+
* effect so the next `start()` provisions fresh instead of reattaching
|
|
1261
|
+
* to a dead id. The diagnostic distinction (pre-flight vs post-hoc)
|
|
1262
|
+
* is preserved in log level: debug for the expected pre-flight skip,
|
|
1263
|
+
* warn for the surprise upstream destroy.
|
|
1264
|
+
*
|
|
1265
|
+
* Concurrent callers on the same instance coalesce onto a single in-flight
|
|
1266
|
+
* `POST /checkpoint` so N simultaneous turn-end fires (e.g. several tabs)
|
|
1267
|
+
* do not each round-trip the proxy. Both the originator and joiners
|
|
1268
|
+
* receive `{ status: 'coalesced', ... }` for the joined result — the
|
|
1269
|
+
* outer contract does not distinguish who started the request, only that
|
|
1270
|
+
* one upstream capture was made.
|
|
1271
|
+
*
|
|
1272
|
+
* Never throws for expected outcomes. Transport failures (5xx, 4xx other
|
|
1273
|
+
* than 410) propagate as {@link PlatformApiError}; a 410 is normalized
|
|
1274
|
+
* to a skip as described above.
|
|
1275
|
+
*/
|
|
1276
|
+
async captureCheckpoint() {
|
|
1277
|
+
if (!this._hasRecoveryKey) {
|
|
1278
|
+
this.logger.debug(`captureCheckpoint skipped: no recovery key configured for sandbox ${this._sandboxId ?? "(unstarted)"}`);
|
|
1279
|
+
return {
|
|
1280
|
+
status: "skipped",
|
|
1281
|
+
reason: "no-checkpoint-name-configured"
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
if (!this._sandboxId) {
|
|
1285
|
+
this.logger.debug(`captureCheckpoint skipped: sandbox not running (local pre-flight, id=${this.id})`);
|
|
1286
|
+
return {
|
|
1287
|
+
status: "skipped",
|
|
1288
|
+
reason: "sandbox-not-running"
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
if (this._captureInFlight) return this._captureInFlight;
|
|
1292
|
+
const sandboxId = this._sandboxId;
|
|
1293
|
+
const capture = this._doCaptureCheckpoint(sandboxId).finally(() => {
|
|
1294
|
+
if (this._captureInFlight === capture) this._captureInFlight = null;
|
|
1295
|
+
});
|
|
1296
|
+
this._captureInFlight = capture;
|
|
1297
|
+
return capture;
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* The single `POST /checkpoint` attempt behind {@link captureCheckpoint}.
|
|
1301
|
+
*
|
|
1302
|
+
* Split out so the coalescing wrapper can install a shared in-flight
|
|
1303
|
+
* promise without inlining the transport + response-mapping logic.
|
|
1304
|
+
* Joined callers observe `{ status: 'coalesced', ... }` — the initiator
|
|
1305
|
+
* sees the underlying `captured` / `coalesced` / `skipped` result the
|
|
1306
|
+
* proxy returned. Both are legitimate: the OSS mirror uses the same
|
|
1307
|
+
* "initiator sees the truth, joiners see coalesced" split.
|
|
1308
|
+
*/
|
|
1309
|
+
async _doCaptureCheckpoint(sandboxId) {
|
|
1310
|
+
let response;
|
|
1311
|
+
try {
|
|
1312
|
+
response = await this._client.request(`/sandbox/${encodeURIComponent(sandboxId)}/checkpoint`, {
|
|
1313
|
+
method: "POST",
|
|
1314
|
+
headers: { "content-type": "application/json" },
|
|
1315
|
+
body: JSON.stringify({ id: this.id })
|
|
1316
|
+
});
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
if (error instanceof PlatformApiError && error.status === 410) {
|
|
1319
|
+
this.logger.warn(`captureCheckpoint skipped: sandbox destroyed upstream (proxy 410, sandboxId=${sandboxId})`);
|
|
1320
|
+
this._clearDestroyedState(sandboxId);
|
|
1321
|
+
return {
|
|
1322
|
+
status: "skipped",
|
|
1323
|
+
reason: "sandbox-not-running"
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
throw error;
|
|
1327
|
+
}
|
|
1328
|
+
const json = await response.json();
|
|
1329
|
+
if (json.status === "skipped") {
|
|
1330
|
+
this.logger.warn(`captureCheckpoint skipped: sandbox destroyed upstream (proxy reported skipped, sandboxId=${sandboxId})`);
|
|
1331
|
+
this._clearDestroyedState(sandboxId);
|
|
1332
|
+
return {
|
|
1333
|
+
status: "skipped",
|
|
1334
|
+
reason: "sandbox-not-running"
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
return {
|
|
1338
|
+
status: json.status,
|
|
1339
|
+
checkpointName: json.checkpointName
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Clear local state that would otherwise let the caller keep exec'ing
|
|
1344
|
+
* against a sandbox the upstream has already destroyed. Mirrors what
|
|
1345
|
+
* `destroy()` does minus the outbound DELETE — the sandbox is already
|
|
1346
|
+
* gone, so all that remains is to stop pointing at it.
|
|
1347
|
+
*
|
|
1348
|
+
* Also resets `status` to `'pending'` so a subsequent `_start()` on this
|
|
1349
|
+
* reused instance re-runs provisioning instead of short-circuiting on
|
|
1350
|
+
* the cached `'running'` state (see `MastraSandbox._start`).
|
|
1351
|
+
*/
|
|
1352
|
+
_clearDestroyedState(destroyedSandboxId) {
|
|
1353
|
+
this._sandboxId = void 0;
|
|
1354
|
+
this._createdAt = null;
|
|
1355
|
+
this._lease = null;
|
|
1356
|
+
this._addressRegistry?.delete(destroyedSandboxId);
|
|
1357
|
+
this.status = "pending";
|
|
1358
|
+
}
|
|
986
1359
|
/**
|
|
987
1360
|
* Execute a command on the remote sandbox.
|
|
988
1361
|
*
|
|
@@ -1006,6 +1379,7 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
|
|
|
1006
1379
|
const started = Date.now();
|
|
1007
1380
|
const fullCommand = buildCommand(command, args);
|
|
1008
1381
|
const effectiveTimeout = options?.timeout ?? this._timeout;
|
|
1382
|
+
await this._awaitTransportReady();
|
|
1009
1383
|
const instanceUrl = this._addressRegistry?.get(this._sandboxId);
|
|
1010
1384
|
if (instanceUrl) {
|
|
1011
1385
|
const privateNet = await this._tryExecViaPrivateNetwork(instanceUrl, fullCommand, effectiveTimeout, options);
|