@mastra/platform-workspace 1.2.0-alpha.1 → 1.2.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/CHANGELOG.md +106 -0
- package/dist/client.d.ts +19 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/index.cjs +149 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +149 -2
- package/dist/index.js.map +1 -1
- package/dist/sandbox.d.ts +57 -0
- package/dist/sandbox.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -18,6 +18,8 @@ function resolvePlatformOptions(options) {
|
|
|
18
18
|
accessToken: requireOption(options.accessToken ?? process.env.MASTRA_PLATFORM_ACCESS_TOKEN, "accessToken"),
|
|
19
19
|
projectId: requireOption(options.projectId ?? process.env.MASTRA_PROJECT_ID, "projectId"),
|
|
20
20
|
proxyUrl: (process.env.MASTRA_WORKSPACE_PROXY_URL ?? DEFAULT_PROXY_URL).replace(/\/$/, ""),
|
|
21
|
+
sessionId: options.sessionId,
|
|
22
|
+
threadId: options.threadId,
|
|
21
23
|
fetch: options.fetch ?? fetch
|
|
22
24
|
};
|
|
23
25
|
}
|
|
@@ -61,12 +63,18 @@ var PlatformClient = class {
|
|
|
61
63
|
accessToken;
|
|
62
64
|
projectId;
|
|
63
65
|
proxyUrl;
|
|
66
|
+
/** Advisory session correlation id — see {@link PlatformClientOptions.sessionId}. */
|
|
67
|
+
sessionId;
|
|
68
|
+
/** Advisory thread correlation id — see {@link PlatformClientOptions.threadId}. */
|
|
69
|
+
threadId;
|
|
64
70
|
fetch;
|
|
65
71
|
constructor(options) {
|
|
66
72
|
const resolved = resolvePlatformOptions(options);
|
|
67
73
|
this.accessToken = resolved.accessToken;
|
|
68
74
|
this.projectId = resolved.projectId;
|
|
69
75
|
this.proxyUrl = resolved.proxyUrl;
|
|
76
|
+
this.sessionId = resolved.sessionId;
|
|
77
|
+
this.threadId = resolved.threadId;
|
|
70
78
|
this.fetch = resolved.fetch;
|
|
71
79
|
}
|
|
72
80
|
async request(path, options = {}) {
|
|
@@ -74,6 +82,8 @@ var PlatformClient = class {
|
|
|
74
82
|
for (const [key, value] of Object.entries(options.query ?? {})) if (value !== void 0) url.searchParams.set(key, String(value));
|
|
75
83
|
const headers = new Headers(options.headers);
|
|
76
84
|
headers.set("authorization", `Bearer ${this.accessToken}`);
|
|
85
|
+
if (this.sessionId) headers.set("x-mastra-session-id", this.sessionId);
|
|
86
|
+
if (this.threadId) headers.set("x-mastra-thread-id", this.threadId);
|
|
77
87
|
const { query: _query, ...fetchOptions } = options;
|
|
78
88
|
const signal = fetchOptions.signal ?? AbortSignal.timeout(DEFAULT_REQUEST_TIMEOUT_MS);
|
|
79
89
|
const response = await this.fetch(url, {
|
|
@@ -669,6 +679,24 @@ const CREATE_MAX_ATTEMPTS = 3;
|
|
|
669
679
|
/** Base delay between create retries; multiplied by the attempt number. */
|
|
670
680
|
const CREATE_RETRY_BASE_DELAY_MS = 2e3;
|
|
671
681
|
/**
|
|
682
|
+
* How long to wait for the in-sandbox sidecar's `/health` endpoint to respond
|
|
683
|
+
* before giving up and leaving the address registry unpopulated (execs fall
|
|
684
|
+
* back to the lease path). This bounds the fire-and-forget probe that runs
|
|
685
|
+
* after `start()` resolves; the sandbox is usable immediately — the probe
|
|
686
|
+
* only controls whether early execs go via private-net or lease.
|
|
687
|
+
*/
|
|
688
|
+
const SIDECAR_PROBE_TIMEOUT_MS = 3e4;
|
|
689
|
+
/** Delay between sidecar probe attempts. */
|
|
690
|
+
const SIDECAR_PROBE_INTERVAL_MS = 250;
|
|
691
|
+
/**
|
|
692
|
+
* How long `executeCommand` waits for the transport to become ready before
|
|
693
|
+
* falling back to the lease path. This is much shorter than
|
|
694
|
+
* `SIDECAR_PROBE_TIMEOUT_MS` because we want execs to proceed quickly if
|
|
695
|
+
* the sidecar is slow to boot — the probe continues in the background and
|
|
696
|
+
* later execs will use private-net once it succeeds.
|
|
697
|
+
*/
|
|
698
|
+
const TRANSPORT_READY_WAIT_MS = 5e3;
|
|
699
|
+
/**
|
|
672
700
|
* Diagnostic error thrown when the direct-exec WebSocket transport fails
|
|
673
701
|
* twice in a row (opening handshake refused or socket closed mid-stream
|
|
674
702
|
* without an `exit` frame). Distinguishes "the sandbox transport is broken"
|
|
@@ -863,6 +891,22 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
|
|
|
863
891
|
* Mirrors OSS `@mastra/railway` `RailwaySandbox._startInFlight`.
|
|
864
892
|
*/
|
|
865
893
|
_startInFlight = null;
|
|
894
|
+
/**
|
|
895
|
+
* Generation token for the sidecar probe. Incremented on every `start()`
|
|
896
|
+
* and on teardown. The probe captures this value when it begins; if the
|
|
897
|
+
* generation has changed by the time the probe succeeds, the probe skips
|
|
898
|
+
* the `set()` to avoid re-populating a deleted or superseded sandbox entry.
|
|
899
|
+
*/
|
|
900
|
+
_probeGeneration = 0;
|
|
901
|
+
/**
|
|
902
|
+
* In-flight sidecar probe promise. Concurrent `executeCommand` callers that
|
|
903
|
+
* arrive before the registry is populated all await this single promise so
|
|
904
|
+
* we don't fire N independent lease requests during the sidecar boot window.
|
|
905
|
+
* Once the probe resolves (success or timeout), callers check the registry
|
|
906
|
+
* and proceed — either via private-net (probe succeeded) or via lease (probe
|
|
907
|
+
* failed/timed out, but now coalesced via `_leaseInFlight`).
|
|
908
|
+
*/
|
|
909
|
+
_transportReadyPromise = null;
|
|
866
910
|
constructor(options = {}) {
|
|
867
911
|
super({
|
|
868
912
|
...options,
|
|
@@ -905,6 +949,8 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
|
|
|
905
949
|
...id !== void 0 && { id },
|
|
906
950
|
accessToken: this._client.accessToken,
|
|
907
951
|
projectId: this._client.projectId,
|
|
952
|
+
...this._client.sessionId !== void 0 && { sessionId: this._client.sessionId },
|
|
953
|
+
...this._client.threadId !== void 0 && { threadId: this._client.threadId },
|
|
908
954
|
fetch: this._client.fetch,
|
|
909
955
|
environmentId: this._environmentId,
|
|
910
956
|
...options.sandboxId !== void 0 && { sandboxId: options.sandboxId },
|
|
@@ -934,11 +980,16 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
|
|
|
934
980
|
* awaiter.
|
|
935
981
|
*/
|
|
936
982
|
async _doStart() {
|
|
983
|
+
const startedAt = Date.now();
|
|
937
984
|
if (this._sandboxId) try {
|
|
938
|
-
const
|
|
985
|
+
const requestStartedAt = Date.now();
|
|
986
|
+
const response = await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`);
|
|
987
|
+
const requestMs = Date.now() - requestStartedAt;
|
|
988
|
+
const json = await response.json();
|
|
939
989
|
if (!json.destroyedAt) {
|
|
940
990
|
this._createdAt = json.createdAt ? new Date(json.createdAt) : /* @__PURE__ */ new Date();
|
|
941
991
|
this._populateAddressFromResponse(json);
|
|
992
|
+
this._logStartComplete(json.id, startedAt, requestMs, "reattach");
|
|
942
993
|
return;
|
|
943
994
|
}
|
|
944
995
|
this._sandboxId = void 0;
|
|
@@ -955,6 +1006,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
|
|
|
955
1006
|
env: this._env
|
|
956
1007
|
});
|
|
957
1008
|
let response;
|
|
1009
|
+
const requestStartedAt = Date.now();
|
|
958
1010
|
for (let attempt = 1;; attempt++) try {
|
|
959
1011
|
response = await this._client.request("/sandbox", {
|
|
960
1012
|
method: "POST",
|
|
@@ -966,10 +1018,32 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
|
|
|
966
1018
|
if (!(error instanceof PlatformApiError && error.status >= 500) || attempt >= CREATE_MAX_ATTEMPTS) throw error;
|
|
967
1019
|
await new Promise((resolve) => setTimeout(resolve, CREATE_RETRY_BASE_DELAY_MS * attempt));
|
|
968
1020
|
}
|
|
1021
|
+
const requestMs = Date.now() - requestStartedAt;
|
|
969
1022
|
const json = await response.json();
|
|
970
1023
|
this._sandboxId = json.id;
|
|
971
1024
|
this._createdAt = json.createdAt ? new Date(json.createdAt) : /* @__PURE__ */ new Date();
|
|
972
1025
|
this._populateAddressFromResponse(json);
|
|
1026
|
+
this._logStartComplete(json.id, startedAt, requestMs, "provision");
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* One timing summary per completed `start()` — the whole
|
|
1030
|
+
* `PlatformSandbox`-visible boot in a single greppable line.
|
|
1031
|
+
*
|
|
1032
|
+
* `requestMs` is the proxy round-trip (`GET /sandbox/:id` on reattach,
|
|
1033
|
+
* `POST /sandbox` including transient-5xx retries on provision) — a black
|
|
1034
|
+
* box from this side that rolls up Railway RPC, sidecar launch, and the
|
|
1035
|
+
* proxy's discovery exec. Sidecar probe cost is intentionally NOT here: the
|
|
1036
|
+
* probe is fire-and-forget and outlives `start()` by design, so its
|
|
1037
|
+
* duration lands on the `platform-workspace probe ok` line instead.
|
|
1038
|
+
*/
|
|
1039
|
+
_logStartComplete(sandboxId, startedAt, requestMs, mode) {
|
|
1040
|
+
this.logger.info("platform-workspace start complete", {
|
|
1041
|
+
sandboxId,
|
|
1042
|
+
sessionId: this._client.sessionId,
|
|
1043
|
+
mode,
|
|
1044
|
+
totalMs: Date.now() - startedAt,
|
|
1045
|
+
requestMs
|
|
1046
|
+
});
|
|
973
1047
|
}
|
|
974
1048
|
/**
|
|
975
1049
|
* Copy `response.instanceUrl` into the injected {@link SandboxAddressRegistry}
|
|
@@ -990,7 +1064,74 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
|
|
|
990
1064
|
_populateAddressFromResponse(json) {
|
|
991
1065
|
if (!this._addressRegistry) return;
|
|
992
1066
|
if (!json.instanceUrl) return;
|
|
993
|
-
this._addressRegistry.
|
|
1067
|
+
this._addressRegistry.delete(json.id);
|
|
1068
|
+
const generation = ++this._probeGeneration;
|
|
1069
|
+
this._transportReadyPromise = this._probeSidecarThenRegister(json.id, json.instanceUrl, generation);
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Fire-and-forget probe that polls the sidecar's `/health` endpoint until
|
|
1073
|
+
* it responds, then populates the address registry. Runs detached from
|
|
1074
|
+
* `start()` so sandbox provision latency is unchanged; early execs simply
|
|
1075
|
+
* fall back to the lease path until the probe succeeds.
|
|
1076
|
+
*
|
|
1077
|
+
* If the sidecar never comes up within {@link SIDECAR_PROBE_TIMEOUT_MS},
|
|
1078
|
+
* the registry stays unpopulated and all execs go via lease for this
|
|
1079
|
+
* sandbox's lifetime (or until a future `start()` re-runs the probe).
|
|
1080
|
+
*
|
|
1081
|
+
* @param generation - The probe generation captured at call time. If this
|
|
1082
|
+
* no longer matches `_probeGeneration` when the probe succeeds, the probe
|
|
1083
|
+
* was superseded by a teardown or a new `start()`, so we skip the `set()`.
|
|
1084
|
+
*/
|
|
1085
|
+
async _probeSidecarThenRegister(sandboxId, instanceUrl, generation) {
|
|
1086
|
+
const probeStartedAt = Date.now();
|
|
1087
|
+
const deadline = probeStartedAt + SIDECAR_PROBE_TIMEOUT_MS;
|
|
1088
|
+
const fetchFn = this._privateNetFetch ?? fetch;
|
|
1089
|
+
let attempts = 0;
|
|
1090
|
+
while (Date.now() < deadline) {
|
|
1091
|
+
if (generation !== this._probeGeneration) return;
|
|
1092
|
+
attempts++;
|
|
1093
|
+
try {
|
|
1094
|
+
const res = await fetchFn(`${instanceUrl}/health`, {
|
|
1095
|
+
method: "GET",
|
|
1096
|
+
signal: AbortSignal.timeout(1e3)
|
|
1097
|
+
});
|
|
1098
|
+
const ok = res.ok;
|
|
1099
|
+
await res.body?.cancel().catch(() => {});
|
|
1100
|
+
if (ok) {
|
|
1101
|
+
this.logger.info("platform-workspace probe ok", {
|
|
1102
|
+
sandboxId,
|
|
1103
|
+
sessionId: this._client.sessionId,
|
|
1104
|
+
probeDurationMs: Date.now() - probeStartedAt,
|
|
1105
|
+
attempts
|
|
1106
|
+
});
|
|
1107
|
+
if (generation === this._probeGeneration && this._sandboxId === sandboxId) this._addressRegistry?.set(sandboxId, instanceUrl);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
} catch {}
|
|
1111
|
+
await new Promise((r) => setTimeout(r, SIDECAR_PROBE_INTERVAL_MS));
|
|
1112
|
+
}
|
|
1113
|
+
this.logger.warn("platform-workspace probe timed out", {
|
|
1114
|
+
sandboxId,
|
|
1115
|
+
sessionId: this._client.sessionId,
|
|
1116
|
+
timeoutMs: SIDECAR_PROBE_TIMEOUT_MS,
|
|
1117
|
+
attempts
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Wait for the transport to become ready (sidecar probe succeeds) or time
|
|
1122
|
+
* out. Concurrent callers all await the same probe promise, coalescing the
|
|
1123
|
+
* cold-start storm into a single warmup attempt.
|
|
1124
|
+
*
|
|
1125
|
+
* If no probe is in flight (no registry, or registry already populated),
|
|
1126
|
+
* this returns immediately. After the wait (success or timeout), callers
|
|
1127
|
+
* check the registry and proceed — either via private-net or lease. The
|
|
1128
|
+
* lease path is still coalesced via `_leaseInFlight`, so even if the probe
|
|
1129
|
+
* times out, we only mint one lease for all concurrent execs.
|
|
1130
|
+
*/
|
|
1131
|
+
async _awaitTransportReady() {
|
|
1132
|
+
if (this._sandboxId && this._addressRegistry?.get(this._sandboxId)) return;
|
|
1133
|
+
if (!this._transportReadyPromise) return;
|
|
1134
|
+
await Promise.race([this._transportReadyPromise, new Promise((r) => setTimeout(r, TRANSPORT_READY_WAIT_MS))]);
|
|
994
1135
|
}
|
|
995
1136
|
/**
|
|
996
1137
|
* Stop the sandbox while **preserving its recovery checkpoint**.
|
|
@@ -1055,12 +1196,17 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
|
|
|
1055
1196
|
async _teardownSandbox() {
|
|
1056
1197
|
if (!this._sandboxId) return;
|
|
1057
1198
|
const destroyedSandboxId = this._sandboxId;
|
|
1199
|
+
this._probeGeneration++;
|
|
1058
1200
|
await this._client.request(`/sandbox/${encodeURIComponent(destroyedSandboxId)}`, { method: "DELETE" });
|
|
1059
1201
|
this._sandboxId = void 0;
|
|
1060
1202
|
this._createdAt = null;
|
|
1061
1203
|
this._lease = null;
|
|
1062
1204
|
this._addressRegistry?.delete(destroyedSandboxId);
|
|
1063
1205
|
}
|
|
1206
|
+
/** Persist the configured recovery checkpoint when available. */
|
|
1207
|
+
async snapshot() {
|
|
1208
|
+
await this.captureCheckpoint();
|
|
1209
|
+
}
|
|
1064
1210
|
/**
|
|
1065
1211
|
* Capture the sandbox's checkpoint on demand, outside any refresh timer the
|
|
1066
1212
|
* workspace-proxy owns internally.
|
|
@@ -1209,6 +1355,7 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
|
|
|
1209
1355
|
const started = Date.now();
|
|
1210
1356
|
const fullCommand = buildCommand(command, args);
|
|
1211
1357
|
const effectiveTimeout = options?.timeout ?? this._timeout;
|
|
1358
|
+
await this._awaitTransportReady();
|
|
1212
1359
|
const instanceUrl = this._addressRegistry?.get(this._sandboxId);
|
|
1213
1360
|
if (instanceUrl) {
|
|
1214
1361
|
const privateNet = await this._tryExecViaPrivateNetwork(instanceUrl, fullCommand, effectiveTimeout, options);
|