@tangle-network/sandbox 0.30.1 → 0.30.2-develop.20260820122029.869ebfa

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.
Files changed (37) hide show
  1. package/README.md +1 -0
  2. package/dist/abort-signal-si1WMfJb.js +31 -0
  3. package/dist/agent/index.d.ts +3 -3
  4. package/dist/agent/index.js +3 -3
  5. package/dist/agent-run-outcome-CM7xD7Mg.d.ts +27 -0
  6. package/dist/auth/index.d.ts +1 -1
  7. package/dist/browser-interactive-control-controller-B476gcui.d.ts +96 -0
  8. package/dist/browser-interactive-control-controller-wXQ7W5pT.js +276 -0
  9. package/dist/{client-Bg9vBEfL.js → client-BfnJ44gF.js} +29 -6
  10. package/dist/{client-DRV5bzRc.d.ts → client-Do_HS671.d.ts} +28 -3
  11. package/dist/collaboration/index.d.ts +1 -1
  12. package/dist/collaboration/index.js +1 -1
  13. package/dist/core.d.ts +4 -4
  14. package/dist/core.js +2 -2
  15. package/dist/{index-ldDRk8Jh.d.ts → index-ebsVOlUc.d.ts} +2 -2
  16. package/dist/index.d.ts +11 -8
  17. package/dist/index.js +7 -6
  18. package/dist/interactive-control.d.ts +2 -0
  19. package/dist/interactive-control.js +2 -0
  20. package/dist/openai/index.d.ts +24 -2
  21. package/dist/openai/index.js +147 -67
  22. package/dist/{runtime-api-CK-4S7oA.js → runtime-api-B0tx7NJM.js} +389 -7
  23. package/dist/runtime.d.ts +4 -3
  24. package/dist/runtime.js +3 -2
  25. package/dist/{sandbox-4F9CNqdR.js → sandbox-AUHcCJe1.js} +1435 -784
  26. package/dist/{sandbox-B49ZGQv7.d.ts → sandbox-Cr-4qzaA.d.ts} +139 -194
  27. package/dist/tangle/index.d.ts +1 -1
  28. package/dist/tangle/index.js +1 -1
  29. package/dist/{tangle-CHpXtqlI.js → tangle-iDk_s8vt.js} +1 -1
  30. package/dist/terminal-stream-4KNJ6SmL.d.ts +256 -0
  31. package/dist/{types-Qn3x7R8O.d.ts → types-BwYLVqxX.d.ts} +24 -256
  32. package/package.json +9 -4
  33. /package/dist/{collaboration-COCXdRrk.js → collaboration-DQD9kCdN.js} +0 -0
  34. /package/dist/{errors-Bdy5jXzc.d.ts → errors-CQOvGddH.d.ts} +0 -0
  35. /package/dist/{index-DtRFVx5U.d.ts → index-Bq_z_u6A.d.ts} +0 -0
  36. /package/dist/{index-Cp1KYanB.d.ts → index-CMLW3u5K.d.ts} +0 -0
  37. /package/dist/{local-cli-auth-D4u7fsnH.js → local-cli-auth-CJmSpTjk.js} +0 -0
@@ -1,7 +1,10 @@
1
- import { a as uploadChunked, c as combineAbortSignals, d as applyRequestedModel, f as assertCurrentBackendType, i as sessionHeaders, l as encodePromptForWire, n as normalizeSessionInfo, o as SANDBOX_PROXY_REQUEST_MAX_BYTES, p as normalizeRuntimeBackendConfig, r as appendSessionIdQuery, t as SandboxRuntimeApi } from "./runtime-api-CK-4S7oA.js";
1
+ import { a as uploadChunked, c as encodePromptForWire, d as assertCurrentBackendType, f as normalizeRuntimeBackendConfig, i as sessionHeaders, m as deriveAgentResultOutcome, n as normalizeSessionInfo, o as SANDBOX_PROXY_REQUEST_MAX_BYTES, p as createAgentRunOutcomeTracker, r as appendSessionIdQuery, t as SandboxRuntimeApi, u as applyRequestedModel } from "./runtime-api-B0tx7NJM.js";
2
2
  import { a as NetworkError, c as QuotaError, d as StateError, f as TimeoutError, l as SandboxError, m as parseErrorResponse, n as CapabilityError, o as NotFoundError, p as ValidationError, t as AuthError, u as ServerError } from "./errors-C6kn-3zt.js";
3
- import { AgentExactRunControlRefSchema, AgentRunCancellationAcknowledgementSchema, AgentRunCancellationRequestSchema, InteractionAcknowledgementSchema, InteractionBindingSchema, InteractionRequestSchema, InteractionResponseCommandSchema, InteractionResponseSchema, agentRunCancellationAcknowledgementMatchesRequest, canonicalCandidateDigest, sha256DigestSchema } from "@tangle-network/agent-interface";
3
+ import { c as createInteractiveControlHolderId, d as interactiveSessionControlReleaseResponseSchema, f as interactiveSessionStatusResponseSchema, i as createBrowserInteractiveSessionControlTransport, o as InteractiveSessionControlController, r as browserInteractiveTerminalEndpoint, s as assertInteractiveSessionRef, u as interactiveSessionControlClaimResponseSchema } from "./browser-interactive-control-controller-wXQ7W5pT.js";
4
+ import { n as combineAbortSignals, t as awaitWithAbort } from "./abort-signal-si1WMfJb.js";
5
+ import { AgentExactRunControlRefSchema, AgentInteractiveSessionAttachSchema, AgentInteractiveSessionControlClaimAcknowledgementSchema, AgentInteractiveSessionControlClaimRequestSchema, AgentInteractiveSessionControlClaimSchema, AgentInteractiveSessionPromptAcknowledgementSchema, AgentInteractiveSessionPromptCommandSchema, AgentInteractiveSessionRefSchema, AgentInteractiveSessionStartSchema, AgentInteractiveSessionStopAcknowledgementSchema, AgentInteractiveSessionStopCommandSchema, AgentRunCancellationAcknowledgementSchema, AgentRunCancellationRequestSchema, InteractionAcknowledgementSchema, InteractionBindingSchema, InteractionRequestSchema, InteractionResponseCommandSchema, InteractionResponseSchema, agentInteractiveSessionControlClaimAcknowledgementMatchesRequest, agentInteractiveSessionControlClaimIsNewer, agentInteractiveSessionControlClaimMatchesRef, agentInteractiveSessionPromptAcknowledgementMatchesCommand, agentInteractiveSessionPromptRequestDigest, agentInteractiveSessionRefMatchesStart, agentInteractiveSessionStatusMatchesRef, agentInteractiveSessionStopAcknowledgementMatchesCommand, agentRunCancellationAcknowledgementMatchesRequest, canonicalCandidateDigest, exactAgentInteractiveSessionStart, sha256DigestSchema } from "@tangle-network/agent-interface";
4
6
  import { z } from "zod";
7
+ import { AgentInteractiveSessionControlClaimSchema as AgentInteractiveSessionControlClaimSchema$1, AgentInteractiveSessionPromptAcknowledgementSchema as AgentInteractiveSessionPromptAcknowledgementSchema$1, AgentInteractiveSessionRefSchema as AgentInteractiveSessionRefSchema$1, AgentInteractiveSessionStatusSchema, AgentInteractiveSessionStopAcknowledgementSchema as AgentInteractiveSessionStopAcknowledgementSchema$1 } from "@tangle-network/agent-interface/environment-interactive-control";
5
8
  import { readTokenCostUsd, readTokenUsage } from "@tangle-network/agent-core";
6
9
  //#region ../../../packages/sse-core/dist/index.js
7
10
  /**
@@ -475,9 +478,9 @@ function readFlagGroup(record, group, keys) {
475
478
  * - Anything else (non-object, missing or non-numeric `schema`) throws:
476
479
  * that is not a capability document at all.
477
480
  */
478
- function parseSandboxRuntimeCapabilities(value) {
479
- if (value === null || typeof value !== "object" || Array.isArray(value)) invalidCapabilities("a non-object document");
480
- const record = value;
481
+ function parseSandboxRuntimeCapabilities(raw) {
482
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) invalidCapabilities("a non-object document");
483
+ const record = raw;
481
484
  if (typeof record.schema === "number" && Number.isInteger(record.schema) && record.schema > 1) return null;
482
485
  if (record.schema !== 1) invalidCapabilities(`schema version ${JSON.stringify(record.schema)} — this SDK understands 1`);
483
486
  if (typeof record.agentInterface !== "string" || typeof record.sidecarVersion !== "string" || typeof record.image !== "string") invalidCapabilities("a document missing its identity fields");
@@ -485,6 +488,19 @@ function parseSandboxRuntimeCapabilities(value) {
485
488
  if (interactionsValue === null || typeof interactionsValue !== "object" || Array.isArray(interactionsValue)) invalidCapabilities("a malformed 'interactions' group");
486
489
  const interactionsRecord = interactionsValue;
487
490
  if ("responseDedupe" in interactionsRecord && typeof interactionsRecord.responseDedupe !== "boolean") invalidCapabilities("a non-boolean 'interactions.responseDedupe' flag");
491
+ const interactiveAgentValue = record.interactiveAgent;
492
+ let interactiveAgent;
493
+ if (interactiveAgentValue !== void 0) interactiveAgent = readFlagGroup(record, "interactiveAgent", [
494
+ "start",
495
+ "status",
496
+ "attach",
497
+ "control",
498
+ "reattach",
499
+ "sendPrompt",
500
+ "input",
501
+ "resize",
502
+ "stop"
503
+ ]);
488
504
  return {
489
505
  schema: 1,
490
506
  agentInterface: record.agentInterface,
@@ -497,7 +513,8 @@ function parseSandboxRuntimeCapabilities(value) {
497
513
  "idempotent"
498
514
  ]),
499
515
  runs: readFlagGroup(record, "runs", ["executionScopedStatus", "eventReplay"]),
500
- interactions: typeof interactionsRecord.responseDedupe === "boolean" ? { responseDedupe: interactionsRecord.responseDedupe } : {}
516
+ interactions: typeof interactionsRecord.responseDedupe === "boolean" ? { responseDedupe: interactionsRecord.responseDedupe } : {},
517
+ ...interactiveAgent === void 0 ? {} : { interactiveAgent }
501
518
  };
502
519
  }
503
520
  /**
@@ -603,11 +620,19 @@ function validateAcceptedDispatchRunRef(raw, expected, sandboxId, sessionId, exe
603
620
  return accepted;
604
621
  }
605
622
  //#endregion
606
- //#region src/interaction-response.ts
607
- const APPLIED_INTERACTION_STATUS = z.enum(["accepted", "already_resolved_same"]);
608
- const REFUSED_INTERACTION_STATUS = z.enum(["already_resolved_different", "binding_mismatch"]);
609
- const InteractionResolutionSchema = z.strictObject({
610
- status: APPLIED_INTERACTION_STATUS,
623
+ //#region ../../../packages/runtime-contracts/dist/interaction-response.js
624
+ /** Durable states reported for a recorded interaction response. */
625
+ const interactionResolutionDeliveryStateSchema = z.enum([
626
+ "pending",
627
+ "delivering",
628
+ "delivered",
629
+ "delivery_failed",
630
+ "backend_lost",
631
+ "effect_unknown"
632
+ ]);
633
+ /** Canonical result returned after a response is recorded by Sidecar. */
634
+ const interactionResolutionAcknowledgementSchema = z.strictObject({
635
+ status: z.enum(["accepted", "already_resolved_same"]),
611
636
  operationId: z.string().min(1),
612
637
  responseDigest: sha256DigestSchema,
613
638
  interactionId: z.string().min(1),
@@ -617,14 +642,19 @@ const InteractionResolutionSchema = z.strictObject({
617
642
  "declined",
618
643
  "cancelled"
619
644
  ]),
645
+ deliveryState: interactionResolutionDeliveryStateSchema,
620
646
  delivered: z.boolean(),
621
647
  resolvedAt: z.string().min(1),
622
648
  binding: InteractionBindingSchema.optional()
623
649
  });
624
- const InteractionResolutionEnvelopeSchema = z.object({
650
+ /** Canonical success envelope for the public session interaction endpoint. */
651
+ const interactionResolutionResponseSchema = z.strictObject({
625
652
  success: z.literal(true),
626
- resolution: InteractionResolutionSchema
653
+ resolution: interactionResolutionAcknowledgementSchema
627
654
  });
655
+ //#endregion
656
+ //#region src/interaction-response.ts
657
+ const REFUSED_INTERACTION_STATUS = z.enum(["already_resolved_different", "binding_mismatch"]);
628
658
  const InteractionConflictEnvelopeSchema = z.object({
629
659
  success: z.literal(false),
630
660
  currentState: REFUSED_INTERACTION_STATUS,
@@ -667,7 +697,7 @@ function assertResolutionMatches(resolution, sessionId, submitted, response, end
667
697
  async function readInteractionResult(response, endpoint, sessionId, submitted) {
668
698
  const body = await response.text();
669
699
  if (response.ok) {
670
- const parsed = InteractionResolutionEnvelopeSchema.safeParse(parseJson(body, response, endpoint));
700
+ const parsed = interactionResolutionResponseSchema.safeParse(parseJson(body, response, endpoint));
671
701
  if (!parsed.success) throw invalidRuntimeResponse("Sidecar returned an invalid interaction resolution", response, endpoint);
672
702
  assertResolutionMatches(parsed.data.resolution, sessionId, submitted, response, endpoint);
673
703
  return parsed.data.resolution;
@@ -840,7 +870,7 @@ async function submitSandboxInteractionCommand(transport, sessionId, command, op
840
870
  /**
841
871
  * The text carried by a single event, or `undefined` when the event carries no
842
872
  * text payload. Mirrors the event shapes the sidecar/OpenCode runtime emits:
843
- * - `result` — the terminal event; final text under `finalText`/`response`/
873
+ * - `result` — the final response summary; text under `finalText`/`response`/
844
874
  * `output`/`content`/`text`.
845
875
  * - `token` — an incremental token; text under `value`/`delta`/`text`/
846
876
  * `content`.
@@ -1037,277 +1067,558 @@ function collectAgentFinalMessageText(events) {
1037
1067
  return composed.trim().length > 0 ? composed : void 0;
1038
1068
  }
1039
1069
  //#endregion
1040
- //#region src/interactive.ts
1041
- /**
1042
- * Handle for one session's interactive harness. Obtained via
1043
- * `box.session(id).interactive()`; does not hit the network until a method is
1044
- * called.
1045
- */
1046
- var InteractiveSessionHandle = class {
1047
- constructor(host, sessionId) {
1070
+ //#region ../../../packages/runtime-contracts/dist/interactive-session-http.js
1071
+ const runningStatusSchema = AgentInteractiveSessionStatusSchema.options[0];
1072
+ const exitedStatusSchema = AgentInteractiveSessionStatusSchema.options[1];
1073
+ /** Start result for one exact interactive process. */
1074
+ const interactiveSessionStartDataSchema = z.discriminatedUnion("state", [runningStatusSchema.extend({
1075
+ control: AgentInteractiveSessionControlClaimSchema$1,
1076
+ streamUrl: z.string().min(1)
1077
+ }).strict(), exitedStatusSchema.extend({ control: AgentInteractiveSessionControlClaimSchema$1 }).strict()]);
1078
+ const interactiveSessionStartResponseSchema = z.object({
1079
+ success: z.literal(true),
1080
+ data: interactiveSessionStartDataSchema
1081
+ }).strict();
1082
+ z.object({
1083
+ success: z.literal(false),
1084
+ error: z.object({
1085
+ code: z.string().min(1),
1086
+ message: z.string().min(1)
1087
+ }).strict()
1088
+ }).strict();
1089
+ const interactiveSessionPromptResponseSchema = z.object({
1090
+ success: z.literal(true),
1091
+ data: AgentInteractiveSessionPromptAcknowledgementSchema$1
1092
+ }).strict();
1093
+ const interactiveSessionStopResponseSchema = z.object({
1094
+ success: z.literal(true),
1095
+ data: AgentInteractiveSessionStopAcknowledgementSchema$1
1096
+ }).strict();
1097
+ z.object({ ref: AgentInteractiveSessionRefSchema$1 }).strict();
1098
+ z.object({ incarnationId: AgentInteractiveSessionRefSchema$1.shape.incarnationId }).strict();
1099
+ //#endregion
1100
+ //#region src/sandbox-interactive-operations.ts
1101
+ function assertInteractiveRefBelongsToSandbox(ref, sandboxId) {
1102
+ if (ref.run.environmentId !== sandboxId) throw new ValidationError("Interactive session reference does not belong to this sandbox", { "ref.run.environmentId": `Expected ${sandboxId}` });
1103
+ }
1104
+ var SandboxInteractiveOperations = class {
1105
+ constructor(host) {
1048
1106
  this.host = host;
1049
- this.sessionId = sessionId;
1050
1107
  }
1051
- /**
1052
- * Spawn the harness's interactive TUI for this session. Returns the
1053
- * `streamUrl` of the live framebuffer — open it programmatically with
1054
- * `attach()`, or hand it to a terminal frontend. Throws if the harness has
1055
- * no interactive entrypoint (e.g. cursor, which is SDK-only), the binary is
1056
- * not installed, or a session is already running for this id.
1057
- */
1058
- start(options) {
1059
- return this.host._startInteractive(this.sessionId, options);
1108
+ /** @internal — invoked by InteractiveSessionHandle.start(). */
1109
+ async _startInteractive(id, options, requestOptions) {
1110
+ requestOptions?.signal?.throwIfAborted();
1111
+ const exactStart = exactAgentInteractiveSessionStart(options);
1112
+ if (!exactStart.profile.harness) throw new ValidationError("Interactive profile harness is required");
1113
+ assertCurrentBackendType(exactStart.profile.harness, "profile.harness");
1114
+ if (exactStart.run.sessionId !== id) throw new ValidationError("Interactive start request does not belong to the session");
1115
+ if (exactStart.run.environmentId !== this.host.id) throw new ValidationError("Interactive start request does not belong to this sandbox");
1116
+ await this.host.ensureRunning(requestOptions?.signal);
1117
+ const response = await this.host.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/interactive`, {
1118
+ method: "POST",
1119
+ body: JSON.stringify(exactStart),
1120
+ ...requestOptions?.signal === void 0 ? {} : { signal: requestOptions.signal }
1121
+ });
1122
+ if (!response.ok) {
1123
+ const body = await response.text();
1124
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1125
+ }
1126
+ const endpoint = `/agents/sessions/${encodeURIComponent(id)}/interactive`;
1127
+ const parsed = interactiveSessionStartResponseSchema.safeParse(await response.json());
1128
+ if (!parsed.success) throw new ServerError("Interactive session response has an invalid exact identity", 502, {
1129
+ endpoint,
1130
+ origin: "runtime"
1131
+ });
1132
+ const result = parsed.data.data;
1133
+ if (!agentInteractiveSessionRefMatchesStart(exactStart, result.ref) || !agentInteractiveSessionControlClaimMatchesRef(result.ref, result.control)) throw new ServerError("Interactive session response has an invalid exact identity", 502, {
1134
+ endpoint,
1135
+ origin: "runtime"
1136
+ });
1137
+ return result;
1060
1138
  }
1061
- /** Inject a prompt as keystrokes into the live harness (submitted on send). */
1062
- sendPrompt(prompt) {
1063
- return this.host._sendInteractivePrompt(this.sessionId, prompt);
1139
+ /** @internal — invoked by InteractiveSessionHandle.sendPrompt(). */
1140
+ async _sendInteractivePrompt(id, command, options) {
1141
+ options?.signal?.throwIfAborted();
1142
+ const exactCommand = AgentInteractiveSessionPromptCommandSchema.parse(command);
1143
+ if (exactCommand.ref.run.sessionId !== id) throw new ValidationError("Interactive prompt command does not belong to the session");
1144
+ await this.host.ensureRunning(options?.signal);
1145
+ const status = await this._interactiveStatus(id, exactCommand.ref.incarnationId, options);
1146
+ if (!status) throw new NotFoundError("Interactive session", id, {
1147
+ origin: "runtime",
1148
+ endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`
1149
+ });
1150
+ if (canonicalCandidateDigest(status.ref) !== canonicalCandidateDigest(exactCommand.ref)) throw new ValidationError("Interactive prompt ref or control does not match the session");
1151
+ const endpoint = `/agents/sessions/${encodeURIComponent(id)}/interactive/prompt`;
1152
+ const response = await this.host.runtimeFetch(endpoint, {
1153
+ method: "POST",
1154
+ body: JSON.stringify(exactCommand),
1155
+ ...options?.signal === void 0 ? {} : { signal: options.signal }
1156
+ });
1157
+ if (!response.ok) {
1158
+ const body = await response.text();
1159
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1160
+ }
1161
+ const parsed = interactiveSessionPromptResponseSchema.safeParse(await response.json());
1162
+ if (!parsed.success) throw new ServerError("Interactive prompt acknowledgement is invalid", 502, {
1163
+ endpoint,
1164
+ origin: "runtime"
1165
+ });
1166
+ const acknowledgement = parsed.data.data;
1167
+ if (!agentInteractiveSessionPromptAcknowledgementMatchesCommand(exactCommand, acknowledgement)) throw new ServerError("Interactive prompt acknowledgement does not match the command", 502, {
1168
+ endpoint,
1169
+ origin: "runtime"
1170
+ });
1171
+ return acknowledgement;
1064
1172
  }
1065
- /**
1066
- * Attach to the live harness PTY as a {@link TerminalStream}: the same PTY
1067
- * the interactive TUI runs in (the sidecar keys it by this session id), so
1068
- * the stream sees the harness's screen and its keystrokes drive the
1069
- * harness — never a fresh shell. The host mints a short-lived scoped
1070
- * terminal token and dials the session's `streamUrl` through the secured
1071
- * sidecar proxy; resolves once the server acknowledges the attach (the
1072
- * buffered screen then replays through `handlers.onData`).
1073
- *
1074
- * Throws when no interactive session is running for this id — attach after
1075
- * `start()`, or to a session another caller started.
1076
- */
1077
- attach(options) {
1078
- if (!this.host._attachInteractive) return Promise.reject(/* @__PURE__ */ new Error("Interactive session attach is not supported by this host"));
1079
- return this.host._attachInteractive(this.sessionId, options);
1173
+ /** @internal — invoked by InteractiveSessionHandle.claimControl(). */
1174
+ async _claimInteractiveControl(id, request, options) {
1175
+ options?.signal?.throwIfAborted();
1176
+ const exactRequest = AgentInteractiveSessionControlClaimRequestSchema.parse(request);
1177
+ if (exactRequest.ref.run.sessionId !== id) throw new ValidationError("Interactive control claim does not belong to the session");
1178
+ assertInteractiveRefBelongsToSandbox(exactRequest.ref, this.host.id);
1179
+ await this.host.ensureRunning(options?.signal);
1180
+ options?.signal?.throwIfAborted();
1181
+ const endpoint = `/agents/sessions/${encodeURIComponent(id)}/interactive/control`;
1182
+ const response = await this.host.runtimeFetch(endpoint, {
1183
+ method: "POST",
1184
+ body: JSON.stringify(exactRequest),
1185
+ ...options?.signal === void 0 ? {} : { signal: options.signal }
1186
+ });
1187
+ if (!response.ok) {
1188
+ const body = await response.text();
1189
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1190
+ }
1191
+ const parsed = interactiveSessionControlClaimResponseSchema.safeParse(await response.json());
1192
+ if (!parsed.success) throw new ServerError("Interactive control claim acknowledgement is invalid", 502, {
1193
+ endpoint,
1194
+ origin: "runtime"
1195
+ });
1196
+ const acknowledgement = parsed.data.data;
1197
+ if (!agentInteractiveSessionControlClaimAcknowledgementMatchesRequest(exactRequest, acknowledgement)) throw new ServerError("Interactive control claim acknowledgement does not match the request", 502, {
1198
+ endpoint,
1199
+ origin: "runtime"
1200
+ });
1201
+ return acknowledgement;
1080
1202
  }
1081
- /**
1082
- * Observe the session: running, exited (with its exit code), or `null`
1083
- * when the sidecar no longer knows the id. Poll this to detect completion —
1084
- * an interactive session has no structured result stream, so the exit
1085
- * tombstone is the completion signal.
1086
- */
1087
- status() {
1088
- if (!this.host._interactiveStatus) return Promise.reject(/* @__PURE__ */ new Error("Interactive session status is not supported by this host"));
1089
- return this.host._interactiveStatus(this.sessionId);
1203
+ /** @internal — invoked by the interactive attach controller on cleanup. */
1204
+ async _releaseInteractiveControl(id, ref, control, options) {
1205
+ options?.signal?.throwIfAborted();
1206
+ const exactRef = AgentInteractiveSessionRefSchema.parse(ref);
1207
+ const exactControl = AgentInteractiveSessionControlClaimSchema.parse(control);
1208
+ if (exactRef.run.sessionId !== id) throw new ValidationError("Interactive control release does not belong to the session");
1209
+ assertInteractiveRefBelongsToSandbox(exactRef, this.host.id);
1210
+ if (!agentInteractiveSessionControlClaimMatchesRef(exactRef, exactControl)) throw new ValidationError("Interactive control release does not match the session ref");
1211
+ const endpoint = `/agents/sessions/${encodeURIComponent(id)}/interactive/control/release`;
1212
+ const response = await this.host.runtimeFetch(endpoint, {
1213
+ method: "POST",
1214
+ body: JSON.stringify({
1215
+ ref: exactRef,
1216
+ control: exactControl
1217
+ }),
1218
+ ...options?.signal === void 0 ? {} : { signal: options.signal }
1219
+ });
1220
+ if (!response.ok) {
1221
+ const body = await response.text();
1222
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1223
+ }
1224
+ if (!interactiveSessionControlReleaseResponseSchema.safeParse(await response.json()).success) throw new ServerError("Interactive control release response is invalid", 502, {
1225
+ endpoint,
1226
+ origin: "runtime"
1227
+ });
1228
+ }
1229
+ /** @internal — invoked by InteractiveSessionHandle.status(). */
1230
+ async _interactiveStatus(id, incarnationId, options) {
1231
+ options?.signal?.throwIfAborted();
1232
+ await this.host.ensureRunning(options?.signal);
1233
+ const endpoint = `/agents/sessions/${encodeURIComponent(id)}/interactive`;
1234
+ const response = await this.host.runtimeFetch(`${endpoint}?incarnationId=${encodeURIComponent(incarnationId)}`, {
1235
+ method: "GET",
1236
+ ...options?.signal === void 0 ? {} : { signal: options.signal }
1237
+ });
1238
+ if (response.status === 404) return null;
1239
+ if (!response.ok) {
1240
+ const body = await response.text();
1241
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1242
+ }
1243
+ const parsed = interactiveSessionStatusResponseSchema.safeParse(await response.json());
1244
+ if (!parsed.success) throw new ServerError("Interactive status response has an invalid exact identity", 502, {
1245
+ endpoint,
1246
+ origin: "runtime"
1247
+ });
1248
+ const status = parsed.data.data.status;
1249
+ if (status.ref.run.sessionId !== id || status.ref.incarnationId !== incarnationId) throw new ServerError("Interactive status response has an invalid exact identity", 502, {
1250
+ endpoint,
1251
+ origin: "runtime"
1252
+ });
1253
+ assertInteractiveRefBelongsToSandbox(status.ref, this.host.id);
1254
+ return status;
1255
+ }
1256
+ /** @internal — invoked by InteractiveSessionHandle.stop(). */
1257
+ async _stopInteractive(id, command, options) {
1258
+ options?.signal?.throwIfAborted();
1259
+ const exactCommand = AgentInteractiveSessionStopCommandSchema.parse(command);
1260
+ if (exactCommand.ref.run.sessionId !== id) throw new ValidationError("Interactive stop command does not belong to the session");
1261
+ assertInteractiveRefBelongsToSandbox(exactCommand.ref, this.host.id);
1262
+ await this.host.ensureRunning(options?.signal);
1263
+ const endpoint = `/agents/sessions/${encodeURIComponent(id)}/interactive`;
1264
+ const response = await this.host.runtimeFetch(endpoint, {
1265
+ method: "DELETE",
1266
+ body: JSON.stringify(exactCommand),
1267
+ ...options?.signal === void 0 ? {} : { signal: options.signal }
1268
+ });
1269
+ if (!response.ok) {
1270
+ const body = await response.text();
1271
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1272
+ }
1273
+ const parsed = interactiveSessionStopResponseSchema.safeParse(await response.json());
1274
+ if (!parsed.success) throw new ServerError("Interactive stop acknowledgement is invalid", 502, {
1275
+ endpoint,
1276
+ origin: "runtime"
1277
+ });
1278
+ const acknowledgement = parsed.data.data;
1279
+ if (!agentInteractiveSessionStopAcknowledgementMatchesCommand(exactCommand, acknowledgement)) throw new ServerError("Interactive stop acknowledgement does not match the command", 502, {
1280
+ endpoint,
1281
+ origin: "runtime"
1282
+ });
1283
+ return acknowledgement;
1090
1284
  }
1091
- /** Stop the interactive harness and reap its PTY. */
1092
- stop() {
1093
- return this.host._stopInteractive(this.sessionId);
1285
+ /** @internal — lifecycle authority for exact provider teardown. */
1286
+ async _stopInteractiveLifecycle(id, ref, options) {
1287
+ options?.signal?.throwIfAborted();
1288
+ const exactRef = AgentInteractiveSessionRefSchema.parse(ref);
1289
+ if (exactRef.run.sessionId !== id) throw new ValidationError("Interactive lifecycle stop reference does not belong to the session");
1290
+ assertInteractiveRefBelongsToSandbox(exactRef, this.host.id);
1291
+ await this.host.ensureRunning(options?.signal);
1292
+ const endpoint = `/agents/sessions/${encodeURIComponent(id)}/interactive/lifecycle`;
1293
+ const response = await this.host.runtimeFetch(endpoint, {
1294
+ method: "DELETE",
1295
+ body: JSON.stringify({ ref: exactRef }),
1296
+ ...options?.signal === void 0 ? {} : { signal: options.signal }
1297
+ });
1298
+ if (!response.ok) {
1299
+ const body = await response.text();
1300
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1301
+ }
1302
+ const parsed = interactiveSessionStopResponseSchema.safeParse(await response.json());
1303
+ if (!parsed.success) throw new ServerError("Interactive lifecycle stop acknowledgement is invalid", 502, {
1304
+ endpoint,
1305
+ origin: "runtime"
1306
+ });
1307
+ const acknowledgement = parsed.data.data;
1308
+ if (canonicalCandidateDigest(acknowledgement.ref) !== canonicalCandidateDigest(exactRef)) throw new ServerError("Interactive lifecycle stop acknowledgement does not match the exact ref", 502, {
1309
+ endpoint,
1310
+ origin: "runtime"
1311
+ });
1312
+ return acknowledgement;
1094
1313
  }
1095
1314
  };
1096
1315
  //#endregion
1097
- //#region src/session.ts
1098
- /**
1099
- * A single agent session inside a sandbox. Created via
1100
- * `box.session(id)` — does not hit the network until a method is called.
1101
- */
1102
- var SandboxSession = class SandboxSession {
1103
- /**
1104
- * @internal SDK-internal constructor — apps should call `box.session(id)`.
1105
- */
1106
- constructor(box, id) {
1107
- this.box = box;
1108
- this.id = id;
1316
+ //#region src/scoped-token.ts
1317
+ /** Maximum time for the exact interactive terminal-token request. */
1318
+ const SCOPED_TOKEN_REQUEST_TIMEOUT_MS = 5e3;
1319
+ const scopedTokenResponseSchema = z.object({
1320
+ token: z.string().min(1),
1321
+ expiresAt: z.number().finite().nonnegative(),
1322
+ runtimeUrl: z.string().min(1),
1323
+ sidecarProxyUrl: z.string().min(1),
1324
+ scope: z.enum([
1325
+ "session",
1326
+ "session-runtime",
1327
+ "project",
1328
+ "read-only"
1329
+ ])
1330
+ });
1331
+ /** Reject a scoped credential that the SDK must not send to a consumer. */
1332
+ function assertScopedTokenActive(token, expectedScope) {
1333
+ if (expectedScope !== void 0 && token.scope !== expectedScope) throw new ValidationError(`Scoped token must use the ${expectedScope} scope`);
1334
+ if (typeof token.token !== "string" || !token.token.trim() || !(token.expiresAt instanceof Date) || !Number.isFinite(token.expiresAt.getTime()) || token.expiresAt.getTime() <= Date.now()) throw new ValidationError("Scoped token is expired or invalid");
1335
+ }
1336
+ function parseScopedTokenResponse(raw, expectedScope) {
1337
+ const parsed = scopedTokenResponseSchema.safeParse(raw);
1338
+ if (!parsed.success) throw new ServerError("Sandbox API returned an invalid scoped token response.", 502, { origin: "sandbox-api" });
1339
+ if (parsed.data.scope !== expectedScope) throw new ServerError(`Sandbox API returned a ${parsed.data.scope} token for a ${expectedScope} request.`, 502, { origin: "sandbox-api" });
1340
+ const token = {
1341
+ token: parsed.data.token,
1342
+ expiresAt: /* @__PURE__ */ new Date(parsed.data.expiresAt * 1e3),
1343
+ runtimeUrl: parsed.data.runtimeUrl,
1344
+ sidecarProxyUrl: parsed.data.sidecarProxyUrl,
1345
+ scope: parsed.data.scope
1346
+ };
1347
+ try {
1348
+ assertScopedTokenActive(token, expectedScope);
1349
+ } catch {
1350
+ throw new ServerError("Sandbox API returned an expired or invalid scoped token.", 502, { origin: "sandbox-api" });
1109
1351
  }
1110
- /**
1111
- * Fetch the current session state from the sandbox. Includes status,
1112
- * model, prompt count, token usage if known, and timing metadata.
1113
- *
1114
- * Throws on transport error; returns `null` if the session id is not
1115
- * known to the sandbox (e.g. it ended and was reaped, or the id is
1116
- * invalid).
1117
- */
1118
- async status() {
1119
- return this.box._sessionStatus(this.id);
1352
+ return token;
1353
+ }
1354
+ async function requestScopedToken(client, sandboxId, options, requestOptions) {
1355
+ if ((options.scope === "session" || options.scope === "session-runtime") && !options.sessionId) throw new ValidationError(`${options.scope} scope requires a sessionId`);
1356
+ if (options.scope === "session" && !options.runtimeSessionId) throw new ValidationError("session scope requires the actual runtimeSessionId");
1357
+ const response = await client.fetch(`/v1/sandboxes/${encodeURIComponent(sandboxId)}/scoped-token`, {
1358
+ method: "POST",
1359
+ headers: { "Content-Type": "application/json" },
1360
+ body: JSON.stringify({
1361
+ scope: options.scope,
1362
+ ...options.sessionId ? { sessionId: options.sessionId } : {},
1363
+ ...options.scope === "session" ? { runtimeSessionId: options.runtimeSessionId } : {},
1364
+ ...options.ttlMinutes ? { ttlMinutes: options.ttlMinutes } : {}
1365
+ }),
1366
+ ...options.signal === void 0 ? {} : { signal: options.signal }
1367
+ }, requestOptions);
1368
+ if (!response.ok) {
1369
+ const body = await response.text();
1370
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1120
1371
  }
1121
- /**
1122
- * Stream events from this session as they arrive. With no `since`,
1123
- * starts at the live tail; with `since`, replays strictly after that
1124
- * event id — useful for reconnect-after-disconnect flows.
1125
- *
1126
- * The async iterator terminates when the session reaches a terminal
1127
- * state (`completed`, `failed`, `cancelled`) and the corresponding
1128
- * terminal event has been yielded, OR when the caller's signal aborts.
1129
- */
1130
- events(opts) {
1131
- return this.box._sessionEvents(this.id, opts);
1372
+ let raw;
1373
+ try {
1374
+ raw = await response.json();
1375
+ } catch {
1376
+ throw new ServerError("Sandbox API returned invalid scoped token JSON.", 502, { origin: "sandbox-api" });
1132
1377
  }
1133
- /**
1134
- * Await a terminal result. With `executionId`, drains only that execution;
1135
- * without it, reads status and selects the latest execution. Event replay
1136
- * makes either form safe after the execution has already finished.
1137
- *
1138
- * Use this to wait for a session that was started by another caller
1139
- * (e.g. `dispatchPrompt`).
1140
- */
1141
- async result(options) {
1142
- return options ? this.box._sessionResult(this.id, options) : this.box._sessionResult(this.id);
1378
+ return parseScopedTokenResponse(raw, options.scope);
1379
+ }
1380
+ function parseInteractiveSessionTokenRef(ref) {
1381
+ const parsed = AgentInteractiveSessionRefSchema.safeParse(ref);
1382
+ if (!parsed.success) throw new ValidationError("Interactive session ref is invalid.", { ref: parsed.error.issues[0]?.message ?? "invalid ref" });
1383
+ return parsed.data;
1384
+ }
1385
+ function interactiveSessionTokenOptions(ref, options = {}) {
1386
+ const exactRef = parseInteractiveSessionTokenRef(ref);
1387
+ return {
1388
+ sandboxId: exactRef.run.environmentId,
1389
+ request: {
1390
+ scope: "session-runtime",
1391
+ sessionId: exactRef.run.sessionId,
1392
+ ...options.ttlMinutes === void 0 ? {} : { ttlMinutes: options.ttlMinutes },
1393
+ ...options.signal === void 0 ? {} : { signal: options.signal }
1394
+ }
1395
+ };
1396
+ }
1397
+ function requestInteractiveSessionToken(client, ref, options) {
1398
+ const exact = interactiveSessionTokenOptions(ref, options);
1399
+ return requestScopedToken(client, exact.sandboxId, exact.request, { timeoutMs: SCOPED_TOKEN_REQUEST_TIMEOUT_MS });
1400
+ }
1401
+ //#endregion
1402
+ //#region src/sandbox-interactive-terminal.ts
1403
+ var SandboxInteractiveTerminalOperations = class {
1404
+ constructor(host) {
1405
+ this.host = host;
1143
1406
  }
1144
1407
  /**
1145
- * List persisted messages for this session, including in-flight assistant
1146
- * content when the runtime has flushed partial output.
1408
+ * Resolve a sandbox-relative terminal stream path against the browser-safe
1409
+ * sidecar proxy base returned by the scoped-token mint. Concatenation, not
1410
+ * URL resolution: the proxy base carries a path prefix
1411
+ * (`/v1/sidecar-proxy/{id}`) that `new URL(absolutePath, base)` would drop.
1147
1412
  */
1148
- async messages(opts = {}) {
1149
- return this.box.messages({
1150
- ...opts,
1151
- sessionId: this.id
1152
- });
1413
+ resolveTerminalStreamUrl(sidecarProxyUrl, streamPath) {
1414
+ if (!streamPath.startsWith("/")) throw new ServerError(`Terminal stream path must be sandbox-relative (got "${streamPath}")`, 502, { origin: "runtime" });
1415
+ return `${sidecarProxyUrl.replace(/\/+$/, "")}${streamPath}`;
1153
1416
  }
1154
- /**
1155
- * Continue this session with an additional prompt. Equivalent to
1156
- * `box.prompt(message, { ...opts, sessionId: this.id })` but reads
1157
- * naturally on a Session reference.
1158
- */
1159
- async prompt(message, opts) {
1160
- return this.box.prompt(message, {
1161
- ...opts,
1162
- sessionId: this.id
1417
+ /** @internal — invoked by InteractiveSessionHandle.attach(). */
1418
+ async _attachInteractive(id, ref, request, options, reconnectAuthority) {
1419
+ options?.signal?.throwIfAborted();
1420
+ const exactRef = AgentInteractiveSessionRefSchema.parse(ref);
1421
+ if (exactRef.run.sessionId !== id) throw new ValidationError("Interactive attach ref does not belong to the session");
1422
+ assertInteractiveRefBelongsToSandbox(exactRef, this.host.id);
1423
+ const { handlers, ...attachRequest } = request;
1424
+ const exactRequest = AgentInteractiveSessionAttachSchema.parse(attachRequest);
1425
+ if (!agentInteractiveSessionControlClaimMatchesRef(exactRef, exactRequest.control)) throw new ValidationError("Interactive attach control does not match the session ref");
1426
+ const status = await this.host.interactiveStatus(id, exactRef.incarnationId, options);
1427
+ if (!status) throw new NotFoundError("Interactive session", id, {
1428
+ origin: "runtime",
1429
+ endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`
1430
+ });
1431
+ if (status.state !== "running") throw new StateError(`Interactive session ${id} has exited; there is no live PTY to attach`, status.state, "running", {
1432
+ origin: "runtime",
1433
+ endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`
1434
+ }, "INTERACTIVE_SESSION_EXITED");
1435
+ if (canonicalCandidateDigest(status.ref) !== canonicalCandidateDigest(exactRef)) throw new ValidationError("Interactive attach ref does not match the current session");
1436
+ const scoped = await this.host.mintScopedToken({
1437
+ scope: "session-runtime",
1438
+ sessionId: id,
1439
+ ...options?.signal === void 0 ? {} : { signal: options.signal }
1440
+ });
1441
+ const stream = await this.host.terminalStreamConnect({
1442
+ url: this.resolveTerminalStreamUrl(scoped.sidecarProxyUrl, `/terminals/${encodeURIComponent(id)}/ws`),
1443
+ token: this.terminalTokenProvider(scoped, id),
1444
+ connectionId: id,
1445
+ cols: exactRequest.cols,
1446
+ rows: exactRequest.rows,
1447
+ incarnationId: exactRef.incarnationId,
1448
+ control: exactRequest.control,
1449
+ handlers,
1450
+ signal: options?.signal,
1451
+ ...reconnectAuthority === void 0 ? {} : { reconnectAuthority }
1163
1452
  });
1453
+ if (options?.signal?.aborted) {
1454
+ await stream.close();
1455
+ options.signal.throwIfAborted();
1456
+ }
1457
+ return stream;
1164
1458
  }
1165
1459
  /**
1166
- * Submit a structured message through the session message endpoint. The
1167
- * returned receipt confirms admission; follow progress with `events()` or
1168
- * await the terminal response with `result()`. Model and message identifiers
1169
- * are translated to the runtime's wire names by the SDK.
1460
+ * Per-dial credential for a terminal attach. The first dial consumes the
1461
+ * token minted alongside the proxy URL (no second mint); every later dial
1462
+ * (a `reconnect()` after the socket dropped or the detach window) re-mints,
1463
+ * because scoped terminal tokens are clamped to a <= 15-minute TTL and the
1464
+ * original is likely dead by then.
1170
1465
  */
1171
- async sendMessage(request, options) {
1172
- return this.box._sessionSendMessage(this.id, request, options);
1173
- }
1174
- /** Delete this session and its persisted runtime state. */
1175
- async delete() {
1176
- return this.box._sessionDelete(this.id);
1466
+ terminalTokenProvider(initialToken, sessionId) {
1467
+ let unused = initialToken;
1468
+ return async (signal) => {
1469
+ signal?.throwIfAborted();
1470
+ if (unused !== null) {
1471
+ const token = unused;
1472
+ unused = null;
1473
+ assertScopedTokenActive(token, "session-runtime");
1474
+ return token;
1475
+ }
1476
+ const scoped = await this.host.mintScopedToken({
1477
+ scope: "session-runtime",
1478
+ sessionId,
1479
+ signal
1480
+ });
1481
+ assertScopedTokenActive(scoped, "session-runtime");
1482
+ return scoped;
1483
+ };
1177
1484
  }
1178
- /**
1179
- * Fork this session into a new queued session. The fork shares the
1180
- * parent workspace and copies conversation history up to `messageId`
1181
- * when supplied.
1182
- */
1183
- async fork(opts) {
1184
- const info = await this.box._sessionFork(this.id, opts);
1185
- return {
1186
- session: new SandboxSession(this.box, info.id),
1187
- info
1188
- };
1189
- }
1190
- /**
1191
- * Drive this session's harness in INTERACTIVE mode: spawn its native TUI,
1192
- * stream the live framebuffer, and inject prompts as keystrokes. Distinct
1193
- * from the headless `prompt()`/`events()` surface above. Lazy — does not hit
1194
- * the network until a handle method is called.
1195
- */
1196
- interactive() {
1197
- return new InteractiveSessionHandle(this.box, this.id);
1198
- }
1199
- /**
1200
- * Resolve a pending permission interaction the agent raised mid-turn,
1201
- * forwarding `allow`/`deny` to the blocked agent through the unified
1202
- * interaction channel. Throws if no matching permission is outstanding.
1203
- */
1204
- async respondToPermission(permissionID, options) {
1205
- return this.box._respondToPermission(this.id, permissionID, options);
1206
- }
1207
- /** Return every interaction that still awaits a response on this session. */
1208
- async interactions(options) {
1209
- return this.box._listInteractions(this.id, options);
1210
- }
1211
- /** Respond to one exact interaction with a retry-safe command. */
1212
- async respondToInteraction(command, options) {
1213
- return this.box._respondToInteraction(this.id, command, options);
1214
- }
1215
- /**
1216
- * Interrupt an execution without deleting the session. Pass `executionId`
1217
- * to make the cancellation exact: a newer run on the same session is never
1218
- * touched. Without it, the current execution is selected. Returns
1219
- * `{ cancelled: false }` when the selected execution is not running.
1220
- */
1221
- async interrupt(options) {
1222
- return options ? this.box._interrupt(this.id, options) : this.box._interrupt(this.id);
1223
- }
1224
- /**
1225
- * Cancel one exact run through the durable operation contract. Reusing the
1226
- * same request after a lost response returns the sidecar's replayed
1227
- * acknowledgement instead of issuing a second cancellation.
1228
- */
1229
- async cancelRun(request, options) {
1230
- return this.box._cancelRun(this.id, request, options);
1231
- }
1232
- /** Answer one exact question without selecting another outstanding ask. */
1233
- async answer(interactionId, answers, options) {
1234
- return this.box._answerQuestion(this.id, interactionId, answers, options);
1235
- }
1236
- /** Return the newest plan awaiting a decision for this session. */
1237
- async plan() {
1238
- return this.box._currentPlan(this.id);
1239
- }
1240
- /** Approve a durable plan and enqueue its separate execution turn. */
1241
- async approvePlan(planId) {
1242
- const id = planId ?? (await this.requireCurrentPlan()).id;
1243
- return this.box._decidePlan(this.id, id, { outcome: "approved" });
1244
- }
1245
- /** Reject a durable plan with required feedback and enqueue a revision turn. */
1246
- async rejectPlan(feedback, planId) {
1247
- if (!feedback.trim()) throw new Error("Plan rejection feedback is required");
1248
- const id = planId ?? (await this.requireCurrentPlan()).id;
1249
- return this.box._decidePlan(this.id, id, {
1250
- outcome: "rejected",
1251
- feedback: feedback.trim()
1485
+ /** @internal — invoked by `box.terminals.attach()`. */
1486
+ async _attachTerminal(connectionId, options = {}) {
1487
+ if (!connectionId.trim()) throw new ValidationError("Terminal connectionId is required");
1488
+ const scoped = await this.host.mintScopedToken({
1489
+ scope: "session-runtime",
1490
+ sessionId: connectionId
1491
+ });
1492
+ return this.host.terminalStreamConnect({
1493
+ url: this.resolveTerminalStreamUrl(scoped.sidecarProxyUrl, `/terminals/${encodeURIComponent(connectionId)}/ws`),
1494
+ token: this.terminalTokenProvider(scoped, connectionId),
1495
+ connectionId,
1496
+ cols: options.cols,
1497
+ rows: options.rows,
1498
+ command: options.command,
1499
+ cwd: options.cwd,
1500
+ env: options.env,
1501
+ handlers: options.handlers
1252
1502
  });
1253
- }
1254
- async requireCurrentPlan() {
1255
- const plan = await this.plan();
1256
- if (!plan) throw new Error("No durable plan is awaiting a decision");
1257
- return plan;
1258
1503
  }
1259
1504
  };
1505
+ const TERMINAL_WS_MAX_DIMENSION = 4096;
1506
+ /** Input chunk size that leaves room for JSON escaping inside one frame. */
1507
+ const TERMINAL_WS_INPUT_FRAME_CHUNK_CHARS = 128 * 1024;
1508
+ /** WebSocket authentication envelope prefixes. */
1509
+ const TERMINAL_WS_EDGE_BEARER_SUBPROTOCOL_PREFIX = "bearer.";
1510
+ const TERMINAL_WS_SIDECAR_BEARER_SUBPROTOCOL_PREFIX = "tangle.bearer.";
1511
+ /** Non-credential protocol selected during a successful terminal handshake. */
1512
+ const TERMINAL_WS_ECHO_SUBPROTOCOL = "tangle.terminal.v1";
1513
+ const terminalGeometry = {
1514
+ cols: z.number().int().min(1).max(TERMINAL_WS_MAX_DIMENSION),
1515
+ rows: z.number().int().min(1).max(TERMINAL_WS_MAX_DIMENSION)
1516
+ };
1517
+ const terminalInitMessageSchema = z.object({
1518
+ type: z.literal("init"),
1519
+ connectionId: z.string().optional(),
1520
+ /** Required for an interactive-agent PTY; absent for generic terminals. */
1521
+ incarnationId: z.string().uuid().optional(),
1522
+ ...terminalGeometry,
1523
+ command: z.string().min(1).optional(),
1524
+ cwd: z.string().min(1).optional(),
1525
+ env: z.record(z.string(), z.string()).optional(),
1526
+ sessionId: z.string().min(1).optional(),
1527
+ /** Canonical control claim for an interactive-agent PTY. */
1528
+ control: AgentInteractiveSessionControlClaimSchema.optional()
1529
+ }).superRefine((message, refinement) => {
1530
+ if (message.incarnationId !== void 0 && message.control === void 0) refinement.addIssue({
1531
+ code: "custom",
1532
+ path: ["control"],
1533
+ message: "Interactive session attach requires a control claim"
1534
+ });
1535
+ if (message.incarnationId === void 0 && message.control !== void 0) refinement.addIssue({
1536
+ code: "custom",
1537
+ path: ["control"],
1538
+ message: "Generic terminal attach cannot carry an interactive control claim"
1539
+ });
1540
+ });
1541
+ const terminalInputMessageSchema = z.object({
1542
+ type: z.literal("input"),
1543
+ data: z.string()
1544
+ });
1545
+ const terminalResizeMessageSchema = z.object({
1546
+ type: z.literal("resize"),
1547
+ ...terminalGeometry
1548
+ });
1549
+ const terminalRenewMessageSchema = z.object({ type: z.literal("renew") });
1550
+ const terminalCloseMessageSchema = z.object({ type: z.literal("close") });
1551
+ z.union([
1552
+ terminalInitMessageSchema,
1553
+ terminalInputMessageSchema,
1554
+ terminalResizeMessageSchema,
1555
+ terminalRenewMessageSchema,
1556
+ terminalCloseMessageSchema
1557
+ ]);
1558
+ const terminalReadyMessageSchema = z.object({
1559
+ type: z.literal("ready"),
1560
+ connectionId: z.string(),
1561
+ sessionId: z.string(),
1562
+ restored: z.boolean(),
1563
+ detachTimeoutMs: z.number().int().positive()
1564
+ });
1565
+ const terminalExitMessageSchema = z.object({
1566
+ type: z.literal("exit"),
1567
+ exitCode: z.number().optional(),
1568
+ exitSignal: z.string().optional()
1569
+ });
1570
+ const terminalRenewedMessageSchema = z.object({
1571
+ type: z.literal("renewed"),
1572
+ sessionId: z.string()
1573
+ });
1574
+ const terminalErrorMessageSchema = z.object({
1575
+ type: z.literal("error"),
1576
+ code: z.string(),
1577
+ message: z.string()
1578
+ });
1579
+ const terminalServerMessageSchema = z.union([
1580
+ terminalReadyMessageSchema,
1581
+ terminalExitMessageSchema,
1582
+ terminalRenewedMessageSchema,
1583
+ terminalErrorMessageSchema
1584
+ ]);
1260
1585
  //#endregion
1261
- //#region src/tangle/tee-intercept.ts
1262
- /** hono's default unmatched-route body. */
1263
- const HONO_NOT_FOUND_BODY = "404 Not Found";
1586
+ //#region src/terminal-stream.ts
1264
1587
  /**
1265
- * A 404 carrying hono's own unmatched-route body came from the router.
1588
+ * TerminalStream — typed client for the sandbox terminal WebSocket protocol.
1266
1589
  *
1267
- * hono answers an unmatched route with exactly "404 Not Found", so that body
1268
- * is the deployment saying it has no such route — which for `/tee` means the
1269
- * operator intercept is absent, not that the sandbox is missing.
1270
- * `ensureRunning()` has already confirmed the sandbox at this point, which is
1271
- * what makes the distinction safe to draw.
1272
- */
1273
- function isRouterNotFound(body) {
1274
- return body === HONO_NOT_FOUND_BODY;
1275
- }
1276
- /**
1277
- * Request a TEE surface, refusing by name when the deployment cannot serve it.
1590
+ * Drives a live PTY session (an interactive harness TUI or a plain shell)
1591
+ * from the caller's own program: binary frames stream raw PTY output bytes,
1592
+ * TEXT JSON frames carry the control protocol. The browser-safe wire contract
1593
+ * is defined by the shared browser-safe runtime contract and shared with the
1594
+ * sidecar:
1278
1595
  *
1279
- * Returns the successful response for the caller to parse. Throws
1280
- * `CapabilityError` when the intercept is absent, and otherwise the same
1281
- * parsed error every other SDK call throws.
1596
+ * client → server (TEXT JSON):
1597
+ * init {connectionId?, cols, rows, command?, cwd?, env?} — MUST be first
1598
+ * input {data}
1599
+ * resize {cols, rows}
1600
+ *
1601
+ * Interactive-agent init frames add the exact process authority:
1602
+ * {incarnationId, control: {refDigest, generation, leaseId, holderId, expiresAt}}
1603
+ * Subsequent input and resize frames carry only their operation fields; the
1604
+ * server binds authority to the init for the socket lifetime.
1605
+ * server → client (TEXT JSON):
1606
+ * ready {connectionId, sessionId, restored, detachTimeoutMs}
1607
+ * exit {exitCode?, exitSignal?}
1608
+ * error {code, message}
1609
+ * rec / rec.done — recording tee frames for the platform's session
1610
+ * capture consumer; not part of the client surface
1611
+ * server → client (BINARY): raw PTY output bytes
1612
+ *
1613
+ * Detach/reconnect: the server keeps the PTY alive for `detachTimeoutMs`
1614
+ * (5 minutes today) after the socket closes; re-dialing with the SAME
1615
+ * connectionId reattaches and replays the buffered output history, so a
1616
+ * fresh client repaints the current screen.
1617
+ *
1618
+ * Browser-safe: uses the global `WebSocket` (browsers, Workers, Node >= 22)
1619
+ * and never imports `ws` — the same environment contract as
1620
+ * `session-gateway/client.ts`.
1282
1621
  */
1283
- async function fetchTeeSurface(client, sandboxId, surface, init) {
1284
- const path = `/v1/sandboxes/${encodeURIComponent(sandboxId)}/tee/${surface}`;
1285
- const response = await client.fetch(path, init);
1286
- if (response.ok) return response;
1287
- const body = await response.text();
1288
- if (response.status === 404 && isRouterNotFound(body)) throw new CapabilityError(`TEE ${surface} is not served by this deployment. The /tee paths are answered by a Tangle confidential operator that intercepts them before the sandbox API; a sandbox reached through the public API has no such route. Point the client at the operator, or create the sandbox with driver.type "tangle" on a deployment that fronts it.`, "TEE_UNSUPPORTED", 404, void 0, "tee");
1289
- throw parseErrorResponse(response.status, body, void 0, response.headers);
1290
- }
1291
- //#endregion
1292
- //#region src/task-session.ts
1293
- /** A background task session with isolated changeset operations. */
1294
- var SandboxTaskSession = class extends SandboxSession {
1295
- /** @internal Apps should call `box.taskSession(id)`. */
1296
- constructor(taskBox, id) {
1297
- super(taskBox, id);
1298
- this.taskBox = taskBox;
1299
- }
1300
- /** Read the task's isolated file changes. */
1301
- async changes() {
1302
- return this.taskBox._taskSessionChanges(this.id);
1303
- }
1304
- /** Apply all or a selected subset of the task's isolated file changes. */
1305
- async commit(options) {
1306
- return this.taskBox._taskSessionCommit(this.id, options);
1307
- }
1308
- };
1309
- //#endregion
1310
- //#region src/terminal-stream.ts
1311
1622
  /** A protocol `error` control frame or a transport failure. */
1312
1623
  var TerminalStreamError = class extends Error {
1313
1624
  /** Protocol error code (e.g. `INIT_FAILED`) or `TRANSPORT` for socket faults. */
@@ -1319,38 +1630,17 @@ var TerminalStreamError = class extends Error {
1319
1630
  }
1320
1631
  };
1321
1632
  /**
1322
- * The sidecar caps WebSocket frames at 1 MiB (`WS_MAX_PAYLOAD_BYTES` in
1323
- * `apps/sidecar/src/routes/terminal-ws.ts`) and drops the connection on
1633
+ * The sidecar caps WebSocket frames at 1 MiB
1634
+ * (`TERMINAL_WS_MAX_PAYLOAD_BYTES` in the shared runtime contract) and drops
1635
+ * the connection on
1324
1636
  * larger frames. `write()` chunks input so every serialized `input` frame
1325
1637
  * stays under that cap: 128 Ki characters per chunk × a worst-case JSON
1326
1638
  * escape expansion of 6 bytes per character (`` etc.) ≈ 768 KiB,
1327
1639
  * comfortably below the limit including the frame's JSON envelope.
1328
1640
  */
1329
- const INPUT_FRAME_CHUNK_CHARS = 128 * 1024;
1330
1641
  const DEFAULT_COLS = 80;
1331
1642
  const DEFAULT_ROWS = 24;
1332
1643
  const DEFAULT_CONNECT_TIMEOUT_MS = 15e3;
1333
- /** Geometry bounds mirrored from the sidecar's init/resize schemas. */
1334
- const MIN_DIMENSION = 1;
1335
- const MAX_DIMENSION = 4096;
1336
- const EDGE_BEARER_SUBPROTOCOL_PREFIX = "bearer.";
1337
- const SIDECAR_BEARER_SUBPROTOCOL_PREFIX = "tangle.bearer.";
1338
- /**
1339
- * Fixed non-credential marker offered on EVERY dial (both auth formats),
1340
- * giving a spec-compliant server something safe to select into the 101's
1341
- * `Sec-WebSocket-Protocol`. Two reasons it exists:
1342
- *
1343
- * 1. RFC 6455 §4.1 says a client that offered subprotocols MUST fail the
1344
- * connection when the server selects none. Node's WebSocket (undici,
1345
- * per the WHATWG spec) enforces this strictly; browsers are lenient and
1346
- * tolerate a 101 with no selected subprotocol — which is why the
1347
- * browser-only reference client got away with offering just the
1348
- * credential.
1349
- * 2. The server must never echo the token-bearing offer back (that would
1350
- * leak the live credential into a response header) — so it needs a
1351
- * non-credential protocol it CAN select.
1352
- */
1353
- const TERMINAL_WS_ECHO_SUBPROTOCOL = "tangle.terminal.v1";
1354
1644
  /**
1355
1645
  * base64url-encode a UTF-8 string without padding — the exact envelope the
1356
1646
  * edge's `parseTerminalWsSubprotocolToken` decodes (its strict round-trip
@@ -1364,19 +1654,56 @@ function base64UrlEncodeUtf8(value) {
1364
1654
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1365
1655
  }
1366
1656
  function buildAuthSubprotocols(format, token) {
1367
- if (format === "sidecar-direct") return [`${SIDECAR_BEARER_SUBPROTOCOL_PREFIX}${token}`, TERMINAL_WS_ECHO_SUBPROTOCOL];
1368
- return [`${EDGE_BEARER_SUBPROTOCOL_PREFIX}${base64UrlEncodeUtf8(token)}`, TERMINAL_WS_ECHO_SUBPROTOCOL];
1657
+ if (format === "sidecar-direct") return [`${TERMINAL_WS_SIDECAR_BEARER_SUBPROTOCOL_PREFIX}${token}`, TERMINAL_WS_ECHO_SUBPROTOCOL];
1658
+ return [`${TERMINAL_WS_EDGE_BEARER_SUBPROTOCOL_PREFIX}${base64UrlEncodeUtf8(token)}`, TERMINAL_WS_ECHO_SUBPROTOCOL];
1369
1659
  }
1370
1660
  /**
1371
1661
  * Resolve the dial credential: invoke a provider (fresh token per dial) or
1372
1662
  * pass a string through. Fails loud on an empty result — a blank credential
1373
1663
  * would dial and be rejected with an opaque 401 downstream.
1374
1664
  */
1375
- async function resolveTerminalToken(token) {
1376
- const value = typeof token === "function" ? await token() : token;
1665
+ async function resolveTerminalToken(token, signal) {
1666
+ const value = typeof token === "function" ? await token(signal) : token;
1667
+ if (typeof value === "object" && value !== null) {
1668
+ try {
1669
+ assertScopedTokenActive(value, "session-runtime");
1670
+ } catch {
1671
+ throw new TerminalStreamError("Terminal stream scoped token is expired or invalid", "INVALID_TOKEN");
1672
+ }
1673
+ return value.token;
1674
+ }
1377
1675
  if (typeof value !== "string" || !value.trim()) throw new TerminalStreamError(typeof token === "function" ? "Terminal stream token provider returned an empty token" : "Terminal stream token must not be empty", "INVALID_TOKEN");
1378
1676
  return value;
1379
1677
  }
1678
+ function terminalAbortReason(signal) {
1679
+ return signal.reason instanceof Error ? signal.reason : new DOMException("Terminal attach was aborted", "AbortError");
1680
+ }
1681
+ function awaitTerminalOperation(promise, signal) {
1682
+ if (!signal) return promise;
1683
+ signal.throwIfAborted();
1684
+ return new Promise((resolve, reject) => {
1685
+ let settled = false;
1686
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
1687
+ const onAbort = () => {
1688
+ if (settled) return;
1689
+ settled = true;
1690
+ cleanup();
1691
+ reject(terminalAbortReason(signal));
1692
+ };
1693
+ signal.addEventListener("abort", onAbort, { once: true });
1694
+ promise.then((value) => {
1695
+ if (settled) return;
1696
+ settled = true;
1697
+ cleanup();
1698
+ resolve(value);
1699
+ }, (cause) => {
1700
+ if (settled) return;
1701
+ settled = true;
1702
+ cleanup();
1703
+ reject(cause);
1704
+ });
1705
+ });
1706
+ }
1380
1707
  function toWebSocketUrl(url) {
1381
1708
  let parsed;
1382
1709
  try {
@@ -1390,7 +1717,48 @@ function toWebSocketUrl(url) {
1390
1717
  return parsed.toString();
1391
1718
  }
1392
1719
  function assertDimension(name, value) {
1393
- if (!Number.isInteger(value) || value < MIN_DIMENSION || value > MAX_DIMENSION) throw new TerminalStreamError(`${name} must be an integer in [${MIN_DIMENSION}, ${MAX_DIMENSION}], got ${value}`, "INVALID_DIMENSION");
1720
+ if (!Number.isInteger(value) || value < 1 || value > 4096) throw new TerminalStreamError(`${name} must be an integer in [1, ${TERMINAL_WS_MAX_DIMENSION}], got ${value}`, "INVALID_DIMENSION");
1721
+ }
1722
+ function normalizeInteractiveSessionIdentity(incarnationId, control) {
1723
+ if (incarnationId === void 0 && control === void 0) return void 0;
1724
+ if (typeof incarnationId !== "string" || incarnationId.length === 0 || incarnationId.trim() !== incarnationId) throw new TerminalStreamError("Interactive terminal attach requires an exact incarnationId", "INVALID_INTERACTIVE_IDENTITY");
1725
+ if (control === void 0) throw new TerminalStreamError("Interactive terminal attach requires an exact control claim", "INVALID_INTERACTIVE_CONTROL");
1726
+ const parsedControl = AgentInteractiveSessionControlClaimSchema.safeParse(control);
1727
+ if (!parsedControl.success) throw new TerminalStreamError("Interactive terminal attach requires an exact control claim", "INVALID_INTERACTIVE_CONTROL");
1728
+ const expiresAt = Date.parse(parsedControl.data.expiresAt);
1729
+ if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) throw new TerminalStreamError("Interactive terminal control claim is expired", "INTERACTIVE_CONTROL_EXPIRED");
1730
+ return {
1731
+ incarnationId,
1732
+ control: parsedControl.data
1733
+ };
1734
+ }
1735
+ function interactiveInitFields(identity) {
1736
+ if (identity === void 0) return {};
1737
+ return {
1738
+ incarnationId: identity.incarnationId,
1739
+ control: identity.control
1740
+ };
1741
+ }
1742
+ function buildTerminalInitMessage(options, identity) {
1743
+ if (typeof options.connectionId !== "string" || !options.connectionId.trim()) throw new TerminalStreamError("Terminal stream connectionId must not be empty", "INVALID_CONNECTION_ID");
1744
+ if (options.cols !== void 0) assertDimension("cols", options.cols);
1745
+ if (options.rows !== void 0) assertDimension("rows", options.rows);
1746
+ const parsed = terminalInitMessageSchema.safeParse({
1747
+ type: "init",
1748
+ connectionId: options.connectionId,
1749
+ cols: options.cols ?? DEFAULT_COLS,
1750
+ rows: options.rows ?? DEFAULT_ROWS,
1751
+ ...options.command !== void 0 ? { command: options.command } : {},
1752
+ ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
1753
+ ...options.env !== void 0 ? { env: options.env } : {},
1754
+ ...interactiveInitFields(identity)
1755
+ });
1756
+ if (!parsed.success) throw new TerminalStreamError("Terminal stream init does not match the shared terminal protocol", "INVALID_INIT");
1757
+ return parsed.data;
1758
+ }
1759
+ function assertInteractiveControlActive(identity) {
1760
+ if (identity === void 0) return;
1761
+ if (Date.parse(identity.control.expiresAt) <= Date.now()) throw new TerminalStreamError("Interactive terminal control claim is expired", "INTERACTIVE_CONTROL_EXPIRED");
1394
1762
  }
1395
1763
  /**
1396
1764
  * Split input so each serialized `input` frame stays under the server's
@@ -1398,13 +1766,13 @@ function assertDimension(name, value) {
1398
1766
  * pair would reach the PTY as two invalid UTF-8 writes).
1399
1767
  */
1400
1768
  function* chunkInput(text) {
1401
- if (text.length <= INPUT_FRAME_CHUNK_CHARS) {
1769
+ if (text.length <= 131072) {
1402
1770
  yield text;
1403
1771
  return;
1404
1772
  }
1405
1773
  let index = 0;
1406
1774
  while (index < text.length) {
1407
- let end = Math.min(index + INPUT_FRAME_CHUNK_CHARS, text.length);
1775
+ let end = Math.min(index + TERMINAL_WS_INPUT_FRAME_CHUNK_CHARS, text.length);
1408
1776
  if (end < text.length) {
1409
1777
  const trailing = text.charCodeAt(end - 1);
1410
1778
  if (trailing >= 55296 && trailing <= 56319) end -= 1;
@@ -1422,11 +1790,14 @@ function* chunkInput(text) {
1422
1790
  var TerminalStream = class TerminalStream {
1423
1791
  ws;
1424
1792
  options;
1793
+ interactiveIdentity;
1794
+ initMessage;
1425
1795
  handlers;
1426
1796
  readyInfo = null;
1427
1797
  closed = false;
1428
1798
  closeWaiters = [];
1429
1799
  textDecoder = new TextDecoder();
1800
+ reconnectInFlight;
1430
1801
  /**
1431
1802
  * Dial the endpoint, send `init` as the first frame, and resolve once the
1432
1803
  * server responds `ready`. Rejects on a protocol `error` frame, a socket
@@ -1436,20 +1807,32 @@ var TerminalStream = class TerminalStream {
1436
1807
  * fresh credential.
1437
1808
  */
1438
1809
  static async connect(options) {
1439
- return new TerminalStream(options, await resolveTerminalToken(options.token)).establish();
1440
- }
1441
- constructor(options, resolvedToken) {
1442
- if (!options.connectionId.trim()) throw new TerminalStreamError("Terminal stream connectionId must not be empty", "INVALID_CONNECTION_ID");
1443
- if (options.cols !== void 0) assertDimension("cols", options.cols);
1444
- if (options.rows !== void 0) assertDimension("rows", options.rows);
1810
+ const interactiveIdentity = normalizeInteractiveSessionIdentity(options.incarnationId, options.control);
1811
+ options.signal?.throwIfAborted();
1812
+ const initMessage = buildTerminalInitMessage(options, interactiveIdentity);
1813
+ const token = await awaitTerminalOperation(resolveTerminalToken(options.token, options.signal), options.signal);
1814
+ options.signal?.throwIfAborted();
1815
+ return new TerminalStream(options, token, interactiveIdentity, initMessage).establish(options.signal);
1816
+ }
1817
+ constructor(options, resolvedToken, interactiveIdentity, initMessage) {
1818
+ const exactOptions = interactiveIdentity === void 0 ? options : {
1819
+ ...options,
1820
+ incarnationId: interactiveIdentity.incarnationId,
1821
+ control: interactiveIdentity.control
1822
+ };
1445
1823
  const WebSocketCtor = globalThis.WebSocket;
1446
1824
  if (typeof WebSocketCtor !== "function") throw new TerminalStreamError("global WebSocket is unavailable in this environment (Node >= 22 or a browser is required)", "ENVIRONMENT");
1447
- this.options = options;
1448
- this.handlers = options.handlers ?? {};
1449
- this.ws = new WebSocketCtor(toWebSocketUrl(options.url), buildAuthSubprotocols(options.authFormat ?? "edge", resolvedToken));
1825
+ this.options = {
1826
+ ...exactOptions,
1827
+ signal: void 0
1828
+ };
1829
+ this.interactiveIdentity = interactiveIdentity;
1830
+ this.initMessage = initMessage;
1831
+ this.handlers = exactOptions.handlers ?? {};
1832
+ this.ws = new WebSocketCtor(toWebSocketUrl(exactOptions.url), buildAuthSubprotocols(exactOptions.authFormat ?? "edge", resolvedToken));
1450
1833
  this.ws.binaryType = "arraybuffer";
1451
1834
  }
1452
- establish() {
1835
+ establish(signal) {
1453
1836
  return new Promise((resolve, reject) => {
1454
1837
  let settled = false;
1455
1838
  const timeoutMs = this.options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
@@ -1457,28 +1840,41 @@ var TerminalStream = class TerminalStream {
1457
1840
  fail(new TerminalStreamError(`Terminal attach timed out after ${timeoutMs}ms waiting for ready`, "CONNECT_TIMEOUT"));
1458
1841
  this.ws.close(1e3, "connect timeout");
1459
1842
  }, timeoutMs);
1843
+ const cleanup = () => {
1844
+ clearTimeout(timer);
1845
+ signal?.removeEventListener("abort", onAbort);
1846
+ };
1460
1847
  const succeed = () => {
1461
1848
  if (settled) return;
1462
1849
  settled = true;
1463
- clearTimeout(timer);
1850
+ cleanup();
1464
1851
  resolve(this);
1465
1852
  };
1466
1853
  const fail = (error) => {
1467
1854
  if (settled) return;
1468
1855
  settled = true;
1469
- clearTimeout(timer);
1856
+ cleanup();
1470
1857
  reject(error);
1471
1858
  };
1859
+ const onAbort = () => {
1860
+ if (!signal) return;
1861
+ fail(terminalAbortReason(signal));
1862
+ this.ws.close(1e3, "connect aborted");
1863
+ };
1864
+ signal?.addEventListener("abort", onAbort, { once: true });
1865
+ if (signal?.aborted) {
1866
+ onAbort();
1867
+ return;
1868
+ }
1472
1869
  this.ws.addEventListener("open", () => {
1473
- this.ws.send(JSON.stringify({
1474
- type: "init",
1475
- connectionId: this.options.connectionId,
1476
- cols: this.options.cols ?? DEFAULT_COLS,
1477
- rows: this.options.rows ?? DEFAULT_ROWS,
1478
- ...this.options.command !== void 0 ? { command: this.options.command } : {},
1479
- ...this.options.cwd !== void 0 ? { cwd: this.options.cwd } : {},
1480
- ...this.options.env !== void 0 ? { env: this.options.env } : {}
1481
- }));
1870
+ if (settled) return;
1871
+ try {
1872
+ assertInteractiveControlActive(this.interactiveIdentity);
1873
+ } catch (error) {
1874
+ fail(error instanceof TerminalStreamError ? error : new TerminalStreamError("Interactive terminal control claim is expired", "INTERACTIVE_CONTROL_EXPIRED"));
1875
+ return;
1876
+ }
1877
+ this.ws.send(JSON.stringify(this.initMessage));
1482
1878
  });
1483
1879
  this.ws.addEventListener("message", (event) => {
1484
1880
  this.handleMessage(event.data, {
@@ -1499,25 +1895,44 @@ var TerminalStream = class TerminalStream {
1499
1895
  });
1500
1896
  });
1501
1897
  }
1502
- handleMessage(data, connectPhase) {
1503
- if (typeof data !== "string") {
1504
- if (data instanceof ArrayBuffer) this.handlers.onData?.(new Uint8Array(data));
1898
+ handleMessage(raw, connectPhase) {
1899
+ if (typeof raw !== "string") {
1900
+ if (raw instanceof ArrayBuffer) this.handlers.onData?.(new Uint8Array(raw));
1505
1901
  return;
1506
1902
  }
1507
- let message;
1903
+ const failProtocolFrame = () => {
1904
+ const error = new TerminalStreamError("Terminal control frame does not match the shared terminal protocol", "INVALID_FRAME");
1905
+ const beforeReady = !connectPhase.settled();
1906
+ if (this.ws.readyState === WebSocket.OPEN) this.ws.close(1e3, "invalid terminal frame");
1907
+ if (beforeReady) connectPhase.fail(error);
1908
+ else this.handlers.onError?.(error);
1909
+ };
1910
+ let decoded;
1508
1911
  try {
1509
- message = JSON.parse(data);
1912
+ decoded = JSON.parse(raw);
1510
1913
  } catch {
1511
- this.handlers.onError?.(new TerminalStreamError("Terminal control frame is not valid JSON", "INVALID_FRAME"));
1914
+ failProtocolFrame();
1512
1915
  return;
1513
1916
  }
1514
- switch (message.type) {
1917
+ if (decoded === null || typeof decoded !== "object" || Array.isArray(decoded) || typeof decoded.type !== "string") {
1918
+ failProtocolFrame();
1919
+ return;
1920
+ }
1921
+ const message = decoded;
1922
+ if (message.type === "rec" || message.type === "rec.done") return;
1923
+ if (message.type !== "ready" && message.type !== "exit" && message.type !== "renewed" && message.type !== "error") return;
1924
+ const parsed = terminalServerMessageSchema.safeParse(message);
1925
+ if (!parsed.success) {
1926
+ failProtocolFrame();
1927
+ return;
1928
+ }
1929
+ switch (parsed.data.type) {
1515
1930
  case "ready": {
1516
1931
  const info = {
1517
- connectionId: String(message.connectionId ?? ""),
1518
- sessionId: String(message.sessionId ?? ""),
1519
- restored: message.restored === true,
1520
- detachTimeoutMs: Number(message.detachTimeoutMs ?? 0)
1932
+ connectionId: parsed.data.connectionId,
1933
+ sessionId: parsed.data.sessionId,
1934
+ restored: parsed.data.restored,
1935
+ detachTimeoutMs: parsed.data.detachTimeoutMs
1521
1936
  };
1522
1937
  this.readyInfo = info;
1523
1938
  connectPhase.succeed();
@@ -1526,12 +1941,13 @@ var TerminalStream = class TerminalStream {
1526
1941
  }
1527
1942
  case "exit":
1528
1943
  this.handlers.onExit?.({
1529
- exitCode: typeof message.exitCode === "number" ? message.exitCode : void 0,
1530
- exitSignal: typeof message.exitSignal === "string" ? message.exitSignal : void 0
1944
+ exitCode: parsed.data.exitCode,
1945
+ exitSignal: parsed.data.exitSignal
1531
1946
  });
1532
1947
  return;
1948
+ case "renewed": return;
1533
1949
  case "error": {
1534
- const error = new TerminalStreamError(typeof message.message === "string" ? message.message : "terminal error", typeof message.code === "string" ? message.code : "UNKNOWN");
1950
+ const error = new TerminalStreamError(parsed.data.message, parsed.data.code);
1535
1951
  if (!connectPhase.settled()) {
1536
1952
  connectPhase.fail(error);
1537
1953
  return;
@@ -1539,9 +1955,6 @@ var TerminalStream = class TerminalStream {
1539
1955
  this.handlers.onError?.(error);
1540
1956
  return;
1541
1957
  }
1542
- case "rec":
1543
- case "rec.done": return;
1544
- default: return;
1545
1958
  }
1546
1959
  }
1547
1960
  /** PTY connection id this stream is attached to. */
@@ -1565,6 +1978,7 @@ var TerminalStream = class TerminalStream {
1565
1978
  */
1566
1979
  write(data) {
1567
1980
  this.assertOpen("write");
1981
+ assertInteractiveControlActive(this.interactiveIdentity);
1568
1982
  const text = typeof data === "string" ? data : this.textDecoder.decode(data);
1569
1983
  for (const chunk of chunkInput(text)) this.ws.send(JSON.stringify({
1570
1984
  type: "input",
@@ -1576,6 +1990,7 @@ var TerminalStream = class TerminalStream {
1576
1990
  assertDimension("cols", cols);
1577
1991
  assertDimension("rows", rows);
1578
1992
  this.assertOpen("resize");
1993
+ assertInteractiveControlActive(this.interactiveIdentity);
1579
1994
  this.ws.send(JSON.stringify({
1580
1995
  type: "resize",
1581
1996
  cols,
@@ -1595,26 +2010,568 @@ var TerminalStream = class TerminalStream {
1595
2010
  this.ws.close(1e3, "client detach");
1596
2011
  });
1597
2012
  }
1598
- /**
1599
- * Re-dial the same endpoint with the same connectionId on a fresh socket
1600
- * — the server reattaches the PTY and replays its buffered history. When
1601
- * the stream was constructed with a token PROVIDER, it is re-invoked for
1602
- * the new dial (scoped terminal tokens expire within minutes, so the
1603
- * original credential may be dead by reconnect time); with a plain string
1604
- * token, pass a fresh `token` override yourself when it may have expired.
1605
- * `handlers` rebinds callbacks for the new stream.
1606
- */
1607
- reconnect(overrides) {
1608
- return TerminalStream.connect({
1609
- ...this.options,
1610
- token: overrides?.token ?? this.options.token,
1611
- handlers: overrides?.handlers ?? this.options.handlers,
1612
- cols: overrides?.cols ?? this.options.cols,
1613
- rows: overrides?.rows ?? this.options.rows
2013
+ /**
2014
+ * Re-dial the same endpoint with the same connectionId on a fresh socket
2015
+ * — the server reattaches the PTY and replays its buffered history. When
2016
+ * the stream was constructed with a token PROVIDER, it is re-invoked for
2017
+ * the new dial (scoped terminal tokens expire within minutes, so the
2018
+ * original credential may be dead by reconnect time); with a plain string
2019
+ * token, pass a fresh `token` override yourself when it may have expired.
2020
+ * `handlers` rebinds callbacks for the new stream.
2021
+ */
2022
+ reconnect(overrides) {
2023
+ const existing = this.reconnectInFlight;
2024
+ if (existing !== void 0) return awaitTerminalOperation(existing, overrides?.signal);
2025
+ const operation = this.reconnectOnce(overrides);
2026
+ let tracked;
2027
+ tracked = operation.finally(() => {
2028
+ if (this.reconnectInFlight === tracked) this.reconnectInFlight = void 0;
2029
+ });
2030
+ this.reconnectInFlight = tracked;
2031
+ return awaitTerminalOperation(tracked, overrides?.signal);
2032
+ }
2033
+ async reconnectOnce(overrides) {
2034
+ const binding = await this.options.reconnectAuthority?.acquire(overrides?.signal);
2035
+ try {
2036
+ return await TerminalStream.connect({
2037
+ ...this.options,
2038
+ ...binding === void 0 ? {} : {
2039
+ incarnationId: binding.incarnationId,
2040
+ control: binding.control
2041
+ },
2042
+ token: overrides?.token ?? this.options.token,
2043
+ handlers: this.handlersForReconnect(overrides?.handlers),
2044
+ cols: overrides?.cols ?? this.options.cols,
2045
+ rows: overrides?.rows ?? this.options.rows,
2046
+ signal: overrides?.signal
2047
+ });
2048
+ } catch (error) {
2049
+ try {
2050
+ await binding?.onFailure?.();
2051
+ } catch {}
2052
+ throw error;
2053
+ }
2054
+ }
2055
+ handlersForReconnect(overrides) {
2056
+ const lifecycleHandlers = this.options.handlers;
2057
+ if (overrides === void 0 || lifecycleHandlers?.onClose === void 0) return overrides ?? lifecycleHandlers;
2058
+ return {
2059
+ ...overrides,
2060
+ onClose: (code, reason) => {
2061
+ lifecycleHandlers.onClose?.(code, reason);
2062
+ overrides.onClose?.(code, reason);
2063
+ }
2064
+ };
2065
+ }
2066
+ assertOpen(action) {
2067
+ if (!this.isOpen) throw new TerminalStreamError(`Cannot ${action}: terminal stream is not open`, "NOT_OPEN");
2068
+ }
2069
+ };
2070
+ //#endregion
2071
+ //#region src/interactive-controller.ts
2072
+ /**
2073
+ * Attach-capable controller for one exact interactive process.
2074
+ *
2075
+ * Control ownership lives in the dependency-light base controller. This layer
2076
+ * adds only the terminal attach operation after that ownership is resolved.
2077
+ */
2078
+ var InteractiveSessionController = class extends InteractiveSessionControlController {
2079
+ attachInFlight;
2080
+ constructor(attachTransport, options) {
2081
+ super(attachTransport, options);
2082
+ this.attachTransport = attachTransport;
2083
+ }
2084
+ async attach(request, requestOptions) {
2085
+ requestOptions?.signal?.throwIfAborted();
2086
+ if (this.attachInFlight !== void 0) return await awaitWithAbort(this.attachInFlight, requestOptions?.signal);
2087
+ const operation = (async () => {
2088
+ const identity = await this.resolve(requestOptions);
2089
+ return this.attachWithIdentity(identity, request, requestOptions);
2090
+ })();
2091
+ let tracked;
2092
+ tracked = operation.finally(() => {
2093
+ if (this.attachInFlight === tracked) this.attachInFlight = void 0;
2094
+ });
2095
+ this.attachInFlight = tracked;
2096
+ return await tracked;
2097
+ }
2098
+ /**
2099
+ * Open a stream for a claim returned by this controller's `resolve()`.
2100
+ * The controller releases that claim when the stream cannot be opened.
2101
+ */
2102
+ async attachWithIdentity(identity, request, requestOptions) {
2103
+ const exactRef = AgentInteractiveSessionRefSchema.parse(identity.ref);
2104
+ assertInteractiveSessionRef(exactRef, this.options);
2105
+ if (!agentInteractiveSessionControlClaimMatchesRef(exactRef, identity.control)) throw new Error("Interactive control claim is not bound to the session");
2106
+ let activeIdentity = {
2107
+ ref: exactRef,
2108
+ control: identity.control
2109
+ };
2110
+ const releasePromises = /* @__PURE__ */ new WeakMap();
2111
+ const releaseClaim = (target) => {
2112
+ const existing = releasePromises.get(target);
2113
+ if (existing !== void 0) return existing;
2114
+ const release = this.safelyReleaseClaim(target);
2115
+ releasePromises.set(target, release);
2116
+ return release;
2117
+ };
2118
+ let reconnectInFlight;
2119
+ const reconnectAuthority = { acquire: async (signal) => {
2120
+ if (!releasePromises.has(activeIdentity)) throw new Error("Interactive terminal must be closed before reconnecting");
2121
+ if (reconnectInFlight !== void 0) return await awaitWithAbort(reconnectInFlight, signal);
2122
+ const operation = (async () => {
2123
+ const next = await this.resolve({ signal });
2124
+ const nextRef = AgentInteractiveSessionRefSchema.parse(next.ref);
2125
+ assertInteractiveSessionRef(nextRef, this.options);
2126
+ const nextIdentity = {
2127
+ ref: nextRef,
2128
+ control: next.control
2129
+ };
2130
+ activeIdentity = nextIdentity;
2131
+ return {
2132
+ incarnationId: nextRef.incarnationId,
2133
+ control: next.control,
2134
+ onFailure: () => releaseClaim(nextIdentity)
2135
+ };
2136
+ })();
2137
+ let tracked;
2138
+ tracked = operation.finally(() => {
2139
+ if (reconnectInFlight === tracked) reconnectInFlight = void 0;
2140
+ });
2141
+ reconnectInFlight = tracked;
2142
+ return await tracked;
2143
+ } };
2144
+ try {
2145
+ requestOptions?.signal?.throwIfAborted();
2146
+ const callerHandlers = request.handlers;
2147
+ const exactRequest = {
2148
+ ...request,
2149
+ control: identity.control,
2150
+ handlers: {
2151
+ ...callerHandlers,
2152
+ onClose: (code, reason) => {
2153
+ releaseClaim(activeIdentity);
2154
+ callerHandlers?.onClose?.(code, reason);
2155
+ }
2156
+ }
2157
+ };
2158
+ const stream = await this.attachTransport.attach(this.options.sessionId, exactRef, exactRequest, requestOptions, reconnectAuthority);
2159
+ if (requestOptions?.signal?.aborted) {
2160
+ await stream.close();
2161
+ requestOptions.signal.throwIfAborted();
2162
+ }
2163
+ return stream;
2164
+ } catch (error) {
2165
+ await releaseClaim(activeIdentity);
2166
+ throw error;
2167
+ }
2168
+ }
2169
+ };
2170
+ function browserTransport(options) {
2171
+ return {
2172
+ ...createBrowserInteractiveSessionControlTransport(options),
2173
+ async attach(sessionId, ref, request, requestOptions, reconnectAuthority) {
2174
+ requestOptions?.signal?.throwIfAborted();
2175
+ if (options.terminalTokenProvider === void 0) throw new Error("Browser interactive terminal attach requires terminalTokenProvider");
2176
+ const exactRef = AgentInteractiveSessionRefSchema.parse(ref);
2177
+ const { handlers, ...wireRequest } = request;
2178
+ const { control, ...terminalRequest } = AgentInteractiveSessionAttachSchema.parse(wireRequest);
2179
+ const terminalOptions = {
2180
+ url: browserInteractiveTerminalEndpoint(options.sessionApiUrl, sessionId),
2181
+ token: options.terminalTokenProvider,
2182
+ connectionId: sessionId,
2183
+ ...terminalRequest,
2184
+ incarnationId: exactRef.incarnationId,
2185
+ control,
2186
+ authFormat: "edge",
2187
+ signal: requestOptions?.signal,
2188
+ ...reconnectAuthority === void 0 ? {} : { reconnectAuthority },
2189
+ ...handlers === void 0 ? {} : { handlers }
2190
+ };
2191
+ requestOptions?.signal?.throwIfAborted();
2192
+ const stream = await TerminalStream.connect(terminalOptions);
2193
+ if (requestOptions?.signal?.aborted) {
2194
+ await stream.close();
2195
+ requestOptions.signal.throwIfAborted();
2196
+ }
2197
+ return stream;
2198
+ }
2199
+ };
2200
+ }
2201
+ /** Concrete controller for browser deep links and terminal attachment. */
2202
+ var BrowserInteractiveSessionController = class extends InteractiveSessionController {
2203
+ constructor(options) {
2204
+ super(browserTransport(options), options);
2205
+ }
2206
+ };
2207
+ //#endregion
2208
+ //#region src/interactive.ts
2209
+ /**
2210
+ * Exact native coding-agent sessions on the public SDK.
2211
+ *
2212
+ * Agent Interface owns the wire contracts. The SDK only adds local stream
2213
+ * handlers and binds the returned ref/control to a convenient session handle.
2214
+ */
2215
+ function createInteractiveSessionController(host, sessionId, ref, holderId) {
2216
+ const releaseInteractiveControl = host._releaseInteractiveControl;
2217
+ return new InteractiveSessionController({
2218
+ status: (id, incarnationId, requestOptions) => requestOptions === void 0 ? host._interactiveStatus(id, incarnationId) : host._interactiveStatus(id, incarnationId, requestOptions),
2219
+ claimControl: (id, claimRequest, requestOptions) => host._claimInteractiveControl(id, claimRequest, requestOptions),
2220
+ attach: (id, attachRef, attachRequest, requestOptions, reconnectAuthority) => {
2221
+ if (reconnectAuthority === void 0) return requestOptions === void 0 ? host._attachInteractive(id, attachRef, attachRequest) : host._attachInteractive(id, attachRef, attachRequest, requestOptions);
2222
+ return host._attachInteractive(id, attachRef, attachRequest, requestOptions, reconnectAuthority);
2223
+ },
2224
+ releaseControl: (id, releaseRef, control, requestOptions) => requestOptions === void 0 ? releaseInteractiveControl(id, releaseRef, control) : releaseInteractiveControl(id, releaseRef, control, requestOptions)
2225
+ }, {
2226
+ sessionId,
2227
+ incarnationId: ref.incarnationId,
2228
+ environmentId: ref.run.environmentId,
2229
+ holderId
2230
+ });
2231
+ }
2232
+ /**
2233
+ * Handle for one exact interactive harness session.
2234
+ *
2235
+ * `start()` binds the returned ref and control for prompt and stop commands.
2236
+ * Attach always observes and claims a fresh control before opening its stream.
2237
+ */
2238
+ var InteractiveSessionHandle = class {
2239
+ ref;
2240
+ control;
2241
+ attachHolderId = createInteractiveControlHolderId("sdk");
2242
+ attachController;
2243
+ attachInFlight;
2244
+ constructor(host, sessionId, options = {}) {
2245
+ this.host = host;
2246
+ this.sessionId = sessionId;
2247
+ this.ref = options.ref;
2248
+ this.control = options.control;
2249
+ }
2250
+ start(options, requestOptions) {
2251
+ requestOptions?.signal?.throwIfAborted();
2252
+ const exactStart = AgentInteractiveSessionStartSchema.parse(options);
2253
+ if (exactStart.run.sessionId !== this.sessionId) throw new Error(`Interactive session ${this.sessionId} start is bound to session ${exactStart.run.sessionId}`);
2254
+ return (requestOptions === void 0 ? this.host._startInteractive(this.sessionId, exactStart) : this.host._startInteractive(this.sessionId, exactStart, requestOptions)).then((info) => {
2255
+ const ref = AgentInteractiveSessionRefSchema.parse(info.ref);
2256
+ const control = AgentInteractiveSessionControlClaimSchema.parse(info.control);
2257
+ if (!agentInteractiveSessionRefMatchesStart(exactStart, ref) || !agentInteractiveSessionControlClaimMatchesRef(ref, control)) throw new Error(`Interactive session ${this.sessionId} start returned an inexact ref or control claim`);
2258
+ const exactInfo = {
2259
+ ...info,
2260
+ ref,
2261
+ control
2262
+ };
2263
+ this.ref = ref;
2264
+ this.control = control;
2265
+ this.attachController = void 0;
2266
+ return exactInfo;
2267
+ });
2268
+ }
2269
+ requireBinding() {
2270
+ const ref = this.requireRef();
2271
+ if (!this.control) throw new Error(`Interactive session ${this.sessionId} handle is not bound to an exact ref and control claim`);
2272
+ const control = AgentInteractiveSessionControlClaimSchema.parse(this.control);
2273
+ if (!agentInteractiveSessionControlClaimMatchesRef(ref, control)) throw new Error(`Interactive session ${this.sessionId} handle is not bound to a control claim for its exact ref`);
2274
+ return {
2275
+ ref,
2276
+ control
2277
+ };
2278
+ }
2279
+ requireRef() {
2280
+ if (!this.ref) throw new Error(`Interactive session ${this.sessionId} handle is not bound to an exact ref`);
2281
+ const ref = AgentInteractiveSessionRefSchema.parse(this.ref);
2282
+ if (ref.run.sessionId !== this.sessionId) throw new Error(`Interactive session ${this.sessionId} handle is bound to session ${ref.run.sessionId}`);
2283
+ return ref;
2284
+ }
2285
+ sendPrompt(command, options) {
2286
+ options?.signal?.throwIfAborted();
2287
+ const binding = this.requireBinding();
2288
+ const exactCommand = AgentInteractiveSessionPromptCommandSchema.parse(command);
2289
+ if (canonicalCandidateDigest(exactCommand.ref) !== canonicalCandidateDigest(binding.ref) || canonicalCandidateDigest(exactCommand.control) !== canonicalCandidateDigest(binding.control)) throw new Error(`Interactive session ${this.sessionId} prompt is bound to a different exact ref`);
2290
+ return (options === void 0 ? this.host._sendInteractivePrompt(this.sessionId, exactCommand) : this.host._sendInteractivePrompt(this.sessionId, exactCommand, options)).then((acknowledgement) => {
2291
+ const exactAcknowledgement = AgentInteractiveSessionPromptAcknowledgementSchema.parse(acknowledgement);
2292
+ if (!agentInteractiveSessionPromptAcknowledgementMatchesCommand(exactCommand, exactAcknowledgement)) throw new Error(`Interactive session ${this.sessionId} prompt acknowledgement does not match the command`);
2293
+ return exactAcknowledgement;
2294
+ });
2295
+ }
2296
+ attach(request = {}, options) {
2297
+ options?.signal?.throwIfAborted();
2298
+ if (this.attachInFlight !== void 0) return awaitWithAbort(this.attachInFlight, options?.signal);
2299
+ const ref = this.requireRef();
2300
+ let controller = this.attachController;
2301
+ if (controller === void 0) {
2302
+ controller = createInteractiveSessionController(this.host, this.sessionId, ref, this.attachHolderId);
2303
+ this.attachController = controller;
2304
+ }
2305
+ const operation = controller.resolve(options).then(async (identity) => {
2306
+ const stream = await controller.attachWithIdentity(identity, request, options);
2307
+ this.ref = identity.ref;
2308
+ this.control = identity.control;
2309
+ return stream;
2310
+ });
2311
+ let tracked;
2312
+ tracked = operation.finally(() => {
2313
+ if (this.attachInFlight === tracked) this.attachInFlight = void 0;
2314
+ });
2315
+ this.attachInFlight = tracked;
2316
+ return tracked;
2317
+ }
2318
+ status(options) {
2319
+ options?.signal?.throwIfAborted();
2320
+ const ref = this.requireRef();
2321
+ return (options === void 0 ? this.host._interactiveStatus(this.sessionId, ref.incarnationId) : this.host._interactiveStatus(this.sessionId, ref.incarnationId, options)).then((status) => {
2322
+ if (status !== null && !agentInteractiveSessionStatusMatchesRef(ref, status)) throw new Error(`Interactive session ${this.sessionId} status returned an inexact ref`);
2323
+ return status;
2324
+ });
2325
+ }
2326
+ claimControl(request, options) {
2327
+ options?.signal?.throwIfAborted();
2328
+ const ref = this.requireRef();
2329
+ const exactRequest = AgentInteractiveSessionControlClaimRequestSchema.parse(request);
2330
+ if (canonicalCandidateDigest(exactRequest.ref) !== canonicalCandidateDigest(ref)) throw new Error(`Interactive session ${this.sessionId} control claim is bound to a different exact ref`);
2331
+ return this.host._claimInteractiveControl(this.sessionId, exactRequest, options).then((acknowledgement) => {
2332
+ const exactAcknowledgement = AgentInteractiveSessionControlClaimAcknowledgementSchema.parse(acknowledgement);
2333
+ if (!agentInteractiveSessionControlClaimAcknowledgementMatchesRequest(exactRequest, exactAcknowledgement)) throw new Error(`Interactive session ${this.sessionId} control claim acknowledgement does not match the request`);
2334
+ if ((exactAcknowledgement.status === "accepted" || exactAcknowledgement.status === "replayed") && exactAcknowledgement.control) {
2335
+ const current = this.control ? AgentInteractiveSessionControlClaimSchema.parse(this.control) : void 0;
2336
+ if (current === void 0 || agentInteractiveSessionControlClaimIsNewer(exactAcknowledgement.control, current)) this.control = exactAcknowledgement.control;
2337
+ }
2338
+ return exactAcknowledgement;
2339
+ });
2340
+ }
2341
+ stop(command, options) {
2342
+ options?.signal?.throwIfAborted();
2343
+ const binding = this.requireBinding();
2344
+ const exactCommand = AgentInteractiveSessionStopCommandSchema.parse(command);
2345
+ if (canonicalCandidateDigest(exactCommand.ref) !== canonicalCandidateDigest(binding.ref) || canonicalCandidateDigest(exactCommand.control) !== canonicalCandidateDigest(binding.control)) throw new Error(`Interactive session ${this.sessionId} stop is bound to a different exact ref`);
2346
+ return (options === void 0 ? this.host._stopInteractive(this.sessionId, exactCommand) : this.host._stopInteractive(this.sessionId, exactCommand, options)).then((acknowledgement) => {
2347
+ const exactAcknowledgement = AgentInteractiveSessionStopAcknowledgementSchema.parse(acknowledgement);
2348
+ if (!agentInteractiveSessionStopAcknowledgementMatchesCommand(exactCommand, exactAcknowledgement)) throw new Error(`Interactive session ${this.sessionId} stop acknowledgement does not match the command`);
2349
+ return exactAcknowledgement;
2350
+ });
2351
+ }
2352
+ stopLifecycle(options) {
2353
+ options?.signal?.throwIfAborted();
2354
+ const ref = this.requireRef();
2355
+ return (options === void 0 ? this.host._stopInteractiveLifecycle(this.sessionId, ref) : this.host._stopInteractiveLifecycle(this.sessionId, ref, options)).then((acknowledgement) => {
2356
+ const exactAcknowledgement = AgentInteractiveSessionStopAcknowledgementSchema.parse(acknowledgement);
2357
+ if (canonicalCandidateDigest(exactAcknowledgement.ref) !== canonicalCandidateDigest(ref)) throw new Error(`Interactive session ${this.sessionId} lifecycle stop acknowledgement does not match the exact ref`);
2358
+ if (exactAcknowledgement.status !== "accepted" && exactAcknowledgement.status !== "replayed") throw new Error(`Interactive session ${this.sessionId} lifecycle stop was not confirmed (${exactAcknowledgement.status})`);
2359
+ if (exactAcknowledgement.effect === "unknown") throw new Error(`Interactive session ${this.sessionId} lifecycle stop returned an unknown outcome`);
2360
+ return exactAcknowledgement;
2361
+ });
2362
+ }
2363
+ };
2364
+ //#endregion
2365
+ //#region src/session.ts
2366
+ /**
2367
+ * A single agent session inside a sandbox. Created via
2368
+ * `box.session(id)` — does not hit the network until a method is called.
2369
+ */
2370
+ var SandboxSession = class SandboxSession {
2371
+ /**
2372
+ * @internal SDK-internal constructor — apps should call `box.session(id)`.
2373
+ */
2374
+ constructor(box, id) {
2375
+ this.box = box;
2376
+ this.id = id;
2377
+ }
2378
+ /**
2379
+ * Fetch the current session state from the sandbox. Includes status,
2380
+ * model, prompt count, token usage if known, and timing metadata.
2381
+ *
2382
+ * Throws on transport error; returns `null` if the session id is not
2383
+ * known to the sandbox (e.g. it ended and was reaped, or the id is
2384
+ * invalid).
2385
+ */
2386
+ async status() {
2387
+ return this.box._sessionStatus(this.id);
2388
+ }
2389
+ /**
2390
+ * Stream events from this session as they arrive. With no `since`,
2391
+ * starts at the live tail; with `since`, replays strictly after that
2392
+ * event id — useful for reconnect-after-disconnect flows.
2393
+ *
2394
+ * The async iterator terminates when the session reaches a terminal
2395
+ * state (`completed`, `failed`, `cancelled`) and the corresponding
2396
+ * terminal event has been yielded, OR when the caller's signal aborts.
2397
+ */
2398
+ events(opts) {
2399
+ return this.box._sessionEvents(this.id, opts);
2400
+ }
2401
+ /**
2402
+ * Await a terminal result. With `executionId`, drains only that execution;
2403
+ * without it, reads status and selects the latest execution. Event replay
2404
+ * makes either form safe after the execution has already finished.
2405
+ *
2406
+ * Use this to wait for a session that was started by another caller
2407
+ * (e.g. `dispatchPrompt`).
2408
+ */
2409
+ async result(options) {
2410
+ return options ? this.box._sessionResult(this.id, options) : this.box._sessionResult(this.id);
2411
+ }
2412
+ /**
2413
+ * List persisted messages for this session, including in-flight assistant
2414
+ * content when the runtime has flushed partial output.
2415
+ */
2416
+ async messages(opts = {}) {
2417
+ return this.box.messages({
2418
+ ...opts,
2419
+ sessionId: this.id
2420
+ });
2421
+ }
2422
+ /**
2423
+ * Continue this session with an additional prompt. Equivalent to
2424
+ * `box.prompt(message, { ...opts, sessionId: this.id })` but reads
2425
+ * naturally on a Session reference.
2426
+ */
2427
+ async prompt(message, opts) {
2428
+ return this.box.prompt(message, {
2429
+ ...opts,
2430
+ sessionId: this.id
2431
+ });
2432
+ }
2433
+ /**
2434
+ * Submit a structured message through the session message endpoint. The
2435
+ * returned receipt confirms admission; follow progress with `events()` or
2436
+ * await the terminal response with `result()`. Model and message identifiers
2437
+ * are translated to the runtime's wire names by the SDK.
2438
+ */
2439
+ async sendMessage(request, options) {
2440
+ return this.box._sessionSendMessage(this.id, request, options);
2441
+ }
2442
+ /** Delete this session and its persisted runtime state. */
2443
+ async delete() {
2444
+ return this.box._sessionDelete(this.id);
2445
+ }
2446
+ /**
2447
+ * Fork this session into a new queued session. The fork shares the
2448
+ * parent workspace and copies conversation history up to `messageId`
2449
+ * when supplied.
2450
+ */
2451
+ async fork(opts) {
2452
+ const info = await this.box._sessionFork(this.id, opts);
2453
+ return {
2454
+ session: new SandboxSession(this.box, info.id),
2455
+ info
2456
+ };
2457
+ }
2458
+ /**
2459
+ * Drive this session's harness in INTERACTIVE mode: spawn its native TUI,
2460
+ * stream the live framebuffer, and inject prompts as keystrokes. Distinct
2461
+ * from the headless `prompt()`/`events()` surface above. Lazy — does not hit
2462
+ * the network until a handle method is called.
2463
+ */
2464
+ interactive(options = {}) {
2465
+ return new InteractiveSessionHandle(this.box, this.id, options);
2466
+ }
2467
+ /**
2468
+ * Resolve a pending permission interaction the agent raised mid-turn,
2469
+ * forwarding `allow`/`deny` to the blocked agent through the unified
2470
+ * interaction channel. Throws if no matching permission is outstanding.
2471
+ */
2472
+ async respondToPermission(permissionID, options) {
2473
+ return this.box._respondToPermission(this.id, permissionID, options);
2474
+ }
2475
+ /** Return every interaction that still awaits a response on this session. */
2476
+ async interactions(options) {
2477
+ return this.box._listInteractions(this.id, options);
2478
+ }
2479
+ /** Respond to one exact interaction with a retry-safe command. */
2480
+ async respondToInteraction(command, options) {
2481
+ return this.box._respondToInteraction(this.id, command, options);
2482
+ }
2483
+ /**
2484
+ * Interrupt an execution without deleting the session. Pass `executionId`
2485
+ * to make the cancellation exact: a newer run on the same session is never
2486
+ * touched. Without it, the current execution is selected. Returns
2487
+ * `{ cancelled: false }` when the selected execution is not running.
2488
+ */
2489
+ async interrupt(options) {
2490
+ return options ? this.box._interrupt(this.id, options) : this.box._interrupt(this.id);
2491
+ }
2492
+ /**
2493
+ * Cancel one exact run through the durable operation contract. Reusing the
2494
+ * same request after a lost response returns the sidecar's replayed
2495
+ * acknowledgement instead of issuing a second cancellation.
2496
+ */
2497
+ async cancelRun(request, options) {
2498
+ return this.box._cancelRun(this.id, request, options);
2499
+ }
2500
+ /** Answer one exact question without selecting another outstanding ask. */
2501
+ async answer(interactionId, answers, options) {
2502
+ return this.box._answerQuestion(this.id, interactionId, answers, options);
2503
+ }
2504
+ /** Return the newest plan awaiting a decision for this session. */
2505
+ async plan() {
2506
+ return this.box._currentPlan(this.id);
2507
+ }
2508
+ /** Approve a durable plan and enqueue its separate execution turn. */
2509
+ async approvePlan(planId) {
2510
+ const id = planId ?? (await this.requireCurrentPlan()).id;
2511
+ return this.box._decidePlan(this.id, id, { outcome: "approved" });
2512
+ }
2513
+ /** Reject a durable plan with required feedback and enqueue a revision turn. */
2514
+ async rejectPlan(feedback, planId) {
2515
+ if (!feedback.trim()) throw new Error("Plan rejection feedback is required");
2516
+ const id = planId ?? (await this.requireCurrentPlan()).id;
2517
+ return this.box._decidePlan(this.id, id, {
2518
+ outcome: "rejected",
2519
+ feedback: feedback.trim()
1614
2520
  });
1615
2521
  }
1616
- assertOpen(action) {
1617
- if (!this.isOpen) throw new TerminalStreamError(`Cannot ${action}: terminal stream is not open`, "NOT_OPEN");
2522
+ async requireCurrentPlan() {
2523
+ const plan = await this.plan();
2524
+ if (!plan) throw new Error("No durable plan is awaiting a decision");
2525
+ return plan;
2526
+ }
2527
+ };
2528
+ //#endregion
2529
+ //#region src/tangle/tee-intercept.ts
2530
+ /** hono's default unmatched-route body. */
2531
+ const HONO_NOT_FOUND_BODY = "404 Not Found";
2532
+ /**
2533
+ * A 404 carrying hono's own unmatched-route body came from the router.
2534
+ *
2535
+ * hono answers an unmatched route with exactly "404 Not Found", so that body
2536
+ * is the deployment saying it has no such route — which for `/tee` means the
2537
+ * operator intercept is absent, not that the sandbox is missing.
2538
+ * `ensureRunning()` has already confirmed the sandbox at this point, which is
2539
+ * what makes the distinction safe to draw.
2540
+ */
2541
+ function isRouterNotFound(body) {
2542
+ return body === HONO_NOT_FOUND_BODY;
2543
+ }
2544
+ /**
2545
+ * Request a TEE surface, refusing by name when the deployment cannot serve it.
2546
+ *
2547
+ * Returns the successful response for the caller to parse. Throws
2548
+ * `CapabilityError` when the intercept is absent, and otherwise the same
2549
+ * parsed error every other SDK call throws.
2550
+ */
2551
+ async function fetchTeeSurface(client, sandboxId, surface, init) {
2552
+ const path = `/v1/sandboxes/${encodeURIComponent(sandboxId)}/tee/${surface}`;
2553
+ const response = await client.fetch(path, init);
2554
+ if (response.ok) return response;
2555
+ const body = await response.text();
2556
+ if (response.status === 404 && isRouterNotFound(body)) throw new CapabilityError(`TEE ${surface} is not served by this deployment. The /tee paths are answered by a Tangle confidential operator that intercepts them before the sandbox API; a sandbox reached through the public API has no such route. Point the client at the operator, or create the sandbox with driver.type "tangle" on a deployment that fronts it.`, "TEE_UNSUPPORTED", 404, void 0, "tee");
2557
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2558
+ }
2559
+ //#endregion
2560
+ //#region src/task-session.ts
2561
+ /** A background task session with isolated changeset operations. */
2562
+ var SandboxTaskSession = class extends SandboxSession {
2563
+ /** @internal Apps should call `box.taskSession(id)`. */
2564
+ constructor(taskBox, id) {
2565
+ super(taskBox, id);
2566
+ this.taskBox = taskBox;
2567
+ }
2568
+ /** Read the task's isolated file changes. */
2569
+ async changes() {
2570
+ return this.taskBox._taskSessionChanges(this.id);
2571
+ }
2572
+ /** Apply all or a selected subset of the task's isolated file changes. */
2573
+ async commit(options) {
2574
+ return this.taskBox._taskSessionCommit(this.id, options);
1618
2575
  }
1619
2576
  };
1620
2577
  //#endregion
@@ -2011,153 +2968,6 @@ function runtimeFilesystemPath(endpoint, path) {
2011
2968
  const encodedPath = segments.map(encodeURIComponent).join("/");
2012
2969
  return `${endpoint}/${isAbsolute ? "/" : ""}${encodedPath}`;
2013
2970
  }
2014
- /**
2015
- * Single source of truth for a run's outcome, shared by
2016
- * `prompt`/`task`/`_sessionResult`/turn settling. A run succeeds only when it
2017
- * reached a terminal event, raised no run-level error, hit no approval gate,
2018
- * left no unanswered question, and recorded no failed (`isError`) tool. A
2019
- * tool-only turn with no text succeeds unless the server enforces visible-output
2020
- * requirements; a turn whose tool failed (even if narrated around) fails.
2021
- */
2022
- function deriveOutcome(inputs) {
2023
- if (inputs.plan) return {
2024
- status: "awaiting_plan_decision",
2025
- success: false,
2026
- error: "Agent is awaiting a durable plan decision"
2027
- };
2028
- if (inputs.question) return {
2029
- status: "awaiting_question",
2030
- success: false,
2031
- error: inputs.runError ?? "Agent is awaiting an answer to a question"
2032
- };
2033
- if (inputs.approval) return {
2034
- status: "blocked_on_approval",
2035
- success: false,
2036
- error: inputs.runError ?? inputs.approval.message ?? "Agent is blocked on a hub approval"
2037
- };
2038
- if (inputs.runError) return {
2039
- status: "failed",
2040
- success: false,
2041
- error: inputs.runError,
2042
- errorCode: inputs.runErrorCode
2043
- };
2044
- const failedTool = inputs.toolInvocations?.find((t) => t.isError === true);
2045
- if (failedTool) return {
2046
- status: "failed",
2047
- success: false,
2048
- error: `Tool "${failedTool.toolName}" failed`
2049
- };
2050
- if (!inputs.terminalReached) return {
2051
- status: "failed",
2052
- success: false,
2053
- error: "Agent stream ended without a terminal event"
2054
- };
2055
- return {
2056
- status: "success",
2057
- success: true
2058
- };
2059
- }
2060
- function readAwaitingPlan(data) {
2061
- const direct = data.plan;
2062
- const outcome = data.outcome;
2063
- const candidate = direct && typeof direct === "object" ? direct : outcome && typeof outcome === "object" && outcome.type === "awaiting_plan_decision" ? outcome.plan : void 0;
2064
- if (!candidate || typeof candidate !== "object") return void 0;
2065
- const plan = candidate;
2066
- if (typeof plan.id !== "string" || typeof plan.revision !== "number" || typeof plan.body !== "string" || typeof plan.submittedAt !== "string") return;
2067
- return plan;
2068
- }
2069
- function readPlanFromEvent(event) {
2070
- return event.type === "result" || event.type === "done" || event.type === "plan.submitted" ? readAwaitingPlan(event.data) : void 0;
2071
- }
2072
- /** Read tool invocations off a `result` event payload. */
2073
- function readToolInvocations(data) {
2074
- const raw = data.toolInvocations;
2075
- return Array.isArray(raw) ? raw : void 0;
2076
- }
2077
- /** Hub error code surfaced when a write tool hits an approval gate. */
2078
- const HUB_APPROVAL_CODE = "HUB_APPROVAL_REQUIRED";
2079
- /** Best-effort string view of an arbitrary tool result for signal matching. */
2080
- function stringifyToolResult(result) {
2081
- if (typeof result === "string") return result;
2082
- try {
2083
- return JSON.stringify(result) ?? "";
2084
- } catch {
2085
- return String(result);
2086
- }
2087
- }
2088
- /**
2089
- * Walk a tool result for an `approval` object and return its `id`/`connectionId`.
2090
- * The in-sandbox hub bridge carries these in `structuredContent.details.approval`,
2091
- * but the exact nesting depends on how the backend serialized the MCP result, so
2092
- * search defensively. Returns `undefined` when not present — the id is then
2093
- * surfaced honestly as `undefined`, never invented.
2094
- */
2095
- function findApprovalDetails(value, depth = 0) {
2096
- if (depth > 6 || value === null || typeof value !== "object") return void 0;
2097
- const obj = value;
2098
- const approval = obj.approval;
2099
- if (approval && typeof approval === "object") {
2100
- const a = approval;
2101
- return {
2102
- ...typeof a.id === "string" ? { id: a.id } : {},
2103
- ...typeof a.connectionId === "string" ? { connectionId: a.connectionId } : {}
2104
- };
2105
- }
2106
- for (const v of Object.values(obj)) {
2107
- const found = findApprovalDetails(v, depth + 1);
2108
- if (found) return found;
2109
- }
2110
- }
2111
- /** Find a human-readable `message` string anywhere in a tool result. */
2112
- function findResultMessage(value, depth = 0) {
2113
- if (depth > 6 || value === null || typeof value !== "object") return void 0;
2114
- const obj = value;
2115
- if (typeof obj.message === "string" && obj.message.length > 0) return obj.message;
2116
- for (const v of Object.values(obj)) {
2117
- const found = findResultMessage(v, depth + 1);
2118
- if (found) return found;
2119
- }
2120
- }
2121
- /**
2122
- * Detect a hub approval gate among a run's tool invocations: a failed tool whose
2123
- * result carries `HUB_APPROVAL_REQUIRED`. Returns the approval id/connectionId
2124
- * when the structured signal survived serialization, else just the message — so
2125
- * the run can settle as `blocked_on_approval` even when the id is unrecoverable.
2126
- */
2127
- function detectHubApproval(toolInvocations) {
2128
- if (!toolInvocations) return void 0;
2129
- for (const tool of toolInvocations) {
2130
- if (tool.isError !== true) continue;
2131
- if (!stringifyToolResult(tool.result).includes(HUB_APPROVAL_CODE)) continue;
2132
- const approval = findApprovalDetails(tool.result);
2133
- const message = findResultMessage(tool.result) ?? "Hub action requires approval";
2134
- return {
2135
- ...approval?.id ? { approvalId: approval.id } : {},
2136
- ...approval?.connectionId ? { connectionId: approval.connectionId } : {},
2137
- message
2138
- };
2139
- }
2140
- }
2141
- /** Read an `interaction` event (kind `question`) into an awaiting-question requirement. */
2142
- function readQuestionEvent(data) {
2143
- const request = data.request;
2144
- if (!request || request.kind !== "question" || typeof request.id !== "string") return;
2145
- return {
2146
- questionId: request.id,
2147
- questions: questionsFromAnswerSpec(request)
2148
- };
2149
- }
2150
- /** Reconstruct the prompt/options shape from a question interaction's answerSpec. */
2151
- function questionsFromAnswerSpec(request) {
2152
- return request.answerSpec.fields.map((field) => field.type === "select" ? {
2153
- question: field.label,
2154
- options: field.options.map((o) => ({
2155
- label: o.label,
2156
- description: o.description
2157
- })),
2158
- multiSelect: field.multi === true
2159
- } : { question: field.label });
2160
- }
2161
2971
  function isProfileNotCachedError(err) {
2162
2972
  return err instanceof SandboxError && err.code === "PROFILE_NOT_CACHED";
2163
2973
  }
@@ -2314,6 +3124,10 @@ var SandboxInstance = class SandboxInstance {
2314
3124
  defaultRuntimeBackend;
2315
3125
  runtime;
2316
3126
  workspaceImages;
3127
+ interactiveOperations;
3128
+ interactiveTerminalOperations;
3129
+ /** @internal Test seam for terminal attach unit tests. */
3130
+ _terminalStreamConnect = (options) => TerminalStream.connect(options);
2317
3131
  /**
2318
3132
  * A profile the runtime has cached (create-time priming), letting turns
2319
3133
  * send `backend.profileSha256` instead of re-shipping the profile's static
@@ -2341,6 +3155,17 @@ var SandboxInstance = class SandboxInstance {
2341
3155
  fetch: (path, init) => this.runtimeFetch(path, init)
2342
3156
  });
2343
3157
  this.workspaceImages = new WorkspaceImages(this.client, this.info.id, () => this.ensureRunning(), () => this.info.filesystemIncarnationId);
3158
+ this.interactiveOperations = new SandboxInteractiveOperations({
3159
+ id: this.info.id,
3160
+ ensureRunning: (signal) => this.ensureRunning(signal),
3161
+ runtimeFetch: (endpoint, request) => this.runtimeFetch(endpoint, request)
3162
+ });
3163
+ this.interactiveTerminalOperations = new SandboxInteractiveTerminalOperations({
3164
+ id: this.info.id,
3165
+ mintScopedToken: (options) => this.mintScopedToken(options),
3166
+ terminalStreamConnect: (options) => this._terminalStreamConnect(options),
3167
+ interactiveStatus: (id, incarnationId, options) => this._interactiveStatus(id, incarnationId, options)
3168
+ });
2344
3169
  }
2345
3170
  /** Unique sandbox identifier */
2346
3171
  get id() {
@@ -2541,15 +3366,16 @@ var SandboxInstance = class SandboxInstance {
2541
3366
  /**
2542
3367
  * Refresh sandbox information from the server.
2543
3368
  */
2544
- async refresh() {
3369
+ async refresh(signal) {
3370
+ signal?.throwIfAborted();
2545
3371
  if (this.refreshInFlight) {
2546
- await this.refreshInFlight;
3372
+ await awaitWithAbort(this.refreshInFlight, signal);
2547
3373
  return;
2548
3374
  }
2549
3375
  const refresh = this.refreshInfo();
2550
3376
  this.refreshInFlight = refresh;
2551
3377
  try {
2552
- await refresh;
3378
+ await awaitWithAbort(refresh, signal);
2553
3379
  } finally {
2554
3380
  if (this.refreshInFlight === refresh) this.refreshInFlight = void 0;
2555
3381
  }
@@ -2588,7 +3414,7 @@ var SandboxInstance = class SandboxInstance {
2588
3414
  this.info = {
2589
3415
  ...this.info,
2590
3416
  metadata: {
2591
- ...this.info.metadata ?? {},
3417
+ ...this.info.metadata,
2592
3418
  teeAttestationJson: JSON.stringify(data.attestation),
2593
3419
  ...data.attestationNonce ? { attestationNonce: data.attestationNonce } : {}
2594
3420
  }
@@ -2613,7 +3439,7 @@ var SandboxInstance = class SandboxInstance {
2613
3439
  this.info = {
2614
3440
  ...this.info,
2615
3441
  metadata: {
2616
- ...this.info.metadata ?? {},
3442
+ ...this.info.metadata,
2617
3443
  teePublicKeyJson: JSON.stringify(data.public_key)
2618
3444
  }
2619
3445
  };
@@ -2945,16 +3771,11 @@ var SandboxInstance = class SandboxInstance {
2945
3771
  await this.ensureRunning();
2946
3772
  const startTime = Date.now();
2947
3773
  let responseText;
2948
- let runError;
2949
- let runErrorCode;
3774
+ const outcomeTracker = createAgentRunOutcomeTracker();
2950
3775
  let executionId = options?.lastEventId ? options.executionId : void 0;
2951
3776
  let traceId;
2952
3777
  let usage;
2953
3778
  let costUsd;
2954
- let toolInvocations;
2955
- let question;
2956
- let plan;
2957
- let terminalReached = false;
2958
3779
  const controller = new AbortController();
2959
3780
  try {
2960
3781
  const signal = options?.signal ? combineAbortSignals([options.signal, controller.signal]) : controller.signal;
@@ -2962,6 +3783,7 @@ var SandboxInstance = class SandboxInstance {
2962
3783
  ...options,
2963
3784
  signal
2964
3785
  })) {
3786
+ outcomeTracker.observe(event);
2965
3787
  responseText = applySandboxEventText(responseText, event);
2966
3788
  const eventExecutionId = event.data.executionId;
2967
3789
  if (typeof eventExecutionId === "string" && eventExecutionId.length > 0) executionId = eventExecutionId;
@@ -2969,26 +3791,9 @@ var SandboxInstance = class SandboxInstance {
2969
3791
  usage = readTokenUsage(event.data) ?? usage;
2970
3792
  costUsd = readTokenCostUsd(event.data) ?? costUsd;
2971
3793
  }
2972
- plan = readPlanFromEvent(event) ?? plan;
2973
- if (event.type === "result") toolInvocations = readToolInvocations(event.data) ?? toolInvocations;
2974
- if (event.type === "result" || event.type === "done") terminalReached = true;
2975
- if (event.type === "interaction") question = readQuestionEvent(event.data) ?? question;
2976
3794
  if (event.type === "trace.id") traceId = event.data.traceId;
2977
- if (event.type === "error") {
2978
- runError = event.data.message;
2979
- runErrorCode = event.data.code;
2980
- }
2981
3795
  }
2982
- const approval = detectHubApproval(toolInvocations);
2983
- const outcome = deriveOutcome({
2984
- terminalReached,
2985
- runError,
2986
- runErrorCode,
2987
- toolInvocations,
2988
- approval,
2989
- question,
2990
- plan
2991
- });
3796
+ const outcome = outcomeTracker.finish();
2992
3797
  return {
2993
3798
  success: outcome.success,
2994
3799
  status: outcome.status,
@@ -2996,10 +3801,11 @@ var SandboxInstance = class SandboxInstance {
2996
3801
  response: responseText,
2997
3802
  error: outcome.error,
2998
3803
  errorCode: outcome.errorCode,
2999
- toolInvocations,
3000
- approval,
3001
- question,
3002
- plan,
3804
+ toolInvocations: outcome.toolInvocations,
3805
+ approval: outcome.approval,
3806
+ interaction: outcome.interaction,
3807
+ question: outcome.question,
3808
+ plan: outcome.plan,
3003
3809
  traceId,
3004
3810
  durationMs: Date.now() - startTime,
3005
3811
  usage,
@@ -3028,15 +3834,13 @@ var SandboxInstance = class SandboxInstance {
3028
3834
  * Stream events from an agent prompt.
3029
3835
  * Use this for real-time updates during agent execution.
3030
3836
  *
3031
- * Guarantees a terminal event on every non-cancelled path: a clean run
3032
- * ends with the runtime's `result`/`done`; a failure (pre-stream HTTP
3033
- * error, mid-stream network drop, timeout, or reconnect exhaustion) is
3034
- * surfaced as an in-band `error` event followed by a synthetic `done`,
3035
- * never a thrown generator. Caller-initiated cancellation (aborting
3036
- * `options.signal`) ends the stream silently with no synthetic terminal.
3837
+ * Guarantees terminal completion on every non-cancelled path. A clean run
3838
+ * emits a `result` summary and ends with `done`. A failure is an in-band
3839
+ * `error` followed by `done`, never a thrown generator. Caller cancellation
3840
+ * ends the stream silently with no synthetic terminal.
3037
3841
  *
3038
3842
  * Automatically reconnects via the runtime event replay endpoint if the
3039
- * SSE stream drops before a terminal event (`result` or `done`) is
3843
+ * SSE stream drops before a terminal event (`done` or `error`) is
3040
3844
  * received. Reconnection is transparent — replayed events that were
3041
3845
  * already yielded (based on event ID tracking) are deduplicated.
3042
3846
  */
@@ -3119,7 +3923,7 @@ var SandboxInstance = class SandboxInstance {
3119
3923
  try {
3120
3924
  for await (const event of this.streamPromptInner(message, options, { forceFullProfile: profileResendAttempted })) {
3121
3925
  yieldedThisAttempt = true;
3122
- if (event.type === "result" || event.type === "done") receivedTerminal = true;
3926
+ if (event.type === "done") receivedTerminal = true;
3123
3927
  if (event.type === "execution.started" && event.data.executionId) executionId = event.data.executionId;
3124
3928
  const sid = event.data.sessionId;
3125
3929
  if (typeof sid === "string" && sid.length > 0) sessionId = sid;
@@ -3278,7 +4082,7 @@ var SandboxInstance = class SandboxInstance {
3278
4082
  if (event.type === "execution.started" && event.data?.executionId) executionId = event.data.executionId;
3279
4083
  const eventSessionId = event.data?.sessionId;
3280
4084
  if (typeof eventSessionId === "string" && eventSessionId.length > 0) sessionId = eventSessionId;
3281
- if (event.type === "result" || event.type === "done") {
4085
+ if (event.type === "done" || event.type === "error") {
3282
4086
  receivedTerminal = true;
3283
4087
  logStreamCheckpoint("terminal_event", {
3284
4088
  eventType: event.type,
@@ -3348,6 +4152,7 @@ var SandboxInstance = class SandboxInstance {
3348
4152
  sessionId,
3349
4153
  executionId,
3350
4154
  status: info.status,
4155
+ ...info.status === "completed" ? { outcome: { type: "completed" } } : {},
3351
4156
  replayUnavailable: true
3352
4157
  }
3353
4158
  };
@@ -3372,7 +4177,7 @@ var SandboxInstance = class SandboxInstance {
3372
4177
  if (event.id) lastEventId = event.id;
3373
4178
  const eventSessionId = event.data?.sessionId;
3374
4179
  if (typeof eventSessionId === "string" && eventSessionId.length > 0) sessionId = eventSessionId;
3375
- if (event.type === "result" || event.type === "done") {
4180
+ if (event.type === "done" || event.type === "error") {
3376
4181
  receivedTerminal = true;
3377
4182
  logStreamCheckpoint("terminal_event", {
3378
4183
  eventType: event.type,
@@ -4622,62 +5427,64 @@ var SandboxInstance = class SandboxInstance {
4622
5427
  return this.createProcessHandle(pid, data.data.command);
4623
5428
  }
4624
5429
  createProcessHandle(pid, command) {
4625
- const self = this;
5430
+ const status = async () => {
5431
+ const response = await this.runtimeFetch(`/process/${pid}`, { method: "GET" });
5432
+ if (!response.ok) {
5433
+ const body = await response.text();
5434
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
5435
+ }
5436
+ const process = (await response.json()).data;
5437
+ return {
5438
+ pid: process.pid,
5439
+ ...process.idempotencyKey === void 0 ? {} : { idempotencyKey: process.idempotencyKey },
5440
+ command: process.command,
5441
+ cwd: process.cwd,
5442
+ running: process.running,
5443
+ exitCode: process.exitCode,
5444
+ exitSignal: process.exitSignal,
5445
+ startedAt: new Date(process.startedAt),
5446
+ exitedAt: process.exitedAt ? new Date(process.exitedAt) : void 0
5447
+ };
5448
+ };
5449
+ const kill = async (signal = "SIGTERM", options) => {
5450
+ const response = await this.runtimeFetch(`/process/${pid}`, {
5451
+ method: "DELETE",
5452
+ body: JSON.stringify({
5453
+ signal,
5454
+ ...options?.tree === true ? { tree: true } : {}
5455
+ })
5456
+ });
5457
+ if (!response.ok && response.status !== 404) {
5458
+ const body = await response.text();
5459
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
5460
+ }
5461
+ };
5462
+ const logs = async function* (runtimeFetch, parseProcessLogStream) {
5463
+ const response = await runtimeFetch(`/process/${pid}/logs`, { method: "GET" });
5464
+ if (!response.ok) {
5465
+ const body = await response.text();
5466
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
5467
+ }
5468
+ yield* parseProcessLogStream(response);
5469
+ }.bind(void 0, this.runtimeFetch.bind(this), this.parseProcessLogStream.bind(this));
4626
5470
  return {
4627
5471
  pid,
4628
5472
  command,
4629
- async status() {
4630
- const response = await self.runtimeFetch(`/process/${pid}`, { method: "GET" });
4631
- if (!response.ok) {
4632
- const body = await response.text();
4633
- throw parseErrorResponse(response.status, body, void 0, response.headers);
4634
- }
4635
- const p = (await response.json()).data;
4636
- return {
4637
- pid: p.pid,
4638
- ...p.idempotencyKey === void 0 ? {} : { idempotencyKey: p.idempotencyKey },
4639
- command: p.command,
4640
- cwd: p.cwd,
4641
- running: p.running,
4642
- exitCode: p.exitCode,
4643
- exitSignal: p.exitSignal,
4644
- startedAt: new Date(p.startedAt),
4645
- exitedAt: p.exitedAt ? new Date(p.exitedAt) : void 0
4646
- };
4647
- },
5473
+ status,
4648
5474
  async wait() {
4649
5475
  while (true) {
4650
- const s = await this.status();
5476
+ const s = await status();
4651
5477
  if (!s.running) return s.exitCode;
4652
5478
  await new Promise((resolve) => setTimeout(resolve, 500));
4653
5479
  }
4654
5480
  },
4655
- async kill(signal = "SIGTERM", options) {
4656
- const response = await self.runtimeFetch(`/process/${pid}`, {
4657
- method: "DELETE",
4658
- body: JSON.stringify({
4659
- signal,
4660
- ...options?.tree === true ? { tree: true } : {}
4661
- })
4662
- });
4663
- if (!response.ok && response.status !== 404) {
4664
- const body = await response.text();
4665
- throw parseErrorResponse(response.status, body, void 0, response.headers);
4666
- }
4667
- },
4668
- async *logs() {
4669
- const response = await self.runtimeFetch(`/process/${pid}/logs`, { method: "GET" });
4670
- if (!response.ok) {
4671
- const body = await response.text();
4672
- throw parseErrorResponse(response.status, body, void 0, response.headers);
4673
- }
4674
- yield* self.parseProcessLogStream(response);
4675
- },
5481
+ kill,
5482
+ logs,
4676
5483
  async *stdout() {
4677
- for await (const entry of this.logs()) if (entry.type === "stdout") yield entry.data;
5484
+ for await (const entry of logs()) if (entry.type === "stdout") yield entry.data;
4678
5485
  },
4679
5486
  async *stderr() {
4680
- for await (const entry of this.logs()) if (entry.type === "stderr") yield entry.data;
5487
+ for await (const entry of logs()) if (entry.type === "stderr") yield entry.data;
4681
5488
  }
4682
5489
  };
4683
5490
  }
@@ -5911,10 +6718,12 @@ var SandboxInstance = class SandboxInstance {
5911
6718
  if (!statuses.includes(this.status)) return false;
5912
6719
  return this.status !== "running" || this.filesystemIncarnationReadiness === "ready";
5913
6720
  }
5914
- async ensureRunning() {
6721
+ async ensureRunning(signal) {
6722
+ signal?.throwIfAborted();
5915
6723
  const statusObservedAt = sandboxStatusObservedAt.get(this) ?? 0;
5916
6724
  const statusAgeMs = Date.now() - statusObservedAt;
5917
- if (directRuntimeInstances.has(this) || statusObservedAt <= 0 || statusAgeMs < 0 || statusAgeMs >= SANDBOX_STATUS_FRESHNESS_MS) await this.refresh();
6725
+ if (directRuntimeInstances.has(this) || statusObservedAt <= 0 || statusAgeMs < 0 || statusAgeMs >= SANDBOX_STATUS_FRESHNESS_MS) await this.refresh(signal);
6726
+ signal?.throwIfAborted();
5918
6727
  if (this.status !== "running") throw new StateError(`Sandbox is not running (status: ${this.status})`, this.status, "running");
5919
6728
  this.assertFilesystemIncarnationReady();
5920
6729
  }
@@ -6399,31 +7208,7 @@ var SandboxInstance = class SandboxInstance {
6399
7208
  * while session-runtime/read-only scopes work with SandboxRuntimeClient.
6400
7209
  */
6401
7210
  async mintScopedToken(opts) {
6402
- if ((opts.scope === "session" || opts.scope === "session-runtime") && !opts.sessionId) throw new ValidationError(`${opts.scope} scope requires a sessionId`);
6403
- if (opts.scope === "session" && !opts.runtimeSessionId) throw new ValidationError("session scope requires the actual runtimeSessionId");
6404
- const response = await this.client.fetch(`/v1/sandboxes/${encodeURIComponent(this.id)}/scoped-token`, {
6405
- method: "POST",
6406
- headers: { "Content-Type": "application/json" },
6407
- body: JSON.stringify({
6408
- scope: opts.scope,
6409
- ...opts.sessionId ? { sessionId: opts.sessionId } : {},
6410
- ...opts.scope === "session" ? { runtimeSessionId: opts.runtimeSessionId } : {},
6411
- ...opts.ttlMinutes ? { ttlMinutes: opts.ttlMinutes } : {}
6412
- })
6413
- });
6414
- if (!response.ok) {
6415
- const body = await response.text();
6416
- throw parseErrorResponse(response.status, body, void 0, response.headers);
6417
- }
6418
- const data = await response.json();
6419
- if (typeof data.runtimeUrl !== "string" || typeof data.sidecarProxyUrl !== "string") throw new Error("Scoped token response missing runtime URLs.");
6420
- return {
6421
- token: data.token,
6422
- expiresAt: /* @__PURE__ */ new Date(data.expiresAt * 1e3),
6423
- runtimeUrl: data.runtimeUrl,
6424
- sidecarProxyUrl: data.sidecarProxyUrl,
6425
- scope: data.scope
6426
- };
7211
+ return requestScopedToken(this.client, this.id, opts);
6427
7212
  }
6428
7213
  /** @internal — invoked by SandboxSession.status(). */
6429
7214
  async _sessionStatus(id) {
@@ -6450,7 +7235,7 @@ var SandboxInstance = class SandboxInstance {
6450
7235
  }
6451
7236
  for await (const event of this.parseSSEStream(response, opts?.signal)) {
6452
7237
  yield event;
6453
- const turnTerminal = event.type === "result" || event.type === "done" || event.type === "error";
7238
+ const turnTerminal = event.type === "done" || event.type === "error";
6454
7239
  if ((stopOnTurnTerminal || opts?.executionId !== void 0) && turnTerminal) return;
6455
7240
  }
6456
7241
  }
@@ -6469,7 +7254,7 @@ var SandboxInstance = class SandboxInstance {
6469
7254
  }
6470
7255
  for await (const event of this.parseSSEStream(response)) {
6471
7256
  yield event;
6472
- if (event.type === "result" || event.type === "done" || event.type === "error") return;
7257
+ if (event.type === "done" || event.type === "error") return;
6473
7258
  }
6474
7259
  }
6475
7260
  /** @internal — invoked by SandboxSession.result(). */
@@ -6491,20 +7276,13 @@ var SandboxInstance = class SandboxInstance {
6491
7276
  }
6492
7277
  let resultExecutionId = executionId;
6493
7278
  let response;
6494
- let runError;
6495
- let runErrorCode;
7279
+ const outcomeTracker = createAgentRunOutcomeTracker();
6496
7280
  let traceId;
6497
7281
  let usage;
6498
7282
  let costUsd;
6499
- let toolInvocations;
6500
- let question;
6501
- let plan;
6502
- let terminalReached = false;
6503
- const events = options?.executionId ? this._executionEvents(id, executionId) : this._sessionEvents(id, {
6504
- since: "0",
6505
- executionId
6506
- }, true);
7283
+ const events = this._executionEvents(id, executionId);
6507
7284
  for await (const event of events) {
7285
+ outcomeTracker.observe(event);
6508
7286
  response = applySandboxEventText(response, event);
6509
7287
  if (event.type === "result" || event.type === "done") {
6510
7288
  const data = event.data;
@@ -6512,33 +7290,10 @@ var SandboxInstance = class SandboxInstance {
6512
7290
  usage = readTokenUsage(data) ?? usage;
6513
7291
  costUsd = readTokenCostUsd(data) ?? costUsd;
6514
7292
  }
6515
- plan = readPlanFromEvent(event) ?? plan;
6516
- if (event.type === "result") {
6517
- const data = event.data;
6518
- toolInvocations = readToolInvocations(data) ?? toolInvocations;
6519
- }
6520
- if (event.type === "interaction") question = readQuestionEvent(event.data) ?? question;
6521
7293
  if (event.type === "trace.id") traceId = event.data.traceId;
6522
- if (event.type === "error") {
6523
- const data = event.data;
6524
- runError = data.message;
6525
- runErrorCode = data.code;
6526
- }
6527
- if (event.type === "done" || event.type === "result") {
6528
- terminalReached = true;
6529
- break;
6530
- }
7294
+ if (event.type === "done" || event.type === "error") break;
6531
7295
  }
6532
- const approval = detectHubApproval(toolInvocations);
6533
- const outcome = deriveOutcome({
6534
- terminalReached,
6535
- runError,
6536
- runErrorCode,
6537
- toolInvocations,
6538
- approval,
6539
- question,
6540
- plan
6541
- });
7296
+ const outcome = outcomeTracker.finish();
6542
7297
  return {
6543
7298
  success: outcome.success,
6544
7299
  status: outcome.status,
@@ -6546,151 +7301,52 @@ var SandboxInstance = class SandboxInstance {
6546
7301
  response,
6547
7302
  error: outcome.error,
6548
7303
  errorCode: outcome.errorCode,
6549
- toolInvocations,
6550
- approval,
6551
- question,
6552
- plan,
7304
+ toolInvocations: outcome.toolInvocations,
7305
+ approval: outcome.approval,
7306
+ interaction: outcome.interaction,
7307
+ question: outcome.question,
7308
+ plan: outcome.plan,
6553
7309
  traceId,
6554
7310
  durationMs: Date.now() - startTime,
6555
7311
  usage,
6556
7312
  costUsd
6557
7313
  };
6558
7314
  }
6559
- /** @internal — invoked by InteractiveSessionHandle.start(). */
6560
- async _startInteractive(id, options) {
6561
- assertCurrentBackendType(options.harness, "harness");
6562
- await this.ensureRunning();
6563
- const response = await this.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/interactive`, {
6564
- method: "POST",
6565
- body: JSON.stringify(options)
6566
- });
6567
- if (!response.ok) {
6568
- const body = await response.text();
6569
- throw parseErrorResponse(response.status, body, void 0, response.headers);
6570
- }
6571
- const data = (await response.json())?.data;
6572
- if (!data || typeof data.sessionId !== "string" || typeof data.streamUrl !== "string") throw new ServerError("Interactive session response missing data", 502, {
6573
- endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`,
6574
- origin: "runtime"
6575
- });
6576
- return data;
7315
+ /** @internal — delegated to the native interactive operations module. */
7316
+ async _startInteractive(id, options, requestOptions) {
7317
+ return this.interactiveOperations._startInteractive(id, options, requestOptions);
6577
7318
  }
6578
- /** @internal — invoked by InteractiveSessionHandle.sendPrompt(). */
6579
- async _sendInteractivePrompt(id, prompt) {
6580
- await this.ensureRunning();
6581
- const response = await this.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/interactive/prompt`, {
6582
- method: "POST",
6583
- body: JSON.stringify({ prompt })
6584
- });
6585
- if (!response.ok) {
6586
- const body = await response.text();
6587
- throw parseErrorResponse(response.status, body, void 0, response.headers);
6588
- }
7319
+ /** @internal — delegated to the native interactive operations module. */
7320
+ async _sendInteractivePrompt(id, command, options) {
7321
+ return this.interactiveOperations._sendInteractivePrompt(id, command, options);
6589
7322
  }
6590
- /** @internal — invoked by InteractiveSessionHandle.status(). */
6591
- async _interactiveStatus(id) {
6592
- await this.ensureRunning();
6593
- const response = await this.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/interactive`, { method: "GET" });
6594
- if (response.status === 404) return null;
6595
- if (!response.ok) {
6596
- const body = await response.text();
6597
- throw parseErrorResponse(response.status, body, void 0, response.headers);
6598
- }
6599
- const data = (await response.json())?.data;
6600
- if (!data || data.state !== "running" && data.state !== "exited") throw new ServerError("Interactive status response missing data", 502, {
6601
- endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`,
6602
- origin: "runtime"
6603
- });
6604
- return data;
7323
+ /** @internal — delegated to the native interactive operations module. */
7324
+ async _claimInteractiveControl(id, request, options) {
7325
+ return this.interactiveOperations._claimInteractiveControl(id, request, options);
6605
7326
  }
6606
- /**
6607
- * @internal Test seam: unit tests replace this to intercept terminal
6608
- * WebSocket dials without opening a real socket. Production paths always
6609
- * go through `TerminalStream.connect`.
6610
- */
6611
- _terminalStreamConnect = (options) => TerminalStream.connect(options);
6612
- /**
6613
- * Resolve a sandbox-relative terminal stream path against the browser-safe
6614
- * sidecar proxy base returned by the scoped-token mint. Concatenation, not
6615
- * URL resolution: the proxy base carries a path prefix
6616
- * (`/v1/sidecar-proxy/{id}`) that `new URL(absolutePath, base)` would drop.
6617
- */
6618
- resolveTerminalStreamUrl(sidecarProxyUrl, streamPath) {
6619
- if (!streamPath.startsWith("/")) throw new ServerError(`Terminal stream path must be sandbox-relative (got "${streamPath}")`, 502, { origin: "runtime" });
6620
- return `${sidecarProxyUrl.replace(/\/+$/, "")}${streamPath}`;
7327
+ /** @internal — delegated to the native interactive operations module. */
7328
+ async _releaseInteractiveControl(id, ref, control, options) {
7329
+ return this.interactiveOperations._releaseInteractiveControl(id, ref, control, options);
6621
7330
  }
6622
- /** @internal — invoked by InteractiveSessionHandle.attach(). */
6623
- async _attachInteractive(id, options = {}) {
6624
- const status = await this._interactiveStatus(id);
6625
- if (!status) throw new NotFoundError("Interactive session", id, {
6626
- origin: "runtime",
6627
- endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`
6628
- });
6629
- if (status.state !== "running") throw new StateError(`Interactive session ${id} has exited; there is no live PTY to attach`, status.state, "running", {
6630
- origin: "runtime",
6631
- endpoint: `/agents/sessions/${encodeURIComponent(id)}/interactive`
6632
- }, "INTERACTIVE_SESSION_EXITED");
6633
- const scoped = await this.mintScopedToken({
6634
- scope: "session-runtime",
6635
- sessionId: id
6636
- });
6637
- return this._terminalStreamConnect({
6638
- url: this.resolveTerminalStreamUrl(scoped.sidecarProxyUrl, status.streamUrl),
6639
- token: this.terminalTokenProvider(scoped.token, id),
6640
- connectionId: id,
6641
- cols: options.cols,
6642
- rows: options.rows,
6643
- handlers: options.handlers
6644
- });
7331
+ /** @internal — delegated to the native interactive operations module. */
7332
+ async _interactiveStatus(id, incarnationId, options) {
7333
+ return this.interactiveOperations._interactiveStatus(id, incarnationId, options);
6645
7334
  }
6646
- /**
6647
- * Per-dial credential for a terminal attach. The first dial consumes the
6648
- * token minted alongside the proxy URL (no second mint); every later dial
6649
- * (a `reconnect()` after the socket dropped or the detach window) re-mints,
6650
- * because scoped terminal tokens are clamped to a <= 15-minute TTL and the
6651
- * original is likely dead by then.
6652
- */
6653
- terminalTokenProvider(initialToken, sessionId) {
6654
- let unused = initialToken;
6655
- return async () => {
6656
- if (unused !== null) {
6657
- const token = unused;
6658
- unused = null;
6659
- return token;
6660
- }
6661
- return (await this.mintScopedToken({
6662
- scope: "session-runtime",
6663
- sessionId
6664
- })).token;
6665
- };
7335
+ /** @internal — delegated to the native interactive operations module. */
7336
+ async _attachInteractive(id, ref, request, options, reconnectAuthority) {
7337
+ return this.interactiveTerminalOperations._attachInteractive(id, ref, request, options, reconnectAuthority);
6666
7338
  }
6667
- /** @internal — invoked by `box.terminals.attach()`. */
7339
+ /** @internal — delegated to the native interactive operations module. */
6668
7340
  async _attachTerminal(connectionId, options = {}) {
6669
- if (!connectionId.trim()) throw new ValidationError("Terminal connectionId is required");
6670
- const scoped = await this.mintScopedToken({
6671
- scope: "session-runtime",
6672
- sessionId: connectionId
6673
- });
6674
- return this._terminalStreamConnect({
6675
- url: this.resolveTerminalStreamUrl(scoped.sidecarProxyUrl, `/terminals/${encodeURIComponent(connectionId)}/ws`),
6676
- token: this.terminalTokenProvider(scoped.token, connectionId),
6677
- connectionId,
6678
- cols: options.cols,
6679
- rows: options.rows,
6680
- command: options.command,
6681
- cwd: options.cwd,
6682
- env: options.env,
6683
- handlers: options.handlers
6684
- });
7341
+ return this.interactiveTerminalOperations._attachTerminal(connectionId, options);
6685
7342
  }
6686
- /** @internal — invoked by InteractiveSessionHandle.stop(). */
6687
- async _stopInteractive(id) {
6688
- await this.ensureRunning();
6689
- const response = await this.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/interactive`, { method: "DELETE" });
6690
- if (!response.ok && response.status !== 404) {
6691
- const body = await response.text();
6692
- throw parseErrorResponse(response.status, body, void 0, response.headers);
6693
- }
7343
+ /** @internal — delegated to the native interactive operations module. */
7344
+ async _stopInteractive(id, command, options) {
7345
+ return this.interactiveOperations._stopInteractive(id, command, options);
7346
+ }
7347
+ /** @internal — delegated to the native interactive operations module. */
7348
+ async _stopInteractiveLifecycle(id, ref, options) {
7349
+ return this.interactiveOperations._stopInteractiveLifecycle(id, ref, options);
6694
7350
  }
6695
7351
  interactionTransport() {
6696
7352
  return {
@@ -6843,21 +7499,16 @@ function isSessionInFlight(info) {
6843
7499
  return info.status === "queued" && !isSessionAwaitingFirstTurn(info);
6844
7500
  }
6845
7501
  function settleTurnDrive(result) {
6846
- const plan = readAwaitingPlan(result);
6847
- if (plan) return {
7502
+ const outcome = deriveAgentResultOutcome(result);
7503
+ if (outcome.status === "awaiting_plan_decision" && outcome.plan) return {
6848
7504
  state: "awaiting_plan_decision",
6849
- plan,
7505
+ plan: outcome.plan,
6850
7506
  result
6851
7507
  };
6852
7508
  const text = typeof result.text === "string" ? result.text : typeof result.response === "string" ? result.response : void 0;
6853
- const status = typeof result.status === "string" ? result.status : deriveOutcome({
6854
- terminalReached: true,
6855
- runError: typeof result.error === "string" ? result.error : void 0,
6856
- toolInvocations: Array.isArray(result.toolInvocations) ? result.toolInvocations : void 0
6857
- }).status;
6858
- if (status !== "success") return {
7509
+ if (outcome.status !== "success") return {
6859
7510
  state: "failed",
6860
- error: typeof result.error === "string" && result.error.length > 0 ? result.error : `turn ${status}`
7511
+ error: outcome.error ?? `turn ${outcome.status}`
6861
7512
  };
6862
7513
  const usage = readTokenUsage(result);
6863
7514
  return {
@@ -6933,4 +7584,4 @@ function quoteForShell(value) {
6933
7584
  return `'${value.replace(/'/g, "'\\''")}'`;
6934
7585
  }
6935
7586
  //#endregion
6936
- export { toOtelJson as C, otelTraceIdForTangleTrace as S, getSandboxEventText as _, WorkspaceImages as a, buildTraceExportPayload as b, TERMINAL_WS_ECHO_SUBPROTOCOL as c, SandboxTaskSession as d, SandboxSession as f, collectAgentResponseText as g, collectAgentFinalMessageText as h, normalizeStartupDiagnostics as i, TerminalStream as l, applySandboxEventText as m, createSandboxInstanceFromResponse as n, GPU_LEASE_PROVIDER_NAMES as o, InteractiveSessionHandle as p, normalizeSandboxCreateReceipt as r, MAX_EGRESS_DENIALS_LIMIT as s, SandboxInstance as t, TerminalStreamError as u, isToolBearingEvent as v, parseSSEStream as w, exportTraceBundle as x, normalizeConnection as y };
7587
+ export { normalizeConnection as C, toOtelJson as D, otelTraceIdForTangleTrace as E, parseSSEStream as O, isToolBearingEvent as S, exportTraceBundle as T, requestInteractiveSessionToken as _, WorkspaceImages as a, collectAgentResponseText as b, SandboxTaskSession as c, agentInteractiveSessionPromptRequestDigest as d, BrowserInteractiveSessionController as f, TERMINAL_WS_ECHO_SUBPROTOCOL as g, TerminalStreamError as h, normalizeStartupDiagnostics as i, SandboxSession as l, TerminalStream as m, createSandboxInstanceFromResponse as n, GPU_LEASE_PROVIDER_NAMES as o, InteractiveSessionController as p, normalizeSandboxCreateReceipt as r, MAX_EGRESS_DENIALS_LIMIT as s, SandboxInstance as t, InteractiveSessionHandle as u, applySandboxEventText as v, buildTraceExportPayload as w, getSandboxEventText as x, collectAgentFinalMessageText as y };