@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/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 json = await (await this._client.request(`/sandbox/${encodeURIComponent(this._sandboxId)}`)).json();
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.set(json.id, json.instanceUrl);
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);