feat(ai-sandbox): SandboxInstanceStore instance durability (BYO) - #988
Conversation
📝 WalkthroughWalkthroughAdds durable sandbox instance resume through a new store capability, middleware integration, conformance testkit, documentation, package exports, and tests for create-once and resume-on-subsequent-request behavior. ChangesSandbox instance durability
Estimated code review effort: 4 (Complex) | ~45 minutes Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Playwright
participant SandboxDurabilityRoute
participant Chat
participant SandboxProvider
participant SandboxInstanceStore
Playwright->>SandboxDurabilityRoute: POST threadId and runId
SandboxDurabilityRoute->>Chat: start chat stream
Chat->>SandboxProvider: create or resume sandbox
Chat->>SandboxInstanceStore: upsert instance record
SandboxDurabilityRoute->>SandboxInstanceStore: read stored record
SandboxDurabilityRoute-->>Playwright: return durability result
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🚀 Changeset Version Preview19 package(s) bumped directly, 32 bumped as dependents. 🟥 Major bumps
🟨 Minor bumps
🟩 Patch bumps
|
|
View your CI Pipeline Execution ↗ for commit 2d8e35f
☁️ Nx Cloud last updated this comment at |
4b2e68a to
e1e51d0
Compare
@tanstack/ai
@tanstack/ai-acp
@tanstack/ai-angular
@tanstack/ai-anthropic
@tanstack/ai-bedrock
@tanstack/ai-claude-code
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-codex
@tanstack/ai-devtools-core
@tanstack/ai-durable-stream
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-grok-build
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-mcp
@tanstack/ai-memory
@tanstack/ai-mistral
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-opencode
@tanstack/ai-openrouter
@tanstack/ai-persistence
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-sandbox
@tanstack/ai-sandbox-cloudflare
@tanstack/ai-sandbox-daytona
@tanstack/ai-sandbox-docker
@tanstack/ai-sandbox-local-process
@tanstack/ai-sandbox-sprites
@tanstack/ai-sandbox-vercel
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-utils
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
e1e51d0 to
ef3e5f5
Compare
0b17612 to
d2d08e8
Compare
ef3e5f5 to
f4c053c
Compare
|
Rebased onto latest Adapted to the post-schema-first core design rather than a raw cherry-pick of the pre-refactor commit:
Local green: typecheck + |
4a1adb2 to
860f284
Compare
Drop the unlisted @tanstack/ai-persistence import from sandbox unit tests (use shared capability tokens instead) and make sandbox persistence docs snippets type-check under kiira.
0a7be30 to
d040b16
Compare
e6716bc to
e87d8e1
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
packages/ai-sandbox/tests/store.test.ts (1)
2-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSplit and colocate these unit tests with their implementations.
Move the sandbox-store tests beside
packages/ai-sandbox/src/instance-store.tsand the lock tests besidepackages/ai/src/activities/chat/middleware/locks.ts; this file currently covers sources in two separate locations.As per coding guidelines, “Place unit tests in
*.test.tsfiles alongside the source they cover.”Also applies to: 22-65
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-sandbox/tests/store.test.ts` around lines 2 - 7, Split the tests in store.test.ts by implementation: move the InMemorySandboxInstanceStore tests beside instance-store.ts, and move the InMemoryLockStore tests beside locks.ts under the chat middleware implementation. Preserve each test’s existing behavior while ensuring both suites are colocated in *.test.ts files next to their covered source.Source: Coding guidelines
testing/e2e/src/routes/api.sandbox-durability.ts (1)
138-171: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider a concurrent-request test for the lock contention path.
This route wires
withLocksinto the middleware chain, but the only assertions in the accompanying spec exercise strictly sequential requests. A test issuing two concurrent POSTs with the samethreadIdwould validate the actual purpose of the lock (preventing duplicate creation under a race), which is central to this PR's "multi-instance-correct" durability claim.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@testing/e2e/src/routes/api.sandbox-durability.ts` around lines 138 - 171, Add a concurrent-request test for the /api/sandbox-durability route using the same threadId, starting both POST requests before awaiting either. Assert that lock contention prevents duplicate sandbox creation and that the resulting create/resume counts and provider sandbox identity reflect a single shared instance, exercising the withLocks middleware path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/ai-sandbox/package.json`:
- Line 62: Update the `@tanstack/ai` dependency entry in package.json from the
workspace:^ protocol to workspace:* while preserving the existing internal
package reference.
In `@packages/ai-sandbox/skills/ai-sandbox/SKILL.md`:
- Around line 222-237: Update the multi-instance example around chat to import
withLocks and LockStore from `@tanstack/ai/locks` instead of the main `@tanstack/ai`
barrel, and replace InMemoryLockStore with the production BYO distributed
LockStore instance. Preserve the existing withSandboxInstanceStore, withLocks,
and withSandbox middleware order.
In `@packages/ai-sandbox/src/instance-store.ts`:
- Around line 80-83: Update InMemoryStore.upsert to normalize omitted optional
fields before storing records, removing undefined-valued properties such as
latestSnapshotId while preserving defined values. Ensure the in-memory fallback
matches the store contract and durable adapters without changing the upsert
return behavior.
In `@packages/ai-sandbox/src/testkit/conformance.ts`:
- Around line 68-77: Update the overwrite fixture in the conformance test around
store.upsert and store.get to seed both latestSnapshotId and latestRunId on the
initial record. After replacing the record, assert that loaded contains neither
optional field, preserving the existing latestSnapshotId assertion and adding
equivalent coverage for latestRunId.
---
Nitpick comments:
In `@packages/ai-sandbox/tests/store.test.ts`:
- Around line 2-7: Split the tests in store.test.ts by implementation: move the
InMemorySandboxInstanceStore tests beside instance-store.ts, and move the
InMemoryLockStore tests beside locks.ts under the chat middleware
implementation. Preserve each test’s existing behavior while ensuring both
suites are colocated in *.test.ts files next to their covered source.
In `@testing/e2e/src/routes/api.sandbox-durability.ts`:
- Around line 138-171: Add a concurrent-request test for the
/api/sandbox-durability route using the same threadId, starting both POST
requests before awaiting either. Assert that lock contention prevents duplicate
sandbox creation and that the resulting create/resume counts and provider
sandbox identity reflect a single shared instance, exercising the withLocks
middleware path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 558feb63-3ea1-478b-a2f2-50ab698d93a2
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (21)
.changeset/sandbox-durability.mddocs/config.jsondocs/sandbox/durability.mddocs/sandbox/overview.mdpackages/ai-sandbox/package.jsonpackages/ai-sandbox/skills/ai-sandbox/SKILL.mdpackages/ai-sandbox/src/capabilities.tspackages/ai-sandbox/src/index.tspackages/ai-sandbox/src/instance-store.tspackages/ai-sandbox/src/middleware.tspackages/ai-sandbox/src/sandbox.tspackages/ai-sandbox/src/store.tspackages/ai-sandbox/src/testkit/conformance.tspackages/ai-sandbox/src/workspace.tspackages/ai-sandbox/tests/ensure.test.tspackages/ai-sandbox/tests/store.test.tspackages/ai-sandbox/vite.config.tstesting/e2e/package.jsontesting/e2e/src/routeTree.gen.tstesting/e2e/src/routes/api.sandbox-durability.tstesting/e2e/tests/sandbox-durability.spec.ts
💤 Files with no reviewable changes (1)
- packages/ai-sandbox/src/store.ts
| "peerDependencies": { | ||
| "@ngrok/ngrok": "^1.0.0", | ||
| "@tanstack/ai": "workspace:^" | ||
| "@tanstack/ai": "workspace:^", |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use workspace:* for the internal peer dependency.
@tanstack/ai uses workspace:^, which does not follow the required internal dependency protocol.
Proposed fix
- "`@tanstack/ai`": "workspace:^",
+ "`@tanstack/ai`": "workspace:*",As per coding guidelines, “Use the workspace:* protocol for internal package dependencies in package.json.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "@tanstack/ai": "workspace:^", | |
| "`@tanstack/ai`": "workspace:*", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-sandbox/package.json` at line 62, Update the `@tanstack/ai`
dependency entry in package.json from the workspace:^ protocol to workspace:*
while preserving the existing internal package reference.
Source: Coding guidelines
| import { chat, InMemoryLockStore, withLocks } from '@tanstack/ai' | ||
| import { | ||
| InMemorySandboxInstanceStore, | ||
| withSandbox, | ||
| withSandboxInstanceStore, | ||
| } from '@tanstack/ai-sandbox' | ||
| // Production: your BYO store — docs/sandbox/durability.md | ||
| import { instanceStore } from './sandbox-instance-store' | ||
|
|
||
| chat({ | ||
| adapter, | ||
| messages, | ||
| middleware: [ | ||
| withSandboxInstanceStore(instanceStore), | ||
| withLocks(new InMemoryLockStore()), // multi-instance: distributed lock | ||
| withSandbox(sandbox), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== tracked files matching SKILL/durability/changeset =="
git ls-files | rg '(^|/)SKILL\.md$|durability|sandbox-instance-store|sandbox-lock-store|sandbox-lock' || true
echo
echo "== target lines in SKILL.md =="
fd -a 'SKILL.md' . | sed 's#^\./##' | while read -r f; do
if [ "$(basename "$(dirname "$f")")" = "ai-sandbox" ] && grep -q "InMemoryLockStore" "$f"; then
echo "--- $f"
wc -l "$f"
sed -n '200,250p' "$f" | nl -ba -v200
fi
done
echo
echo "== lock import/usage references =="
rg -n "`@tanstack/ai/locks`|InMemoryLockStore|withLocks|distributed lock|multi-instance" -S . --glob '!node_modules' --glob '!dist' --glob '!build' || true
echo
echo "== candidate store files =="
git ls-files | rg 'sandbox.*(lock|instance).*store\.|lock.*store\.|instance.*store\.' || trueRepository: TanStack/ai
Length of output: 2680
🏁 Script executed:
#!/bin/bash
set -uuo pipefail
echo "== target lines in packages/ai-sandbox/skills/ai-sandbox/SKILL.md =="
sed -n '200,250p' packages/ai-sandbox/skills/ai-sandbox/SKILL.md | awk '{printf "%5d\t%s\n", NR+199, $0}'
echo
echo "== lock/store references in ai-sandbox skill, durability docs, changeset, and repo =="
rg -n "`@tanstack/ai`$|`@tanstack/ai/locks`|InMemoryLockStore|withLocks|distributed lock|multi-instance|sandbox-lock-store|lockStore" packages/ai-sandbox/skills/ai-sandbox/SKILL.md docs/sandbox/durability.md .changeset/sandbox-durability.md -S || true
echo
echo "== package exports / lock entry point =="
for f in packages/ai/package.json packages/ai/src/index.ts packages/ai/src/locks entry points...; do
if [ -e "$f" ]; then
echo "--- $f"
sed -n '1,220p' "$f" | awk '{printf "%5d\t%s\n", NR, $0}'
fi
doneRepository: TanStack/ai
Length of output: 15067
🏁 Script executed:
#!/bin/bash
set -uuo pipefail
echo "== docs sandbox durability relevant lines =="
sed -n '1,130p' docs/sandbox/durability.md | awk '{printf "%5d\t%s\n", NR, $0}'
echo "---"
sed -n '160,185p' docs/sandbox/durability.md | awk '{printf "%5d\t%s\n", NR+159, $0}'
echo
echo "== changeset relevant lines =="
sed -n '1,80p' .changeset/sandbox-durability.md | awk '{printf "%5d\t%s\n", NR, $0}'
echo
echo "== lock source files =="
git ls-files | rg 'packages/ai/src/(locks|.*lock|locks/)' || true
for f in packages/ai/src/locks.ts packages/ai/src/locks index.ts; do
if [ -e "$f" ]; then
echo "--- $f"
sed -n '1,180p' "$f" | awk '{printf "%5d\t%s\n", NR, $0}'
fi
done
echo
echo "== lock definitions and InMemoryLockStore references =="
rg -n "class InMemoryLockStore|InMemoryLockStore|LockStore|withLocks|withLocks\\(" packages/ai/src docs packages/ai-sandbox/skills/ai-sandbox/SKILL.md -S || trueRepository: TanStack/ai
Length of output: 14025
Use the locks subpath and a real distributed LockStore in this multi-instance example.
This sandbox durability section introduces @tanstack/ai/locks; importing withLocks / InMemoryLockStore from the main @tanstack/ai barrel is wrong, and InMemoryLockStore is process-local, so the multi-instance: distributed lock use case can still race. Import from @tanstack/ai/locks and pass a BYO distributed LockStore for production.
🧰 Tools
🪛 SkillSpector (2.3.11)
[warning] 200: [PE2] Sudo/Root Execution: Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
Remediation: Avoid sudo/root unless strictly required. Prefer least-privilege patterns. If elevation is needed, document the justification and scope.
(Privilege Escalation (PE2))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-sandbox/skills/ai-sandbox/SKILL.md` around lines 222 - 237,
Update the multi-instance example around chat to import withLocks and LockStore
from `@tanstack/ai/locks` instead of the main `@tanstack/ai` barrel, and replace
InMemoryLockStore with the production BYO distributed LockStore instance.
Preserve the existing withSandboxInstanceStore, withLocks, and withSandbox
middleware order.
| upsert(record: SandboxInstanceRecord): Promise<void> { | ||
| this.map.set(record.key, record) | ||
| return Promise.resolve() | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Normalize omitted optional fields in the in-memory store.
upsert preserves undefined properties. packages/ai-sandbox/src/sandbox.ts writes latestSnapshotId: undefined on the create path (Line 229-237), while the store contract/conformance expects absent optional fields to remain omitted. Normalize optional fields before storing, or construct the create record conditionally, so the in-memory fallback matches durable adapters.
Proposed fix
upsert(record: SandboxInstanceRecord): Promise<void> {
- this.map.set(record.key, record)
+ this.map.set(record.key, {
+ key: record.key,
+ provider: record.provider,
+ providerSandboxId: record.providerSandboxId,
+ threadId: record.threadId,
+ updatedAt: record.updatedAt,
+ ...(record.latestSnapshotId !== undefined
+ ? { latestSnapshotId: record.latestSnapshotId }
+ : {}),
+ ...(record.latestRunId !== undefined
+ ? { latestRunId: record.latestRunId }
+ : {}),
+ })
return Promise.resolve()
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| upsert(record: SandboxInstanceRecord): Promise<void> { | |
| this.map.set(record.key, record) | |
| return Promise.resolve() | |
| } | |
| upsert(record: SandboxInstanceRecord): Promise<void> { | |
| this.map.set(record.key, { | |
| key: record.key, | |
| provider: record.provider, | |
| providerSandboxId: record.providerSandboxId, | |
| threadId: record.threadId, | |
| updatedAt: record.updatedAt, | |
| ...(record.latestSnapshotId !== undefined | |
| ? { latestSnapshotId: record.latestSnapshotId } | |
| : {}), | |
| ...(record.latestRunId !== undefined | |
| ? { latestRunId: record.latestRunId } | |
| : {}), | |
| }) | |
| return Promise.resolve() | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-sandbox/src/instance-store.ts` around lines 80 - 83, Update
InMemoryStore.upsert to normalize omitted optional fields before storing
records, removing undefined-valued properties such as latestSnapshotId while
preserving defined values. Ensure the in-memory fallback matches the store
contract and durable adapters without changing the upsert return behavior.
| await store.upsert(makeRecord({ latestSnapshotId: 'snap-1' })) | ||
| await store.upsert( | ||
| makeRecord({ providerSandboxId: 'sb-2', updatedAt: 2 }), | ||
| ) | ||
| const loaded = await store.get('thread-1') | ||
| expect(loaded?.providerSandboxId).toBe('sb-2') | ||
| expect(loaded?.updatedAt).toBe(2) | ||
| // The overwrite dropped latestSnapshotId — a durable store must clear it, | ||
| // not retain the prior value. | ||
| expect(loaded && 'latestSnapshotId' in loaded).toBe(false) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Cover latestRunId clearing on replacement.
The overwrite fixture only seeds latestSnapshotId, so a store that incorrectly merges and retains latestRunId still passes conformance. Seed both optional fields and assert both are absent after the replacement.
Proposed test update
- await store.upsert(makeRecord({ latestSnapshotId: 'snap-1' }))
+ await store.upsert(
+ makeRecord({ latestSnapshotId: 'snap-1', latestRunId: 'run-1' }),
+ )
await store.upsert(
makeRecord({ providerSandboxId: 'sb-2', updatedAt: 2 }),
)
@@
expect(loaded && 'latestSnapshotId' in loaded).toBe(false)
+ expect(loaded && 'latestRunId' in loaded).toBe(false)Based on supplied contract context, full replacement must clear both optional fields.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await store.upsert(makeRecord({ latestSnapshotId: 'snap-1' })) | |
| await store.upsert( | |
| makeRecord({ providerSandboxId: 'sb-2', updatedAt: 2 }), | |
| ) | |
| const loaded = await store.get('thread-1') | |
| expect(loaded?.providerSandboxId).toBe('sb-2') | |
| expect(loaded?.updatedAt).toBe(2) | |
| // The overwrite dropped latestSnapshotId — a durable store must clear it, | |
| // not retain the prior value. | |
| expect(loaded && 'latestSnapshotId' in loaded).toBe(false) | |
| await store.upsert( | |
| makeRecord({ latestSnapshotId: 'snap-1', latestRunId: 'run-1' }), | |
| ) | |
| await store.upsert( | |
| makeRecord({ providerSandboxId: 'sb-2', updatedAt: 2 }), | |
| ) | |
| const loaded = await store.get('thread-1') | |
| expect(loaded?.providerSandboxId).toBe('sb-2') | |
| expect(loaded?.updatedAt).toBe(2) | |
| // The overwrite dropped latestSnapshotId — a durable store must clear it, | |
| // not retain the prior value. | |
| expect(loaded && 'latestSnapshotId' in loaded).toBe(false) | |
| expect(loaded && 'latestRunId' in loaded).toBe(false) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-sandbox/src/testkit/conformance.ts` around lines 68 - 77, Update
the overwrite fixture in the conformance test around store.upsert and store.get
to seed both latestSnapshotId and latestRunId on the initial record. After
replacing the record, assert that loaded contains neither optional field,
preserving the existing latestSnapshotId assertion and adding equivalent
coverage for latestRunId.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/ai-sandbox/tests/resume-from-store.test.ts`:
- Line 1: Move packages/ai-sandbox/tests/resume-from-store.test.ts alongside the
middleware/source module it covers, preserving its tests and imports. Also move
packages/ai-sandbox/tests/store.conformance.test.ts alongside
packages/ai-sandbox/src/testkit/conformance.ts, updating relative imports as
needed; both files should remain *.test.ts unit-test files co-located with their
covered source.
- Around line 81-95: Strengthen the resume-from-store test around run-2 by
storing a provider sandbox ID distinct from the sandbox key after run-1, then
capture the ID supplied to provider.resume and assert it equals that stored
provider ID. Update the existing fake/provider setup and assertions near
run('run-1') and run('run-2') without changing the production flow.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: de53f0bf-0ca0-4681-a850-e5d320408a57
📒 Files selected for processing (10)
.changeset/sandbox-durability.mddocs/advanced/locks.mddocs/config.jsondocs/sandbox/durability.mdpackages/ai-sandbox/skills/ai-sandbox/SKILL.mdpackages/ai-sandbox/src/capabilities.tspackages/ai-sandbox/src/index.tspackages/ai-sandbox/src/instance-store.tspackages/ai-sandbox/tests/resume-from-store.test.tspackages/ai-sandbox/tests/store.conformance.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- docs/sandbox/durability.md
- .changeset/sandbox-durability.md
- packages/ai-sandbox/src/instance-store.ts
- packages/ai-sandbox/src/index.ts
- packages/ai-sandbox/src/capabilities.ts
| @@ -0,0 +1,97 @@ | |||
| import { describe, expect, it } from 'vitest' | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Co-locate these unit tests with their covered source modules.
packages/ai-sandbox/tests/resume-from-store.test.ts#L1-L1: move alongside its primary middleware/source coverage.packages/ai-sandbox/tests/store.conformance.test.ts#L1-L1: move alongsidepackages/ai-sandbox/src/testkit/conformance.ts.
As per coding guidelines, “Place unit tests in *.test.ts files alongside the source they cover.”
📍 Affects 2 files
packages/ai-sandbox/tests/resume-from-store.test.ts#L1-L1(this comment)packages/ai-sandbox/tests/store.conformance.test.ts#L1-L1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-sandbox/tests/resume-from-store.test.ts` at line 1, Move
packages/ai-sandbox/tests/resume-from-store.test.ts alongside the
middleware/source module it covers, preserving its tests and imports. Also move
packages/ai-sandbox/tests/store.conformance.test.ts alongside
packages/ai-sandbox/src/testkit/conformance.ts, updating relative imports as
needed; both files should remain *.test.ts unit-test files co-located with their
covered source.
Sources: Coding guidelines, Learnings
| await run('run-1') | ||
| expect(provider.calls.create).toBe(1) | ||
| expect(provider.calls.resume).toBe(0) | ||
|
|
||
| await run('run-2') | ||
| expect(provider.calls.create).toBe(1) | ||
| expect(provider.calls.resume).toBe(1) | ||
|
|
||
| const key = sandbox.key({ | ||
| threadId: 'thread-1', | ||
| runId: 'run-2', | ||
| }) | ||
| const rec = await instanceStore.get(key) | ||
| expect(rec?.latestRunId).toBe('run-2') | ||
| expect(rec?.providerSandboxId).toBeTruthy() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert the ID passed to provider.resume.
create is given key as its ID, so a regression that resumes using the computed key instead of existing.providerSandboxId still passes these assertions. Store a distinct provider ID after run one and capture/assert resume’s input ID on run two.
Based on provided context, packages/ai-sandbox/src/sandbox.ts passes key to create, while packages/ai-sandbox/tests/fakes.ts resumes the ID it receives.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-sandbox/tests/resume-from-store.test.ts` around lines 81 - 95,
Strengthen the resume-from-store test around run-2 by storing a provider sandbox
ID distinct from the sandbox key after run-1, then capture the ID supplied to
provider.resume and assert it equals that stored provider ID. Update the
existing fake/provider setup and assertions near run('run-1') and run('run-2')
without changing the production flow.
Note: instance-store conformance / testkitThis PR ships Open question for reviewers: do we think this is necessary now, and is it even discoverable enough?
Preference welcome: keep / drop / keep-but-hide-until-adapters. |
… (BYO) Additive on top of the core persistence PR (#984, now in main): SandboxInstanceStore contract, InMemorySandboxInstanceStore, withSandboxInstanceStore, and a conformance testkit. withSandbox consumes it in ensure (in-memory fallback); pair withLocks (@tanstack/ai/locks) for multi-instance. Docs, skill, e2e.
- defineSandboxInstanceStore helper (defineLock / defineMessageStore style) - Skill and comments use @tanstack/ai/locks (not main barrel) - Conformance suite wired for InMemorySandboxInstanceStore - Middleware resume test via withSandboxInstanceStore + withLocks + withSandbox - Locks doc links to sandbox instance durability
Replaces the withSandboxInstanceStore pass-through middleware with withSandbox(sandbox, { instances, locks? }). The store had exactly one reader, so routing it through the capability bus bought nothing and cost an ordering rule whose violation silently degraded to the in-memory fallback. SandboxInstanceStoreCapability + provideSandboxInstanceStore stay exported for ambient/platform wiring; precedence is option -> bus -> in-memory.
7b28930 to
2d8e35f
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/ai-sandbox/src/index.ts`:
- Around line 18-28: Preserve the legacy `@tanstack/ai-sandbox` exports
InMemorySandboxStore, SandboxStore, and SandboxRecord by adding deprecated
compatibility re-exports that point to their replacements in `@tanstack/ai`, and
document the migration path. Keep the current SandboxInstanceStore exports
intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d2b14974-f6b6-47b1-aa1c-c6c5ab2005e1
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (24)
.changeset/sandbox-durability.mddocs/advanced/locks.mddocs/config.jsondocs/sandbox/durability.mddocs/sandbox/overview.mdpackages/ai-sandbox/package.jsonpackages/ai-sandbox/skills/ai-sandbox/SKILL.mdpackages/ai-sandbox/src/capabilities.tspackages/ai-sandbox/src/index.tspackages/ai-sandbox/src/instance-store.tspackages/ai-sandbox/src/middleware.tspackages/ai-sandbox/src/sandbox.tspackages/ai-sandbox/src/store.tspackages/ai-sandbox/src/testkit/conformance.tspackages/ai-sandbox/src/workspace.tspackages/ai-sandbox/tests/ensure.test.tspackages/ai-sandbox/tests/resume-from-store.test.tspackages/ai-sandbox/tests/store.conformance.test.tspackages/ai-sandbox/tests/store.test.tspackages/ai-sandbox/vite.config.tstesting/e2e/package.jsontesting/e2e/src/routeTree.gen.tstesting/e2e/src/routes/api.sandbox-durability.tstesting/e2e/tests/sandbox-durability.spec.ts
💤 Files with no reviewable changes (1)
- packages/ai-sandbox/src/store.ts
🚧 Files skipped from review as they are similar to previous changes (20)
- docs/advanced/locks.md
- packages/ai-sandbox/vite.config.ts
- packages/ai-sandbox/src/workspace.ts
- packages/ai-sandbox/package.json
- docs/config.json
- testing/e2e/tests/sandbox-durability.spec.ts
- packages/ai-sandbox/tests/store.conformance.test.ts
- .changeset/sandbox-durability.md
- testing/e2e/package.json
- packages/ai-sandbox/tests/store.test.ts
- docs/sandbox/overview.md
- testing/e2e/src/routes/api.sandbox-durability.ts
- docs/sandbox/durability.md
- packages/ai-sandbox/src/capabilities.ts
- packages/ai-sandbox/src/sandbox.ts
- testing/e2e/src/routeTree.gen.ts
- packages/ai-sandbox/src/middleware.ts
- packages/ai-sandbox/tests/ensure.test.ts
- packages/ai-sandbox/src/instance-store.ts
- packages/ai-sandbox/tests/resume-from-store.test.ts
| export { | ||
| SandboxInstanceStoreCapability, | ||
| getSandboxInstanceStore, | ||
| provideSandboxInstanceStore, | ||
| InMemorySandboxInstanceStore, | ||
| defineSandboxInstanceStore, | ||
| } from './instance-store' | ||
| export type { | ||
| SandboxInstanceStore, | ||
| SandboxInstanceRecord, | ||
| } from './instance-store' |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
rg -n \
'\b(InMemorySandboxStore|SandboxStore|SandboxRecord)\b|`@tanstack/ai-sandbox/`(store|instance-store)' \
packages testing docs
rg -n -C 3 '"exports"|"\./store"|"\./instance-store"' \
packages/ai-sandbox/package.jsonRepository: TanStack/ai
Length of output: 4587
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '--- package export map ---\n'
cat -n packages/ai-sandbox/package.json | sed -n '1,80p'
printf '\n--- repository exports for sandbox root/import paths ---\n'
rg -n '(`@tanstack/ai-sandbox`\b|from ["'\''](\/|\.\/)dist|from ["'\'']\./source\/|from ["'\'']\./src\/|from ["'\'']\./dist|from ["'\'']\./source|from ["'\'']\./src)' packages --glob '*.{js,ts,tsx,md,mdx,json}'
printf '\n--- package files and relevant source ---\n'
git ls-files packages/ai-sandbox
printf '\n--- instance-store and store files ---\n'
git ls-files packages/ai-sandbox/src | sed -n '1,200p'Repository: TanStack/ai
Length of output: 240
Preserve or version the removed @tanstack/ai-sandbox exports.
If InMemorySandboxStore, SandboxStore, or SandboxRecord are published to @tanstack/ai-sandbox, dropping these exports is a compile-time breaking change. Add deprecated re-exports, document the migration to @tanstack/ai, or release this as a deliberate breaking change.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-sandbox/src/index.ts` around lines 18 - 28, Preserve the legacy
`@tanstack/ai-sandbox` exports InMemorySandboxStore, SandboxStore, and
SandboxRecord by adding deprecated compatibility re-exports that point to their
replacements in `@tanstack/ai`, and document the migration path. Keep the current
SandboxInstanceStore exports intact.
main squash-merged the two PRs this branch was stacked on (#988 sandbox instance durability, #1011 generation run persistence), so this branch's own copy of that foundation collided with the squashes. 17 files conflicted. Notable resolutions, beyond taking the union: - #1004 made `RunStore.findActiveRun` REQUIRED, and main added an explicit store-contract evolution policy naming that exact regression. This branch had relocated `RunStore` into `@tanstack/ai` with `findActiveRun?` optional, and `run-store.ts` merged CLEANLY -- so keeping our side would have silently reverted #1004. `findActiveRun` is now required in core too, dropped from the conformance suite's `skipMethods` union, and `fenceRunStore` forwards it unconditionally. `listByThread`/`listReclaimable` stay optional. - Generation persistence moved to main's `generationRuns` store, but main writes `status: 'interrupted'` with a `finishedAt` on abort. This branch made `interrupted` non-terminal ("parked, waiting for a human"), so that pairing would leave an aborted generation looking permanently active. Now writes `'aborted'`. - `snapshotStatus` in `reconstruct-generation.ts` switched exhaustively over the old 4-member `RunStatus`; ours adds `aborted`, so an aborted generation fell through and the function returned `undefined`. Now maps to `'error'`. - `chat-persistence.md`: kept main's new lifecycle mermaid diagram, corrected to the current semantics (completed/failed/aborted terminal, interrupted parked, detached stays running). - `docs/sandbox/durability.md`: kept our real `import` over main's `declare const`, per the repo's kiira snippet rule. Verified: 17 typechecks green (including examples/ts-react-chat and testing/e2e), oxlint green, kiira 911/911, test:docs, sherif, knip and oxfmt all clean. Unit: ai 1409, ai-persistence 150, ai-client 585, ai-react 176, ai-durable-stream 45, ai-sandbox 602/603 (pre-existing Windows path case). E2E not run: port 4010 is held by an unrelated showcase-aimock container and another worktree's in-flight Playwright run.
Summary
Durable sandbox instance resume for multi-process / multi-replica deploy, owned by
@tanstack/ai-sandbox.Based on main (after #984). Locks already live in
@tanstack/ai/locks— this PR does not re-ship them. Chat persistence is independent: nostoresbag key for sandbox instances.API
SandboxInstanceStore/SandboxInstanceRecord/InMemorySandboxInstanceStore/SandboxInstanceStoreCapabilitywithSandboxInstanceStore(store)— provide beforewithSandboxdefineSandboxInstanceStore— inline BYO typing (same pattern asdefineLock/defineMessageStore)withSandboxconsumes the capability inensure(in-memory fallback when absent)withLocksfrom@tanstack/ai/locksrunSandboxInstanceStoreConformancefrom@tanstack/ai-sandbox/testkitDocs / e2e
docs/sandbox/durability.md(Instance Durability)sandbox-durability(store shared across independentchat()runs)Test plan
Summary by CodeRabbit