@tanstack/ai-sandbox 0.5.4 → 0.5.5

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.
@@ -12,7 +12,7 @@ import { provideSandboxDurability, resolveSandboxDurability } from "./durability
12
12
  import { buildFileHookEvent, resolveFileEvents } from "./file-diff.js";
13
13
  import { createToolHistoryRecorder, stripObservedToolCalls } from "./tool-history.js";
14
14
  import { watchWorkspace } from "./watch.js";
15
- import { defineChatMiddleware, provideDetachableRun, provideRunDetached, wasCancelRequested } from "@tanstack/ai";
15
+ import { defineChatMiddleware, isTerminalRunStatus, provideDetachableRun, provideRunDetached, wasCancelRequested } from "@tanstack/ai";
16
16
  import { InMemoryLockStore, LocksCapability } from "@tanstack/ai/locks";
17
17
  import { getPendingTurn, getRunDisconnect, getSandboxRuntime } from "@tanstack/ai/adapter-internals";
18
18
  //#region src/middleware.ts
@@ -125,6 +125,10 @@ function canPublishPortableSnapshot(state, lease) {
125
125
  */
126
126
  async function recordDetach(definition, state, durability, ctx, phase) {
127
127
  try {
128
+ try {
129
+ const current = await durability.runs.get(ctx.runId);
130
+ if (current !== null && isTerminalRunStatus(current.status)) return true;
131
+ } catch {}
128
132
  await durability.runs.update(ctx.runId, {
129
133
  detachedSince: Date.now(),
130
134
  sandboxKey: definition.key(state.ensureCtx)
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.js","names":[],"sources":["../../src/middleware.ts"],"sourcesContent":["/**\n * `withSandbox(definition, options?)` — the middleware that PROVIDES the\n * {@link SandboxCapability} a harness adapter requires.\n *\n * - `setup`: resume-or-create the sandbox (via the definition's ensure\n * algorithm), provide the handle, using the durability seams from\n * {@link SandboxMiddlewareOptions} (or, failing that, a bus-provided\n * SandboxInstanceStoreCapability / LocksCapability, then an in-memory\n * fallback). If `fileEvents` is not false, starts a\n * watcher that dispatches to sandbox-scoped hooks and forwards to the runtime\n * sink.\n * - `onFinish`/`onAbort`/`onError`: stop the watcher, snapshot (`after-run`)\n * and/or destroy per lifecycle.\n *\n * NOTE: streamed sandbox lifecycle events (sandbox.created, workspace.setup.*)\n * are emitted by the harness adapter's chatStream (which can yield CUSTOM\n * chunks), not from here — middleware setup runs before streaming begins.\n */\nimport {\n defineChatMiddleware,\n provideDetachableRun,\n provideRunDetached,\n wasCancelRequested,\n} from '@tanstack/ai'\nimport { InMemoryLockStore, LocksCapability } from '@tanstack/ai/locks'\nimport {\n getPendingTurn,\n getRunDisconnect,\n getSandboxRuntime,\n} from '@tanstack/ai/adapter-internals'\nimport {\n SandboxCapability,\n provideSandbox,\n provideSandboxPolicy,\n} from './capabilities'\nimport {\n provideSandboxDurability,\n resolveSandboxDurability,\n} from './durability'\nimport { SandboxInstanceStoreCapability } from './instance-store'\nimport { computeWorkspaceHash } from './key'\nimport { buildFileHookEvent, resolveFileEvents } from './file-diff'\nimport { ProjectionCapability, provideWorkspaceProjection } from './projection'\nimport { resolveAllSecrets, resolveSecret } from './secrets'\nimport {\n createToolHistoryRecorder,\n stripObservedToolCalls,\n} from './tool-history'\nimport { watchWorkspace } from './watch'\nimport { DEFAULT_WORKSPACE_ROOT } from './bootstrap'\nimport { resolveHarnessCwd } from './harness-cwd'\nimport { ensureSandboxWithOutcome } from './sandbox'\nimport {\n restoreSandboxFiles,\n captureSandboxFiles,\n captureSandboxArtifacts,\n resolveSandboxSnapshotPolicy,\n} from './snapshots'\nimport type { SandboxSnapshotPolicy } from './snapshots'\nimport type {\n SandboxCheckpointStore,\n SandboxCheckpointWriterLease,\n} from './checkpoint-store'\nimport { SandboxCheckpointError } from './checkpoint-store'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type { LockStore } from '@tanstack/ai/locks'\nimport type {\n AbortInfo,\n ChatMiddlewareContext,\n DefinedChatMiddleware,\n ModelMessage,\n RunStore,\n SandboxFileEvent,\n SandboxFileHookEvent,\n} from '@tanstack/ai'\nimport type {\n SandboxDurabilityOptions,\n SandboxRunDurability,\n} from './durability'\nimport type { SandboxInstanceStore } from './instance-store'\nimport type { ToolHistoryRecorder } from './tool-history'\nimport type { SandboxHandle } from './contracts'\nimport type {\n SandboxDefinition,\n SandboxEnsureContext,\n SandboxHooks,\n} from './sandbox'\nimport type { SandboxWatchHandle } from './watch'\n\n/** Per-request state we need to carry from `setup` to the terminal hooks. */\ninterface SandboxRunState {\n snapshotLease?: SandboxCheckpointWriterLease\n snapshotRenewal?: ReturnType<typeof setTimeout>\n snapshotRenewTask?: Promise<void>\n snapshotCaptureTask?: Promise<void>\n snapshotRenewalGeneration: number\n snapshotStop?: Promise<void>\n /** A detached or paused run cannot later publish portable state. */\n snapshotClosed?: boolean\n snapshotLost?: Error\n snapshotCleaned?: boolean\n snapshotConfig?: NonNullable<SandboxMiddlewareOptions['snapshots']>\n snapshotPolicy?: SandboxSnapshotPolicy\n snapshotRuntime?: {\n persistence: NonNullable<\n SandboxMiddlewareOptions['snapshots']\n >['persistence']\n completion: { waitForRunCompletion: () => Promise<void> }\n }\n /**\n * OPTIONAL because the state is registered BEFORE `definition.ensure()` is\n * awaited, and `ensure` is the slowest thing in the whole run — cloning a repo\n * into a fresh sandbox is minutes wide. That window is where the most common\n * disconnect of all lands (a user starts a run and switches away while the UI\n * still says \"starting the sandbox\"), so it is the one window the teardown and\n * disconnect hooks most need to be able to act in. Registering only after the\n * handle exists left exactly that window uncovered.\n *\n * Nothing the disconnect path does needs the handle: `detachedSince` and\n * `sandboxKey` come from `ensureCtx`, which is built before `ensure` is called.\n * Only `onFinish`'s snapshot needs it, and that cannot run before `setup` has\n * completed.\n */\n handle?: SandboxHandle\n privateHandle?: boolean\n ensureCtx: SandboxEnsureContext\n watcher?: SandboxWatchHandle\n /** In-flight `enriched.diff()` promises queued by the `fileEvents.diff`\n * watcher callback, awaited before teardown so a pending diff isn't\n * dropped when the run finishes/aborts/errors mid-computation. */\n pendingDiffs: Array<Promise<void>>\n /** Logger captured at setup, so terminal hooks can log watcher teardown. */\n logger?: InternalLogger\n /**\n * Durability resolved once at setup (absent when the run is not durable), so\n * `onAbort` cannot reach a different verdict than the one `setup` published\n * on the capability bus.\n */\n durability?: SandboxRunDurability\n /**\n * Records the harness's own tool calls into the transcript, so a finished run\n * restores its tool cards from the message store instead of only from the (live,\n * rejoin-only) delivery log. See `./tool-history`.\n */\n toolHistory: ToolHistoryRecorder\n}\n\nconst runState = new WeakMap<object, SandboxRunState>()\n\nfunction stopSnapshotLease(\n state: SandboxRunState,\n options: { closePortable?: boolean } = {},\n): Promise<void> {\n if (options.closePortable) state.snapshotClosed = true\n if (state.snapshotStop) return state.snapshotStop\n if (state.snapshotCleaned) return Promise.resolve()\n state.snapshotCleaned = true\n state.snapshotRenewalGeneration++\n if (state.snapshotRenewal !== undefined) clearTimeout(state.snapshotRenewal)\n state.snapshotRenewal = undefined\n const renewTask = state.snapshotRenewTask\n const captureTask = state.snapshotCaptureTask\n const lease = state.snapshotLease\n state.snapshotLease = undefined\n state.snapshotStop = (async () => {\n await renewTask?.catch(() => {})\n await captureTask?.catch(() => {})\n await lease?.release()\n })()\n return state.snapshotStop\n}\n\nfunction startSnapshotRenewal(state: SandboxRunState): void {\n const lease = state.snapshotLease\n if (!lease) return\n const schedule = (): void => {\n const generation = state.snapshotRenewalGeneration\n state.snapshotRenewal = setTimeout(() => {\n void (async (): Promise<void> => {\n state.snapshotRenewal = undefined\n if (\n state.snapshotCleaned ||\n generation !== state.snapshotRenewalGeneration\n )\n return\n const renewal = Promise.resolve().then(async (): Promise<void> => {\n await lease.renew()\n })\n state.snapshotRenewTask = renewal\n try {\n await renewal\n } catch (error) {\n state.snapshotLost =\n error instanceof Error ? error : new Error(String(error))\n } finally {\n if (state.snapshotRenewTask === renewal)\n state.snapshotRenewTask = undefined\n }\n if (state.snapshotLost) await stopSnapshotLease(state).catch(() => {})\n else if (\n !state.snapshotCleaned &&\n generation === state.snapshotRenewalGeneration\n )\n schedule()\n })()\n }, lease.renewAfterMs)\n }\n schedule()\n}\n\n/**\n * Stop the watcher and drain any in-flight `diff()` promises before teardown,\n * so the final file's diff isn't dropped when a run finishes/aborts/errors\n * mid-computation. The `pendingDiffs` await is the load-bearing line — without\n * it a deferred diff resolves after the run is gone and its chunk is lost.\n */\nasync function drainWatcher(\n state: SandboxRunState,\n phase: 'finish' | 'pause' | 'abort' | 'error',\n): Promise<void> {\n // Guard `stop()`: a rejecting watcher teardown must NOT propagate out of\n // here, or the caller skips the `definition.destroy(...)` that follows —\n // leaking the sandbox on exactly the abort path that must ALWAYS tear down.\n try {\n await state.watcher?.stop()\n } catch (error) {\n state.logger?.warn('sandbox watcher stop failed', { phase, error })\n }\n await Promise.allSettled(state.pendingDiffs)\n if (state.watcher) state.logger?.sandbox('sandbox watcher stopped', { phase })\n}\n\nfunction canPublishPortableSnapshot(\n state: SandboxRunState,\n lease: SandboxCheckpointWriterLease,\n): boolean {\n if (state.snapshotLost) throw state.snapshotLost\n return (\n !state.snapshotClosed &&\n !state.snapshotCleaned &&\n state.snapshotLease === lease\n )\n}\n\n/**\n * Record the two facts a later attach and the reaper both need, then publish the\n * detach verdict core reads.\n *\n * Shared by the DISCONNECT subscriber registered in `setup` (the run is still\n * going — the normal case) and `onAbort`'s detach branch (the run is being torn\n * down while detachable), so the two can never write a different shape of detach.\n *\n * GUARDED, and reports failure rather than throwing. `update` is a documented\n * no-op for an unknown runId, so a vanished record does not turn teardown into a\n * throw; a genuinely rejecting store is the caller's to react to — `onAbort` falls\n * through to destroying the sandbox, because a DESTROYED sandbox beats an\n * unreachable one, while the disconnect subscriber has nothing to fall back to\n * (the run is alive and still using the sandbox) and simply leaves the verdict\n * unpublished.\n *\n * The verdict is published ONLY on success. Publishing it after a failed record\n * write would leave core holding the log open for a takeover that can never be\n * found, since nothing in the store points at the run.\n */\nasync function recordDetach(\n definition: SandboxDefinition,\n state: SandboxRunState,\n durability: SandboxRunDurability,\n ctx: ChatMiddlewareContext,\n phase: 'disconnect' | 'abort',\n): Promise<boolean> {\n try {\n // The record already exists: `setup` pre-creates it for every durable run\n // BEFORE `ensure`, precisely so this stamp cannot land on a runId the store has\n // never heard of — `RunStore.update` is a documented no-op for an unknown\n // runId, which is how the detach used to be lost silently (measured against the\n // browser repro: `detached_since` and `sandbox_key` both stayed NULL for a run\n // that had genuinely detached). If it has since vanished, that no-op is the\n // correct outcome and this must not throw.\n await durability.runs.update(ctx.runId, {\n detachedSince: Date.now(),\n sandboxKey: definition.key(state.ensureCtx),\n })\n } catch (error) {\n state.logger?.warn('sandbox detach record write failed', {\n runId: ctx.runId,\n phase,\n error,\n })\n return false\n }\n // Core's durable delivery sink reads this (see `RunDetachedCapability`) and\n // leaves the run's log OPEN instead of appending a synthetic terminal\n // `RUN_ERROR` and closing it — a terminalized log ends a later attach's replay\n // at the prefix and diverges the takeover's journal replay, which recorded a\n // healthy detached run as `'failed'`.\n provideRunDetached(ctx, true)\n return true\n}\n\n/**\n * Whether an out-of-band cancel has been recorded for this run, in EITHER band.\n * A user pressing Stop and a user closing the tab produce the IDENTICAL\n * connection close, so intent is never inferred from the disconnect itself: it\n * arrives in-process (the abort reason carried the cancel sentinel) or durably\n * (another host recorded it on the run record).\n */\nasync function cancelIntent(\n durability: SandboxRunDurability | undefined,\n runId: string,\n inProcess: boolean,\n): Promise<boolean> {\n if (inProcess) return true\n if (durability === undefined) return false\n // No guard needed here, and one would be dead code: `wasCancelRequested` already\n // answers `false` for a store read that rejects. That matters on this path,\n // because a rejection escaping into `onAbort` would skip BOTH of its branches at\n // once, leaving a sandbox that is neither reclaimable nor destroyed. The test\n // 'DETACHES when the cancel probe REJECTS' pins the composition.\n return wasCancelRequested(durability.runs, runId)\n}\n\n/** Defensively pull tenant scoping out of the runtime context, if present. */\nfunction tenantFrom(\n context: unknown,\n): { userId?: string; orgId?: string } | undefined {\n if (context === null || typeof context !== 'object') return undefined\n const c = context as Record<string, unknown>\n const userId = typeof c.userId === 'string' ? c.userId : undefined\n const orgId = typeof c.orgId === 'string' ? c.orgId : undefined\n if (userId === undefined && orgId === undefined) return undefined\n return { userId, orgId }\n}\n\n/**\n * Durability seams for a sandboxed run. Both are optional; each independently\n * falls back to a process-lifetime in-memory default, which is correct for a\n * single process but NOT across replicas.\n */\nexport interface SandboxMiddlewareOptions<TOffset extends string = string> {\n snapshots?: {\n persistence: {\n stores: {\n messages: {\n loadThread: (threadId: string) => Promise<ReadonlyArray<ModelMessage>>\n }\n artifacts: {\n listForThread: (threadId: string) => Promise<\n ReadonlyArray<{\n artifactId: string\n runId: string\n threadId: string\n blobKey?: string\n name: string\n mimeType: string\n size: number\n createdAt: number\n }>\n >\n }\n blobs: {\n get: (key: string) => Promise<{\n arrayBuffer: () => Promise<ArrayBuffer>\n } | null>\n head: (key: string) => Promise<unknown>\n put: (key: string, body: Uint8Array) => Promise<unknown>\n }\n }\n }\n checkpoints: SandboxCheckpointStore\n policy?: SandboxSnapshotPolicy\n }\n /**\n * Durable instance map (which provider sandbox to resume for a key). Pass\n * your own store to make resume survive across processes/replicas.\n *\n * Takes precedence over a store provided on the capability bus (see\n * `provideSandboxInstanceStore`), so the call site wins over ambient wiring.\n */\n instances?: SandboxInstanceStore\n /**\n * Distributed lock serializing resume-or-create for one key. Needed for\n * multi-replica correctness so two concurrent runs don't both create.\n *\n * Prefer `withLocks` from `@tanstack/ai/locks` when other middleware also\n * needs the lock; use this option to scope one to this sandbox. Takes\n * precedence over a bus-provided lock.\n */\n locks?: LockStore\n /**\n * Run lifecycle records. Pair with `durability.adapter` to make a run\n * DETACHABLE: a client disconnect then leaves the agent running and records\n * `detachedSince` instead of destroying the sandbox.\n *\n * Pass the SAME store chat persistence uses (`persistence.stores.runs`) so\n * one record describes the run instead of two that can disagree.\n *\n * Defaults to `undefined`: an app that passes neither this nor `durability`\n * keeps today's destroy-on-disconnect behavior exactly.\n */\n runs?: RunStore\n /**\n * Delivery durability for the run's event log, plus the journal and detach\n * knobs. Requires `runs`; either alone is not durable.\n *\n * `TOffset` is inferred from the adapter passed here, so a branded-cursor\n * backend (`durableStream`) wires without a cast and without the call site\n * ever naming the parameter.\n */\n durability?: SandboxDurabilityOptions<TOffset>\n}\n\n/**\n * Resolve the ensure seams. Precedence is explicit option → capability bus →\n * (in `ensure`) the in-memory fallback. The option wins because it is visible\n * at the call site; the bus remains for platform/framework injection.\n */\nfunction buildEnsureCtx(\n ctx: ChatMiddlewareContext,\n // Narrowed to the two seams it reads rather than taking the whole options\n // object: `SandboxMiddlewareOptions` is now generic in the durability offset,\n // and `SandboxMiddlewareOptions<TOffset>` is not assignable to\n // `SandboxMiddlewareOptions<string>`. Both members here are offset-free, so\n // the narrowing keeps this helper independent of that parameter entirely.\n options: Pick<SandboxMiddlewareOptions, 'instances' | 'locks'> | undefined,\n): SandboxEnsureContext {\n return {\n threadId: ctx.threadId,\n runId: ctx.runId,\n store:\n options?.instances ?? ctx.getOptional(SandboxInstanceStoreCapability),\n locks: options?.locks ?? ctx.getOptional(LocksCapability),\n tenant: tenantFrom(ctx.context),\n signal: ctx.signal,\n adapterName: ctx.provider,\n }\n}\n\n/**\n * Dispatch a sandbox file event to the per-type hooks declared on the\n * definition. Errors in individual hooks are swallowed so one bad hook\n * cannot break the run — but are logged under the `errors` category first, so\n * a throwing hook is observable (matching the run-scoped path in the engine\n * and the behavior the observability docs promise).\n */\nasync function dispatchDefinitionHooks(\n hooks: SandboxHooks | undefined,\n event: SandboxFileHookEvent,\n logger?: InternalLogger,\n): Promise<void> {\n if (!hooks) return\n const typed = (\n {\n create: 'onFileCreate',\n change: 'onFileChange',\n delete: 'onFileDelete',\n } as const\n )[event.type]\n for (const fn of [hooks.onFile, hooks[typed]]) {\n if (!fn) continue\n try {\n await fn(event)\n } catch (error) {\n // swallowed — one bad hook must not break the run — but logged so the\n // failure isn't invisible.\n logger?.errors('sandbox file hook failed', {\n path: event.path,\n type: event.type,\n error,\n })\n }\n }\n}\n\nexport function withSandbox<TOffset extends string = string>(\n definition: SandboxDefinition,\n options?: SandboxMiddlewareOptions<TOffset>,\n): DefinedChatMiddleware<\n unknown,\n readonly [],\n readonly [typeof SandboxCapability, typeof ProjectionCapability]\n> {\n return defineChatMiddleware({\n name: 'sandbox',\n provides: [SandboxCapability, ProjectionCapability],\n // SandboxPolicyCapability is provided conditionally (only when the\n // definition has a policy), so it is intentionally NOT declared here —\n // consumers read it via `getOptional`. SandboxDurabilityCapability and\n // DetachableRunCapability are conditional for the same reason (only when\n // `runs` + `durability` are both wired), so they are intentionally NOT\n // declared here either.\n optionalRequires: [SandboxInstanceStoreCapability, LocksCapability],\n\n async setup(ctx) {\n const ensureCtx = buildEnsureCtx(ctx, options)\n const snapshotConfig = options?.snapshots\n const snapshotWorkspaceHash = definition.workspace\n ? computeWorkspaceHash(definition.workspace)\n : undefined\n const snapshotPolicy = snapshotConfig\n ? resolveSandboxSnapshotPolicy(\n snapshotConfig.policy,\n snapshotWorkspaceHash,\n )\n : undefined\n let snapshotRuntime:\n | {\n persistence: {\n stores: {\n messages: {\n loadThread: (\n id: string,\n ) => Promise<ReadonlyArray<ModelMessage>>\n }\n artifacts: {\n listForThread: (id: string) => Promise<\n ReadonlyArray<{\n artifactId: string\n runId: string\n threadId: string\n blobKey?: string\n name: string\n mimeType: string\n size: number\n createdAt: number\n }>\n >\n }\n blobs: {\n get: (key: string) => Promise<{\n arrayBuffer: () => Promise<ArrayBuffer>\n } | null>\n head: (key: string) => Promise<unknown>\n put: (key: string, body: Uint8Array) => Promise<unknown>\n }\n }\n }\n completion: { waitForRunCompletion: () => Promise<void> }\n }\n | undefined\n let snapshotLease: SandboxCheckpointWriterLease | undefined\n if (snapshotConfig) {\n if (\n !snapshotConfig.persistence?.stores?.messages ||\n !snapshotConfig.persistence.stores.artifacts ||\n !snapshotConfig.persistence.stores.blobs\n )\n throw new Error(\n 'Sandbox snapshots require persistence stores.messages, stores.artifacts, and stores.blobs',\n )\n const persistenceModule = await import('@tanstack/ai-persistence')\n const persistence = ctx.getOptional(\n persistenceModule.PersistenceCapability,\n )\n if (persistence === undefined)\n throw new Error(\n 'Sandbox snapshots require withPersistence(snapshots.persistence) before withSandbox',\n )\n if (persistence !== snapshotConfig.persistence)\n throw new Error(\n 'Sandbox snapshots require the same persistence instance passed to withPersistence',\n )\n const completion = ctx.getOptional(\n persistenceModule.PersistenceCompletionCapability,\n )\n if (!completion)\n throw new Error(\n 'Sandbox snapshots require withPersistence before withSandbox',\n )\n snapshotRuntime = {\n persistence: snapshotConfig.persistence,\n completion,\n }\n snapshotLease = await snapshotConfig.checkpoints.acquireWriter(\n ctx.threadId,\n )\n }\n\n // Resolving here (not lazily on the abort path) is what keeps `setup` and\n // `onAbort` on one verdict: the payload the bus carries is the same object\n // the teardown path consults.\n // `TOffset` is passed explicitly: `options` is possibly `undefined` here,\n // so inference has nothing to work from on that branch and would fall\n // back to the `= string` default, re-erecting the very wall this\n // parameter exists to remove.\n const durability = resolveSandboxDurability<TOffset>(options)\n if (durability !== undefined) {\n provideSandboxDurability(ctx, durability)\n // A neutral boolean core owns, so `@tanstack/ai-persistence` can ask\n // \"is this run detachable?\" without depending on this package.\n provideDetachableRun(ctx, true)\n }\n\n // Pull the runtime (and its logger) up front so `baseSha` capture and\n // hook dispatch below can log through the same `sandbox`/`errors`\n // categories the engine uses.\n const runtime = getSandboxRuntime(ctx, { optional: true })\n const logger = runtime?.logger\n\n // REGISTER THE RUN STATE NOW — before `definition.ensure()`, not merely\n // before the end of `setup`.\n //\n // `onAbort` and the disconnect subscriber both need this state, so until\n // this map is populated they are silent no-ops. `ensure` is the LONGEST\n // await in the entire run (create a sandbox, clone a repo — minutes), and it\n // is where the most common disconnect of all lands: a user starts a run and\n // switches away while the UI still says \"starting the sandbox\". Registering\n // after `ensure` returned still left that whole window uncovered.\n //\n // Leaving it uncovered loses every teardown behavior at once: no\n // `detachedSince`/`sandboxKey`, so `listReclaimable` can never surface the\n // run and the reaper can never reclaim it; no `definition.destroy`, so the\n // sandbox leaks; and no detach verdict for core to read.\n //\n // Everything those hooks read is already resolved above: the ensure context\n // (which is all `definition.key` needs), the durability verdict, and the\n // logger. The fields discovered later (`handle`, `watcher`) are ASSIGNED onto\n // this same object as they become available, so the teardown path always\n // sees the most complete state that exists at the moment it runs.\n const state: SandboxRunState = {\n ensureCtx,\n snapshotRenewalGeneration: 0,\n pendingDiffs: [],\n toolHistory: createToolHistoryRecorder(),\n ...(logger ? { logger } : {}),\n ...(durability ? { durability } : {}),\n }\n runState.set(ctx, state)\n if (snapshotLease) {\n state.snapshotLease = snapshotLease\n startSnapshotRenewal(state)\n }\n\n // MAKE THE RUN FINDABLE BEFORE `ensure`, not after the run finally streams.\n //\n // Chat persistence creates the run record from `onConfig`, which runs after\n // EVERY middleware `setup` — so for the whole of `definition.ensure` (create a\n // sandbox, clone a repo: minutes) the run has no record at all, and\n // `findActiveRun` answers \"no active run\" for a run that is demonstrably\n // starting. Measured: a status sidebar read straight off `findActiveRun`\n // reported `idle` for 6.5 minutes while the sandbox was being built, and a\n // client returning to the thread in that window had nothing to tell it a run\n // was in flight — so it rendered an empty pane instead of \"starting sandbox\".\n //\n // A crash in the same window is worse: no record means `listReclaimable` can\n // never surface the run, so the sandbox leaks with no recovery path.\n //\n // `createOrResume` is idempotent and never resurrects a finished run, so\n // persistence's own later call stays correct and simply finds this record.\n if (durability !== undefined) {\n try {\n await durability.runs.createOrResume({\n runId: ctx.runId,\n threadId: ctx.threadId,\n startedAt: Date.now(),\n })\n } catch (error) {\n // Best-effort: a store blip must not stop a run that is otherwise fine.\n // The run is simply invisible until persistence's own `onConfig` call.\n logger?.warn('sandbox run record pre-create failed', {\n runId: ctx.runId,\n error,\n })\n }\n\n // NO ATTACH MARKER HERE. A joiner does need a chunk in the log before the\n // harness has emitted anything — an empty log fails every joiner's\n // fast-fail (`memoryStream`'s first-chunk deadline, the client's rejoin\n // connect deadline) and flushes no HTTP headers, so a reload during\n // `ensure` reads a live run as gone. Core does it: a fresh durable producer\n // appends `RUN_ACCEPTED_EVENT` before the producer stream is first pulled,\n // for EVERY durable run rather than only sandboxed ones, and never on an\n // attach. A second marker from here would only land mid-stream in a run\n // that is already producing.\n\n // STORE THE USER'S TURN NOW, before `ensure` takes minutes.\n //\n // Chat persistence stores it from `onStart`, which runs after every\n // middleware `setup` — so without this the thread holds NOTHING for the\n // whole sandbox build. Measured: a reload during the build asked the server\n // for the conversation and got `{\"messages\":[],…}`, so the user saw no sign\n // of the message they had just sent, and a second device saw an empty\n // thread.\n //\n // The persistence layer owns WHAT gets stored (see `PendingTurnCapability`):\n // `saveThread` replaces the thread, so deciding the list here would risk\n // deleting the history. Absent when the app wires no persistence, which is\n // simply a run with no transcript to store.\n try {\n await getPendingTurn(ctx, { optional: true })?.snapshot()\n } catch (error) {\n // Best-effort: the run is still worth doing, and `onStart` stores the\n // turn again once setup completes.\n logger?.warn('sandbox pending-turn snapshot failed', {\n runId: ctx.runId,\n error,\n })\n }\n }\n\n // SUBSCRIBE BEFORE `ensure`, for the same reason the state is registered\n // before it: `ensure` is the minutes-wide await a disconnect actually lands\n // in. Core calls back immediately if the socket has already closed, so\n // subscribing here cannot miss a disconnect that beat us to it.\n //\n // This is what makes a durable run SURVIVE losing its viewer. The only route\n // a disconnect previously had into this middleware was the application\n // mirroring `request.signal` into `chat()`'s `abortController` — which aborts\n // the run, so `chat()` returned right after this `setup` and the harness\n // adapter's `chatStream` was never called: the agent in the sandbox we just\n // spent minutes creating was NEVER LAUNCHED, and no takeover could recover it\n // because an agent that never ran wrote no journal to replay.\n if (durability !== undefined && durability.detachOnDisconnect) {\n getRunDisconnect(ctx, { optional: true })?.subscribe(async () => {\n const snapshotStop = stopSnapshotLease(state, {\n closePortable: true,\n })\n void snapshotStop.catch(() => {})\n // BOOKKEEPING ONLY — the run is still executing. Deliberately absent:\n // `drainWatcher` (would blind a live agent's file events for the whole\n // remainder) and `definition.destroy` (the run is still using the\n // sandbox). Both belong to the terminal hooks, which still run exactly\n // once afterwards.\n //\n // A run with a cancel already recorded is left alone: that is `onAbort`'s\n // path, and stamping `detachedSince` on a deliberately-stopped run would\n // hand it to the reaper as reclaimable work.\n if (await cancelIntent(durability, ctx.runId, false)) {\n await snapshotStop.catch((error: unknown) => {\n state.logger?.warn('sandbox snapshot writer release failed', {\n runId: ctx.runId,\n phase: 'disconnect',\n error,\n })\n })\n return\n }\n if (\n await recordDetach(definition, state, durability, ctx, 'disconnect')\n ) {\n try {\n await snapshotStop\n } catch (error) {\n state.logger?.warn('sandbox snapshot writer release failed', {\n runId: ctx.runId,\n phase: 'disconnect',\n error,\n })\n }\n state.logger?.sandbox(\n 'sandbox run detached on disconnect; the run continues',\n { runId: ctx.runId },\n )\n } else {\n await snapshotStop.catch((error: unknown) => {\n state.logger?.warn('sandbox snapshot writer release failed', {\n runId: ctx.runId,\n phase: 'disconnect',\n error,\n })\n })\n }\n })\n }\n\n let outcome: 'resumed' | 'native-restored' | 'created' = 'created'\n let handle: SandboxHandle\n try {\n if (snapshotConfig)\n ({ handle, outcome } = await ensureSandboxWithOutcome(\n definition,\n ensureCtx,\n ))\n else handle = await definition.ensure(ensureCtx)\n state.handle = handle\n state.privateHandle = snapshotConfig ? outcome !== 'resumed' : true\n if (snapshotConfig && outcome !== 'resumed') {\n const head = await snapshotConfig.checkpoints.getHead(ctx.threadId)\n if (head) {\n const checkpoint = await snapshotConfig.checkpoints.get(head)\n if (!checkpoint)\n throw new SandboxCheckpointError(\n 'SANDBOX_SNAPSHOT_CHECKPOINT_NOT_FOUND',\n `Checkpoint '${head}' was not found`,\n )\n await restoreSandboxFiles(\n handle,\n {\n blobs: snapshotConfig.persistence.stores.blobs,\n workspaceRoot:\n definition.workspace?.root ?? DEFAULT_WORKSPACE_ROOT,\n },\n checkpoint,\n snapshotPolicy,\n )\n }\n }\n } catch (error) {\n await stopSnapshotLease(state).catch(() => {})\n if (state.handle && state.privateHandle)\n await definition.destroy(ensureCtx).catch(() => {})\n throw error\n }\n // MUTATE, don't re-`set`: a disconnect that landed during `ensure` already\n // captured this object.\n state.handle = handle\n if (snapshotConfig) {\n state.snapshotConfig = snapshotConfig\n state.snapshotPolicy = snapshotPolicy\n state.snapshotRuntime = snapshotRuntime\n }\n try {\n provideSandbox(ctx, handle)\n if (definition.policy) provideSandboxPolicy(ctx, definition.policy)\n\n // Deliberately placed AFTER `logger` is in scope rather than next to the\n // `provideSandboxDurability` call above — there is no logger to warn\n // through until the runtime has been read.\n //\n // `ensureCtx.locks === undefined` counts as in-memory: `defineSandbox`'s\n // `ensure` falls back to a process-lifetime `InMemoryLockStore` when no\n // lock is wired, so an unwired lock has exactly the deficiency being\n // warned about — it is the MOST in-memory case, not an exempt one.\n if (\n durability !== undefined &&\n (ensureCtx.locks === undefined ||\n ensureCtx.locks instanceof InMemoryLockStore)\n ) {\n logger?.warn(\n 'sandbox durability is wired over an InMemoryLockStore: run claims are ' +\n 'serialized within this process only and the lease never signals loss, ' +\n 'so two hosts can drive one run and duplicate its event log. Use a ' +\n 'distributed LockStore via withLocks for any multi-replica deploy.',\n { runId: ctx.runId },\n )\n }\n\n const watchRoot = definition.workspace?.root ?? DEFAULT_WORKSPACE_ROOT\n let baseSha = ''\n try {\n const shaRes = await handle.process.exec('git rev-parse HEAD', {\n cwd: watchRoot,\n })\n if (shaRes.exitCode === 0) {\n baseSha = shaRes.stdout.trim()\n logger?.sandbox('sandbox git baseline captured', {\n root: watchRoot,\n baseSha,\n })\n } else {\n // Non-zero exit: either not a git repository (non-git workspace) or a\n // repo with no commits (no HEAD). Expected, but it silently degrades\n // every subsequent diff to a full-file add-patch, so surface it\n // under `sandbox` (with stderr) rather than leaving nothing to grep.\n logger?.sandbox(\n 'sandbox git baseline unavailable (non-zero exit)',\n {\n root: watchRoot,\n exitCode: shaRes.exitCode,\n stderr: shaRes.stderr,\n },\n )\n }\n } catch (error) {\n // exec rejected (git not on PATH, exec seam broken) → baseSha stays ''\n // and accessors fall back, but this is a real anomaly, not a plain\n // non-git workspace, so warn.\n logger?.warn('sandbox git baseline capture failed', {\n root: watchRoot,\n error,\n })\n }\n\n const workspace = definition.workspace\n if (workspace !== undefined) {\n const virtualRoot = workspace.root ?? DEFAULT_WORKSPACE_ROOT\n const root = resolveHarnessCwd(handle, virtualRoot)\n const workspaceHash = computeWorkspaceHash(workspace)\n const secrets = workspace.secrets\n provideWorkspaceProjection(ctx, {\n skills: workspace.skills ?? [],\n plugins: workspace.plugins ?? [],\n resolveSecret: (ref) => {\n if (secrets === undefined) {\n throw new Error(\n `resolveSecret: no secrets defined on this workspace (ref: \"${ref.__secretName}\")`,\n )\n }\n return resolveSecret(secrets, ref)\n },\n markerPath: `${root}/.tanstack-projected-${workspaceHash}`,\n root,\n ...(workspace.scripts !== undefined\n ? { scripts: workspace.scripts }\n : {}),\n })\n }\n\n const hooks = definition.hooks\n await hooks?.onReady?.(handle)\n\n const fe = resolveFileEvents(definition.fileEvents)\n // THE SAME array the run state already holds, not a fresh one. The watcher\n // callback below closes over this reference, and `drainWatcher` awaits\n // `state.pendingDiffs` — a second array would silently drop every in-flight\n // diff from the teardown drain.\n const pendingDiffs = state.pendingDiffs\n let watcher: SandboxWatchHandle | undefined\n if (fe.enabled) {\n watcher = await watchWorkspace(handle, {\n onEvent: (event: SandboxFileEvent) => {\n const enriched = buildFileHookEvent(\n handle,\n watchRoot,\n baseSha,\n event,\n logger,\n )\n void dispatchDefinitionHooks(hooks, enriched, logger)\n runtime?.emit(enriched)\n if (fe.diff) {\n pendingDiffs.push(\n enriched\n .diff()\n .then((diff) => {\n runtime?.emitFileDiff({ path: event.path, diff })\n })\n .catch((error: unknown) => {\n logger?.warn('sandbox file diff emit failed', {\n path: event.path,\n error,\n })\n }),\n )\n }\n },\n // Watch the SAME root the enrichment layer relativizes against\n // (`buildFileHookEvent(handle, watchRoot, …)` and the `baseSha`\n // capture). Without this the watcher defaults to `/workspace` while\n // enrichment uses `watchRoot`, so a custom `workspace.root` makes the\n // two look at different directories and git pathspecs break.\n root: watchRoot,\n ...(ctx.signal !== undefined ? { signal: ctx.signal } : {}),\n ...(logger !== undefined ? { logger } : {}),\n })\n logger?.sandbox('sandbox watcher started', {\n root: watchRoot,\n diff: fe.diff,\n })\n }\n\n // MUTATE the object registered above rather than `set`-ing a second one: an\n // abort that landed mid-setup already captured a reference to it (and may\n // already be draining `pendingDiffs`), so replacing the entry would hand the\n // teardown path a different object than the watcher writes into.\n // `pendingDiffs` needs no copying — it IS `state.pendingDiffs`.\n if (watcher) state.watcher = watcher\n } catch (error) {\n await drainWatcher(state, 'error')\n await stopSnapshotLease(state).catch(() => {})\n if (state.privateHandle)\n await definition.destroy(ensureCtx).catch(() => {})\n throw error\n }\n },\n\n // Keep the recorded tool history OUT of the request to the model. It is stored\n // history for the next turn, it names tools the provider was never given, and one\n // triage-sized run is hundreds of kilobytes — so replaying it is wasteful at best\n // and rejected at worst. `ctx.messages` keeps it (that is what gets stored and\n // rendered); only `config.messages` loses it.\n onConfig(_ctx, config) {\n const messages = stripObservedToolCalls(config.messages)\n if (messages.length === config.messages.length) return\n return { messages }\n },\n\n // The engine re-syncs `middlewareCtx.messages` from its own array once per agent\n // iteration, which drops whatever the recorder appended during the previous\n // iteration's stream. Restoring it here — AFTER that sync — is what makes a\n // multi-iteration run keep its full history without depending on where this\n // middleware sits relative to persistence in the middleware array.\n onIteration(ctx) {\n runState.get(ctx)?.toolHistory.reconcile(ctx)\n },\n\n // Record the harness's own tool calls as transcript messages. Observe only:\n // returning nothing passes every chunk through untouched.\n async onChunk(ctx, chunk) {\n const state = runState.get(ctx)\n state?.toolHistory.observe(chunk, ctx)\n if (\n state &&\n chunk.type === 'RUN_FINISHED' &&\n chunk.outcome?.type === 'interrupt'\n ) {\n await drainWatcher(state, 'pause')\n await stopSnapshotLease(state, { closePortable: true })\n }\n },\n\n async onFinish(ctx) {\n const state = runState.get(ctx)\n if (!state) return\n const { handle, ensureCtx } = state\n\n // Last chance before persistence writes the transcript. Only matters if a\n // config sync landed after the final tool chunk; the recorder is idempotent, so\n // in the normal case this changes nothing.\n state.toolHistory.reconcile(ctx)\n\n await drainWatcher(state, 'finish')\n\n let primaryError: unknown\n try {\n const snapshotCaptureTask = Promise.resolve().then(\n async (): Promise<void> => {\n const config = state.snapshotConfig\n const runtime = state.snapshotRuntime\n const lease = state.snapshotLease\n if (!config || !runtime || !handle || !lease) {\n if (state.snapshotLost) throw state.snapshotLost\n return\n }\n if (!canPublishPortableSnapshot(state, lease)) return\n\n await runtime.completion.waitForRunCompletion()\n if (!canPublishPortableSnapshot(state, lease)) return\n\n const conversation =\n await runtime.persistence.stores.messages.loadThread(ctx.threadId)\n if (!canPublishPortableSnapshot(state, lease)) return\n\n const files = await captureSandboxFiles(\n handle,\n {\n blobs: config.persistence.stores.blobs,\n workspaceRoot:\n definition.workspace?.root ?? DEFAULT_WORKSPACE_ROOT,\n },\n state.snapshotPolicy,\n definition.workspace?.secrets !== undefined\n ? resolveAllSecrets(definition.workspace.secrets)\n : {},\n )\n if (!canPublishPortableSnapshot(state, lease)) return\n\n const artifacts = await captureSandboxArtifacts(\n {\n blobs: config.persistence.stores.blobs,\n artifacts: config.persistence.stores.artifacts,\n },\n ctx.threadId,\n definition.workspace?.secrets !== undefined\n ? resolveAllSecrets(definition.workspace.secrets)\n : {},\n )\n if (!canPublishPortableSnapshot(state, lease)) return\n\n const parentCheckpointId = await config.checkpoints.getHead(\n ctx.threadId,\n )\n if (!canPublishPortableSnapshot(state, lease)) return\n\n try {\n await config.checkpoints.append({\n checkpoint: {\n id: `checkpoint-${ctx.runId}`,\n threadId: ctx.threadId,\n parentCheckpointId,\n createdAt: Date.now(),\n reason: 'automatic',\n sourceRunId: ctx.runId,\n files: files.files,\n conversation,\n artifacts,\n },\n expectedHeadId: parentCheckpointId,\n writer: lease,\n })\n } catch (error) {\n if (state.snapshotLost) throw state.snapshotLost\n throw error\n }\n if (state.snapshotLost) throw state.snapshotLost\n },\n )\n state.snapshotCaptureTask = snapshotCaptureTask\n try {\n await snapshotCaptureTask\n } finally {\n if (state.snapshotCaptureTask === snapshotCaptureTask)\n state.snapshotCaptureTask = undefined\n }\n\n const lifecycle = definition.lifecycle\n\n // `handle` is absent only if `setup` never got past `definition.ensure`, in\n // which case there is no sandbox to snapshot.\n if (\n lifecycle?.snapshot === 'after-run' &&\n handle?.capabilities.snapshots &&\n handle.snapshot\n ) {\n const snapshot = await handle.snapshot(`after-run-${ctx.runId}`)\n const store = ensureCtx.store\n if (store) {\n const key = definition.key(ensureCtx)\n const existing = await store.get(key)\n if (existing) {\n await store.upsert({\n ...existing,\n latestSnapshotId: snapshot.id,\n updatedAt: Date.now(),\n })\n }\n }\n }\n\n if (lifecycle?.destroyOnComplete) {\n await definition.destroy(ensureCtx)\n await definition.hooks?.onDestroy?.()\n }\n } catch (error) {\n primaryError = error\n if (definition.lifecycle?.destroyOnComplete) {\n try {\n await definition.destroy(ensureCtx)\n await definition.hooks?.onDestroy?.()\n } catch (cleanupError) {\n state.logger?.warn(\n 'sandbox destroy after terminal failure failed',\n {\n runId: ctx.runId,\n phase: 'finish',\n error: cleanupError,\n },\n )\n }\n }\n }\n\n let snapshotCleanupError: unknown\n try {\n await stopSnapshotLease(state, { closePortable: true })\n } catch (error) {\n snapshotCleanupError = error\n }\n if (primaryError !== undefined) {\n if (snapshotCleanupError !== undefined)\n state.logger?.warn('sandbox snapshot writer release failed', {\n runId: ctx.runId,\n phase: 'finish',\n error: snapshotCleanupError,\n })\n throw primaryError\n }\n if (snapshotCleanupError !== undefined) throw snapshotCleanupError\n },\n\n async onAbort(ctx, info: AbortInfo) {\n const state = runState.get(ctx)\n if (!state) return\n\n // First on BOTH branches: a diff still in flight must be drained whether\n // the sandbox is about to be destroyed or merely detached, or the final\n // file's diff is dropped.\n await drainWatcher(state, 'abort')\n let releaseError: unknown\n try {\n await stopSnapshotLease(state, { closePortable: true })\n } catch (error) {\n releaseError = error\n }\n\n const durability = state.durability\n const cancelled = await cancelIntent(\n durability,\n ctx.runId,\n info.cancelRequested === true,\n )\n\n if (\n durability !== undefined &&\n !cancelled &&\n durability.detachOnDisconnect\n ) {\n // DETACH on the teardown path. Reached when the run is aborted for a\n // reason that is NOT an out-of-band cancel while detachable — a genuine\n // stop from elsewhere, or a host going down. The ordinary disconnect is\n // handled by the disconnect subscriber in `setup`, which does not end the\n // run at all.\n //\n // On a failed record write this branch is ABANDONED for the destroy one\n // below, because a rejection here is the worst shape available: the\n // verdict is unpublished, so core terminalizes the log and records a\n // healthy detached run as failed; `detachedSince`/`sandboxKey` are\n // unwritten, so `listReclaimable` can never surface the run and\n // `reapDetachedRuns` can never reclaim it. A DESTROYED sandbox beats an\n // unreachable one — the same reasoning `drainWatcher` applies to its own\n // guarded `stop()`.\n if (await recordDetach(definition, state, durability, ctx, 'abort')) {\n if (releaseError) throw releaseError\n return\n }\n await definition.destroy(state.ensureCtx)\n await definition.hooks?.onDestroy?.()\n if (releaseError) throw releaseError\n return\n }\n\n // ALWAYS tear down on an explicit abort, regardless of `destroyOnComplete`.\n // The in-sandbox agent process is not killed by closing its IO stream\n // (e.g. a Docker exec survives client disconnect), so the only reliable way\n // to stop it — and the token/cost drain of its ongoing API calls — is to\n // destroy the sandbox (stop the container/VM). `keepAlive` /\n // `destroyOnComplete:false` governs *successful completion*, never cancel.\n await definition.destroy(state.ensureCtx)\n await definition.hooks?.onDestroy?.()\n if (releaseError) throw releaseError\n },\n\n async onError(ctx, info) {\n const state = runState.get(ctx)\n if (!state) return\n\n await drainWatcher(state, 'error')\n let releaseError: unknown\n try {\n await stopSnapshotLease(state)\n } catch (error) {\n releaseError = error\n }\n await definition.hooks?.onError?.(info.error)\n\n // On failure, only tear down when the lifecycle says so; otherwise leave\n // the sandbox for a resumed retry.\n if (definition.lifecycle?.destroyOnComplete) {\n await definition.destroy(state.ensureCtx)\n await definition.hooks?.onDestroy?.()\n }\n if (releaseError) throw releaseError\n },\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"middleware.js","names":[],"sources":["../../src/middleware.ts"],"sourcesContent":["/**\n * `withSandbox(definition, options?)` — the middleware that PROVIDES the\n * {@link SandboxCapability} a harness adapter requires.\n *\n * - `setup`: resume-or-create the sandbox (via the definition's ensure\n * algorithm), provide the handle, using the durability seams from\n * {@link SandboxMiddlewareOptions} (or, failing that, a bus-provided\n * SandboxInstanceStoreCapability / LocksCapability, then an in-memory\n * fallback). If `fileEvents` is not false, starts a\n * watcher that dispatches to sandbox-scoped hooks and forwards to the runtime\n * sink.\n * - `onFinish`/`onAbort`/`onError`: stop the watcher, snapshot (`after-run`)\n * and/or destroy per lifecycle.\n *\n * NOTE: streamed sandbox lifecycle events (sandbox.created, workspace.setup.*)\n * are emitted by the harness adapter's chatStream (which can yield CUSTOM\n * chunks), not from here — middleware setup runs before streaming begins.\n */\nimport {\n defineChatMiddleware,\n isTerminalRunStatus,\n provideDetachableRun,\n provideRunDetached,\n wasCancelRequested,\n} from '@tanstack/ai'\nimport { InMemoryLockStore, LocksCapability } from '@tanstack/ai/locks'\nimport {\n getPendingTurn,\n getRunDisconnect,\n getSandboxRuntime,\n} from '@tanstack/ai/adapter-internals'\nimport {\n SandboxCapability,\n provideSandbox,\n provideSandboxPolicy,\n} from './capabilities'\nimport {\n provideSandboxDurability,\n resolveSandboxDurability,\n} from './durability'\nimport { SandboxInstanceStoreCapability } from './instance-store'\nimport { computeWorkspaceHash } from './key'\nimport { buildFileHookEvent, resolveFileEvents } from './file-diff'\nimport { ProjectionCapability, provideWorkspaceProjection } from './projection'\nimport { resolveAllSecrets, resolveSecret } from './secrets'\nimport {\n createToolHistoryRecorder,\n stripObservedToolCalls,\n} from './tool-history'\nimport { watchWorkspace } from './watch'\nimport { DEFAULT_WORKSPACE_ROOT } from './bootstrap'\nimport { resolveHarnessCwd } from './harness-cwd'\nimport { ensureSandboxWithOutcome } from './sandbox'\nimport {\n restoreSandboxFiles,\n captureSandboxFiles,\n captureSandboxArtifacts,\n resolveSandboxSnapshotPolicy,\n} from './snapshots'\nimport type { SandboxSnapshotPolicy } from './snapshots'\nimport type {\n SandboxCheckpointStore,\n SandboxCheckpointWriterLease,\n} from './checkpoint-store'\nimport { SandboxCheckpointError } from './checkpoint-store'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type { LockStore } from '@tanstack/ai/locks'\nimport type {\n AbortInfo,\n ChatMiddlewareContext,\n DefinedChatMiddleware,\n ModelMessage,\n RunStore,\n SandboxFileEvent,\n SandboxFileHookEvent,\n} from '@tanstack/ai'\nimport type {\n SandboxDurabilityOptions,\n SandboxRunDurability,\n} from './durability'\nimport type { SandboxInstanceStore } from './instance-store'\nimport type { ToolHistoryRecorder } from './tool-history'\nimport type { SandboxHandle } from './contracts'\nimport type {\n SandboxDefinition,\n SandboxEnsureContext,\n SandboxHooks,\n} from './sandbox'\nimport type { SandboxWatchHandle } from './watch'\n\n/** Per-request state we need to carry from `setup` to the terminal hooks. */\ninterface SandboxRunState {\n snapshotLease?: SandboxCheckpointWriterLease\n snapshotRenewal?: ReturnType<typeof setTimeout>\n snapshotRenewTask?: Promise<void>\n snapshotCaptureTask?: Promise<void>\n snapshotRenewalGeneration: number\n snapshotStop?: Promise<void>\n /** A detached or paused run cannot later publish portable state. */\n snapshotClosed?: boolean\n snapshotLost?: Error\n snapshotCleaned?: boolean\n snapshotConfig?: NonNullable<SandboxMiddlewareOptions['snapshots']>\n snapshotPolicy?: SandboxSnapshotPolicy\n snapshotRuntime?: {\n persistence: NonNullable<\n SandboxMiddlewareOptions['snapshots']\n >['persistence']\n completion: { waitForRunCompletion: () => Promise<void> }\n }\n /**\n * OPTIONAL because the state is registered BEFORE `definition.ensure()` is\n * awaited, and `ensure` is the slowest thing in the whole run — cloning a repo\n * into a fresh sandbox is minutes wide. That window is where the most common\n * disconnect of all lands (a user starts a run and switches away while the UI\n * still says \"starting the sandbox\"), so it is the one window the teardown and\n * disconnect hooks most need to be able to act in. Registering only after the\n * handle exists left exactly that window uncovered.\n *\n * Nothing the disconnect path does needs the handle: `detachedSince` and\n * `sandboxKey` come from `ensureCtx`, which is built before `ensure` is called.\n * Only `onFinish`'s snapshot needs it, and that cannot run before `setup` has\n * completed.\n */\n handle?: SandboxHandle\n privateHandle?: boolean\n ensureCtx: SandboxEnsureContext\n watcher?: SandboxWatchHandle\n /** In-flight `enriched.diff()` promises queued by the `fileEvents.diff`\n * watcher callback, awaited before teardown so a pending diff isn't\n * dropped when the run finishes/aborts/errors mid-computation. */\n pendingDiffs: Array<Promise<void>>\n /** Logger captured at setup, so terminal hooks can log watcher teardown. */\n logger?: InternalLogger\n /**\n * Durability resolved once at setup (absent when the run is not durable), so\n * `onAbort` cannot reach a different verdict than the one `setup` published\n * on the capability bus.\n */\n durability?: SandboxRunDurability\n /**\n * Records the harness's own tool calls into the transcript, so a finished run\n * restores its tool cards from the message store instead of only from the (live,\n * rejoin-only) delivery log. See `./tool-history`.\n */\n toolHistory: ToolHistoryRecorder\n}\n\nconst runState = new WeakMap<object, SandboxRunState>()\n\nfunction stopSnapshotLease(\n state: SandboxRunState,\n options: { closePortable?: boolean } = {},\n): Promise<void> {\n if (options.closePortable) state.snapshotClosed = true\n if (state.snapshotStop) return state.snapshotStop\n if (state.snapshotCleaned) return Promise.resolve()\n state.snapshotCleaned = true\n state.snapshotRenewalGeneration++\n if (state.snapshotRenewal !== undefined) clearTimeout(state.snapshotRenewal)\n state.snapshotRenewal = undefined\n const renewTask = state.snapshotRenewTask\n const captureTask = state.snapshotCaptureTask\n const lease = state.snapshotLease\n state.snapshotLease = undefined\n state.snapshotStop = (async () => {\n await renewTask?.catch(() => {})\n await captureTask?.catch(() => {})\n await lease?.release()\n })()\n return state.snapshotStop\n}\n\nfunction startSnapshotRenewal(state: SandboxRunState): void {\n const lease = state.snapshotLease\n if (!lease) return\n const schedule = (): void => {\n const generation = state.snapshotRenewalGeneration\n state.snapshotRenewal = setTimeout(() => {\n void (async (): Promise<void> => {\n state.snapshotRenewal = undefined\n if (\n state.snapshotCleaned ||\n generation !== state.snapshotRenewalGeneration\n )\n return\n const renewal = Promise.resolve().then(async (): Promise<void> => {\n await lease.renew()\n })\n state.snapshotRenewTask = renewal\n try {\n await renewal\n } catch (error) {\n state.snapshotLost =\n error instanceof Error ? error : new Error(String(error))\n } finally {\n if (state.snapshotRenewTask === renewal)\n state.snapshotRenewTask = undefined\n }\n if (state.snapshotLost) await stopSnapshotLease(state).catch(() => {})\n else if (\n !state.snapshotCleaned &&\n generation === state.snapshotRenewalGeneration\n )\n schedule()\n })()\n }, lease.renewAfterMs)\n }\n schedule()\n}\n\n/**\n * Stop the watcher and drain any in-flight `diff()` promises before teardown,\n * so the final file's diff isn't dropped when a run finishes/aborts/errors\n * mid-computation. The `pendingDiffs` await is the load-bearing line — without\n * it a deferred diff resolves after the run is gone and its chunk is lost.\n */\nasync function drainWatcher(\n state: SandboxRunState,\n phase: 'finish' | 'pause' | 'abort' | 'error',\n): Promise<void> {\n // Guard `stop()`: a rejecting watcher teardown must NOT propagate out of\n // here, or the caller skips the `definition.destroy(...)` that follows —\n // leaking the sandbox on exactly the abort path that must ALWAYS tear down.\n try {\n await state.watcher?.stop()\n } catch (error) {\n state.logger?.warn('sandbox watcher stop failed', { phase, error })\n }\n await Promise.allSettled(state.pendingDiffs)\n if (state.watcher) state.logger?.sandbox('sandbox watcher stopped', { phase })\n}\n\nfunction canPublishPortableSnapshot(\n state: SandboxRunState,\n lease: SandboxCheckpointWriterLease,\n): boolean {\n if (state.snapshotLost) throw state.snapshotLost\n return (\n !state.snapshotClosed &&\n !state.snapshotCleaned &&\n state.snapshotLease === lease\n )\n}\n\n/**\n * Record the two facts a later attach and the reaper both need, then publish the\n * detach verdict core reads.\n *\n * Shared by the DISCONNECT subscriber registered in `setup` (the run is still\n * going — the normal case) and `onAbort`'s detach branch (the run is being torn\n * down while detachable), so the two can never write a different shape of detach.\n *\n * GUARDED, and reports failure rather than throwing. `update` is a documented\n * no-op for an unknown runId, so a vanished record does not turn teardown into a\n * throw; a genuinely rejecting store is the caller's to react to — `onAbort` falls\n * through to destroying the sandbox, because a DESTROYED sandbox beats an\n * unreachable one, while the disconnect subscriber has nothing to fall back to\n * (the run is alive and still using the sandbox) and simply leaves the verdict\n * unpublished.\n *\n * The verdict is published ONLY on success. Publishing it after a failed record\n * write would leave core holding the log open for a takeover that can never be\n * found, since nothing in the store points at the run.\n */\nasync function recordDetach(\n definition: SandboxDefinition,\n state: SandboxRunState,\n durability: SandboxRunDurability,\n ctx: ChatMiddlewareContext,\n phase: 'disconnect' | 'abort',\n): Promise<boolean> {\n try {\n // A late abort from a superseded host can run after takeover has already\n // completed the run. Re-stamping `detachedSince` then leaves a finished\n // record looking detached. Skip the write; return true so `onAbort` does\n // not treat this as a failed stamp and destroy the sandbox.\n try {\n const current = await durability.runs.get(ctx.runId)\n if (current !== null && isTerminalRunStatus(current.status)) {\n return true\n }\n } catch {\n // Unreadable store: still try the update. A get() failure must not skip\n // the stamp — that is how a cancel-probe rejection used to skip both\n // detach and destroy.\n }\n // The record already exists: `setup` pre-creates it for every durable run\n // BEFORE `ensure`, precisely so this stamp cannot land on a runId the store has\n // never heard of — `RunStore.update` is a documented no-op for an unknown\n // runId, which is how the detach used to be lost silently (measured against the\n // browser repro: `detached_since` and `sandbox_key` both stayed NULL for a run\n // that had genuinely detached). If it has since vanished, that no-op is the\n // correct outcome and this must not throw.\n await durability.runs.update(ctx.runId, {\n detachedSince: Date.now(),\n sandboxKey: definition.key(state.ensureCtx),\n })\n } catch (error) {\n state.logger?.warn('sandbox detach record write failed', {\n runId: ctx.runId,\n phase,\n error,\n })\n return false\n }\n // Core's durable delivery sink reads this (see `RunDetachedCapability`) and\n // leaves the run's log OPEN instead of appending a synthetic terminal\n // `RUN_ERROR` and closing it — a terminalized log ends a later attach's replay\n // at the prefix and diverges the takeover's journal replay, which recorded a\n // healthy detached run as `'failed'`.\n provideRunDetached(ctx, true)\n return true\n}\n\n/**\n * Whether an out-of-band cancel has been recorded for this run, in EITHER band.\n * A user pressing Stop and a user closing the tab produce the IDENTICAL\n * connection close, so intent is never inferred from the disconnect itself: it\n * arrives in-process (the abort reason carried the cancel sentinel) or durably\n * (another host recorded it on the run record).\n */\nasync function cancelIntent(\n durability: SandboxRunDurability | undefined,\n runId: string,\n inProcess: boolean,\n): Promise<boolean> {\n if (inProcess) return true\n if (durability === undefined) return false\n // No guard needed here, and one would be dead code: `wasCancelRequested` already\n // answers `false` for a store read that rejects. That matters on this path,\n // because a rejection escaping into `onAbort` would skip BOTH of its branches at\n // once, leaving a sandbox that is neither reclaimable nor destroyed. The test\n // 'DETACHES when the cancel probe REJECTS' pins the composition.\n return wasCancelRequested(durability.runs, runId)\n}\n\n/** Defensively pull tenant scoping out of the runtime context, if present. */\nfunction tenantFrom(\n context: unknown,\n): { userId?: string; orgId?: string } | undefined {\n if (context === null || typeof context !== 'object') return undefined\n const c = context as Record<string, unknown>\n const userId = typeof c.userId === 'string' ? c.userId : undefined\n const orgId = typeof c.orgId === 'string' ? c.orgId : undefined\n if (userId === undefined && orgId === undefined) return undefined\n return { userId, orgId }\n}\n\n/**\n * Durability seams for a sandboxed run. Both are optional; each independently\n * falls back to a process-lifetime in-memory default, which is correct for a\n * single process but NOT across replicas.\n */\nexport interface SandboxMiddlewareOptions<TOffset extends string = string> {\n snapshots?: {\n persistence: {\n stores: {\n messages: {\n loadThread: (threadId: string) => Promise<ReadonlyArray<ModelMessage>>\n }\n artifacts: {\n listForThread: (threadId: string) => Promise<\n ReadonlyArray<{\n artifactId: string\n runId: string\n threadId: string\n blobKey?: string\n name: string\n mimeType: string\n size: number\n createdAt: number\n }>\n >\n }\n blobs: {\n get: (key: string) => Promise<{\n arrayBuffer: () => Promise<ArrayBuffer>\n } | null>\n head: (key: string) => Promise<unknown>\n put: (key: string, body: Uint8Array) => Promise<unknown>\n }\n }\n }\n checkpoints: SandboxCheckpointStore\n policy?: SandboxSnapshotPolicy\n }\n /**\n * Durable instance map (which provider sandbox to resume for a key). Pass\n * your own store to make resume survive across processes/replicas.\n *\n * Takes precedence over a store provided on the capability bus (see\n * `provideSandboxInstanceStore`), so the call site wins over ambient wiring.\n */\n instances?: SandboxInstanceStore\n /**\n * Distributed lock serializing resume-or-create for one key. Needed for\n * multi-replica correctness so two concurrent runs don't both create.\n *\n * Prefer `withLocks` from `@tanstack/ai/locks` when other middleware also\n * needs the lock; use this option to scope one to this sandbox. Takes\n * precedence over a bus-provided lock.\n */\n locks?: LockStore\n /**\n * Run lifecycle records. Pair with `durability.adapter` to make a run\n * DETACHABLE: a client disconnect then leaves the agent running and records\n * `detachedSince` instead of destroying the sandbox.\n *\n * Pass the SAME store chat persistence uses (`persistence.stores.runs`) so\n * one record describes the run instead of two that can disagree.\n *\n * Defaults to `undefined`: an app that passes neither this nor `durability`\n * keeps today's destroy-on-disconnect behavior exactly.\n */\n runs?: RunStore\n /**\n * Delivery durability for the run's event log, plus the journal and detach\n * knobs. Requires `runs`; either alone is not durable.\n *\n * `TOffset` is inferred from the adapter passed here, so a branded-cursor\n * backend (`durableStream`) wires without a cast and without the call site\n * ever naming the parameter.\n */\n durability?: SandboxDurabilityOptions<TOffset>\n}\n\n/**\n * Resolve the ensure seams. Precedence is explicit option → capability bus →\n * (in `ensure`) the in-memory fallback. The option wins because it is visible\n * at the call site; the bus remains for platform/framework injection.\n */\nfunction buildEnsureCtx(\n ctx: ChatMiddlewareContext,\n // Narrowed to the two seams it reads rather than taking the whole options\n // object: `SandboxMiddlewareOptions` is now generic in the durability offset,\n // and `SandboxMiddlewareOptions<TOffset>` is not assignable to\n // `SandboxMiddlewareOptions<string>`. Both members here are offset-free, so\n // the narrowing keeps this helper independent of that parameter entirely.\n options: Pick<SandboxMiddlewareOptions, 'instances' | 'locks'> | undefined,\n): SandboxEnsureContext {\n return {\n threadId: ctx.threadId,\n runId: ctx.runId,\n store:\n options?.instances ?? ctx.getOptional(SandboxInstanceStoreCapability),\n locks: options?.locks ?? ctx.getOptional(LocksCapability),\n tenant: tenantFrom(ctx.context),\n signal: ctx.signal,\n adapterName: ctx.provider,\n }\n}\n\n/**\n * Dispatch a sandbox file event to the per-type hooks declared on the\n * definition. Errors in individual hooks are swallowed so one bad hook\n * cannot break the run — but are logged under the `errors` category first, so\n * a throwing hook is observable (matching the run-scoped path in the engine\n * and the behavior the observability docs promise).\n */\nasync function dispatchDefinitionHooks(\n hooks: SandboxHooks | undefined,\n event: SandboxFileHookEvent,\n logger?: InternalLogger,\n): Promise<void> {\n if (!hooks) return\n const typed = (\n {\n create: 'onFileCreate',\n change: 'onFileChange',\n delete: 'onFileDelete',\n } as const\n )[event.type]\n for (const fn of [hooks.onFile, hooks[typed]]) {\n if (!fn) continue\n try {\n await fn(event)\n } catch (error) {\n // swallowed — one bad hook must not break the run — but logged so the\n // failure isn't invisible.\n logger?.errors('sandbox file hook failed', {\n path: event.path,\n type: event.type,\n error,\n })\n }\n }\n}\n\nexport function withSandbox<TOffset extends string = string>(\n definition: SandboxDefinition,\n options?: SandboxMiddlewareOptions<TOffset>,\n): DefinedChatMiddleware<\n unknown,\n readonly [],\n readonly [typeof SandboxCapability, typeof ProjectionCapability]\n> {\n return defineChatMiddleware({\n name: 'sandbox',\n provides: [SandboxCapability, ProjectionCapability],\n // SandboxPolicyCapability is provided conditionally (only when the\n // definition has a policy), so it is intentionally NOT declared here —\n // consumers read it via `getOptional`. SandboxDurabilityCapability and\n // DetachableRunCapability are conditional for the same reason (only when\n // `runs` + `durability` are both wired), so they are intentionally NOT\n // declared here either.\n optionalRequires: [SandboxInstanceStoreCapability, LocksCapability],\n\n async setup(ctx) {\n const ensureCtx = buildEnsureCtx(ctx, options)\n const snapshotConfig = options?.snapshots\n const snapshotWorkspaceHash = definition.workspace\n ? computeWorkspaceHash(definition.workspace)\n : undefined\n const snapshotPolicy = snapshotConfig\n ? resolveSandboxSnapshotPolicy(\n snapshotConfig.policy,\n snapshotWorkspaceHash,\n )\n : undefined\n let snapshotRuntime:\n | {\n persistence: {\n stores: {\n messages: {\n loadThread: (\n id: string,\n ) => Promise<ReadonlyArray<ModelMessage>>\n }\n artifacts: {\n listForThread: (id: string) => Promise<\n ReadonlyArray<{\n artifactId: string\n runId: string\n threadId: string\n blobKey?: string\n name: string\n mimeType: string\n size: number\n createdAt: number\n }>\n >\n }\n blobs: {\n get: (key: string) => Promise<{\n arrayBuffer: () => Promise<ArrayBuffer>\n } | null>\n head: (key: string) => Promise<unknown>\n put: (key: string, body: Uint8Array) => Promise<unknown>\n }\n }\n }\n completion: { waitForRunCompletion: () => Promise<void> }\n }\n | undefined\n let snapshotLease: SandboxCheckpointWriterLease | undefined\n if (snapshotConfig) {\n if (\n !snapshotConfig.persistence?.stores?.messages ||\n !snapshotConfig.persistence.stores.artifacts ||\n !snapshotConfig.persistence.stores.blobs\n )\n throw new Error(\n 'Sandbox snapshots require persistence stores.messages, stores.artifacts, and stores.blobs',\n )\n const persistenceModule = await import('@tanstack/ai-persistence')\n const persistence = ctx.getOptional(\n persistenceModule.PersistenceCapability,\n )\n if (persistence === undefined)\n throw new Error(\n 'Sandbox snapshots require withPersistence(snapshots.persistence) before withSandbox',\n )\n if (persistence !== snapshotConfig.persistence)\n throw new Error(\n 'Sandbox snapshots require the same persistence instance passed to withPersistence',\n )\n const completion = ctx.getOptional(\n persistenceModule.PersistenceCompletionCapability,\n )\n if (!completion)\n throw new Error(\n 'Sandbox snapshots require withPersistence before withSandbox',\n )\n snapshotRuntime = {\n persistence: snapshotConfig.persistence,\n completion,\n }\n snapshotLease = await snapshotConfig.checkpoints.acquireWriter(\n ctx.threadId,\n )\n }\n\n // Resolving here (not lazily on the abort path) is what keeps `setup` and\n // `onAbort` on one verdict: the payload the bus carries is the same object\n // the teardown path consults.\n // `TOffset` is passed explicitly: `options` is possibly `undefined` here,\n // so inference has nothing to work from on that branch and would fall\n // back to the `= string` default, re-erecting the very wall this\n // parameter exists to remove.\n const durability = resolveSandboxDurability<TOffset>(options)\n if (durability !== undefined) {\n provideSandboxDurability(ctx, durability)\n // A neutral boolean core owns, so `@tanstack/ai-persistence` can ask\n // \"is this run detachable?\" without depending on this package.\n provideDetachableRun(ctx, true)\n }\n\n // Pull the runtime (and its logger) up front so `baseSha` capture and\n // hook dispatch below can log through the same `sandbox`/`errors`\n // categories the engine uses.\n const runtime = getSandboxRuntime(ctx, { optional: true })\n const logger = runtime?.logger\n\n // REGISTER THE RUN STATE NOW — before `definition.ensure()`, not merely\n // before the end of `setup`.\n //\n // `onAbort` and the disconnect subscriber both need this state, so until\n // this map is populated they are silent no-ops. `ensure` is the LONGEST\n // await in the entire run (create a sandbox, clone a repo — minutes), and it\n // is where the most common disconnect of all lands: a user starts a run and\n // switches away while the UI still says \"starting the sandbox\". Registering\n // after `ensure` returned still left that whole window uncovered.\n //\n // Leaving it uncovered loses every teardown behavior at once: no\n // `detachedSince`/`sandboxKey`, so `listReclaimable` can never surface the\n // run and the reaper can never reclaim it; no `definition.destroy`, so the\n // sandbox leaks; and no detach verdict for core to read.\n //\n // Everything those hooks read is already resolved above: the ensure context\n // (which is all `definition.key` needs), the durability verdict, and the\n // logger. The fields discovered later (`handle`, `watcher`) are ASSIGNED onto\n // this same object as they become available, so the teardown path always\n // sees the most complete state that exists at the moment it runs.\n const state: SandboxRunState = {\n ensureCtx,\n snapshotRenewalGeneration: 0,\n pendingDiffs: [],\n toolHistory: createToolHistoryRecorder(),\n ...(logger ? { logger } : {}),\n ...(durability ? { durability } : {}),\n }\n runState.set(ctx, state)\n if (snapshotLease) {\n state.snapshotLease = snapshotLease\n startSnapshotRenewal(state)\n }\n\n // MAKE THE RUN FINDABLE BEFORE `ensure`, not after the run finally streams.\n //\n // Chat persistence creates the run record from `onConfig`, which runs after\n // EVERY middleware `setup` — so for the whole of `definition.ensure` (create a\n // sandbox, clone a repo: minutes) the run has no record at all, and\n // `findActiveRun` answers \"no active run\" for a run that is demonstrably\n // starting. Measured: a status sidebar read straight off `findActiveRun`\n // reported `idle` for 6.5 minutes while the sandbox was being built, and a\n // client returning to the thread in that window had nothing to tell it a run\n // was in flight — so it rendered an empty pane instead of \"starting sandbox\".\n //\n // A crash in the same window is worse: no record means `listReclaimable` can\n // never surface the run, so the sandbox leaks with no recovery path.\n //\n // `createOrResume` is idempotent and never resurrects a finished run, so\n // persistence's own later call stays correct and simply finds this record.\n if (durability !== undefined) {\n try {\n await durability.runs.createOrResume({\n runId: ctx.runId,\n threadId: ctx.threadId,\n startedAt: Date.now(),\n })\n } catch (error) {\n // Best-effort: a store blip must not stop a run that is otherwise fine.\n // The run is simply invisible until persistence's own `onConfig` call.\n logger?.warn('sandbox run record pre-create failed', {\n runId: ctx.runId,\n error,\n })\n }\n\n // NO ATTACH MARKER HERE. A joiner does need a chunk in the log before the\n // harness has emitted anything — an empty log fails every joiner's\n // fast-fail (`memoryStream`'s first-chunk deadline, the client's rejoin\n // connect deadline) and flushes no HTTP headers, so a reload during\n // `ensure` reads a live run as gone. Core does it: a fresh durable producer\n // appends `RUN_ACCEPTED_EVENT` before the producer stream is first pulled,\n // for EVERY durable run rather than only sandboxed ones, and never on an\n // attach. A second marker from here would only land mid-stream in a run\n // that is already producing.\n\n // STORE THE USER'S TURN NOW, before `ensure` takes minutes.\n //\n // Chat persistence stores it from `onStart`, which runs after every\n // middleware `setup` — so without this the thread holds NOTHING for the\n // whole sandbox build. Measured: a reload during the build asked the server\n // for the conversation and got `{\"messages\":[],…}`, so the user saw no sign\n // of the message they had just sent, and a second device saw an empty\n // thread.\n //\n // The persistence layer owns WHAT gets stored (see `PendingTurnCapability`):\n // `saveThread` replaces the thread, so deciding the list here would risk\n // deleting the history. Absent when the app wires no persistence, which is\n // simply a run with no transcript to store.\n try {\n await getPendingTurn(ctx, { optional: true })?.snapshot()\n } catch (error) {\n // Best-effort: the run is still worth doing, and `onStart` stores the\n // turn again once setup completes.\n logger?.warn('sandbox pending-turn snapshot failed', {\n runId: ctx.runId,\n error,\n })\n }\n }\n\n // SUBSCRIBE BEFORE `ensure`, for the same reason the state is registered\n // before it: `ensure` is the minutes-wide await a disconnect actually lands\n // in. Core calls back immediately if the socket has already closed, so\n // subscribing here cannot miss a disconnect that beat us to it.\n //\n // This is what makes a durable run SURVIVE losing its viewer. The only route\n // a disconnect previously had into this middleware was the application\n // mirroring `request.signal` into `chat()`'s `abortController` — which aborts\n // the run, so `chat()` returned right after this `setup` and the harness\n // adapter's `chatStream` was never called: the agent in the sandbox we just\n // spent minutes creating was NEVER LAUNCHED, and no takeover could recover it\n // because an agent that never ran wrote no journal to replay.\n if (durability !== undefined && durability.detachOnDisconnect) {\n getRunDisconnect(ctx, { optional: true })?.subscribe(async () => {\n const snapshotStop = stopSnapshotLease(state, {\n closePortable: true,\n })\n void snapshotStop.catch(() => {})\n // BOOKKEEPING ONLY — the run is still executing. Deliberately absent:\n // `drainWatcher` (would blind a live agent's file events for the whole\n // remainder) and `definition.destroy` (the run is still using the\n // sandbox). Both belong to the terminal hooks, which still run exactly\n // once afterwards.\n //\n // A run with a cancel already recorded is left alone: that is `onAbort`'s\n // path, and stamping `detachedSince` on a deliberately-stopped run would\n // hand it to the reaper as reclaimable work.\n if (await cancelIntent(durability, ctx.runId, false)) {\n await snapshotStop.catch((error: unknown) => {\n state.logger?.warn('sandbox snapshot writer release failed', {\n runId: ctx.runId,\n phase: 'disconnect',\n error,\n })\n })\n return\n }\n if (\n await recordDetach(definition, state, durability, ctx, 'disconnect')\n ) {\n try {\n await snapshotStop\n } catch (error) {\n state.logger?.warn('sandbox snapshot writer release failed', {\n runId: ctx.runId,\n phase: 'disconnect',\n error,\n })\n }\n state.logger?.sandbox(\n 'sandbox run detached on disconnect; the run continues',\n { runId: ctx.runId },\n )\n } else {\n await snapshotStop.catch((error: unknown) => {\n state.logger?.warn('sandbox snapshot writer release failed', {\n runId: ctx.runId,\n phase: 'disconnect',\n error,\n })\n })\n }\n })\n }\n\n let outcome: 'resumed' | 'native-restored' | 'created' = 'created'\n let handle: SandboxHandle\n try {\n if (snapshotConfig)\n ({ handle, outcome } = await ensureSandboxWithOutcome(\n definition,\n ensureCtx,\n ))\n else handle = await definition.ensure(ensureCtx)\n state.handle = handle\n state.privateHandle = snapshotConfig ? outcome !== 'resumed' : true\n if (snapshotConfig && outcome !== 'resumed') {\n const head = await snapshotConfig.checkpoints.getHead(ctx.threadId)\n if (head) {\n const checkpoint = await snapshotConfig.checkpoints.get(head)\n if (!checkpoint)\n throw new SandboxCheckpointError(\n 'SANDBOX_SNAPSHOT_CHECKPOINT_NOT_FOUND',\n `Checkpoint '${head}' was not found`,\n )\n await restoreSandboxFiles(\n handle,\n {\n blobs: snapshotConfig.persistence.stores.blobs,\n workspaceRoot:\n definition.workspace?.root ?? DEFAULT_WORKSPACE_ROOT,\n },\n checkpoint,\n snapshotPolicy,\n )\n }\n }\n } catch (error) {\n await stopSnapshotLease(state).catch(() => {})\n if (state.handle && state.privateHandle)\n await definition.destroy(ensureCtx).catch(() => {})\n throw error\n }\n // MUTATE, don't re-`set`: a disconnect that landed during `ensure` already\n // captured this object.\n state.handle = handle\n if (snapshotConfig) {\n state.snapshotConfig = snapshotConfig\n state.snapshotPolicy = snapshotPolicy\n state.snapshotRuntime = snapshotRuntime\n }\n try {\n provideSandbox(ctx, handle)\n if (definition.policy) provideSandboxPolicy(ctx, definition.policy)\n\n // Deliberately placed AFTER `logger` is in scope rather than next to the\n // `provideSandboxDurability` call above — there is no logger to warn\n // through until the runtime has been read.\n //\n // `ensureCtx.locks === undefined` counts as in-memory: `defineSandbox`'s\n // `ensure` falls back to a process-lifetime `InMemoryLockStore` when no\n // lock is wired, so an unwired lock has exactly the deficiency being\n // warned about — it is the MOST in-memory case, not an exempt one.\n if (\n durability !== undefined &&\n (ensureCtx.locks === undefined ||\n ensureCtx.locks instanceof InMemoryLockStore)\n ) {\n logger?.warn(\n 'sandbox durability is wired over an InMemoryLockStore: run claims are ' +\n 'serialized within this process only and the lease never signals loss, ' +\n 'so two hosts can drive one run and duplicate its event log. Use a ' +\n 'distributed LockStore via withLocks for any multi-replica deploy.',\n { runId: ctx.runId },\n )\n }\n\n const watchRoot = definition.workspace?.root ?? DEFAULT_WORKSPACE_ROOT\n let baseSha = ''\n try {\n const shaRes = await handle.process.exec('git rev-parse HEAD', {\n cwd: watchRoot,\n })\n if (shaRes.exitCode === 0) {\n baseSha = shaRes.stdout.trim()\n logger?.sandbox('sandbox git baseline captured', {\n root: watchRoot,\n baseSha,\n })\n } else {\n // Non-zero exit: either not a git repository (non-git workspace) or a\n // repo with no commits (no HEAD). Expected, but it silently degrades\n // every subsequent diff to a full-file add-patch, so surface it\n // under `sandbox` (with stderr) rather than leaving nothing to grep.\n logger?.sandbox(\n 'sandbox git baseline unavailable (non-zero exit)',\n {\n root: watchRoot,\n exitCode: shaRes.exitCode,\n stderr: shaRes.stderr,\n },\n )\n }\n } catch (error) {\n // exec rejected (git not on PATH, exec seam broken) → baseSha stays ''\n // and accessors fall back, but this is a real anomaly, not a plain\n // non-git workspace, so warn.\n logger?.warn('sandbox git baseline capture failed', {\n root: watchRoot,\n error,\n })\n }\n\n const workspace = definition.workspace\n if (workspace !== undefined) {\n const virtualRoot = workspace.root ?? DEFAULT_WORKSPACE_ROOT\n const root = resolveHarnessCwd(handle, virtualRoot)\n const workspaceHash = computeWorkspaceHash(workspace)\n const secrets = workspace.secrets\n provideWorkspaceProjection(ctx, {\n skills: workspace.skills ?? [],\n plugins: workspace.plugins ?? [],\n resolveSecret: (ref) => {\n if (secrets === undefined) {\n throw new Error(\n `resolveSecret: no secrets defined on this workspace (ref: \"${ref.__secretName}\")`,\n )\n }\n return resolveSecret(secrets, ref)\n },\n markerPath: `${root}/.tanstack-projected-${workspaceHash}`,\n root,\n ...(workspace.scripts !== undefined\n ? { scripts: workspace.scripts }\n : {}),\n })\n }\n\n const hooks = definition.hooks\n await hooks?.onReady?.(handle)\n\n const fe = resolveFileEvents(definition.fileEvents)\n // THE SAME array the run state already holds, not a fresh one. The watcher\n // callback below closes over this reference, and `drainWatcher` awaits\n // `state.pendingDiffs` — a second array would silently drop every in-flight\n // diff from the teardown drain.\n const pendingDiffs = state.pendingDiffs\n let watcher: SandboxWatchHandle | undefined\n if (fe.enabled) {\n watcher = await watchWorkspace(handle, {\n onEvent: (event: SandboxFileEvent) => {\n const enriched = buildFileHookEvent(\n handle,\n watchRoot,\n baseSha,\n event,\n logger,\n )\n void dispatchDefinitionHooks(hooks, enriched, logger)\n runtime?.emit(enriched)\n if (fe.diff) {\n pendingDiffs.push(\n enriched\n .diff()\n .then((diff) => {\n runtime?.emitFileDiff({ path: event.path, diff })\n })\n .catch((error: unknown) => {\n logger?.warn('sandbox file diff emit failed', {\n path: event.path,\n error,\n })\n }),\n )\n }\n },\n // Watch the SAME root the enrichment layer relativizes against\n // (`buildFileHookEvent(handle, watchRoot, …)` and the `baseSha`\n // capture). Without this the watcher defaults to `/workspace` while\n // enrichment uses `watchRoot`, so a custom `workspace.root` makes the\n // two look at different directories and git pathspecs break.\n root: watchRoot,\n ...(ctx.signal !== undefined ? { signal: ctx.signal } : {}),\n ...(logger !== undefined ? { logger } : {}),\n })\n logger?.sandbox('sandbox watcher started', {\n root: watchRoot,\n diff: fe.diff,\n })\n }\n\n // MUTATE the object registered above rather than `set`-ing a second one: an\n // abort that landed mid-setup already captured a reference to it (and may\n // already be draining `pendingDiffs`), so replacing the entry would hand the\n // teardown path a different object than the watcher writes into.\n // `pendingDiffs` needs no copying — it IS `state.pendingDiffs`.\n if (watcher) state.watcher = watcher\n } catch (error) {\n await drainWatcher(state, 'error')\n await stopSnapshotLease(state).catch(() => {})\n if (state.privateHandle)\n await definition.destroy(ensureCtx).catch(() => {})\n throw error\n }\n },\n\n // Keep the recorded tool history OUT of the request to the model. It is stored\n // history for the next turn, it names tools the provider was never given, and one\n // triage-sized run is hundreds of kilobytes — so replaying it is wasteful at best\n // and rejected at worst. `ctx.messages` keeps it (that is what gets stored and\n // rendered); only `config.messages` loses it.\n onConfig(_ctx, config) {\n const messages = stripObservedToolCalls(config.messages)\n if (messages.length === config.messages.length) return\n return { messages }\n },\n\n // The engine re-syncs `middlewareCtx.messages` from its own array once per agent\n // iteration, which drops whatever the recorder appended during the previous\n // iteration's stream. Restoring it here — AFTER that sync — is what makes a\n // multi-iteration run keep its full history without depending on where this\n // middleware sits relative to persistence in the middleware array.\n onIteration(ctx) {\n runState.get(ctx)?.toolHistory.reconcile(ctx)\n },\n\n // Record the harness's own tool calls as transcript messages. Observe only:\n // returning nothing passes every chunk through untouched.\n async onChunk(ctx, chunk) {\n const state = runState.get(ctx)\n state?.toolHistory.observe(chunk, ctx)\n if (\n state &&\n chunk.type === 'RUN_FINISHED' &&\n chunk.outcome?.type === 'interrupt'\n ) {\n await drainWatcher(state, 'pause')\n await stopSnapshotLease(state, { closePortable: true })\n }\n },\n\n async onFinish(ctx) {\n const state = runState.get(ctx)\n if (!state) return\n const { handle, ensureCtx } = state\n\n // Last chance before persistence writes the transcript. Only matters if a\n // config sync landed after the final tool chunk; the recorder is idempotent, so\n // in the normal case this changes nothing.\n state.toolHistory.reconcile(ctx)\n\n await drainWatcher(state, 'finish')\n\n let primaryError: unknown\n try {\n const snapshotCaptureTask = Promise.resolve().then(\n async (): Promise<void> => {\n const config = state.snapshotConfig\n const runtime = state.snapshotRuntime\n const lease = state.snapshotLease\n if (!config || !runtime || !handle || !lease) {\n if (state.snapshotLost) throw state.snapshotLost\n return\n }\n if (!canPublishPortableSnapshot(state, lease)) return\n\n await runtime.completion.waitForRunCompletion()\n if (!canPublishPortableSnapshot(state, lease)) return\n\n const conversation =\n await runtime.persistence.stores.messages.loadThread(ctx.threadId)\n if (!canPublishPortableSnapshot(state, lease)) return\n\n const files = await captureSandboxFiles(\n handle,\n {\n blobs: config.persistence.stores.blobs,\n workspaceRoot:\n definition.workspace?.root ?? DEFAULT_WORKSPACE_ROOT,\n },\n state.snapshotPolicy,\n definition.workspace?.secrets !== undefined\n ? resolveAllSecrets(definition.workspace.secrets)\n : {},\n )\n if (!canPublishPortableSnapshot(state, lease)) return\n\n const artifacts = await captureSandboxArtifacts(\n {\n blobs: config.persistence.stores.blobs,\n artifacts: config.persistence.stores.artifacts,\n },\n ctx.threadId,\n definition.workspace?.secrets !== undefined\n ? resolveAllSecrets(definition.workspace.secrets)\n : {},\n )\n if (!canPublishPortableSnapshot(state, lease)) return\n\n const parentCheckpointId = await config.checkpoints.getHead(\n ctx.threadId,\n )\n if (!canPublishPortableSnapshot(state, lease)) return\n\n try {\n await config.checkpoints.append({\n checkpoint: {\n id: `checkpoint-${ctx.runId}`,\n threadId: ctx.threadId,\n parentCheckpointId,\n createdAt: Date.now(),\n reason: 'automatic',\n sourceRunId: ctx.runId,\n files: files.files,\n conversation,\n artifacts,\n },\n expectedHeadId: parentCheckpointId,\n writer: lease,\n })\n } catch (error) {\n if (state.snapshotLost) throw state.snapshotLost\n throw error\n }\n if (state.snapshotLost) throw state.snapshotLost\n },\n )\n state.snapshotCaptureTask = snapshotCaptureTask\n try {\n await snapshotCaptureTask\n } finally {\n if (state.snapshotCaptureTask === snapshotCaptureTask)\n state.snapshotCaptureTask = undefined\n }\n\n const lifecycle = definition.lifecycle\n\n // `handle` is absent only if `setup` never got past `definition.ensure`, in\n // which case there is no sandbox to snapshot.\n if (\n lifecycle?.snapshot === 'after-run' &&\n handle?.capabilities.snapshots &&\n handle.snapshot\n ) {\n const snapshot = await handle.snapshot(`after-run-${ctx.runId}`)\n const store = ensureCtx.store\n if (store) {\n const key = definition.key(ensureCtx)\n const existing = await store.get(key)\n if (existing) {\n await store.upsert({\n ...existing,\n latestSnapshotId: snapshot.id,\n updatedAt: Date.now(),\n })\n }\n }\n }\n\n if (lifecycle?.destroyOnComplete) {\n await definition.destroy(ensureCtx)\n await definition.hooks?.onDestroy?.()\n }\n } catch (error) {\n primaryError = error\n if (definition.lifecycle?.destroyOnComplete) {\n try {\n await definition.destroy(ensureCtx)\n await definition.hooks?.onDestroy?.()\n } catch (cleanupError) {\n state.logger?.warn(\n 'sandbox destroy after terminal failure failed',\n {\n runId: ctx.runId,\n phase: 'finish',\n error: cleanupError,\n },\n )\n }\n }\n }\n\n let snapshotCleanupError: unknown\n try {\n await stopSnapshotLease(state, { closePortable: true })\n } catch (error) {\n snapshotCleanupError = error\n }\n if (primaryError !== undefined) {\n if (snapshotCleanupError !== undefined)\n state.logger?.warn('sandbox snapshot writer release failed', {\n runId: ctx.runId,\n phase: 'finish',\n error: snapshotCleanupError,\n })\n throw primaryError\n }\n if (snapshotCleanupError !== undefined) throw snapshotCleanupError\n },\n\n async onAbort(ctx, info: AbortInfo) {\n const state = runState.get(ctx)\n if (!state) return\n\n // First on BOTH branches: a diff still in flight must be drained whether\n // the sandbox is about to be destroyed or merely detached, or the final\n // file's diff is dropped.\n await drainWatcher(state, 'abort')\n let releaseError: unknown\n try {\n await stopSnapshotLease(state, { closePortable: true })\n } catch (error) {\n releaseError = error\n }\n\n const durability = state.durability\n const cancelled = await cancelIntent(\n durability,\n ctx.runId,\n info.cancelRequested === true,\n )\n\n if (\n durability !== undefined &&\n !cancelled &&\n durability.detachOnDisconnect\n ) {\n // DETACH on the teardown path. Reached when the run is aborted for a\n // reason that is NOT an out-of-band cancel while detachable — a genuine\n // stop from elsewhere, or a host going down. The ordinary disconnect is\n // handled by the disconnect subscriber in `setup`, which does not end the\n // run at all.\n //\n // On a failed record write this branch is ABANDONED for the destroy one\n // below, because a rejection here is the worst shape available: the\n // verdict is unpublished, so core terminalizes the log and records a\n // healthy detached run as failed; `detachedSince`/`sandboxKey` are\n // unwritten, so `listReclaimable` can never surface the run and\n // `reapDetachedRuns` can never reclaim it. A DESTROYED sandbox beats an\n // unreachable one — the same reasoning `drainWatcher` applies to its own\n // guarded `stop()`.\n if (await recordDetach(definition, state, durability, ctx, 'abort')) {\n if (releaseError) throw releaseError\n return\n }\n await definition.destroy(state.ensureCtx)\n await definition.hooks?.onDestroy?.()\n if (releaseError) throw releaseError\n return\n }\n\n // ALWAYS tear down on an explicit abort, regardless of `destroyOnComplete`.\n // The in-sandbox agent process is not killed by closing its IO stream\n // (e.g. a Docker exec survives client disconnect), so the only reliable way\n // to stop it — and the token/cost drain of its ongoing API calls — is to\n // destroy the sandbox (stop the container/VM). `keepAlive` /\n // `destroyOnComplete:false` governs *successful completion*, never cancel.\n await definition.destroy(state.ensureCtx)\n await definition.hooks?.onDestroy?.()\n if (releaseError) throw releaseError\n },\n\n async onError(ctx, info) {\n const state = runState.get(ctx)\n if (!state) return\n\n await drainWatcher(state, 'error')\n let releaseError: unknown\n try {\n await stopSnapshotLease(state)\n } catch (error) {\n releaseError = error\n }\n await definition.hooks?.onError?.(info.error)\n\n // On failure, only tear down when the lifecycle says so; otherwise leave\n // the sandbox for a resumed retry.\n if (definition.lifecycle?.destroyOnComplete) {\n await definition.destroy(state.ensureCtx)\n await definition.hooks?.onDestroy?.()\n }\n if (releaseError) throw releaseError\n },\n })\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-sandbox",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Provider-agnostic sandbox layer for TanStack AI — run harness adapters inside isolated sandboxes (defineSandbox, defineWorkspace, withSandbox) with a uniform SandboxHandle, workspace bootstrap, policy, and resumable lifecycle.",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -61,8 +61,8 @@
61
61
  "peerDependencies": {
62
62
  "@ngrok/ngrok": "^1.0.0",
63
63
  "vitest": "^4.1.10",
64
- "@tanstack/ai": "^0.52.0",
65
- "@tanstack/ai-persistence": "^0.5.4"
64
+ "@tanstack/ai": "^0.52.1",
65
+ "@tanstack/ai-persistence": "^0.5.5"
66
66
  },
67
67
  "peerDependenciesMeta": {
68
68
  "@ngrok/ngrok": {
@@ -79,8 +79,8 @@
79
79
  "@ngrok/ngrok": "^1.7.0",
80
80
  "@vitest/coverage-v8": "4.1.10",
81
81
  "vitest": "^4.1.10",
82
- "@tanstack/ai": "0.52.0",
83
- "@tanstack/ai-persistence": "0.5.4"
82
+ "@tanstack/ai": "0.52.1",
83
+ "@tanstack/ai-persistence": "0.5.5"
84
84
  },
85
85
  "scripts": {
86
86
  "build": "vite build",
package/src/middleware.ts CHANGED
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import {
20
20
  defineChatMiddleware,
21
+ isTerminalRunStatus,
21
22
  provideDetachableRun,
22
23
  provideRunDetached,
23
24
  wasCancelRequested,
@@ -270,6 +271,20 @@ async function recordDetach(
270
271
  phase: 'disconnect' | 'abort',
271
272
  ): Promise<boolean> {
272
273
  try {
274
+ // A late abort from a superseded host can run after takeover has already
275
+ // completed the run. Re-stamping `detachedSince` then leaves a finished
276
+ // record looking detached. Skip the write; return true so `onAbort` does
277
+ // not treat this as a failed stamp and destroy the sandbox.
278
+ try {
279
+ const current = await durability.runs.get(ctx.runId)
280
+ if (current !== null && isTerminalRunStatus(current.status)) {
281
+ return true
282
+ }
283
+ } catch {
284
+ // Unreadable store: still try the update. A get() failure must not skip
285
+ // the stamp — that is how a cancel-probe rejection used to skip both
286
+ // detach and destroy.
287
+ }
273
288
  // The record already exists: `setup` pre-creates it for every durable run
274
289
  // BEFORE `ensure`, precisely so this stamp cannot land on a runId the store has
275
290
  // never heard of — `RunStore.update` is a documented no-op for an unknown