You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part 1 of 4 deferred upstream feature ports. Ports upstream PR #880 (vercel-labs/open-agents commit ed2aa3e7, "Start sandbox provisioning on session create") into dennisonbertram/fork-open-agents. This is a fork-aware port, not a clean cherry-pick: our chat-sandbox-runtime.ts carries a managed-runtime engine that upstream deleted, and the port must preserve it.
Epic:#119 — Upstream ports. This issue is the KEYSTONE — #116 (lazy sandbox actions UI, soft-dep), #118 (parallelize chat startup, hard-dep) build on top of it. #117 (workflow v5 bump) is independent.
SDK note: This slice uses workflow@^4.2.0-beta.72 (v4). Port #117 (#883) targets v5 separately and is NOT a dependency here.
Why this matters
Today a session's sandbox is provisioned lazily on the first chat turn inside resolveChatSandboxRuntime. The user creates a session, lands on the chat page, types a message, and only then waits 10-30s+ for connectSandbox to boot the VM, clone the repo, mint/revoke a GitHub installation token, and install global skills — all blocking the first response. Upstream #880 moves provisioning to session-create time so the sandbox is warming (or already warm) by the time the user sends their first message. The chat path then becomes "wait for the in-flight provisioning run, then bare-reconnect," which is far cheaper and removes the cold-start tax from the first turn.
For our fork specifically, this also untangles the 857-line chat-sandbox-runtime.ts that mixes provisioning, managed-runtime profile execution, startup-log reporting, and observability into one function. Extracting provisioning into lib/sandbox/provisioning.ts is a prerequisite for #881 (soft-dep) and #884 (hard-dep).
User/operator path protected
Operator/user path: Create a session (POST /api/sessions) → land on the session chat page → send the first message → receive a streamed response from an agent running in a ready sandbox.
Concretely this slice makes that path faster and safer:
Provisioning starts the instant the session row exists, not on first chat turn.
The first chat turn waits for the already-running provisioning workflow instead of starting its own boot.
A session archived mid-provisioning stops its sandbox and does not leave an orphaned VM or write state onto an archived row.
Managed-runtime sessions still get full profile setup/probe/verify and observability — the protected managed-runtime path is preserved, not regressed.
Behavior contract
Scenario BT-001: provisioning starts on session create
Given an authenticated user with a valid repo selection
When they POST /api/sessions and creation succeeds
Then kickSandboxProvisioningWorkflow(result.session.id) is invoked with the new session id
And the response is still 200 with the created session/chat payload.
Scenario BT-002: a failing kick never blocks session creation
Given session creation otherwise succeeds
When kickSandboxProvisioningWorkflow throws/rejects
Then the error is caught and logged
And POST /api/sessions still returns 200 with the created session.
Scenario BT-003: provisioning run claim is idempotent (first-claim-wins)
Given a session with sandboxProvisioningRunId IS NULL
When two provisioning runs race to claim it
Then exactly one claimSessionSandboxProvisioningRunId succeeds and the duplicate run is a no-op (run-replaced / cancelled).
Scenario BT-004: archived mid-provisioning stops the sandbox and surfaces a typed error
Given a provisioning run is connecting/booting a sandbox
When the session is archived before updateSessionIfNotArchived commits
Then the persist is rejected (returns no row), the booted sandbox is stopped, and SessionArchivedDuringProvisioningError is thrown
And the archived row keeps no stale sandboxState.
Scenario BT-005: chat waits for in-flight provisioning instead of cold-starting
Given a session whose sandbox is not yet active and a provisioning run is in flight
When resolveChatSandboxRuntime runs for the first chat turn
Then it kicks/awaits the provisioning run (waitForSandboxProvisioningRun), re-reads the session, and bare-reconnects via connectSandbox(sandboxState) — it does not run the full provision ({ state, options }) form itself.
Scenario BT-006: managed-runtime sessions still execute their profile
Given a session with runtimeMode === "managed_runtime" and a managedRuntimeProfileId
When the first chat turn resolves the runtime
Then ensureManagedRuntimeEnvironment still runs, managed_runtime.profile.* events still fire, and the returned ResolvedChatSandboxRuntime still includes runtimeMode and the managedRuntime block.
Product and design spec
Entry point: Session creation API apps/web/app/api/sessions/route.ts (POST); first chat turn via the chat workflow that calls resolveChatSandboxRuntime in apps/web/app/workflows/chat-sandbox-runtime.ts.
Primary flow: create session → (server) fire-and-forget kickSandboxProvisioningWorkflow → durable sandboxProvisioningWorkflow claims the run lease and calls provisionSessionSandbox → on success persists sandboxState + active lifecycle, clears the provisioning lease, kicks the lifecycle workflow → first chat turn awaits the run, re-reads, bare-reconnects.
States:
setting-up workspace status is sent when the sandbox is not yet active on first chat turn (existing sendWorkspaceStatus copy: "Setting up the workspace...").
lifecycleState transitions: created with provisioning (already our default at create) → active on success → failed with lifecycleError on provisioning error.
sandboxProvisioningRunId is set on claim and cleared on completion/archive.
Accessibility / copy: No new user-facing copy beyond the existing workspace status messages; no new UI controls in this slice. The WorkspaceStartupReporter startup-log messages are preserved verbatim.
Permissions: Session-create auth/rate-limit/botid/composio/profile-validation are unchanged and must still run before any kick. Provisioning runs under the session owner; provisionSessionSandbox re-checks session.userId (and repoOwner/repoName presence) and re-verifies repo access before minting a scoped installation token.
Error / empty / loading behavior: Kick errors are swallowed (logged) at the API layer. Provisioning errors set lifecycleState: "failed" + lifecycleError; the chat path surfaces lifecycleError ?? "Workspace setup failed". Archived-during-provisioning is a typed no-throw-to-user outcome (session-archived skip) inside the workflow.
Integration spec (REAL paths + reconciliation)
Upstream source
Upstream commit ed2aa3e7 (PR #880, "Start sandbox provisioning on session create", +2202/-187 across 11 files). Inspect with git show ed2aa3e7 --stat. The upstream diff is reachable in the fork's git history via the upstream remote fetch.
This is an ADAPT port, not a blind cherry-pick. Our chat-sandbox-runtime.ts (857 lines) has diverged massively from upstream's post-#880 version (121 lines): we carry ensureManagedRuntimeEnvironment, WorkspaceStartupReporter, emitSessionEvent, sendStart, and managed-runtime observability that upstream never had. Applying git cherry-pick ed2aa3e7 would silently delete our managed-runtime engine. Do not do it.
Routes
apps/web/app/api/sessions/route.ts (currently 428 lines, no provisioning kick):
Add one import: import { kickSandboxProvisioningWorkflow } from "@/lib/sandbox/provisioning-kick";
Add a ~7-line non-blocking kick immediately before return Response.json(result):
awaitkickSandboxProvisioningWorkflow(result.session.id).catch((error)=>{console.error(`Failed to kick sandbox provisioning for session ${result.session.id}:`,error,);});
OMIT any managed-template-trial gating — that module does not exist in our fork and was not part of ed2aa3e7. Upstream's test suite (route.test.ts) has a "blocks additional sessions for managed template trial users" test using a module our fork does not carry; the fork's test file must handle that divergence.
Preserve every fork-only check: checkBotProtection, checkRateLimit, isComposioProfileAllowedForRepository, and isManagedRuntimeProfileId validation.
Current schema has lifecycleRunId and lifecycleError but NO sandboxProvisioningRunId — confirmed by inspection.
Migration: After editing schema.ts, run:
bun run --cwd apps/web db:generate
This produces migration 0048 (latest in fork is 0047_cheerful_thunderbolt_ross.sql — confirmed). Do NOT hand-copy upstream's 0036_faulty_yellow_claw.sql — the fork's migration journal has diverged (our 0047 vs upstream's 0036 at the time of #880). Regenerate.
DB helpers (new exports in apps/web/lib/db/sessions.ts, currently 842 lines)
Confirmed by inspecting ed2aa3e7: none of these exist today.
SessionRecord type: export type SessionRecord = NonNullable<Awaited<ReturnType<typeof getSessionById>>>; — place after getSessionById.
updateSessionIfNotArchived(sessionId, data) — conditional UPDATE where status != 'archived' AND (lifecycleState IS NULL OR lifecycleState != 'archived'). The exact Drizzle form is confirmed from the upstream diff; uses and, eq, ne, or, isNull which are already imported.
claimSessionSandboxProvisioningRunId(sessionId, runId) — atomic UPDATE sessions SET sandbox_provisioning_run_id = runId WHERE id = sessionId AND sandbox_provisioning_run_id IS NULL RETURNING id. Returns Boolean(updated). Mirrors the existing claimSessionLifecycleRunId (line 323).
clearSessionSandboxProvisioningRunIdIfOwned(sessionId, runId) — conditional UPDATE ... SET sandbox_provisioning_run_id = NULL WHERE ... AND sandbox_provisioning_run_id = runId. Confirmed from upstream diff; this is the clearIfOwned semantics the POC proved safe.
Full source confirmed from git show ed2aa3e7 -- apps/web/app/workflows/sandbox-provisioning.ts.
sandboxProvisioningWorkflow(sessionId) reads getWorkflowMetadata().workflowRunId, claims/validates the run lease, calls provisionSessionSandbox, clears the lease.
Handles session-not-found, run-replaced, session-archived as skip returns; re-throws other errors after setting lifecycleState: "failed".
Register this workflow wherever workflows are registered for the v4 runtime (mirror how sandboxLifecycleWorkflow is registered).
provisionSessionSandbox({ sessionId, userId? }), ProvisionSessionSandboxResult, and SessionArchivedDuringProvisioningError.
Uses the full-options connectSandbox({ state, options }) provision form (POC confirmed this form is already supported by our fork's packages/sandbox/factory.ts — zero changes needed to packages/sandbox).
Persists via updateSessionIfNotArchived, stops the sandbox on archive race, installs global skills, kicks the lifecycle workflow.
Fork reconciliation point: our getGitUser signature in chat-sandbox-runtime.ts takes a userId; upstream provisioning.ts takes a UserRecord (loads a UserRecord via getUserById first). Port must use the fork's actual signature or adapt accordingly.
PRESERVE assistantId / chatId / workflowRunId params on resolveChatSandboxRuntime.
PRESERVE runtimeMode and managedRuntime fields on ResolvedChatSandboxRuntime return type.
packages/sandbox — No change
POC confirmed: packages/sandbox/factory.ts already supports both the full-options provision form and the bare reconnect form. Zero changes to packages/sandbox are needed.
Double-fire check
provisionSessionSandbox calls kickSandboxLifecycleWorkflow({ reason: "sandbox-created" }). Today's resolveChatSandboxRuntime also kicks it after the inline connect. After this port, the chat path must NOT re-fire kickSandboxLifecycleWorkflow for the same provisioning — that responsibility moves into provisioning.ts. Verify and guard explicitly.
Compatibility
Migration is a nullable text column add — backward compatible, zero-downtime. Existing sessions with sandboxProvisioningRunId IS NULL behave correctly; the lazy chat-path kick still works as fallback.
Four new sessions.ts exports: SessionRecord type + 3 atomic helpers.
Three new files: provisioning.ts, provisioning-kick.ts, sandbox-provisioning.ts (+ workflow registration).
The non-blocking kick in POST /api/sessions.
Fork-aware rewrite of the chat-path reconnect in chat-sandbox-runtime.ts (getReadySessionSandbox + bare reconnect), preserving the managed-runtime engine, startup reporter, observability, and sendStart ordering.
Upstream's managed-template-trial gating (module absent in fork; never part of ed2aa3e7).
Deleting our managed-runtime engine to match upstream's slimmed 121-line file — explicitly forbidden; we keep it.
Any packages/sandbox changes (none required, POC-proven).
Live end-to-end VM boot proof beyond what the POC covered (tracked as a limitation, not this slice's gate unless infra is available).
This is part 1 of 4 deferred upstream ports. Dependency order: #880 (this) → #881/#116 (soft-dep) → #883/#117 (independent) → #884/#118 (hard-dep on #880 AND #883).
Research and context sources
Upstream commit ed2aa3e7 (PR fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880, "Start sandbox provisioning on session create") — inspected directly via git show ed2aa3e7. 11 files, +2202/-187; key additions are lib/sandbox/provisioning.ts (+296), lib/sandbox/provisioning-kick.ts (+113), app/workflows/sandbox-provisioning.ts (+60), lib/db/sessions.ts (+66 — confirmed 4 new exports), and slimming of app/workflows/chat-sandbox-runtime.ts (-187 net to 121 lines). Schema adds sandboxProvisioningRunId: text("sandbox_provisioning_run_id").
POC/upstream-880-provisioning/FINDINGS.md — POC verdict PASS (low-risk). Proved: (a) dual connectSandbox form already in packages/sandbox/factory.ts → zero packages/sandbox changes; (b) atomic claim/clear semantics (first-claim-wins under 2-way and 50-way races; clearIfOwned only clears the owning run). Limitation: contract proof only, no live sandbox booted.
docs/process/managed-runtime-proof-standard.md — proof standard this issue must satisfy because it touches the managed-runtime path.
Agent todo checklist
Re-read git show ed2aa3e7, POC/upstream-880-provisioning/FINDINGS.md, and the CRUX note in docs/plans/active-plan.md.
Add sandboxProvisioningRunId: text("sandbox_provisioning_run_id") to the sessions table in apps/web/lib/db/schema.ts between lifecycleRunId and lifecycleError.
Run bun run --cwd apps/web db:generate; confirm it produces migration 0048 (do not hand-copy upstream 0036). Commit the generated .sql + meta.
Add to apps/web/lib/db/sessions.ts: SessionRecord type export, updateSessionIfNotArchived, claimSessionSandboxProvisioningRunId, clearSessionSandboxProvisioningRunIdIfOwned (mirror claimSessionLifecycleRunId).
Add apps/web/lib/sandbox/provisioning-kick.ts (kickSandboxProvisioningWorkflow, waitForSandboxProvisioningRun) with live-run dedupe.
Add apps/web/app/workflows/sandbox-provisioning.ts durable workflow (port from confirmed upstream source at ed2aa3e7) and register it with the workflow runtime.
Write the failing route test (kick called with new session id; kick-throws still returns 200) and confirm RED.
Add the non-blocking kick before return Response.json(result) in apps/web/app/api/sessions/route.ts; preserve botid/rate-limit/composio/profile-validation; OMIT managed-template-trial.
Write failing tests for atomic claim idempotency and archived-mid-provisioning; confirm RED.
Rewrite the chat-path reconnect in apps/web/app/workflows/chat-sandbox-runtime.ts: add getReadySessionSandbox, switch to bare connectSandbox(sandboxState), REMOVE the inline provision logic now living in provisioning.ts, PRESERVE ensureManagedRuntimeEnvironment / WorkspaceStartupReporter / emitSessionEvent / sendStart ordering / assistantId/chatId/workflowRunId params / runtimeMode + managedRuntime return fields.
Verify kickSandboxLifecycleWorkflow does not double-fire (provisioning owns it; chat path must not re-fire for the same provisioning).
Make all behavior tests GREEN.
Run bun test apps/web/app/api/sessions/route.test.ts and the adjacent sandbox/workflow suites.
Run git diff --check and bun --bun run ci.
Capture managed-runtime proof evidence (Level 1 minimum; Level 2 if sandbox infra is available) and record limitations.
Tests to add first (red-first)
Each test must be observed RED before implementation, with the named expected-failure reason.
BT-001 — Kick fires on create — apps/web/app/api/sessions/route.test.ts: mock @/lib/sandbox/provisioning-kick; assert a successful POST /api/sessions calls kickSandboxProvisioningWorkflow with String(body.session.id).
Expected RED: provisioningKickCalls is empty / module not imported (kick not wired yet).
Upstream test reference: ed2aa3e7 adds this exact test to route.test.ts with provisioningKickCalls tracking — use the same mock shape but adapt for fork (omit managed-template-trial mock).
BT-002 — Kick failure does not block create — same file: make the mocked kick reject; assert the response is still 200 and the session payload is returned.
Expected RED: the unhandled rejection / missing .catch propagates and the request does not return 200.
BT-003 — Claim idempotency (first-claim-wins) — a sessions.ts/provisioning-level test: two concurrent claimSessionSandboxProvisioningRunId(sessionId, runId) calls; assert exactly one returns true.
Expected RED: claimSessionSandboxProvisioningRunId does not exist (import error / undefined).
BT-004 — Archived mid-provisioning — a provisioning.ts test: simulate the session becoming archived so updateSessionIfNotArchived returns no row; assert sandbox.stop() is called and SessionArchivedDuringProvisioningError is thrown.
Expected RED: provisionSessionSandbox / SessionArchivedDuringProvisioningError do not exist.
BT-005 — Chat awaits provisioning — a chat-sandbox-runtime.ts test: with the sandbox not yet active, assert resolveChatSandboxRuntime calls waitForSandboxProvisioningRun and then bare-reconnects (connectSandbox(sandboxState) with no options), rather than running the full-options provision form.
Expected RED: chat path still runs the inline full-options connectSandbox({ state, options }) and never calls waitForSandboxProvisioningRun.
BT-006 — Managed-runtime fields preserved — a chat-sandbox-runtime.ts test: assert resolveChatSandboxRuntime returns runtimeMode and managedRuntime fields populated for managed-runtime sessions.
Expected RED (pre-implementation): if the chat-path rewrite accidentally removes those return fields.
Observability and user feedback
This issue touches managed runtime, so it must satisfy the Managed Runtime Proof Standard with real evidence (not a label).
User-visible status:setting-up workspace status ("Setting up the workspace...") on the first chat turn when the sandbox is not yet active; lifecycleStateprovisioning → active (or failed with lifecycleError) visible on the session row.
Responsible modules / structured events:
sandboxProvisioningWorkflow run logs (claim outcome, skip reasons: session-not-found, run-replaced, session-archived).
provisioning-kick dedupe outcomes (started / existing / active / skipped) with runId.
emitSessionEvent / managed_runtime.profile.* events from ensureManagedRuntimeEnvironment must continue to fire for managed-runtime sessions (setup, probe, verification, finish).
Typed error kind:SessionArchivedDuringProvisioningError (name === "SessionArchivedDuringProvisioningError"); provisioning failures recorded as lifecycleState: "failed" + lifecycleError message.
Correlation IDs:sessionId, chatId, workflowRunId, sandboxName, profileId (and managedRuntimeProfileRunId for managed runs), runId for the provisioning run.
Redaction: Never log the GitHub installation token (setupToken.token is minted then revoked); never log raw sandboxState secrets, provider tokens, env values, or prompt/session content.
grep '"workflowRunId":"<runId>"' logs | grep managed_runtime.profile — confirm the managed profile still executed for the chat turn.
DB: select id, lifecycle_state, sandbox_provisioning_run_id, lifecycle_error from sessions where id = '<id>' — confirm the lease was set then cleared and lifecycle reached active.
Proof Level
Target level: Level 1 (Local Deterministic Proof) required; Level 2 (Local/Live Sandbox Proof) if sandbox infra is available at port time.
Why sufficient: the behavior contract (kick wiring, claim/clear atomicity, archive race, chat wait-then-reconnect, managed-runtime preservation) is fully assertable with deterministic tests; the live connectSandbox path is the same one production already exercises (POC contract-proved), so Level 1 + the existing managed-runtime suites bound the residual risk. Record the no-live-boot limitation explicitly.
Required Evidence
Claim named (e.g. "this run used managed runtime profile <id>@<version>").
Coordinator enforcement covered (managed mode still strips direct coding tools — existing managed-runtime tests).
Profile setup/probe results captured.
Work attributed to managed worker/sandbox.
Independent verification captured (test commands + any sandbox/service evidence).
User-visible status/inspector feedback captured.
Limitations surfaced (no live VM boot in CI).
Secrets redacted.
Regression harness plan
Route test (apps/web/app/api/sessions/route.test.ts): kick-fires-on-create and kick-failure-still-200. Fail-before: assertions fail without the wired kick / .catch. Pass-after: both pass. This is the durable signal for the create-path protected behavior. Upstream adds provisioningKickCalls tracking with mock.module("@/lib/sandbox/provisioning-kick", ...) — use the same mock shape in the fork's test file.
Claim/clear unit test: first-claim-wins + clearIfOwned-only-by-owner. Fail-before: helpers missing. Pass-after: race resolves to one winner. Mirrors the proven claimSessionLifecycleRunId coverage style (line 323 of sessions.ts).
Chat-runtime test: wait-then-bare-reconnect + managed-runtime fields preserved on the return. Fail-before: chat path still inline-provisions. Pass-after: it awaits the run and bare-reconnects while runtimeMode/managedRuntime remain populated.
Managed-runtime suites: existing managed-runtime profile-execution tests must stay green (no regression to ensureManagedRuntimeEnvironment / managed_runtime.profile.*).
If a live-boot smoke is not practical in CI, the PR must state so and attach the manual/POC contract proof as the substitute.
TDD audit trail
All three commits must appear in git history in order:
Red commit (test(red): TASK-115 failing tests for...): failing route + claim + archive + chat-wait tests committed test-only on the work branch. Record the RED command output and reason in the commit message.
Green commit (feat: TASK-115 implement...): smallest implementation (schema/migration + helpers + 3 new files + kick wiring + chat-path rewrite) that turns the targeted tests green.
Regression commit (test(regression): TASK-115 regression coverage for...): regression tests that would fail if the green commit were reverted.
Exception: none anticipated. If any behavior cannot be covered by a deterministic test, record the exception and the manual proof in the PR.
Regression risks and concerns
Managed-runtime engine deletion risk (highest): the temptation to mirror upstream's slimmed 121-line chat-sandbox-runtime.ts would delete ensureManagedRuntimeEnvironment, the startup reporter, and observability. Mitigation: preserve-and-extract, guarded by the managed-runtime suites and the chat-runtime test.
kickSandboxLifecycleWorkflow double-fire: provisioning now owns the lifecycle kick; the chat path must not re-fire it for the same provisioning. Mitigation: explicit checklist verification + review.
Archive race: if updateSessionIfNotArchived is not used (or the stop path is skipped), an archived session could keep stale sandboxState or orphan a VM. Mitigation: the archive-race test (BT-004).
Migration numbering: hand-copying upstream 0036 would corrupt our journal. Mitigation: regenerate as 0048; checklist enforces it.
Workflow registration: forgetting to register sandboxProvisioningWorkflow means the kick starts an unknown workflow. Mitigation: registration checklist item + the kick test exercising start.
getGitUser signature divergence: upstream's provisioning.tsgetGitUser takes a UserRecord; our runtime's took a userId. Reconcile during extraction (the new provisioning.ts loads a UserRecord via getUserById).
managed-template-trial omission: upstream route.test.ts tests a module our fork doesn't carry. The fork's route test must not import or mock that module.
Deploy or migration impact
Migration 0048: single nullable text column add to sessions — zero-downtime, backward compatible, no backfill. Runs automatically during bun run build on every Vercel deploy (preview gets its own Neon branch via Neon branching; prod uses main).
Workflow registration: the new sandboxProvisioningWorkflow must be deployed/registered for kicks to resolve.
Rollback safety: kick errors are swallowed at the API layer, and the chat path re-kicks provisioning on demand, so a partial rollout (column present, workflow not yet warm) degrades to lazy chat-path provisioning rather than failing session creation. The nullable column is safe to leave in place on rollback.
Definition of done
Schema column added and migration 0048 generated + committed (not hand-copied from upstream 0036).
Four sessions.ts exports added: SessionRecord type, updateSessionIfNotArchived, claimSessionSandboxProvisioningRunId, clearSessionSandboxProvisioningRunIdIfOwned.
provisioning.ts, provisioning-kick.ts, and sandbox-provisioning.ts added; workflow registered.
Non-blocking kick wired into POST /api/sessions with all fork-only checks preserved and managed-template-trial omitted.
chat-sandbox-runtime.ts rewritten to wait-then-bare-reconnect while preserving ensureManagedRuntimeEnvironment, WorkspaceStartupReporter, emitSessionEvent, sendStart ordering, assistantId/chatId/workflowRunId params, and runtimeMode/managedRuntime returns.
kickSandboxLifecycleWorkflow confirmed not double-firing.
All six behavior tests (BT-001 through BT-006) RED-first then GREEN; managed-runtime suites stay green.
git diff --check clean and bun --bun run ci passes.
Managed Runtime Proof Standard evidence bundle captured (Level 1 minimum) with limitations surfaced.
PR opened against dennisonbertram/fork-open-agents (never vercel-labs/open-agents) with a summary of what changed for users and what was verified.
Three TDD audit-trail commits in order (red → green → regression) visible in git log.
Assumptions and alternatives
Assumptions
Upstream commit reachable:ed2aa3e7 is reachable via the upstream remote fetch in this fork's git history — verified by git log --all --oneline | grep ed2aa3e7. Full diff can be inspected at any time with git show ed2aa3e7.
Migration 0048 is next free: Confirmed by listing apps/web/lib/db/migrations/ — latest is 0047_cheerful_thunderbolt_ross.sql. db:generate will produce 0048.
packages/sandbox needs no changes: POC-proven — connectSandbox in packages/sandbox/factory.ts already supports both the full-options provision form and the bare reconnect form.
Atomic claim pattern already production-proven:claimSessionLifecycleRunId (line 323 of sessions.ts) uses the identical IS NULL conditional-update pattern. Adding a second lease column mirrors a proven primitive.
SDK v4 throughout: This port runs on workflow@^4.2.0-beta.72. The "use workflow" / "use step" directives in sandbox-provisioning.ts are confirmed from the upstream source at ed2aa3e7.
No live VM boot in CI: Provisioning requires live Vercel sandbox credentials. Level 1 deterministic tests are the gate; the live connectSandbox path is the same production already exercises.
Alternatives considered
Eager provisioning (this issue) vs lazy-with-warm-pool: Upstream chose eager kick-at-session-create. An alternative would be a warm pool of pre-provisioned sandboxes. Upstream's approach is simpler (no pool management), fits the existing lifecycle model, and is the keystone for dependent ports. Warm-pool is out of scope.
Clean cherry-pick vs adapt-port: A clean git cherry-pick ed2aa3e7 would silently delete the managed-runtime engine. The adapt-port approach (apply intent, preserve fork constructs) is the only safe path. This was the decision made during the POC phase.
Migration: db:generate vs hand-copy: Hand-copying upstream's 0036_faulty_yellow_claw.sql with a renumbered filename would corrupt the Drizzle journal (snapshot IDs would mismatch). db:generate is the only correct path.
Provisioning in route handler vs durable workflow: Upstream uses a durable workflow (sandboxProvisioningWorkflow) with a lease claim/clear, not an inline async fire-and-forget. This guards against double-provisioning and gives observability. The inline approach would be simpler but unsafe under concurrent kicks.
Grooming clarifications (2026-05-31)
Groomer: issue-groomer agent, 2026-05-31. Provenance: Body enriched by cross-referencing the following read-only sources:
gh issue view 115 — original issue body (already detailed; this enrichment adds upstream diff verification, confirmed file states, and reconciliation specifics).
git show ed2aa3e7 -- apps/web/app/api/sessions/route.ts — exact 7-line kick diff confirmed; no managed-template-trial in ed2aa3e7.
git show ed2aa3e7 -- apps/web/lib/db/sessions.ts — all 4 new exports confirmed (type + 3 helpers) with exact Drizzle predicate structure.
git show ed2aa3e7 -- apps/web/app/workflows/sandbox-provisioning.ts — full 60-line workflow source confirmed; v4 "use workflow"/"use step" directives confirmed.
git show ed2aa3e7 -- apps/web/app/api/sessions/route.test.ts — provisioningKickCalls mock shape confirmed; managed-template-trial test divergence noted.
apps/web/lib/db/schema.ts grep — sandboxProvisioningRunId NOT present; lifecycleRunId at line 278, lifecycleError at line 279; confirmed insertion point.
apps/web/app/api/sessions/route.ts grep — kickSandboxProvisioningWorkflow NOT imported; no provisioning kick; file is 428 lines.
apps/web/lib/db/sessions.ts grep — claimSessionLifecycleRunId exists at line 323; updateSessionIfNotArchived, claimSessionSandboxProvisioningRunId, clearSessionSandboxProvisioningRunIdIfOwned, SessionRecord type export — all ABSENT.
Epic: #119 — Upstream ports. This issue is the KEYSTONE — #116 (lazy sandbox actions UI, soft-dep), #118 (parallelize chat startup, hard-dep) build on top of it. #117 (workflow v5 bump) is independent.
SDK note: This slice uses
workflow@^4.2.0-beta.72(v4). Port #117 (#883) targets v5 separately and is NOT a dependency here.Why this matters
Today a session's sandbox is provisioned lazily on the first chat turn inside
resolveChatSandboxRuntime. The user creates a session, lands on the chat page, types a message, and only then waits 10-30s+ forconnectSandboxto boot the VM, clone the repo, mint/revoke a GitHub installation token, and install global skills — all blocking the first response. Upstream #880 moves provisioning to session-create time so the sandbox is warming (or already warm) by the time the user sends their first message. The chat path then becomes "wait for the in-flight provisioning run, then bare-reconnect," which is far cheaper and removes the cold-start tax from the first turn.For our fork specifically, this also untangles the 857-line
chat-sandbox-runtime.tsthat mixes provisioning, managed-runtime profile execution, startup-log reporting, and observability into one function. Extracting provisioning intolib/sandbox/provisioning.tsis a prerequisite for #881 (soft-dep) and #884 (hard-dep).User/operator path protected
Operator/user path: Create a session (
POST /api/sessions) → land on the session chat page → send the first message → receive a streamed response from an agent running in a ready sandbox.Concretely this slice makes that path faster and safer:
Behavior contract
Scenario BT-001: provisioning starts on session create
POST /api/sessionsand creation succeedskickSandboxProvisioningWorkflow(result.session.id)is invoked with the new session id200with the created session/chat payload.Scenario BT-002: a failing kick never blocks session creation
kickSandboxProvisioningWorkflowthrows/rejectsPOST /api/sessionsstill returns200with the created session.Scenario BT-003: provisioning run claim is idempotent (first-claim-wins)
sandboxProvisioningRunId IS NULLclaimSessionSandboxProvisioningRunIdsucceeds and the duplicate run is a no-op (run-replaced/ cancelled).Scenario BT-004: archived mid-provisioning stops the sandbox and surfaces a typed error
updateSessionIfNotArchivedcommitsSessionArchivedDuringProvisioningErroris thrownsandboxState.Scenario BT-005: chat waits for in-flight provisioning instead of cold-starting
resolveChatSandboxRuntimeruns for the first chat turnwaitForSandboxProvisioningRun), re-reads the session, and bare-reconnects viaconnectSandbox(sandboxState)— it does not run the full provision ({ state, options }) form itself.Scenario BT-006: managed-runtime sessions still execute their profile
runtimeMode === "managed_runtime"and amanagedRuntimeProfileIdensureManagedRuntimeEnvironmentstill runs,managed_runtime.profile.*events still fire, and the returnedResolvedChatSandboxRuntimestill includesruntimeModeand themanagedRuntimeblock.Product and design spec
apps/web/app/api/sessions/route.ts(POST); first chat turn via the chat workflow that callsresolveChatSandboxRuntimeinapps/web/app/workflows/chat-sandbox-runtime.ts.kickSandboxProvisioningWorkflow→ durablesandboxProvisioningWorkflowclaims the run lease and callsprovisionSessionSandbox→ on success persistssandboxState+ active lifecycle, clears the provisioning lease, kicks the lifecycle workflow → first chat turn awaits the run, re-reads, bare-reconnects.setting-upworkspace status is sent when the sandbox is not yet active on first chat turn (existingsendWorkspaceStatuscopy: "Setting up the workspace...").lifecycleStatetransitions: created withprovisioning(already our default at create) →activeon success →failedwithlifecycleErroron provisioning error.sandboxProvisioningRunIdis set on claim and cleared on completion/archive.WorkspaceStartupReporterstartup-log messages are preserved verbatim.provisionSessionSandboxre-checkssession.userId(andrepoOwner/repoNamepresence) and re-verifies repo access before minting a scoped installation token.lifecycleState: "failed"+lifecycleError; the chat path surfaceslifecycleError ?? "Workspace setup failed". Archived-during-provisioning is a typed no-throw-to-user outcome (session-archivedskip) inside the workflow.Integration spec (REAL paths + reconciliation)
Upstream source
Upstream commit
ed2aa3e7(PR #880, "Start sandbox provisioning on session create", +2202/-187 across 11 files). Inspect withgit show ed2aa3e7 --stat. The upstream diff is reachable in the fork's git history via the upstream remote fetch.This is an ADAPT port, not a blind cherry-pick. Our
chat-sandbox-runtime.ts(857 lines) has diverged massively from upstream's post-#880 version (121 lines): we carryensureManagedRuntimeEnvironment,WorkspaceStartupReporter,emitSessionEvent,sendStart, and managed-runtime observability that upstream never had. Applyinggit cherry-pick ed2aa3e7would silently delete our managed-runtime engine. Do not do it.Routes
apps/web/app/api/sessions/route.ts(currently 428 lines, no provisioning kick):import { kickSandboxProvisioningWorkflow } from "@/lib/sandbox/provisioning-kick";return Response.json(result):managed-template-trialgating — that module does not exist in our fork and was not part ofed2aa3e7. Upstream's test suite (route.test.ts) has a"blocks additional sessions for managed template trial users"test using a module our fork does not carry; the fork's test file must handle that divergence.checkBotProtection,checkRateLimit,isComposioProfileAllowedForRepository, andisManagedRuntimeProfileIdvalidation.Schema / migration
apps/web/lib/db/schema.ts(sessions table):sandboxProvisioningRunId: text("sandbox_provisioning_run_id")positioned betweenlifecycleRunId(line 278) andlifecycleError(line 279).lifecycleRunIdandlifecycleErrorbut NOsandboxProvisioningRunId— confirmed by inspection.Migration: After editing
schema.ts, run:This produces migration 0048 (latest in fork is
0047_cheerful_thunderbolt_ross.sql— confirmed). Do NOT hand-copy upstream's0036_faulty_yellow_claw.sql— the fork's migration journal has diverged (our 0047 vs upstream's 0036 at the time of #880). Regenerate.DB helpers (new exports in
apps/web/lib/db/sessions.ts, currently 842 lines)Confirmed by inspecting
ed2aa3e7: none of these exist today.SessionRecordtype:export type SessionRecord = NonNullable<Awaited<ReturnType<typeof getSessionById>>>;— place aftergetSessionById.updateSessionIfNotArchived(sessionId, data)— conditional UPDATE wherestatus != 'archived'AND(lifecycleState IS NULL OR lifecycleState != 'archived'). The exact Drizzle form is confirmed from the upstream diff; usesand,eq,ne,or,isNullwhich are already imported.claimSessionSandboxProvisioningRunId(sessionId, runId)— atomicUPDATE sessions SET sandbox_provisioning_run_id = runId WHERE id = sessionId AND sandbox_provisioning_run_id IS NULL RETURNING id. ReturnsBoolean(updated). Mirrors the existingclaimSessionLifecycleRunId(line 323).clearSessionSandboxProvisioningRunIdIfOwned(sessionId, runId)— conditionalUPDATE ... SET sandbox_provisioning_run_id = NULL WHERE ... AND sandbox_provisioning_run_id = runId. Confirmed from upstream diff; this is theclearIfOwnedsemantics the POC proved safe.Workflow surfaces (new files)
apps/web/app/workflows/sandbox-provisioning.ts(new, ~60 lines, durable"use workflow"/"use step"):git show ed2aa3e7 -- apps/web/app/workflows/sandbox-provisioning.ts.sandboxProvisioningWorkflow(sessionId)readsgetWorkflowMetadata().workflowRunId, claims/validates the run lease, callsprovisionSessionSandbox, clears the lease.session-not-found,run-replaced,session-archivedas skip returns; re-throws other errors after settinglifecycleState: "failed".sandboxLifecycleWorkflowis registered).Server modules (new files)
apps/web/lib/sandbox/provisioning.ts(~296 lines,"server-only"):provisionSessionSandbox({ sessionId, userId? }),ProvisionSessionSandboxResult, andSessionArchivedDuringProvisioningError.connectSandbox({ state, options })provision form (POC confirmed this form is already supported by our fork'spackages/sandbox/factory.ts— zero changes needed topackages/sandbox).updateSessionIfNotArchived, stops the sandbox on archive race, installs global skills, kicks the lifecycle workflow.getGitUsersignature inchat-sandbox-runtime.tstakes auserId; upstreamprovisioning.tstakes aUserRecord(loads aUserRecordviagetUserByIdfirst). Port must use the fork's actual signature or adapt accordingly.apps/web/lib/sandbox/provisioning-kick.ts(~113 lines,"server-only"):kickSandboxProvisioningWorkflow(sessionId)— start/dedupe/claim with live-run detection viagetRun.waitForSandboxProvisioningRun(runId)— awaited by the chat path.CRUX —
apps/web/app/workflows/chat-sandbox-runtime.ts(ours 857 lines vs upstream's slimmed 121)Apply intent, not blind-copy. Upstream's version deletes the managed-runtime engine entirely. Our fork MUST NOT replicate that deletion.
Changes to make:
getReadySessionSandbox({ sessionId, userId })— kick →waitForSandboxProvisioningRun→ re-read → assert active.connectSandbox(sandboxState)form (no options).connectSandbox({ state, options })provision logic from the chat path (it moves intoprovisioning.ts).Changes NOT to make:
ensureManagedRuntimeEnvironmentand its call site / ordering.WorkspaceStartupReporterand all startup-log messages verbatim.emitSessionEventobservability instrumentation.sendStart(params.assistantId)and its current position in the execution order (do NOT pre-empt the fix: #879 honor guardrails embedded in the run's definitionSnapshot #884 responsibility shift).assistantId/chatId/workflowRunIdparams onresolveChatSandboxRuntime.runtimeModeandmanagedRuntimefields onResolvedChatSandboxRuntimereturn type.packages/sandbox— No changePOC confirmed:
packages/sandbox/factory.tsalready supports both the full-options provision form and the bare reconnect form. Zero changes topackages/sandboxare needed.Double-fire check
provisionSessionSandboxcallskickSandboxLifecycleWorkflow({ reason: "sandbox-created" }). Today'sresolveChatSandboxRuntimealso kicks it after the inline connect. After this port, the chat path must NOT re-firekickSandboxLifecycleWorkflowfor the same provisioning — that responsibility moves intoprovisioning.ts. Verify and guard explicitly.Compatibility
Migration is a nullable text column add — backward compatible, zero-downtime. Existing sessions with
sandboxProvisioningRunId IS NULLbehave correctly; the lazy chat-path kick still works as fallback.In scope
sandbox_provisioning_run_id+ generated migration 0048 (NOT hand-copied from upstream 0036).sessions.tsexports:SessionRecordtype + 3 atomic helpers.provisioning.ts,provisioning-kick.ts,sandbox-provisioning.ts(+ workflow registration).POST /api/sessions.chat-sandbox-runtime.ts(getReadySessionSandbox+ bare reconnect), preserving the managed-runtime engine, startup reporter, observability, andsendStartordering.Out of scope
sendStartresponsibility shift relies on provisioning being extracted here.managed-template-trialgating (module absent in fork; never part ofed2aa3e7).packages/sandboxchanges (none required, POC-proven).This is part 1 of 4 deferred upstream ports. Dependency order: #880 (this) → #881/#116 (soft-dep) → #883/#117 (independent) → #884/#118 (hard-dep on #880 AND #883).
Research and context sources
ed2aa3e7(PR fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880, "Start sandbox provisioning on session create") — inspected directly viagit show ed2aa3e7. 11 files, +2202/-187; key additions arelib/sandbox/provisioning.ts(+296),lib/sandbox/provisioning-kick.ts(+113),app/workflows/sandbox-provisioning.ts(+60),lib/db/sessions.ts(+66 — confirmed 4 new exports), and slimming ofapp/workflows/chat-sandbox-runtime.ts(-187 net to 121 lines). Schema addssandboxProvisioningRunId: text("sandbox_provisioning_run_id").POC/upstream-880-provisioning/FINDINGS.md— POC verdict PASS (low-risk). Proved: (a) dualconnectSandboxform already inpackages/sandbox/factory.ts→ zero packages/sandbox changes; (b) atomic claim/clear semantics (first-claim-wins under 2-way and 50-way races;clearIfOwnedonly clears the owning run). Limitation: contract proof only, no live sandbox booted.docs/plans/active-plan.md— the fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 digest (Phase 1 findings + Phase 2 POC results), dependency order, and the CRUX note thatchat-sandbox-runtime.tsmust preserve the managed-runtime engine.docs/process/feature-ticket-format.md— required issue shape.docs/process/managed-runtime-proof-standard.md— proof standard this issue must satisfy because it touches the managed-runtime path.Agent todo checklist
git show ed2aa3e7,POC/upstream-880-provisioning/FINDINGS.md, and the CRUX note indocs/plans/active-plan.md.sandboxProvisioningRunId: text("sandbox_provisioning_run_id")to thesessionstable inapps/web/lib/db/schema.tsbetweenlifecycleRunIdandlifecycleError.bun run --cwd apps/web db:generate; confirm it produces migration 0048 (do not hand-copy upstream 0036). Commit the generated.sql+ meta.apps/web/lib/db/sessions.ts:SessionRecordtype export,updateSessionIfNotArchived,claimSessionSandboxProvisioningRunId,clearSessionSandboxProvisioningRunIdIfOwned(mirrorclaimSessionLifecycleRunId).apps/web/lib/sandbox/provisioning.ts(provisionSessionSandbox,ProvisionSessionSandboxResult,SessionArchivedDuringProvisioningError) — full-options connect, token mint/revoke,updateSessionIfNotArchived, stop-on-archive-race, global skills, lifecycle kick. ReconcilegetGitUsersignature (fork takesuserId; upstream takesUserRecord).apps/web/lib/sandbox/provisioning-kick.ts(kickSandboxProvisioningWorkflow,waitForSandboxProvisioningRun) with live-run dedupe.apps/web/app/workflows/sandbox-provisioning.tsdurable workflow (port from confirmed upstream source ated2aa3e7) and register it with the workflow runtime.return Response.json(result)inapps/web/app/api/sessions/route.ts; preserve botid/rate-limit/composio/profile-validation; OMIT managed-template-trial.apps/web/app/workflows/chat-sandbox-runtime.ts: addgetReadySessionSandbox, switch to bareconnectSandbox(sandboxState), REMOVE the inline provision logic now living inprovisioning.ts, PRESERVEensureManagedRuntimeEnvironment/WorkspaceStartupReporter/emitSessionEvent/sendStartordering /assistantId/chatId/workflowRunIdparams /runtimeMode+managedRuntimereturn fields.kickSandboxLifecycleWorkflowdoes not double-fire (provisioning owns it; chat path must not re-fire for the same provisioning).bun test apps/web/app/api/sessions/route.test.tsand the adjacent sandbox/workflow suites.git diff --checkandbun --bun run ci.Tests to add first (red-first)
Each test must be observed RED before implementation, with the named expected-failure reason.
BT-001 — Kick fires on create —
apps/web/app/api/sessions/route.test.ts: mock@/lib/sandbox/provisioning-kick; assert a successfulPOST /api/sessionscallskickSandboxProvisioningWorkflowwithString(body.session.id).provisioningKickCallsis empty / module not imported (kick not wired yet).ed2aa3e7adds this exact test toroute.test.tswithprovisioningKickCallstracking — use the same mock shape but adapt for fork (omit managed-template-trial mock).BT-002 — Kick failure does not block create — same file: make the mocked kick reject; assert the response is still
200and the session payload is returned..catchpropagates and the request does not return 200.BT-003 — Claim idempotency (first-claim-wins) — a
sessions.ts/provisioning-level test: two concurrentclaimSessionSandboxProvisioningRunId(sessionId, runId)calls; assert exactly one returnstrue.claimSessionSandboxProvisioningRunIddoes not exist (import error / undefined).BT-004 — Archived mid-provisioning — a
provisioning.tstest: simulate the session becoming archived soupdateSessionIfNotArchivedreturns no row; assertsandbox.stop()is called andSessionArchivedDuringProvisioningErroris thrown.provisionSessionSandbox/SessionArchivedDuringProvisioningErrordo not exist.BT-005 — Chat awaits provisioning — a
chat-sandbox-runtime.tstest: with the sandbox not yet active, assertresolveChatSandboxRuntimecallswaitForSandboxProvisioningRunand then bare-reconnects (connectSandbox(sandboxState)with no options), rather than running the full-options provision form.connectSandbox({ state, options })and never callswaitForSandboxProvisioningRun.BT-006 — Managed-runtime fields preserved — a
chat-sandbox-runtime.tstest: assertresolveChatSandboxRuntimereturnsruntimeModeandmanagedRuntimefields populated for managed-runtime sessions.Observability and user feedback
This issue touches managed runtime, so it must satisfy the Managed Runtime Proof Standard with real evidence (not a label).
setting-upworkspace status ("Setting up the workspace...") on the first chat turn when the sandbox is not yet active;lifecycleStateprovisioning → active(orfailedwithlifecycleError) visible on the session row.sandboxProvisioningWorkflowrun logs (claim outcome, skip reasons:session-not-found,run-replaced,session-archived).provisioning-kickdedupe outcomes (started/existing/active/skipped) withrunId.emitSessionEvent/managed_runtime.profile.*events fromensureManagedRuntimeEnvironmentmust continue to fire for managed-runtime sessions (setup, probe, verification, finish).SessionArchivedDuringProvisioningError(name === "SessionArchivedDuringProvisioningError"); provisioning failures recorded aslifecycleState: "failed"+lifecycleErrormessage.sessionId,chatId,workflowRunId,sandboxName,profileId(andmanagedRuntimeProfileRunIdfor managed runs),runIdfor the provisioning run.setupToken.tokenis minted then revoked); never log rawsandboxStatesecrets, provider tokens, env values, or prompt/session content.grep '"sessionId":"<id>"' logs | grep sandbox-provisioning— trace a provisioning run.grep '"workflowRunId":"<runId>"' logs | grep managed_runtime.profile— confirm the managed profile still executed for the chat turn.select id, lifecycle_state, sandbox_provisioning_run_id, lifecycle_error from sessions where id = '<id>'— confirm the lease was set then cleared and lifecycle reachedactive.Proof Level
connectSandboxpath is the same one production already exercises (POC contract-proved), so Level 1 + the existing managed-runtime suites bound the residual risk. Record the no-live-boot limitation explicitly.Required Evidence
<id>@<version>").Regression harness plan
apps/web/app/api/sessions/route.test.ts): kick-fires-on-create and kick-failure-still-200. Fail-before: assertions fail without the wired kick /.catch. Pass-after: both pass. This is the durable signal for the create-path protected behavior. Upstream addsprovisioningKickCallstracking withmock.module("@/lib/sandbox/provisioning-kick", ...)— use the same mock shape in the fork's test file.clearIfOwned-only-by-owner. Fail-before: helpers missing. Pass-after: race resolves to one winner. Mirrors the provenclaimSessionLifecycleRunIdcoverage style (line 323 ofsessions.ts).runtimeMode/managedRuntimeremain populated.ensureManagedRuntimeEnvironment/managed_runtime.profile.*).TDD audit trail
All three commits must appear in git history in order:
test(red): TASK-115 failing tests for...): failing route + claim + archive + chat-wait tests committed test-only on the work branch. Record the RED command output and reason in the commit message.feat: TASK-115 implement...): smallest implementation (schema/migration + helpers + 3 new files + kick wiring + chat-path rewrite) that turns the targeted tests green.test(regression): TASK-115 regression coverage for...): regression tests that would fail if the green commit were reverted.Exception: none anticipated. If any behavior cannot be covered by a deterministic test, record the exception and the manual proof in the PR.
Regression risks and concerns
chat-sandbox-runtime.tswould deleteensureManagedRuntimeEnvironment, the startup reporter, and observability. Mitigation: preserve-and-extract, guarded by the managed-runtime suites and the chat-runtime test.kickSandboxLifecycleWorkflowdouble-fire: provisioning now owns the lifecycle kick; the chat path must not re-fire it for the same provisioning. Mitigation: explicit checklist verification + review.sendStartordering / start-chunk:sendStart(assistantId)must keep its current position and not be duplicated; feat: port upstream #884 — parallelize chat workflow startup #118 later shifts this responsibility, so do not pre-empt it here.updateSessionIfNotArchivedis not used (or the stop path is skipped), an archived session could keep stalesandboxStateor orphan a VM. Mitigation: the archive-race test (BT-004).0036would corrupt our journal. Mitigation: regenerate as 0048; checklist enforces it.sandboxProvisioningWorkflowmeans the kick starts an unknown workflow. Mitigation: registration checklist item + the kick test exercisingstart.getGitUsersignature divergence: upstream'sprovisioning.tsgetGitUsertakes aUserRecord; our runtime's took auserId. Reconcile during extraction (the newprovisioning.tsloads aUserRecordviagetUserById).managed-template-trialomission: upstreamroute.test.tstests a module our fork doesn't carry. The fork's route test must not import or mock that module.Deploy or migration impact
textcolumn add tosessions— zero-downtime, backward compatible, no backfill. Runs automatically duringbun run buildon every Vercel deploy (preview gets its own Neon branch via Neon branching; prod uses main).sandboxProvisioningWorkflowmust be deployed/registered for kicks to resolve.Definition of done
sessions.tsexports added:SessionRecordtype,updateSessionIfNotArchived,claimSessionSandboxProvisioningRunId,clearSessionSandboxProvisioningRunIdIfOwned.provisioning.ts,provisioning-kick.ts, andsandbox-provisioning.tsadded; workflow registered.POST /api/sessionswith all fork-only checks preserved and managed-template-trial omitted.chat-sandbox-runtime.tsrewritten to wait-then-bare-reconnect while preservingensureManagedRuntimeEnvironment,WorkspaceStartupReporter,emitSessionEvent,sendStartordering,assistantId/chatId/workflowRunIdparams, andruntimeMode/managedRuntimereturns.kickSandboxLifecycleWorkflowconfirmed not double-firing.git diff --checkclean andbun --bun run cipasses.dennisonbertram/fork-open-agents(neververcel-labs/open-agents) with a summary of what changed for users and what was verified.git log.Assumptions and alternatives
Assumptions
ed2aa3e7is reachable via the upstream remote fetch in this fork's git history — verified bygit log --all --oneline | grep ed2aa3e7. Full diff can be inspected at any time withgit show ed2aa3e7.apps/web/lib/db/migrations/— latest is0047_cheerful_thunderbolt_ross.sql.db:generatewill produce 0048.packages/sandboxneeds no changes: POC-proven —connectSandboxinpackages/sandbox/factory.tsalready supports both the full-options provision form and the bare reconnect form.claimSessionLifecycleRunId(line 323 ofsessions.ts) uses the identicalIS NULLconditional-update pattern. Adding a second lease column mirrors a proven primitive.workflow@^4.2.0-beta.72. The"use workflow"/"use step"directives insandbox-provisioning.tsare confirmed from the upstream source ated2aa3e7.connectSandboxpath is the same production already exercises.Alternatives considered
git cherry-pick ed2aa3e7would silently delete the managed-runtime engine. The adapt-port approach (apply intent, preserve fork constructs) is the only safe path. This was the decision made during the POC phase.0036_faulty_yellow_claw.sqlwith a renumbered filename would corrupt the Drizzle journal (snapshot IDs would mismatch).db:generateis the only correct path.sandboxProvisioningWorkflow) with a lease claim/clear, not an inline async fire-and-forget. This guards against double-provisioning and gives observability. The inline approach would be simpler but unsafe under concurrent kicks.Grooming clarifications (2026-05-31)
Groomer: issue-groomer agent, 2026-05-31.
Provenance: Body enriched by cross-referencing the following read-only sources:
gh issue view 115— original issue body (already detailed; this enrichment adds upstream diff verification, confirmed file states, and reconciliation specifics).git show ed2aa3e7 --stat— upstream commit reachable; 11 files, +2202/-187 confirmed.git show ed2aa3e7 -- apps/web/app/api/sessions/route.ts— exact 7-line kick diff confirmed; nomanaged-template-trialined2aa3e7.git show ed2aa3e7 -- apps/web/lib/db/sessions.ts— all 4 new exports confirmed (type + 3 helpers) with exact Drizzle predicate structure.git show ed2aa3e7 -- apps/web/app/workflows/sandbox-provisioning.ts— full 60-line workflow source confirmed; v4"use workflow"/"use step"directives confirmed.git show ed2aa3e7 -- apps/web/app/api/sessions/route.test.ts—provisioningKickCallsmock shape confirmed;managed-template-trialtest divergence noted.apps/web/lib/db/schema.tsgrep —sandboxProvisioningRunIdNOT present;lifecycleRunIdat line 278,lifecycleErrorat line 279; confirmed insertion point.apps/web/app/api/sessions/route.tsgrep —kickSandboxProvisioningWorkflowNOT imported; no provisioning kick; file is 428 lines.apps/web/lib/db/sessions.tsgrep —claimSessionLifecycleRunIdexists at line 323;updateSessionIfNotArchived,claimSessionSandboxProvisioningRunId,clearSessionSandboxProvisioningRunIdIfOwned,SessionRecordtype export — all ABSENT.apps/web/app/workflows/chat-sandbox-runtime.tsgrep —connectSandbox(full-options form, line 736),ensureManagedRuntimeEnvironment(line 247),sendStart(line 201),resolveChatSandboxRuntime(line 654) — all present;getReadySessionSandbox,waitForSandboxProvisioningRun— ABSENT (not yet ported).apps/web/lib/db/migrations/listing — latest is0047_cheerful_thunderbolt_ross.sql; next will be 0048.POC/upstream-880-provisioning/FINDINGS.md— verdict PASS (low-risk); dual-form and atomic claim both proven.docs/plans/active-plan.md— dependency order and CRUX note confirmed.docs/context/command-intent.md— canonical fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 description and reconciliation notes confirmed.Changes from original body: No behavioral changes to requirements. Enrichments added:
git show ed2aa3e7(not just the commit stat).updateSessionIfNotArchivedandclearSessionSandboxProvisioningRunIdIfOwnedconfirmed from upstream diff.managed-template-trialomission noted as confirmed — not ined2aa3e7, not in fork.getGitUsersignature divergence elevated in the reconciliation spec (it is a concrete friction point, not an open question).