@tangle-network/agent-app 0.60.213 → 0.60.215

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.
@@ -40,7 +40,7 @@ import {
40
40
  flattenHistory,
41
41
  readSandboxBinaryBytes,
42
42
  statSandboxFileSize
43
- } from "../chunk-N7FGEWZU.js";
43
+ } from "../chunk-QA2CBATZ.js";
44
44
  import "../chunk-4LBCTFBP.js";
45
45
  import {
46
46
  ModelFailoverExhaustedError,
@@ -80,7 +80,7 @@ import {
80
80
  rowIdOf,
81
81
  storeSupportsDraftPersistence,
82
82
  streamChatRouteAsSandboxEvents
83
- } from "../chunk-IHLO2DN5.js";
83
+ } from "../chunk-WLH4DEB2.js";
84
84
  import "../chunk-Q7WEEN7P.js";
85
85
  import "../chunk-UTAIRVOK.js";
86
86
  import "../chunk-RN4WPTBH.js";
@@ -24,7 +24,9 @@
24
24
  * instead of hand-rolling a generator — each omittable to the exact behavior
25
25
  * above: `turnLock` (single-flight acquire/release around the turn),
26
26
  * `contextGate` (pre-producer domain-readiness short-circuit), `beforeTurn`
27
- * (observe + augment the producer input), `lifecycle` (deterministic
27
+ * (observe + augment the producer input), `afterAuthorize` (start the box, keys
28
+ * and session the turn will need the moment the caller is admitted, so they
29
+ * overlap the lock and the user-row insert), `lifecycle` (deterministic
28
30
  * start/complete/error telemetry), `heartbeat` (keepalive during silent
29
31
  * producer waits), and `onRawEvent` (the raw producer events, for telemetry) —
30
32
  * plus the `authorize` result's `insertUserMessage` flag (suppress the user row
@@ -230,6 +232,21 @@ export type ChatTurnPrepareResult = {
230
232
  ok: false;
231
233
  response: Response;
232
234
  };
235
+ /** What `afterAuthorize` receives: the admitted turn, before the conversation loads and before any lock, read or write. */
236
+ export interface ChatTurnAfterAuthorizeArgs<TContext> {
237
+ request: Request;
238
+ /** Parsed, validated POST body. */
239
+ body: ChatTurnRequestPayload;
240
+ tenantId: string;
241
+ userId: string;
242
+ threadId: string;
243
+ /** The context `authorize` returned. Keep what the seam starts on it: `prepareTurn`, every later seam and `produce` receive the same object. */
244
+ context: TContext;
245
+ /** The turn's trace, for the spans of the work the seam starts. */
246
+ trace: TurnTrace;
247
+ }
248
+ /** Frees what `afterAuthorize` started, for a turn that ends before `produce` takes it. */
249
+ export type ChatTurnAfterAuthorizeRelease = () => void | Promise<void>;
233
250
  /** Trusted per-turn limits supplied by a server adapter, never by the body. */
234
251
  export interface ChatTurnExecutionLimits {
235
252
  maxInputTokens?: number;
@@ -510,6 +527,22 @@ export interface CreateChatTurnRoutesOptions<TContext = void> {
510
527
  * a plain response without the progress header, a `turn.response` event
511
528
  * with it. Omit → nothing to prepare. */
512
529
  prepareTurn?(args: ChatTurnPrepareArgs<TContext>): ChatTurnPrepareResult | Promise<ChatTurnPrepareResult>;
530
+ /** Start the work this turn will need, the moment `authorize` and `modelPolicy` have admitted it, so that work overlaps
531
+ * `prepareTurn`, the history read, the lock, the history recheck and the user-row insert instead of following them: the
532
+ * workspace box, the keys a producer mints, a session ensure. Keep what it starts on `args.context`; `produce` receives
533
+ * that object and awaits it.
534
+ *
535
+ * Admission has not finished, so the seam must start only work that the turn's refusal leaves harmless: a held lock
536
+ * (409), a changed history, a gate's answer or a throw all end the turn without `produce`, and the work already
537
+ * started runs on. Start nothing a second request for the same thread could not safely repeat, and nothing that
538
+ * writes product state.
539
+ *
540
+ * Not awaited, and it must return synchronously: start the work and return, never wait for it. It may return a release
541
+ * function; the route calls it once, only if the turn ends without `produce` (the chain above, or a throw before the
542
+ * engine took the turn), so a product frees credentials or leases the unused work holds. Once `produce` runs, the
543
+ * work belongs to `produce` and the route never calls the release. A throw is logged and the turn goes on without
544
+ * the early work. Omit → nothing starts early. */
545
+ afterAuthorize?(args: ChatTurnAfterAuthorizeArgs<TContext>): void | ChatTurnAfterAuthorizeRelease;
513
546
  /** Product admission policy over the model a turn NAMES. When the request
514
547
  * body carries a `model`, it is checked here — after `authorize`, before
515
548
  * the conversation loads, the lock is taken, or any row is written — and a
@@ -971,12 +971,13 @@ async function runForegroundSandboxSingleFlight(options) {
971
971
  } catch {
972
972
  }
973
973
  };
974
+ const timed = (name, work) => options.trace ? options.trace.measure(name, "sandbox", () => work(), void 0, options.traceParent) : work();
974
975
  const provisionAsOwner = async (lease) => {
975
976
  try {
976
977
  return await options.provision();
977
978
  } finally {
978
979
  try {
979
- await options.claim.releaseLease(lease);
980
+ await timed("sandbox.lease.release", () => options.claim.releaseLease(lease));
980
981
  } catch (error) {
981
982
  emit({
982
983
  type: "release-failed",
@@ -987,7 +988,17 @@ async function runForegroundSandboxSingleFlight(options) {
987
988
  }
988
989
  }
989
990
  };
990
- const initialLease = await options.claim.acquireLease(key, ttlSeconds);
991
+ if (options.reuse) {
992
+ const [state, reused] = await Promise.allSettled([
993
+ timed("sandbox.lease.inspect", () => options.claim.inspect(key)),
994
+ options.reuse()
995
+ ]);
996
+ if (reused.status === "rejected") throw reused.reason;
997
+ if (reused.value !== void 0 && state.status === "fulfilled" && state.value.status !== "held") {
998
+ return reused.value;
999
+ }
1000
+ }
1001
+ const initialLease = await timed("sandbox.lease.acquire", () => options.claim.acquireLease(key, ttlSeconds));
991
1002
  if (initialLease) return provisionAsOwner(initialLease);
992
1003
  emit({ type: "waiting", key, workspaceId: options.workspaceId });
993
1004
  while (true) {
@@ -1531,8 +1542,46 @@ var DEFAULT_SANDBOX_RESOURCES = {
1531
1542
  idleTimeoutSeconds: 3600
1532
1543
  };
1533
1544
  var _cached = null;
1534
- var livenessVerifiedAt = /* @__PURE__ */ new Map();
1535
- var verifiedEgress = /* @__PURE__ */ new Map();
1545
+ var TRUSTED_REUSE_TTL_MS = 10 * 6e4;
1546
+ var REUSE_CHECKS_MAX = 1e3;
1547
+ var reuseChecks = /* @__PURE__ */ new Map();
1548
+ var runningBoxIds = /* @__PURE__ */ new Map();
1549
+ var trustedBoxes = /* @__PURE__ */ new WeakSet();
1550
+ function boxStartedAt(box) {
1551
+ const startedAt = box.startedAt instanceof Date ? box.startedAt.getTime() : Number.NaN;
1552
+ return Number.isFinite(startedAt) ? startedAt : void 0;
1553
+ }
1554
+ function checksFor(box) {
1555
+ const checks = reuseChecks.get(box.id);
1556
+ if (!checks) return void 0;
1557
+ if (checks.startedAt !== boxStartedAt(box)) {
1558
+ reuseChecks.delete(box.id);
1559
+ return void 0;
1560
+ }
1561
+ return checks;
1562
+ }
1563
+ function recordCheck(box, patch) {
1564
+ const startedAt = boxStartedAt(box);
1565
+ if (startedAt === void 0 || !box.id) return;
1566
+ const current = checksFor(box) ?? { startedAt };
1567
+ reuseChecks.delete(box.id);
1568
+ reuseChecks.set(box.id, { ...current, ...patch });
1569
+ while (reuseChecks.size > REUSE_CHECKS_MAX) {
1570
+ const oldest = reuseChecks.keys().next().value;
1571
+ if (oldest === void 0) break;
1572
+ reuseChecks.delete(oldest);
1573
+ }
1574
+ }
1575
+ function forgetBox(boxId) {
1576
+ reuseChecks.delete(boxId);
1577
+ }
1578
+ function isRecent(at, ttlMs, now = Date.now()) {
1579
+ return at !== void 0 && Number.isFinite(ttlMs) && ttlMs > 0 && now - at < ttlMs;
1580
+ }
1581
+ function dropTrustedBox(box) {
1582
+ forgetBox(box.id);
1583
+ return trustedBoxes.delete(box);
1584
+ }
1536
1585
  function getClientFromCreds(creds) {
1537
1586
  const fingerprint = `${creds.apiKey} ${creds.baseUrl}`;
1538
1587
  if (_cached && _cached.fingerprint === fingerprint) return _cached.client;
@@ -1550,8 +1599,8 @@ function getClient(shell) {
1550
1599
  }
1551
1600
  function resetClientCache() {
1552
1601
  _cached = null;
1553
- livenessVerifiedAt.clear();
1554
- verifiedEgress.clear();
1602
+ reuseChecks.clear();
1603
+ runningBoxIds.clear();
1555
1604
  }
1556
1605
  function buildAppToolMcpServers(options) {
1557
1606
  const entries = {};
@@ -1985,9 +2034,17 @@ async function materializeDeferredFilesForExistingBox(shell, client, box, stage,
1985
2034
  const corpusHash = deferredCorpusHash(deferredFiles);
1986
2035
  const stampedHash = box.metadata?.[DEFERRED_CORPUS_HASH_KEY];
1987
2036
  if (typeof stampedHash === "string" && stampedHash === corpusHash) return ok(box);
1988
- if (corpusMarker && await corpusMarker === corpusHash) return ok(box);
2037
+ const found = checksFor(box)?.corpus;
2038
+ if (isRecent(found?.at, TRUSTED_REUSE_TTL_MS) && found?.hash === corpusHash) return ok(box);
2039
+ if (await (corpusMarker ?? readDeferredCorpusMarker(box)) === corpusHash) {
2040
+ recordCheck(box, { corpus: { hash: corpusHash, at: Date.now() } });
2041
+ return ok(box);
2042
+ }
1989
2043
  const written = await writeDeferredFilesWithRuntimeAuthRefresh(client, box, deferredFiles, stage, name);
1990
- if (written.succeeded) await writeDeferredCorpusMarker(written.value, corpusHash);
2044
+ if (written.succeeded) {
2045
+ await writeDeferredCorpusMarker(written.value, corpusHash);
2046
+ recordCheck(written.value, { corpus: { hash: corpusHash, at: Date.now() } });
2047
+ }
1991
2048
  return written;
1992
2049
  }
1993
2050
  var DEFERRED_CORPUS_MARKER_PATH = ".agent-app/deferred-corpus-hash";
@@ -2016,6 +2073,26 @@ async function listRunning(client, name) {
2016
2073
  return fail(err);
2017
2074
  }
2018
2075
  }
2076
+ function rememberRunningBox(name, box) {
2077
+ runningBoxIds.delete(name);
2078
+ runningBoxIds.set(name, box.id);
2079
+ while (runningBoxIds.size > REUSE_CHECKS_MAX) {
2080
+ const oldest = runningBoxIds.keys().next().value;
2081
+ if (oldest === void 0) break;
2082
+ runningBoxIds.delete(oldest);
2083
+ }
2084
+ return box;
2085
+ }
2086
+ async function getRunningBox(client, id, name) {
2087
+ try {
2088
+ const box = await client.get(id);
2089
+ if (!box || box.name !== name || box.status !== "running") return null;
2090
+ if (typeof box.metadata?.harness !== "string") return null;
2091
+ return box;
2092
+ } catch {
2093
+ return null;
2094
+ }
2095
+ }
2019
2096
  async function listStopped(client, name) {
2020
2097
  try {
2021
2098
  const stopped = await client.list({ status: "stopped" });
@@ -2027,7 +2104,7 @@ async function listStopped(client, name) {
2027
2104
  async function deleteBox(box) {
2028
2105
  try {
2029
2106
  const acknowledgement = await box.delete({ until: "removed" });
2030
- livenessVerifiedAt.delete(box.id);
2107
+ forgetBox(box.id);
2031
2108
  if (acknowledgement.removal === "pending") {
2032
2109
  return fail(new Error(`sandbox ${box.id} removal is still pending; retry replacement after removal completes`));
2033
2110
  }
@@ -2086,14 +2163,8 @@ async function isBoxAlive(box, probe) {
2086
2163
  return fail(new Error("alive check failed", { cause }));
2087
2164
  }
2088
2165
  }
2089
- var DEFAULT_LIVENESS_CACHE_TTL_MS = 5e3;
2090
2166
  function hasRecentLivenessVerification(box, probe, now = Date.now()) {
2091
- const ttlMs = probe.cacheTtlMs ?? DEFAULT_LIVENESS_CACHE_TTL_MS;
2092
- if (!Number.isFinite(ttlMs) || ttlMs <= 0) return false;
2093
- const verifiedAt = livenessVerifiedAt.get(box.id);
2094
- if (verifiedAt !== void 0 && now - verifiedAt < ttlMs) return true;
2095
- livenessVerifiedAt.delete(box.id);
2096
- return false;
2167
+ return isRecent(checksFor(box)?.alive, probe.cacheTtlMs ?? TRUSTED_REUSE_TTL_MS, now);
2097
2168
  }
2098
2169
  var RUNTIME_CONNECTION_WAIT_MS = 3e4;
2099
2170
  var RUNTIME_CONNECTION_POLL_MS = 1e3;
@@ -2218,14 +2289,17 @@ async function writeDeferredFilesWithRuntimeAuthRefresh(client, box, files, stag
2218
2289
  }
2219
2290
  async function isReusableBox(box, probe) {
2220
2291
  if (sandboxEdgeFailed(box) || !sandboxRuntimeUrl(box)) {
2221
- livenessVerifiedAt.delete(box.id);
2292
+ forgetBox(box.id);
2222
2293
  return fail(new Error(sandboxEdgeFailed(box) ? "runtime edge readiness failed" : "runtime URL is missing"));
2223
2294
  }
2224
2295
  if (!probe) return ok(void 0);
2225
- if (hasRecentLivenessVerification(box, probe)) return ok(void 0);
2296
+ if (hasRecentLivenessVerification(box, probe)) {
2297
+ trustedBoxes.add(box);
2298
+ return ok(void 0);
2299
+ }
2226
2300
  const alive = await isBoxAlive(box, probe);
2227
- if (alive.succeeded) livenessVerifiedAt.set(box.id, Date.now());
2228
- else livenessVerifiedAt.delete(box.id);
2301
+ if (alive.succeeded) recordCheck(box, { alive: Date.now() });
2302
+ else forgetBox(box.id);
2229
2303
  return alive;
2230
2304
  }
2231
2305
  function stoppedBoxResumeError(box, cause) {
@@ -2253,7 +2327,7 @@ async function resumeStoppedBox(box, timeoutMs, runtimeEnv, onProgress) {
2253
2327
  }
2254
2328
  };
2255
2329
  try {
2256
- livenessVerifiedAt.delete(box.id);
2330
+ forgetBox(box.id);
2257
2331
  report("started", "Resuming the stopped sandbox");
2258
2332
  await resumeWithRequiredRuntimeEnv(box, timeoutMs, runtimeEnv);
2259
2333
  await box.waitFor("running", { timeoutMs, ...onProgress ? { onProgress } : {} });
@@ -2368,57 +2442,59 @@ async function resolveWorkspaceRuntimeEnv(shell, scope) {
2368
2442
  assertEnvWithinLimits(env);
2369
2443
  return env;
2370
2444
  }
2371
- function prefetchExistingBoxReads(shell, box, runtimeEnv, measure) {
2372
- const prefetch = {};
2373
- if (shell.egressPolicy && hasVerifiedEgress(box, shell.egressPolicy)) {
2374
- prefetch.egressVerified = true;
2375
- } else if (shell.egressPolicy) {
2376
- prefetch.egress = measure("egress", async () => {
2377
- try {
2378
- return ok(await box.egress.get());
2379
- } catch (cause) {
2380
- return fail(cause);
2381
- }
2382
- });
2383
- }
2384
- if (shell.deferProfileFiles) prefetch.corpusMarker = readDeferredCorpusMarker(box);
2385
- if (!egressGatesCredentials(shell.egressPolicy)) {
2386
- prefetch.runtimeEnv = runtimeEnv.then(
2387
- (env) => Object.keys(env).length === 0 ? ok(void 0) : measure("runtime_env_push", () => box.setRuntimeEnv(env)).then(() => ok(void 0), (cause) => fail(cause)),
2388
- () => ok(void 0)
2389
- );
2445
+ function runtimeEnvDigest(env) {
2446
+ return createHash("sha256").update(JSON.stringify(canonicalizeJson(env)), "utf8").digest("hex");
2447
+ }
2448
+ function hasDeliveredEnv(box, env) {
2449
+ const delivered = checksFor(box)?.env;
2450
+ return isRecent(delivered?.at, TRUSTED_REUSE_TTL_MS) && delivered?.digest === runtimeEnvDigest(env);
2451
+ }
2452
+ function hasRecentEgress(box, desired) {
2453
+ const verified = checksFor(box)?.egress;
2454
+ return isRecent(verified?.at, TRUSTED_REUSE_TTL_MS) && verified?.key === normalizedEgressPolicy(desired);
2455
+ }
2456
+ async function pushRuntimeEnv(box, env, measure) {
2457
+ try {
2458
+ await (measure ? measure("runtime_env_push", () => box.setRuntimeEnv(env)) : box.setRuntimeEnv(env));
2459
+ recordCheck(box, { env: { digest: runtimeEnvDigest(env), at: Date.now() } });
2460
+ return ok(void 0);
2461
+ } catch (cause) {
2462
+ return fail(cause);
2390
2463
  }
2464
+ }
2465
+ function prefetchExistingBoxReads(shell, box, runtimeEnv, measure) {
2466
+ const desired = shell.egressPolicy;
2467
+ const readEgress = () => measure("egress", async () => {
2468
+ try {
2469
+ return ok(await box.egress.get());
2470
+ } catch (cause) {
2471
+ return fail(cause);
2472
+ }
2473
+ });
2474
+ const egressRecent = desired !== void 0 && hasRecentEgress(box, desired);
2475
+ const prefetch = { decided: Promise.resolve() };
2476
+ if (desired && !egressRecent) prefetch.egress = readEgress();
2477
+ if (shell.deferProfileFiles && !isRecent(checksFor(box)?.corpus?.at, TRUSTED_REUSE_TTL_MS)) {
2478
+ prefetch.corpusMarker = readDeferredCorpusMarker(box);
2479
+ }
2480
+ prefetch.decided = runtimeEnv.then((env) => {
2481
+ const delivered = Object.keys(env).length === 0 || hasDeliveredEnv(box, env);
2482
+ prefetch.envDelivered = delivered;
2483
+ if (desired && egressRecent) {
2484
+ if (desired.mode === "open" || delivered && !shell.bootstrap) prefetch.egressVerified = true;
2485
+ else prefetch.egress = readEgress();
2486
+ }
2487
+ if (!delivered && !egressGatesCredentials(desired)) {
2488
+ prefetch.runtimeEnv = pushRuntimeEnv(box, env, measure);
2489
+ }
2490
+ }, () => void 0);
2391
2491
  return prefetch;
2392
2492
  }
2393
2493
  function egressGatesCredentials(desired) {
2394
2494
  return desired !== void 0 && desired.mode !== "open";
2395
2495
  }
2396
- var VERIFIED_EGRESS_TTL_MS = 10 * 6e4;
2397
- var VERIFIED_EGRESS_MAX = 1e3;
2398
- function verifiedEgressKey(box, desired) {
2399
- if (desired.mode !== "open" || !box.id) return null;
2400
- const startedAt = box.startedAt instanceof Date ? box.startedAt.getTime() : Number.NaN;
2401
- if (!Number.isFinite(startedAt)) return null;
2402
- return `${box.id}|${startedAt}|${normalizedEgressPolicy(desired)}`;
2403
- }
2404
- function hasVerifiedEgress(box, desired, now = Date.now()) {
2405
- const key = verifiedEgressKey(box, desired);
2406
- if (!key) return false;
2407
- const verifiedAt = verifiedEgress.get(key);
2408
- if (verifiedAt !== void 0 && now - verifiedAt < VERIFIED_EGRESS_TTL_MS) return true;
2409
- verifiedEgress.delete(key);
2410
- return false;
2411
- }
2412
2496
  function recordVerifiedEgress(box, desired) {
2413
- const key = verifiedEgressKey(box, desired);
2414
- if (!key) return;
2415
- verifiedEgress.delete(key);
2416
- verifiedEgress.set(key, Date.now());
2417
- while (verifiedEgress.size > VERIFIED_EGRESS_MAX) {
2418
- const oldest = verifiedEgress.keys().next().value;
2419
- if (oldest === void 0) break;
2420
- verifiedEgress.delete(oldest);
2421
- }
2497
+ recordCheck(box, { egress: { key: normalizedEgressPolicy(desired), at: Date.now() } });
2422
2498
  }
2423
2499
  async function finalizeExistingBox(shell, client, box, stage, name, workspaceId, userId, harness, scope, runtimeEnv, prefetch = {}) {
2424
2500
  if (!prefetch.egressVerified) {
@@ -2442,8 +2518,9 @@ async function finalizeExistingBox(shell, client, box, stage, name, workspaceId,
2442
2518
  if (prefetch.runtimeEnv) {
2443
2519
  const renewed = await prefetch.runtimeEnv;
2444
2520
  if (!renewed.succeeded) throw renewed.error;
2445
- } else if (Object.keys(runtimeEnv).length > 0) {
2446
- await finalBox.setRuntimeEnv(runtimeEnv);
2521
+ } else if (!prefetch.envDelivered && Object.keys(runtimeEnv).length > 0) {
2522
+ const renewed = await pushRuntimeEnv(finalBox, runtimeEnv);
2523
+ if (!renewed.succeeded) throw renewed.error;
2447
2524
  }
2448
2525
  if (shell.bootstrap) {
2449
2526
  const boot = await shell.bootstrap(finalBox, scope);
@@ -2531,11 +2608,37 @@ async function assertExistingBoxEgress(box, desired, migrate, stage, name, prefe
2531
2608
  );
2532
2609
  }
2533
2610
  async function ensureWorkspaceSandbox(shell, options) {
2534
- await options.spend?.beforeProvision?.({
2535
- workspaceId: options.workspaceId,
2536
- ...options.userId ? { userId: options.userId } : {}
2537
- });
2611
+ const box = await claimWorkspaceSandbox(shell, options, false);
2612
+ if (!box) throw new Error("ensureWorkspaceSandbox: a claim that may provision returned no box");
2613
+ return box;
2614
+ }
2615
+ async function reuseWorkspaceSandbox(shell, options) {
2616
+ if (options.forceNew || !hasRecentProbe(shell, options)) return void 0;
2617
+ return claimWorkspaceSandbox(shell, options, true);
2618
+ }
2619
+ function hasRecentProbe(shell, options) {
2620
+ const probe = shell.livenessProbe;
2621
+ if (!probe || options.forceNew) return false;
2622
+ const scope = { workspaceId: options.workspaceId, ...options.userId ? { userId: options.userId } : {} };
2623
+ const id = runningBoxIds.get(shell.boxKey ? shell.boxKey(scope) : shell.name(options.workspaceId));
2624
+ return isRecent(id === void 0 ? void 0 : reuseChecks.get(id)?.alive, probe.cacheTtlMs ?? TRUSTED_REUSE_TTL_MS);
2625
+ }
2626
+ function spendGate(options) {
2627
+ let gated;
2628
+ return () => {
2629
+ const before = options.spend?.beforeProvision;
2630
+ if (!before) return Promise.resolve();
2631
+ const run = () => before({
2632
+ workspaceId: options.workspaceId,
2633
+ ...options.userId ? { userId: options.userId } : {}
2634
+ });
2635
+ return gated ??= Promise.resolve(options.trace ? options.trace.measure("sandbox.spend.before_provision", "sandbox", run, void 0, options.traceParent) : run());
2636
+ };
2637
+ }
2638
+ async function claimWorkspaceSandbox(shell, options, reuseOnly) {
2538
2639
  const { trace } = options;
2640
+ const gate = spendGate(options);
2641
+ if (!hasRecentProbe(shell, options)) await gate();
2539
2642
  const claim = trace?.start("sandbox.claim", "sandbox", { "workspace.harness": options.harness }, options.traceParent);
2540
2643
  const traced = trace && claim ? {
2541
2644
  ...options,
@@ -2545,9 +2648,13 @@ async function ensureWorkspaceSandbox(shell, options) {
2545
2648
  }
2546
2649
  } : options;
2547
2650
  try {
2548
- const box = await bringUpWorkspaceSandbox(shell, traced);
2549
- claim?.end({ "sandbox.id": box.id });
2550
- await observeProvisionedBox(shell, options, box);
2651
+ const box = await bringUpWorkspaceSandbox(shell, traced, gate, reuseOnly);
2652
+ if (!box) {
2653
+ claim?.end({ "sandbox.reuse_declined": true });
2654
+ return void 0;
2655
+ }
2656
+ claim?.end({ "sandbox.id": box.id, ...trustedBoxes.has(box) ? { "sandbox.probe_trusted": true } : {} });
2657
+ if (!trustedBoxes.has(box)) await observeProvisionedBox(shell, options, box);
2551
2658
  return box;
2552
2659
  } catch (error) {
2553
2660
  claim?.end(void 0, error);
@@ -2555,10 +2662,11 @@ async function ensureWorkspaceSandbox(shell, options) {
2555
2662
  }
2556
2663
  }
2557
2664
  async function observeProvisionedBox(shell, options, box) {
2558
- if (!options.spend?.onProvisioned) return;
2665
+ const onProvisioned = options.spend?.onProvisioned;
2666
+ if (!onProvisioned) return;
2559
2667
  const resources = shell.resources ?? DEFAULT_SANDBOX_RESOURCES;
2560
- try {
2561
- await options.spend.onProvisioned({
2668
+ const observe = async () => {
2669
+ await onProvisioned({
2562
2670
  workspaceId: options.workspaceId,
2563
2671
  ...options.userId ? { userId: options.userId } : {},
2564
2672
  sandboxId: box.id,
@@ -2567,17 +2675,20 @@ async function observeProvisionedBox(shell, options, box) {
2567
2675
  maxLifetimeSeconds: resources.maxLifetimeSeconds,
2568
2676
  at: Date.now()
2569
2677
  });
2678
+ };
2679
+ try {
2680
+ await (options.trace ? options.trace.measure("sandbox.spend.observe", "sandbox", observe, void 0, options.traceParent) : observe());
2570
2681
  } catch {
2571
2682
  }
2572
2683
  }
2573
- async function bringUpWorkspaceSandbox(shell, options) {
2684
+ async function bringUpWorkspaceSandbox(shell, options, gate, reuseOnly) {
2574
2685
  try {
2575
- return await provisionWorkspaceSandbox(shell, options);
2686
+ return await provisionWorkspaceSandbox(shell, options, gate, reuseOnly);
2576
2687
  } catch (err) {
2577
2688
  if (!shell.replaceUnbringableBox) throw err;
2578
2689
  if (options.forceNew) throw err;
2579
2690
  if (!isUnbringableBoxError(err)) throw err;
2580
- return await provisionWorkspaceSandbox(shell, { ...options, forceNew: true });
2691
+ return await provisionWorkspaceSandbox(shell, { ...options, forceNew: true }, gate, reuseOnly);
2581
2692
  }
2582
2693
  }
2583
2694
  function isUnbringableBoxError(error) {
@@ -2611,7 +2722,7 @@ async function requestMissingSandboxReplacement(shell, failure) {
2611
2722
  "missing sandbox recovery"
2612
2723
  );
2613
2724
  }
2614
- async function provisionWorkspaceSandbox(shell, options) {
2725
+ async function provisionWorkspaceSandbox(shell, options, gate, reuseOnly) {
2615
2726
  const execTimeoutMs = shell.livenessProbe?.execTimeoutMs;
2616
2727
  if (execTimeoutMs !== void 0 && (!Number.isInteger(execTimeoutMs) || execTimeoutMs < 100 || execTimeoutMs > 6e5)) {
2617
2728
  throw new Error("livenessProbe.execTimeoutMs must be an integer between 100 and 600000");
@@ -2625,7 +2736,17 @@ async function provisionWorkspaceSandbox(shell, options) {
2625
2736
  let replacementRequested = false;
2626
2737
  const resources = shell.resources ?? DEFAULT_SANDBOX_RESOURCES;
2627
2738
  const resumeTimeout = shell.provisionTimeoutMs ?? DEFAULT_PROVISION_TIMEOUT_MS;
2628
- let existing = await measure("list_running", () => listRunning(client, name));
2739
+ let existing = ok(null);
2740
+ const knownId = forceNew ? void 0 : runningBoxIds.get(name);
2741
+ if (knownId) {
2742
+ const known = await measure("get_running", () => getRunningBox(client, knownId, name));
2743
+ if (known) existing = ok(known);
2744
+ else runningBoxIds.delete(name);
2745
+ }
2746
+ if (reuseOnly && !(existing.succeeded && existing.value)) return void 0;
2747
+ if (!(existing.succeeded && existing.value)) {
2748
+ existing = await measure("list_running", () => listRunning(client, name));
2749
+ }
2629
2750
  if (forceNew && existing.succeeded && !existing.value) {
2630
2751
  existing = await listStopped(client, name);
2631
2752
  }
@@ -2645,13 +2766,29 @@ async function provisionWorkspaceSandbox(shell, options) {
2645
2766
  measure("refresh", () => refreshRuntimeConnection(client, found))
2646
2767
  ]);
2647
2768
  if (envResult.status === "rejected") throw envResult.reason;
2769
+ await prefetch.decided;
2648
2770
  const runtimeEnv2 = envResult.value;
2771
+ const refusal = { refused: false };
2772
+ const gateBeforeUse = async () => {
2773
+ try {
2774
+ await gate();
2775
+ } catch (error) {
2776
+ refusal.refused = true;
2777
+ refusal.error = error;
2778
+ throw error;
2779
+ }
2780
+ };
2649
2781
  try {
2650
2782
  if (readyResult.status === "rejected") throw readyResult.reason;
2651
2783
  const ready = readyResult.value;
2652
2784
  if ((await measure("liveness", () => isReusableBox(ready, shell.livenessProbe))).succeeded) {
2653
- return await measure("finalize", () => finalizeExistingBox(shell, client, ready, "reused", name, workspaceId, userId, harness, scope, runtimeEnv2, prefetch));
2785
+ if (!trustedBoxes.has(ready)) await gateBeforeUse();
2786
+ const finalBox = await measure("finalize", () => finalizeExistingBox(shell, client, ready, "reused", name, workspaceId, userId, harness, scope, runtimeEnv2, prefetch));
2787
+ if (trustedBoxes.has(ready)) trustedBoxes.add(finalBox);
2788
+ return rememberRunningBox(name, finalBox);
2654
2789
  }
2790
+ if (reuseOnly) return void 0;
2791
+ await gateBeforeUse();
2655
2792
  const recovered = await recoverUnresponsiveBox(
2656
2793
  client,
2657
2794
  ready,
@@ -2662,8 +2799,10 @@ async function provisionWorkspaceSandbox(shell, options) {
2662
2799
  runtimeEnv2,
2663
2800
  onProgress
2664
2801
  );
2665
- return await finalizeExistingBox(shell, client, recovered, "reused", name, workspaceId, userId, harness, scope, runtimeEnv2);
2802
+ return rememberRunningBox(name, await finalizeExistingBox(shell, client, recovered, "reused", name, workspaceId, userId, harness, scope, runtimeEnv2));
2666
2803
  } catch (cause) {
2804
+ if (refusal.refused) throw refusal.error;
2805
+ if (reuseOnly) return void 0;
2667
2806
  const error = cause instanceof Error ? cause : new Error(String(cause));
2668
2807
  const recovery = await requestMissingSandboxReplacement(shell, {
2669
2808
  box: found,
@@ -2677,6 +2816,8 @@ async function provisionWorkspaceSandbox(shell, options) {
2677
2816
  replacementRequested = true;
2678
2817
  }
2679
2818
  } else {
2819
+ if (reuseOnly) return void 0;
2820
+ await gate();
2680
2821
  const dropped = await deleteBox(found);
2681
2822
  if (!dropped.succeeded) {
2682
2823
  throw new Error(
@@ -2686,6 +2827,8 @@ async function provisionWorkspaceSandbox(shell, options) {
2686
2827
  }
2687
2828
  }
2688
2829
  }
2830
+ if (reuseOnly) return void 0;
2831
+ await gate();
2689
2832
  if (!replacementRequested && !forceNew && shell.resumeStopped !== false) {
2690
2833
  const stopped = await measure("list_stopped", () => listStopped(client, name));
2691
2834
  if (!stopped.succeeded) throw stopped.error;
@@ -2705,7 +2848,7 @@ async function provisionWorkspaceSandbox(shell, options) {
2705
2848
  try {
2706
2849
  const box2 = await refreshRuntimeConnection(client, resumed.value);
2707
2850
  if ((await isReusableBox(box2, shell.livenessProbe)).succeeded) {
2708
- return await finalizeExistingBox(shell, client, box2, "resumed", name, workspaceId, userId, harness, scope, runtimeEnv2);
2851
+ return rememberRunningBox(name, await finalizeExistingBox(shell, client, box2, "resumed", name, workspaceId, userId, harness, scope, runtimeEnv2));
2709
2852
  }
2710
2853
  const recovered = await recoverUnresponsiveBox(
2711
2854
  client,
@@ -2717,7 +2860,7 @@ async function provisionWorkspaceSandbox(shell, options) {
2717
2860
  runtimeEnv2,
2718
2861
  onProgress
2719
2862
  );
2720
- return await finalizeExistingBox(shell, client, recovered, "resumed", name, workspaceId, userId, harness, scope, runtimeEnv2);
2863
+ return rememberRunningBox(name, await finalizeExistingBox(shell, client, recovered, "resumed", name, workspaceId, userId, harness, scope, runtimeEnv2));
2721
2864
  } catch (cause) {
2722
2865
  const error = cause instanceof Error ? cause : new Error(String(cause));
2723
2866
  const recovery = await requestMissingSandboxReplacement(shell, {
@@ -2821,7 +2964,7 @@ async function provisionWorkspaceSandbox(shell, options) {
2821
2964
  throw new Error(`bootstrap failed on new box ${name}`, { cause: boot.error });
2822
2965
  }
2823
2966
  }
2824
- return box;
2967
+ return rememberRunningBox(name, box);
2825
2968
  }
2826
2969
  function historyTranscript(history) {
2827
2970
  return history.map((entry) => `${entry.role === "assistant" ? "Assistant" : "User"}: ${entry.content}`).join("\n\n");
@@ -3217,8 +3360,10 @@ async function* streamSandboxPrompt(shell, box, message, options) {
3217
3360
  ...options.detach ? { "sandbox.detached": true } : {}
3218
3361
  }
3219
3362
  }) : stream;
3363
+ let delivered = false;
3220
3364
  try {
3221
3365
  for await (const event of observed) {
3366
+ delivered = true;
3222
3367
  const step = classifySeveredStream(event);
3223
3368
  if (step) severedFinishReason = step.kind === "step-finish" && step.severed ? step.reason : null;
3224
3369
  if (severedFinishReason && isTerminalPromptEvent(event)) {
@@ -3232,6 +3377,9 @@ async function* streamSandboxPrompt(shell, box, message, options) {
3232
3377
  }
3233
3378
  yield event;
3234
3379
  }
3380
+ } catch (error) {
3381
+ if (!delivered && !options?.signal?.aborted) dropTrustedBox(box);
3382
+ throw error;
3235
3383
  } finally {
3236
3384
  emitSandboxActivity(options?.spend, box);
3237
3385
  }
@@ -3477,6 +3625,7 @@ export {
3477
3625
  PYPI_EGRESS_DOMAINS,
3478
3626
  buildProductEgressPolicy,
3479
3627
  DEFAULT_SANDBOX_RESOURCES,
3628
+ dropTrustedBox,
3480
3629
  getClient,
3481
3630
  resetClientCache,
3482
3631
  buildAppToolMcpServers,
@@ -3504,6 +3653,7 @@ export {
3504
3653
  assertEnvWithinLimits,
3505
3654
  peekWorkspaceSandbox,
3506
3655
  ensureWorkspaceSandbox,
3656
+ reuseWorkspaceSandbox,
3507
3657
  flattenHistory,
3508
3658
  mergeHistoryIntoParts,
3509
3659
  mergeExtraMcp,
@@ -3527,4 +3677,4 @@ export {
3527
3677
  isTerminalPromptEvent,
3528
3678
  detectInteractiveQuestion
3529
3679
  };
3530
- //# sourceMappingURL=chunk-N7FGEWZU.js.map
3680
+ //# sourceMappingURL=chunk-QA2CBATZ.js.map