Skip to content

fix(agent-sessions): storage-measurement generation CAS + reclaim-outbox instance chase - #2274

Merged
2witstudios merged 1 commit into
masterfrom
pu/audit-measure-cas
Jul 30, 2026
Merged

2witstudios merged 1 commit into
masterfrom
pu/audit-measure-cas

Conversation

@2witstudios

@2witstudios 2witstudios commented Jul 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

Wave-2 of the post-#2258 audit: re-lands the generation-CAS half of #2253 (pre-restore, cannot merge as-is) onto current master's session-keyed model, and fixes #2254.

1. Storage-measurement generation CAS (#2253 fix 2)

recordStorageMeasurement writes now CAS on spriteInstanceId (not sandboxId — that name is HMAC-derived from the session and identical across generations) via eqOrIsNull, so a driver reporting no instance id still matches its own null row. Without this, a du against a torn-down generation that completes after re-provisioning could land its bytes and storageMeasuredAt on the new generation's row, silencing the new generation's real measurement for a full throttle window while the reconcile bills its interval against the wrong disk.

The field is now required through refreshSessionStorageMeasurement/PersistSessionStorageMeasurement so a call site can't silently drop it, threaded to all four call sites: web create + warm (agent-sessions-runtime.ts, sandbox-tools-runtime.ts), realtime create + resume (apps/realtime/src/index.ts). The in-memory store fake honours the same CAS as the real store.

2. Handle-sourced generation id (#2253 fix 3)

Every measurement call site now sources spriteInstanceId from the acquired sandbox handle — the identity of the disk actually measured — never from a re-read of the session row, which can name a different generation than the one du walked (the row is read only for the throttle check).

3. Issue #2254 — reclaim-outbox stale instance pointer

agent-session-orphan-reconcile-runtime.ts treated SandboxSpriteReplacedError as { ok: true } unconditionally, deleting the outbox row — but for a reclaim row (whose outbox entry is the last pointer to whatever VM exists under that name), a stale instance id there orphans the live replacement forever.

killSprite now reports a replaced instance distinctly ({ ok: 'replaced', actualInstanceId }) instead of collapsing it to success. The pure reconcileOrphanSprites loop decides per row.kind:

  • reclaim rows: chase the pointer at the live instance via chaseReclaimInstance (reuses the store's own enqueueReclaim upsert — the same ON CONFLICT DO UPDATE ... COALESCE the 0209/0219/0229 triggers use) and retry next tick, rather than releasing.
  • agent-session rows: unaffected — the row's own identity CAS in markSessionTornDown already protects a live replacement, so a replaced instance still counts as confirmed-gone.

Closes #2254. PR #2253 can now be closed (superseded by this PR, adapted to the current session-keyed model).

Test plan

  • New/updated tests pin: stale-generation write dropped (bytes AND timestamp), same-generation persists despite reused name, null-instance driver persists, outbox replaced-instance chases instead of orphaning (plus an agent-session-row replaced-instance case, still confirmed-gone).
  • bun run typecheck && bun run lint && bun run test:unit && bun run knip:check — clean (one pre-existing, unrelated TZ-environment test failure in messages/grouping.test.ts, present on master, untouched by this diff).
  • cd packages/lib && bun run typecheck — clean.
  • bun run test:security — 44/50 suites pass; the same 6 pre-existing failures as master (deleted Login/Signup/Mobile-Login routes, excluded Session Service/Device Auth/Permissions suites — a documented script gap, not a regression).

🤖 Generated with Claude Code

https://claude.ai/code/session_01DrK6Xpi8fu6jtyLxNqnTAt

Summary by CodeRabbit

  • Bug Fixes

    • Storage measurements now remain associated with the correct sandbox instance, preventing stale or misattributed usage data.
    • Measurements from inactive sessions or replaced sandbox instances are safely ignored.
    • Orphan cleanup now follows reclaim records to replacement sandbox instances instead of incorrectly releasing them.
    • Reconciliation handles replacement and failed updates more safely, reducing the risk of incorrect cleanup actions.
  • Tests

    • Expanded coverage for storage measurement safeguards and orphan reconciliation scenarios.

…box instance chase

Re-lands the generation-CAS half of #2253 post-restore, adapted to current
master's session-keyed model (spriteInstanceId, not sandboxId/name — the name
is HMAC-derived and identical across generations).

- recordStorageMeasurement now CASes on spriteInstanceId (eq-or-is-null), so a
  du against a torn-down generation cannot land its bytes/timestamp on the next
  generation's row. Required field threaded through the pure measure module to
  every call site (web create+warm, realtime create+resume); in-memory fakes
  honour the CAS.
- Every measurement call site sources spriteInstanceId from the ACQUIRED
  sandbox handle, never a re-read of the session row, so a torn-down-and-
  reprovisioned session in flight cannot have generation A's bytes CAS
  "successfully" onto generation B.

Fixes #2254: the orphan reconciler treated SandboxSpriteReplacedError as a
confirmed kill for reclaim-outbox rows too, deleting the outbox pointer and
orphaning a live Sprite when the outbox held a stale instance id (the 0234
rescue's ON CONFLICT DO NOTHING vs. the triggers' DO UPDATE COALESCE
instance-chasing). killSprite now reports the replacement distinctly; the pure
reconcile loop chases a reclaim row's pointer at the live instance (mirroring
the trigger's own COALESCE chase) instead of releasing it, while an
agent-session row's own identity CAS still lets a replaced instance count as
confirmed-gone.

Closes #2254.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DrK6Xpi8fu6jtyLxNqnTAt

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f5a60ce39b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// chase the pointer at the live instance and retry next tick, rather
// than confirming a kill that never touched the VM actually running.
try {
await deps.chaseReclaimInstance({ sandboxId: row.sandboxId, actualInstanceId: killed.actualInstanceId });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Check live session ownership before chasing replacements

A reclaim row is not necessarily left by a deleted session: killUnreferencedOrEnqueue in agent-session-sprite.ts also queues an unrecorded generation after a failed cleanup while the session row survives. If that stale entry names instance A and the session later provisions tracked instance B under the same deterministic sandbox name, this branch rewrites the outbox to B; the next cron tick treats B as orphaned and kills the active session without the session liveness check used by agent-session candidates. Before chasing the replacement, verify that actualInstanceId is not referenced by a live session row; if it is, the stale reclaim entry should be released instead.

Useful? React with 👍 / 👎.

{ sandboxId: row.sandboxId, actualInstanceId: killed.actualInstanceId },
);
}
skipped += 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count failed pointer chases as reconcile failures

When chaseReclaimInstance throws, the stale pointer remains unable to target the live Sprite, but execution still increments skipped and reports failed: 0. The cron route publishes these counters to its log and audit record, so a persistent database failure here appears as a benign skip even while the orphan continues billing. Increment failed rather than skipped when the chase write fails, while retaining skipped for a successful chase that intentionally waits for the next tick.

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 14d3d78d-0d3b-4d5f-a433-e454b3c18e4e

📥 Commits

Reviewing files that changed from the base of the PR and between 272a466 and f5a60ce.

📒 Files selected for processing (13)
  • apps/realtime/src/index.ts
  • apps/web/src/lib/agent-sessions/__tests__/agent-session-orphan-reconcile-runtime.test.ts
  • apps/web/src/lib/agent-sessions/agent-session-orphan-reconcile-runtime.ts
  • apps/web/src/lib/agent-sessions/agent-sessions-runtime.ts
  • apps/web/src/lib/ai/tools/__tests__/sandbox-tools-runtime.test.ts
  • apps/web/src/lib/ai/tools/sandbox-tools-runtime.ts
  • packages/lib/src/services/agent-sessions/__tests__/agent-sessions-store.integration.test.ts
  • packages/lib/src/services/agent-sessions/__tests__/fakes.ts
  • packages/lib/src/services/agent-sessions/agent-sessions-store.ts
  • packages/lib/src/services/sandbox/__tests__/sandbox-storage-measure.test.ts
  • packages/lib/src/services/sandbox/__tests__/sprite-orphan-reconcile.test.ts
  • packages/lib/src/services/sandbox/sandbox-storage-measure.ts
  • packages/lib/src/services/sandbox/sprite-orphan-reconcile.ts

📝 Walkthrough

Walkthrough

Storage measurements now carry spriteInstanceId through runtime adapters into generation-guarded session-store writes. Orphan reconciliation reports replaced VMs and repoints reclaim rows to the live instance instead of releasing stale pointers.

Changes

Generation-aware storage measurements

Layer / File(s) Summary
Storage measurement contract
packages/lib/src/services/sandbox/sandbox-storage-measure.ts, packages/lib/src/services/sandbox/__tests__/sandbox-storage-measure.test.ts
Measurement persistence inputs include nullable spriteInstanceId, with forwarding and null-generation coverage.
Generation-scoped measurement writes
packages/lib/src/services/agent-sessions/agent-sessions-store.ts, packages/lib/src/services/agent-sessions/__tests__/fakes.ts, packages/lib/src/services/agent-sessions/__tests__/agent-sessions-store.integration.test.ts
Session storage updates require a live row whose spriteInstanceId matches the measured generation, including null matching.
Runtime generation propagation
apps/web/src/lib/agent-sessions/agent-sessions-runtime.ts, apps/web/src/lib/ai/tools/sandbox-tools-runtime.ts, apps/realtime/src/index.ts, apps/web/src/lib/ai/tools/__tests__/sandbox-tools-runtime.test.ts
Provisioning, warm-session, realtime, and tool flows pass the sandbox generation through storage measurement persistence.

Replaced Sprite reconciliation

Layer / File(s) Summary
Replacement outcome and reclaim wiring
packages/lib/src/services/sandbox/sprite-orphan-reconcile.ts, apps/web/src/lib/agent-sessions/agent-session-orphan-reconcile-runtime.ts, apps/web/src/lib/agent-sessions/__tests__/agent-session-orphan-reconcile-runtime.test.ts
Replacement errors return the live instance id, and runtime dependencies enqueue reclaim rows for that instance.
Reclaim pointer chasing
packages/lib/src/services/sandbox/sprite-orphan-reconcile.ts, packages/lib/src/services/sandbox/__tests__/sprite-orphan-reconcile.test.ts
Reclaim rows are chased to replacement instances; chase failures skip release, while agent-session rows retain their existing handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant reconcileOrphanSprites
  participant killSprite
  participant chaseReclaimInstance
  participant enqueueReclaim
  reconcileOrphanSprites->>killSprite: kill orphan candidate
  killSprite-->>reconcileOrphanSprites: replaced with actualInstanceId
  reconcileOrphanSprites->>chaseReclaimInstance: chase reclaim pointer
  chaseReclaimInstance->>enqueueReclaim: enqueue actualInstanceId
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the two main changes: storage-measurement CAS and reclaim-outbox instance chasing.
Linked Issues check ✅ Passed The reclaim-row chase behavior for stale spriteInstanceId matches #2254, and the related tests cover the replaced-instance flow and retry retention.
Out of Scope Changes check ✅ Passed The changes stay focused on storage-measurement generation CAS and orphan-reconcile chasing, with no obvious unrelated code introduced.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/audit-measure-cas

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@2witstudios
2witstudios merged commit 900bc25 into master Jul 30, 2026
10 checks passed
@2witstudios
2witstudios deleted the pu/audit-measure-cas branch July 30, 2026 09:41
2witstudios added a commit that referenced this pull request Jul 30, 2026
Same correction as the PR description — #2274 is an unrelated,
already-merged PR (storage-measurement CAS), not this fix's issue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5FJqcnWyZXBmuc9iTFd6U
2witstudios added a commit that referenced this pull request Jul 30, 2026
Same correction as the PR description — #2274 is an unrelated,
already-merged PR (storage-measurement CAS), not this fix's issue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5FJqcnWyZXBmuc9iTFd6U
2witstudios added a commit that referenced this pull request Jul 30, 2026
Same correction as the PR description — #2274 is an unrelated,
already-merged PR (storage-measurement CAS), not this fix's issue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5FJqcnWyZXBmuc9iTFd6U
2witstudios added a commit that referenced this pull request Jul 30, 2026
Same correction as the PR description — #2274 is an unrelated,
already-merged PR (storage-measurement CAS), not this fix's issue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5FJqcnWyZXBmuc9iTFd6U
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reclaim outbox can keep a stale spriteInstanceId, orphaning a live Sprite

1 participant