Skip to content

feat(sessions): port GET /api/sessions/[sessionId] from open-agents (Phase 2.4) - #514

Merged
sweetmantech merged 2 commits into
testfrom
feat/port-get-session-route
May 5, 2026
Merged

feat(sessions): port GET /api/sessions/[sessionId] from open-agents (Phase 2.4)#514
sweetmantech merged 2 commits into
testfrom
feat/port-get-session-route

Conversation

@sweetmantech

@sweetmantechsweetmantech commented May 4, 2026

Copy link
Copy Markdown
Contributor

Summary

First route in the route-by-route cutover plan (Phase 2.4 / new strategy). Ports the simplest endpoint open-agents' frontend hits — GET /api/sessions/[sessionId] — into api with byte-identical response shape so the frontend can cut over with zero changes.

Context

Per the strategy shift discussed: instead of building agent runner + workflow + routes in isolation and hoping they integrate, we let the open-agents frontend drive what gets ported, in what order. The frontend becomes the spec; each route ported is validated end-to-end against a real consumer.

This PR is the smallest provable first port. Everything that follows uses the same shape.

Why this route first

PropertyValue
Backend depsDB only — no agent runner, no Vercel Workflow, no sandbox runtime
Schema deps`sessions` table — already migrated in database#20
Frontend usagehit on session-detail page navigation
Blast radiussmallest possible — proves the pattern with minimal new code

Files

FilePurpose
`lib/supabase/sessions/selectSession.ts`Single-row select helper + `SessionRow` type. Hand-typed pending `types/database.types.ts` regen (flagged in code comment)
`app/api/sessions/[sessionId]/route.ts``GET` handler matching open-agents' response shape exactly
`app/api/sessions/[sessionId]/tests/route.test.ts`5 tests (auth fail, not-found, forbidden, happy path, OPTIONS)

Auth + response codes

  • Auth: `validateAuthContext` (Privy Bearer or x-api-key). Same as every other api route.
  • 200 — happy path with `{ session }`
  • 401 — no auth
  • 403 — session exists but not owned by authenticated account
  • 404 — session not found

Wire-format translation

The Supabase row is snake_case (`account_id`, `repo_owner`, `is_new_branch`, …). The open-agents frontend expects camelCase (`userId`, `repoOwner`, `isNewBranch`, …). Translation lives at the route boundary in `toSessionResponse()` so:

  • Schema stays natural (`account_id` is the right name for "FK to accounts table")
  • Frontend stays unchanged (`userId` is what its hooks consume today)
  • Translation is one easy-to-find function to delete when chat absorbs this UI and we can switch to schema-natural naming

Notably the response field is `userId` (the open-agents term) sourced from `account_id` (api's term). Same value, two names — kept compatible during the cutover window.

Verification

CheckResult
`pnpm lint:check`✅ clean
`pnpm test`2379/2379 pass (was 2374 — +5 from new route tests)

What's NOT in scope yet

  • Frontend cutover — separate PR in the open-agents repo: point its frontend at api's URL for this one route. Validates end-to-end. Don't merge that until this api route is in test/main.
  • PATCH / DELETE for the same path — open-agents has them too, but they're write paths; do them after the read works in prod.
  • Types regen — `SessionRow` is hand-typed here. Should be replaced with `Tables<"sessions">` once `pnpm update-types` is run against an environment that has the migrations applied.

Up next in the chain

After this lands and the frontend cutover validates:

  • `GET /api/sessions/[id]/chats` — list chats for a session (single-table read)
  • `GET /api/sessions/[id]/chats/[chatId]` — read one chat (single-table read)
  • `GET /api/sessions` — list sessions with unread (needs a Postgres RPC for the multi-table aggregation; bigger PR)

🤖 Generated with Claude Code


Summary by cubic

Ports GET /api/sessions/[sessionId] with the same camelCase response as open-agents so the frontend can switch over without changes. First route in Phase 2.4’s route-by-route cutover.

  • New Features

    • Added GET and OPTIONS in app/api/sessions/[sessionId]/route.ts; delegates to getSessionByIdHandler; sets dynamic="force-dynamic", fetchCache="force-no-store", and revalidate=0.
    • Auth via validateAuthContext (Privy Bearer or x-api-key) with ownership check; status codes: 200 { session }, 401, 403, 404; JSON errors via { status: "error", error: "..." }; CORS via getCorsHeaders.
    • 5 tests cover 401/404/403/200 and OPTIONS.
  • Refactors

    • Introduced lib/sessions/getSessionByIdHandler.ts; reads via selectSessions({ id }), enforces account_id ownership, and maps rows with toSessionResponse for camelCase output.

Written for commit e6aaba9. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added an API endpoint to retrieve session details by session ID, with authentication and authorization checks to ensure users can only access their own sessions.

@vercel

vercelBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
apiReadyReadyPreviewMay 4, 2026 11:54pm

Request Review

@coderabbitai

coderabbitaiBot commented May 4, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@sweetmantech has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 49 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 766082af-1746-45b6-846a-2c3aa7a3a405

📥 Commits

Reviewing files that changed from the base of the PR and between 00e2823 and e6aaba9.

⛔ Files ignored due to path filters (1)
  • app/api/sessions/[sessionId]/__tests__/route.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
📒 Files selected for processing (2)
  • app/api/sessions/[sessionId]/route.ts
  • lib/sessions/getSessionByIdHandler.ts
📝 Walkthrough

Walkthrough

A new API endpoint retrieves sessions by ID with authentication and authorization checks. The implementation includes a database query function, response transformation layer, handler with auth validation and error handling, and a Next.js route with CORS support and no-cache directives.

Changes

Session Retrieval Endpoint

Layer / File(s)Summary
Data Access
lib/supabase/sessions/selectSession.ts
selectSession(sessionId) queries the Supabase sessions table by ID and returns the row or null on error.
Response Transformation
lib/sessions/toSessionResponse.ts
toSessionResponse(row) converts Supabase snake_case row fields (account_id, repo_owner, clone_url, etc.) to camelCase API response properties.
Handler & Orchestration
lib/sessions/getSessionByIdHandler.ts
getSessionByIdHandler validates auth context, fetches the session, enforces account ownership with 403 for mismatches, returns 404 when absent, and wraps responses with CORS headers.
Route Registration & Caching
app/api/sessions/[sessionId]/route.ts
Exports GET and OPTIONS handlers; extracts sessionId from route params, delegates to getSessionByIdHandler, and applies force-dynamic, force-no-store, and revalidate = 0 directives.

Sequence Diagram

sequenceDiagram
participant Client
participant Route as Route Handler<br/>(route.ts)
participant Auth as Auth Validation
participant DB as Database<br/>(selectSession)
participant Transform as Response<br/>Transformer
participant Response as HTTP Response
Client->>Route: GET /api/sessions/{sessionId}
alt Preflight
Route->>Response: OPTIONS → 200 + CORS headers
Response->>Client: 200 OK
else Fetch Session
Route->>Auth: Validate auth context
alt Auth Invalid
Auth->>Response: Return auth error
else Auth Valid
Route->>DB: selectSession(sessionId)
DB->>Response: Session row or null
alt Session Not Found
Response->>Route: 404 error response
else Session Found
alt Account Mismatch
Response->>Route: 403 Forbidden
else Account Match
Route->>Transform: toSessionResponse(row)
Transform->>Route: Transformed session object
Route->>Response: 200 + session + CORS headers
end
end
end
Response->>Client: Response with headers
end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🔐 A session seeketh by its ID,
Through auth and account checks it flows with glee,
Snake_case bends to camelCase delight,
CORS headers guard it left and right,
One endpoint born to retrieve with care! 📋

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Solid & Clean Code✅ PassedPR demonstrates excellent SOLID and Clean Code adherence with single-responsibility functions, descriptive naming matching filenames, proper separation of concerns across database, transformation, and HTTP layers, and clean error handling patterns.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/port-get-session-route

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 and usage tips.

@cubic-dev-aicubic-dev-aiBot 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.

2 issues found across 3 files

Confidence score: 3/5

  • Potential user-facing error handling issue in app/api/sessions/[sessionId]/route.ts: database failures are surfaced as 404s instead of 500s, which can mask real server problems.
  • Some risk remains due to the concrete error-handling behavior change needed, though it's localized and fixable.
  • Pay close attention to app/api/sessions/[sessionId]/route.ts - distinguish DB failures from missing rows to avoid misleading 404s.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/api/sessions/[sessionId]/__tests__/route.test.ts">
<violation number="1" location="app/api/sessions/[sessionId]/__tests__/route.test.ts:1">
P2: Custom agent: **Enforce Clear Code Style and Maintainability Practices**
New test file exceeds the repository's 100-line file-size limit.</violation>
</file>
<file name="app/api/sessions/[sessionId]/route.ts">
<violation number="1" location="app/api/sessions/[sessionId]/route.ts:85">
P2: `selectSession` errors are currently surfaced as 404 "Session not found" because this route treats any `null` row as not-found. Distinguish DB failures from missing rows and return a 500 with a generic message.
(Based on your team's feedback about safe 500 error handling in API handlers.) [FEEDBACK_USED]</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client as Frontend (Open-Agents)
participant API as Session Route (API)
participant Auth as validateAuthContext
participant DB as Supabase (Sessions Table)
Note over Client, DB: NEW: GET /api/sessions/[sessionId] Flow
Client->>API: GET /api/sessions/[sessionId]
Note right of Client: Headers: Privy Bearer or x-api-key
API->>Auth: validateAuthContext(request)
alt Auth Failed
Auth-->>API: NextResponse (401 Unauthorized)
API-->>Client: 401 Unauthorized
else Auth Success
Auth-->>API: { accountId, authToken }
API->>DB: NEW: selectSession(sessionId)
DB-->>API: SessionRow (snake_case) or null
alt Session Not Found
API-->>Client: 404 Not Found
else Session Exists
API->>API: Check ownership (row.account_id === auth.accountId)
alt Not Owner
API-->>Client: 403 Forbidden
else Is Owner
API->>API: NEW: toSessionResponse(row)
Note right of API: Translates snake_case DB fields<br/>to camelCase (e.g. account_id -> userId)
API-->>Client: 200 OK (JSON { session })
end
end
end
Note over API, Client: Includes CORS headers in all responses via getCorsHeaders()
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@@ -0,0 +1,153 @@
import { describe, it, expect, vi, beforeEach } from "vitest";

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: Custom agent: Enforce Clear Code Style and Maintainability Practices

New test file exceeds the repository's 100-line file-size limit.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/api/sessions/[sessionId]/__tests__/route.test.ts, line 1:
<comment>New test file exceeds the repository's 100-line file-size limit.</comment>
<file context>
@@ -0,0 +1,153 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { NextRequest, NextResponse } from "next/server";
+import { GET, OPTIONS } from "../route";
</file context>

}

const { sessionId } = await options.params;
const row = await selectSession(sessionId);

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: selectSession errors are currently surfaced as 404 "Session not found" because this route treats any null row as not-found. Distinguish DB failures from missing rows and return a 500 with a generic message.

(Based on your team's feedback about safe 500 error handling in API handlers.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/api/sessions/[sessionId]/route.ts, line 85:
<comment>`selectSession` errors are currently surfaced as 404 "Session not found" because this route treats any `null` row as not-found. Distinguish DB failures from missing rows and return a 500 with a generic message.
(Based on your team's feedback about safe 500 error handling in API handlers.) </comment>
<file context>
@@ -0,0 +1,106 @@
+ }
+
+ const { sessionId } = await options.params;
+ const row = await selectSession(sessionId);
+
+ if (!row) {
</file context>

* @param row - The Supabase sessions row.
* @returns The camelCase session payload for HTTP responses.
*/
function toSessionResponse(row: SessionRow) {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

SRP - new lib file for toSessionResponse.ts

Comment threadapp/api/sessions/[sessionId]/route.ts
* Phase 0 schema port). Hand-typed here pending types/database.types.ts
* regeneration; remove once `Tables<"sessions">` is available there.
*/
export interface SessionRow {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

DRY -

  • actual: manually defining response type
  • required: use existing db schema type, matching all other supabase libs.

sweetmantech added a commit that referenced this pull request May 4, 2026
…egen'd types
Three review comments on PR #514:
1. SRP: extract toSessionResponse to its own file
was: defined inline in app/api/sessions/[sessionId]/route.ts
now: lib/sessions/toSessionResponse.ts (one exported fn per file)
2. SRP: add a handler function (mirroring api convention)
was: GET handler logic inline in route.ts
now: lib/sessions/getSessionByIdHandler.ts contains all the auth +
ownership + DB lookup + response logic; route.ts is a thin
shell that awaits options.params and delegates. Matches the
pattern used by every other api route (e.g. socials/[id]/scrape,
artists/[id]/...).
3. DRY: use existing db schema type
was: hand-typed SessionRow interface in selectSession.ts
now: Tables<\"sessions\"> from types/database.types.ts (regenerated
via npx supabase gen types typescript --project-id ...
--schema public)
The types regen also resolved the preview-build failure
(\"Type instantiation is excessively deep and possibly infinite\") on
the .from(\"sessions\") call — Supabase's type inference was choking
because the table was unknown to the generic.
Files added:
lib/sessions/toSessionResponse.ts
lib/sessions/getSessionByIdHandler.ts
Files modified:
app/api/sessions/[sessionId]/route.ts thin shell now
app/api/sessions/[sessionId]/__tests__/
route.test.ts type alias updated
lib/supabase/sessions/selectSession.ts Tables<\"sessions\">
types/database.types.ts Supabase regen
Verification:
- pnpm lint:check: clean
- pnpm test: 2379/2379 pass (no test changes; same 5 route tests)
- tsc compile clean (the local pnpm build progresses past compile
into page-data collection where it fails on missing local env
vars — Vercel preview will have those set, so the preview rebuild
should now succeed)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (2)
app/api/sessions/[sessionId]/route.ts (2)

40-42: 💤 Low value

force-dynamic / revalidate = 0 directives are redundant under Next.js 16.

Under Next.js 16's Cache Components model, caching is entirely opt-in, and all dynamic code in any page, layout, or API route executes at request time by default. The three cache-busting directives are therefore no-ops in this version, though they cause no harm and do signal explicit intent.

No action required, but you can remove them once Next.js 16 is the confirmed minimum version for this codebase.

🤖 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 `@app/api/sessions/`[sessionId]/route.ts around lines 40 - 42, The exported
cache-control directives dynamic, fetchCache and revalidate in route.ts (export
const dynamic = "force-dynamic"; export const fetchCache = "force-no-store";
export const revalidate = 0;) are redundant under Next.js 16; remove those three
exports from the file (delete the symbols dynamic, fetchCache and revalidate) so
the API route relies on the framework's default request-time behavior once
Next.js 16 is the confirmed minimum.

32-38: ⚡ Quick win

Add Zod validation for sessionId per the coding guidelines.

The sessionId path parameter passes directly to the DB query without any schema validation. An invalid/malformed ID currently returns 404 instead of a semantically correct 400. The coding guideline requires Zod validation for all API input.

✨ Proposed fix — validate in the route before delegating
+import { z } from "zod";++const SessionIdSchema = z.string().uuid();+
export async function GET(
request: NextRequest,
options: { params: Promise<{ sessionId: string }> },
) {
const { sessionId } = await options.params;
+ const parsed = SessionIdSchema.safeParse(sessionId);+ if (!parsed.success) {+ return NextResponse.json({ error: "Invalid session ID" }, { status: 400 });+ }- return getSessionByIdHandler(request, sessionId);+ return getSessionByIdHandler(request, parsed.data);
}

Alternatively, validation can live inside getSessionByIdHandler itself if it is reused across routes.

As per coding guidelines: "All API endpoints should use a validate function for input parsing using Zod for schema validation."

🤖 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 `@app/api/sessions/`[sessionId]/route.ts around lines 32 - 38, The route
handler GET currently forwards the raw sessionId directly to
getSessionByIdHandler without schema checks; add Zod validation for the path
param before delegating so malformed IDs return 400. Create/consume a Zod schema
(e.g., z.string().uuid() or appropriate pattern) and run validate/parse on the
extracted sessionId inside GET, returning a 400 response on failure and only
calling getSessionByIdHandler(request, sessionId) when validation succeeds;
alternatively move the same validation into getSessionByIdHandler if it must be
reused.
🤖 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.
Nitpick comments:
In `@app/api/sessions/`[sessionId]/route.ts:
- Around line 40-42: The exported cache-control directives dynamic, fetchCache
and revalidate in route.ts (export const dynamic = "force-dynamic"; export const
fetchCache = "force-no-store"; export const revalidate = 0;) are redundant under
Next.js 16; remove those three exports from the file (delete the symbols
dynamic, fetchCache and revalidate) so the API route relies on the framework's
default request-time behavior once Next.js 16 is the confirmed minimum.
- Around line 32-38: The route handler GET currently forwards the raw sessionId
directly to getSessionByIdHandler without schema checks; add Zod validation for
the path param before delegating so malformed IDs return 400. Create/consume a
Zod schema (e.g., z.string().uuid() or appropriate pattern) and run
validate/parse on the extracted sessionId inside GET, returning a 400 response
on failure and only calling getSessionByIdHandler(request, sessionId) when
validation succeeds; alternatively move the same validation into
getSessionByIdHandler if it must be reused.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 214e687c-3916-4fc3-be56-cd2eba560d21

📥 Commits

Reviewing files that changed from the base of the PR and between 97724c4 and 00e2823.

⛔ Files ignored due to path filters (2)
  • app/api/sessions/[sessionId]/__tests__/route.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • types/database.types.ts is excluded by none and included by none
📒 Files selected for processing (4)
  • app/api/sessions/[sessionId]/route.ts
  • lib/sessions/getSessionByIdHandler.ts
  • lib/sessions/toSessionResponse.ts
  • lib/supabase/sessions/selectSession.ts

@cubic-dev-aicubic-dev-aiBot 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.

0 issues found across 6 files (changes from recent commits).

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

@cubic-dev-aicubic-dev-aiBot 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.

0 issues found across 2 files (changes from recent commits).

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

sweetmantech added a commit that referenced this pull request May 4, 2026
The GET endpoint + handler + tests live in PR #514 and were
inadvertently brought in when this branch was rebased after #514's
work. This PR is scoped to POST only; GET ships in #514.
Shared infrastructure stays (types/database.types.ts regen +
lib/sessions/toSessionResponse.ts) — both are required by the POST
handler too. When either #514 or this PR merges to test first, the
other will see those files already present and resolve cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sweetmantech added a commit that referenced this pull request May 4, 2026
* feat(sessions): port GET /api/sessions/[sessionId] from open-agents (Phase 2.4 — first route)
First route in the route-by-route cutover plan. Strategy: open-agents
frontend stays unchanged in shape; api ports each route it calls in
priority order (simplest first), and the open-agents frontend gets
cut over to api one route at a time.
Why this route first:
- Pure DB read (single-row select by id) — no agent runner, no Vercel
Workflow, no sandbox runtime
- Hits sessions table already migrated in database PR #20
- Frontend usage: agents-frontend hits /api/sessions/{id} on session
detail page navigation
- Smallest possible blast radius for proving the cutover pattern
Files added:
lib/supabase/sessions/selectSession.ts Single-row helper + SessionRow
type (hand-typed; database.types.ts
regen pending — flagged in code
comment)
app/api/sessions/[sessionId]/route.ts GET handler matching open-agents
response shape exactly (camelCase
fields, "userId" preserved on the
wire even though stored as
account_id internally)
app/api/sessions/[sessionId]/__tests__/route.test.ts (5 tests)
Auth: validateAuthContext (Privy Bearer or x-api-key). Response codes
match open-agents: 200 happy path, 401 no auth, 403 not owner, 404 not
found.
Wire-format translation: snake_case Supabase row -> camelCase response,
with account_id surfaced as userId so the existing open-agents frontend
fetches with zero code changes. Translation lives at the route boundary
(toSessionResponse) where it is easy to remove once chat absorbs this
UI and we can switch to schema-natural naming.
Verification:
- pnpm lint:check: clean
- pnpm test: 2379/2379 pass (5 new for this route)
Up next:
- Cutover step (separate PR in open-agents): point the frontend at
api's URL for this single route. Validate end-to-end before porting
the next route.
- Next routes in priority order (still pure DB, no agent/workflow):
GET /api/sessions (list with unread — needs Postgres RPC for the
multi-table aggregation), GET /api/sessions/[id]/chats, GET
/api/sessions/[id]/chats/[chatId].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address PR review — SRP splits + use Tables<\"sessions\"> from regen'd types
Three review comments on PR #514:
1. SRP: extract toSessionResponse to its own file
was: defined inline in app/api/sessions/[sessionId]/route.ts
now: lib/sessions/toSessionResponse.ts (one exported fn per file)
2. SRP: add a handler function (mirroring api convention)
was: GET handler logic inline in route.ts
now: lib/sessions/getSessionByIdHandler.ts contains all the auth +
ownership + DB lookup + response logic; route.ts is a thin
shell that awaits options.params and delegates. Matches the
pattern used by every other api route (e.g. socials/[id]/scrape,
artists/[id]/...).
3. DRY: use existing db schema type
was: hand-typed SessionRow interface in selectSession.ts
now: Tables<\"sessions\"> from types/database.types.ts (regenerated
via npx supabase gen types typescript --project-id ...
--schema public)
The types regen also resolved the preview-build failure
(\"Type instantiation is excessively deep and possibly infinite\") on
the .from(\"sessions\") call — Supabase's type inference was choking
because the table was unknown to the generic.
Files added:
lib/sessions/toSessionResponse.ts
lib/sessions/getSessionByIdHandler.ts
Files modified:
app/api/sessions/[sessionId]/route.ts thin shell now
app/api/sessions/[sessionId]/__tests__/
route.test.ts type alias updated
lib/supabase/sessions/selectSession.ts Tables<\"sessions\">
types/database.types.ts Supabase regen
Verification:
- pnpm lint:check: clean
- pnpm test: 2379/2379 pass (no test changes; same 5 route tests)
- tsc compile clean (the local pnpm build progresses past compile
into page-data collection where it fails on missing local env
vars — Vercel preview will have those set, so the preview rebuild
should now succeed)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): make 404/403 errors emit status:"error" for shape consistency
The 401 returned by validateAuthContext shaped like
{status:"error", error:"..."} but 404/403 from this handler returned
{error:"..."} only. Same endpoint, two error shapes — inconsistent for
clients. Align all error responses on the validateAuthContext shape.
Tests now assert the full error body, not just the status code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): port POST /api/sessions from open-agents
Implements the POST /api/sessions contract documented in
recoupable/docs PR #186 + #187. Creates a session row and an
initial chat row; rolls back the session if chat insert fails so
callers never observe an orphaned session.
Auth: validateAuthContext (Privy Bearer or x-api-key).
Validation: Zod schema + GitHub repo segment regex. Body is
optional — empty body creates a session with sensible defaults
(status=running, lifecycle_state=provisioning, sandbox_state.type=
vercel, title="New session").
Out of scope (will follow once database catches up):
auto_commit_push_override, auto_create_pr_override, pr_number,
pr_status — these columns don't yet exist on api's sessions
table, so the docs spec was trimmed accordingly in docs PR #187.
TDD: 9 handler tests cover 401, 400 (sandboxType / repoOwner /
repoName), 200 happy path, branch generation, title pass-through,
500 (insertSession failure), and 500-with-rollback (insertChat
failure). Plus 1 thin test on the route shell.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): add OPTIONS handler + cache directives to POST route
Match the convention from app/api/sessions/[sessionId]/route.ts:
- OPTIONS handler returning 200 + CORS headers (preflight)
- dynamic="force-dynamic", fetchCache="force-no-store", revalidate=0
POST routes that mutate DB shouldn't be cached, and browsers issuing
preflight checks (POST with JSON body + custom auth headers) need
OPTIONS to respond.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): address PR review feedback
- SRP: extract insert-row construction to lib/sessions/buildSessionInsertRow.ts
- YAGNI: drop generateSessionBranchName + isNewBranch handling (sessions
commit to whatever branch the client provides; auto-generation was
speculative)
- Tighten isValidGitHubRepoOwner: GitHub's actual rules are alphanumeric
+ hyphen only (no `_` or `.`), 1-39 chars, no leading/trailing or
consecutive hyphens
- Tighten isValidGitHubRepoName: reject reserved `.` and `..`, reject
`.git` suffix, cap at 100 chars
- Add unit tests for both validators (15 cases) and for the new
buildSessionInsertRow (4 cases)
- Split createSessionHandler tests into auth/validation + persistence
files; share fixtures via createSessionHandlerFixtures.ts. All test
files now under 100 lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): address second round of PR review
- 500 message: "Failed to create session" → "Internal server error"
(per cubic.dev standardized 500 envelope feedback)
- SRP: extract failedToCreateSession to lib/sessions/failedToCreateSession.ts
- YAGNI: drop repoOwner from request body and remove
isValidGitHubRepoOwner helper entirely (recoupable is the only
owner; no need to validate)
- YAGNI: drop repoName from request body and remove
isValidGitHubRepoName helper (repo identity is derived server-side
from the authenticated account, not accepted from user input)
- Single-export per file: split createSessionHandlerFixtures.ts into
makeCreateSessionReq.ts, baseSessionRow.ts, baseChatRow.ts.
okAuth constant inlined where used.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): port random-city title fallback from open-agents
Generated session titles now match the open-agents UX — names like
"Anchorage", "Vienna", "Philadelphia" — instead of every untitled
session being called "New session". Closes a wire-shape gap with
open-agents production identified by the head-to-head test on PR.
Pieces:
- lib/sessions/cityNames.ts: ~200-city curated list (verbatim port)
- lib/sessions/getRandomCityName.ts: pick a city not in `usedNames`,
numeric-suffix fallback when the curated list is exhausted
- lib/supabase/sessions/selectSessionTitlesByAccountId.ts: Supabase
helper for collision avoidance
- lib/sessions/resolveSessionTitle.ts: orchestrates provided title
(trimmed) > random city fallback. Async. Kept separate from the
insert-row builder so that stays synchronous + pure.
- buildSessionInsertRow now takes `title` as a parameter
- createSessionHandler awaits resolveSessionTitle before building the
row
TDD: 4 tests for getRandomCityName, 4 for resolveSessionTitle. Handler
tests updated to mock resolveSessionTitle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: remove GET-only files (scope this PR to POST)
The GET endpoint + handler + tests live in PR #514 and were
inadvertently brought in when this branch was rebased after #514's
work. This PR is scoped to POST only; GET ships in #514.
Shared infrastructure stays (types/database.types.ts regen +
lib/sessions/toSessionResponse.ts) — both are required by the POST
handler too. When either #514 or this PR merges to test first, the
other will see those files already present and resolve cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): consolidate request validation + DRY supabase select
Two reviewer asks rolled into one commit:
SRP — validateCreateSessionBody now owns the full validation flow.
The handler used to call safeParseJson, validateAuthContext, and the
Zod body schema separately; that was three places to short-circuit
and three places to duplicate the error envelope. Folded them into
validateCreateSessionBody so the handler does one call → success or
NextResponse error. Returns { body, auth } on success.
DRY — replaced lib/supabase/sessions/selectSession.ts and
selectSessionTitlesByAccountId.ts with a single
selectSessions({ id?, accountId? }) that supports both call sites.
resolveSessionTitle now derives titles from the general fetch.
Tests:
- New validateCreateSessionBody.test.ts covers auth-failure / 400 /
success / malformed-JSON tolerance (4 cases)
- Handler tests now mock validateCreateSessionBody (single mock
surface instead of three)
- resolveSessionTitle tests mock selectSessions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): address automated review feedback
Four small fixes from the latest round:
1. Zod v4 migration: { message } → { error } on the sandboxType
literal. v4 unified the error customization API; { message } is
deprecated.
2. Orphan rollback observability: when insertChat fails AND the
session-rollback delete also fails, log the session id so ops
can detect orphaned rows. New persistence test asserts the log.
3. Defensive try/catch in selectSessions so a thrown exception
(network-level rejection, not a Supabase {error} return) doesn't
bubble up and 500 the entire session-creation flow.
4. Deterministic test for getRandomCityName suffix-increment: pin
Math.random instead of looping until the random pick lands on
baseCity. Previous test could pass without ever asserting if the
loop cap was hit.
Skipped: cubic-dev-ai's note about logging raw sessionId in
selectSession.ts — that file was deleted earlier in this PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: prettier format fix on persistence test
The new orphan-session test had a line that exceeded prettier's wrap
width. Auto-format fixed it; format-check now clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sweetmantech added a commit that referenced this pull request May 4, 2026
* refactor(sandbox): callers use open-agents abstraction (Phase 2.2) (#509)
* refactor(sandbox): callers use open-agents abstraction (Phase 2.2)
Replaces direct @vercel/sandbox SDK calls with the open-agents sandbox
abstraction layer (inlined in Phase 2.1) for sandbox lifecycle (create
+ reconnect). HTTP response shapes preserved exactly.
Per the agreed Option B (hybrid): only the lifecycle creator helpers
get refactored. installClaudeCode / runClaudeCode / getSandboxStatus
stay on the SDK directly because the abstraction does not cover their
needs (sudo, stdout/stderr streaming, simple status reads). Those
two install/run files are also dead orphans (defined but never called)
and will be removed entirely after the full migration.
Production refactor:
createSandbox.ts Sandbox.create(...) -> VercelSandbox.create(...)
Input: VercelSandboxConfig (was SDK params)
Snapshot trigger: restoreSnapshotId field
(was source: { type: "snapshot", ... })
Returns VercelSandbox (was SDK Sandbox)
createSandboxWithFallback.ts cascade — passes restoreSnapshotId to createSandbox
createSandboxFromSnapshot.ts type cascade only (Sandbox -> VercelSandbox)
getActiveSandbox.ts Sandbox.get({name}) -> VercelSandbox.connect(name, {})
Status check: sandbox.status -> sandbox.sdkStatus
getOrCreateSandbox.ts no code change — type cascades automatically
processCreateSandbox.ts reads sandbox.sdkStatus instead of sandbox.status
defensive nullish on createdAt
Abstraction extension:
vercel/sandbox/VercelSandbox.ts adds two readonly getters following
the existing host/environmentDetails/expiresAt pattern:
get sdkStatus(): string — raw SDK session status (running/pending/
stopped/failed/aborted/snapshotting),
distinct from the abstraction's normalized
status getter
get createdAt(): Date | undefined — SDK session.createdAt
These give api callers what they need to construct the existing
HTTP response shape without breaking the abstraction's interface.
Tests updated:
createSandbox.test.ts mocks VercelSandbox.create instead of
Sandbox.create; mock object uses
sdkStatus instead of status
createSandboxWithFallback.test.ts asserts restoreSnapshotId pass-through
getActiveSandbox.test.ts mocks VercelSandbox.connect; sdkStatus
on mock objects
processCreateSandbox.test.ts mockSandbox uses sdkStatus
Verification:
- pnpm lint:check: clean
- pnpm test: 2391/2391 pass
- HTTP response shape unchanged: same fields, same enum values for
sandboxStatus (sourced from the SDK now via sdkStatus, was directly
via SDK Sandbox.status before — identical strings either way)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address PR #509 review feedback
Three real issues from CodeRabbit + cubic:
1. createdAt staleness (CodeRabbit minor)
The new `createdAt` getter on VercelSandbox skipped the
`refreshStateFromCurrentSession()` step that `sdkStatus` uses, so
readers right after a reconnect could see stale session metadata.
Add the refresh.
2. Fabricated createdAt (cubic P2)
Both createSandbox.ts and processCreateSandbox.ts had a
`?? new Date().toISOString()` fallback that fabricated creation
metadata when sandbox.createdAt was missing. The SDK guarantees
createdAt is populated for any reachable instance, so the fallback
was both wrong (fabricates data) and unnecessary.
Tighten the getter to return `Date` (not `Date | undefined`) and
throw with an explicit "SDK contract violation" message if the
field is missing — fail-fast surfaces a real contract bug instead
of silently lying.
Drop the `?? new Date()` fallbacks at both call sites.
3. Misleading snapshot-restore branching (CodeRabbit major)
createSandbox.ts had two paths — a "snapshot" branch that omitted
DEFAULT_VCPUS/DEFAULT_RUNTIME (intent: let snapshot dictate), and
a "fresh" branch that applied defaults. But VercelSandbox.create
internally defaults vcpus=4 and runtime="node22" regardless, so
the omission was a no-op — the abstraction always forwarded those
to the SDK.
Drop the misleading branching. Document the actual behavior at
the top of createSandbox: "VercelSandbox.create applies its own
defaults regardless of source — those apply to the runtime
resources of the new sandbox even when restoring from a snapshot."
Updated the snapshot-restore test to assert the actual call shape
(vcpus + runtime + timeout + restoreSnapshotId) instead of just
the original SDK-style truncated args.
Verification:
- pnpm lint:check: clean
- pnpm test: 2391/2391 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(sandbox): delete dead Claude Code helpers (Phase 2.3) (#512)
* chore(sandbox): delete dead Claude Code helpers (Phase 2.3)
installClaudeCode and runClaudeCode were defined but never imported
anywhere in api production code — confirmed by grep on main:
$ grep -rn "installClaudeCode\b\|runClaudeCode\b" lib/ app/
lib/sandbox/installClaudeCode.ts:9: export async function installClaudeCode(...)
lib/sandbox/runClaudeCode.ts:10: export async function runClaudeCode(...)
Both files were skipped during the Phase 2.2 abstraction refactor
(per the agreed Option B — they used SDK features the abstraction
doesn't expose: sudo, stdout/stderr streaming, batched writes). With
the broader migration moving to Vercel Workflow + open-agents' agent
package for sandbox bootstrap, these orphans have no path to being
called again.
Removed:
lib/sandbox/installClaudeCode.ts (32 lines)
lib/sandbox/runClaudeCode.ts (29 lines)
lib/sandbox/__tests__/installClaudeCode.test.ts (4 tests)
lib/sandbox/__tests__/runClaudeCode.test.ts (6 tests)
Verification:
- pnpm lint:check: clean
- pnpm test: 2381/2381 pass (was 2391 — net -10 tests from the
two deleted test files)
Note: getOrCreateSandbox.ts also has zero importers per the audit
and is similarly dead, but is intentionally NOT deleted in this PR
since it was not explicitly flagged as orphan in the migration plan.
Worth a separate follow-up decision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(sandbox): also delete getOrCreateSandbox + getActiveSandbox (YAGNI)
Cascade audit found two more truly-dead helpers per YAGNI:
getOrCreateSandbox.ts 0 importers (self-only references)
getActiveSandbox.ts only called by getOrCreateSandbox — orphan
once that goes
Removed:
lib/sandbox/getOrCreateSandbox.ts (39 lines)
lib/sandbox/getActiveSandbox.ts (33 lines)
lib/sandbox/__tests__/getOrCreateSandbox.test.ts (3 tests)
lib/sandbox/__tests__/getActiveSandbox.test.ts (4 tests)
Live consumers of related helpers preserved:
- createSandboxFromSnapshot still used by processCreateSandbox
- selectAccountSandboxes still used by aggregateAccountSandboxStats,
buildGetSandboxesParams, getSandboxesHandler, validateGetSandboxesRequest
Verification:
- pnpm lint:check: clean
- pnpm test: 2374/2374 pass (was 2381 — net -7 from the two deleted
test files; -3 from getOrCreateSandbox.test.ts + -4 from
getActiveSandbox.test.ts)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): port POST /api/sessions from open-agents (#515)
* feat(sessions): port GET /api/sessions/[sessionId] from open-agents (Phase 2.4 — first route)
First route in the route-by-route cutover plan. Strategy: open-agents
frontend stays unchanged in shape; api ports each route it calls in
priority order (simplest first), and the open-agents frontend gets
cut over to api one route at a time.
Why this route first:
- Pure DB read (single-row select by id) — no agent runner, no Vercel
Workflow, no sandbox runtime
- Hits sessions table already migrated in database PR #20
- Frontend usage: agents-frontend hits /api/sessions/{id} on session
detail page navigation
- Smallest possible blast radius for proving the cutover pattern
Files added:
lib/supabase/sessions/selectSession.ts Single-row helper + SessionRow
type (hand-typed; database.types.ts
regen pending — flagged in code
comment)
app/api/sessions/[sessionId]/route.ts GET handler matching open-agents
response shape exactly (camelCase
fields, "userId" preserved on the
wire even though stored as
account_id internally)
app/api/sessions/[sessionId]/__tests__/route.test.ts (5 tests)
Auth: validateAuthContext (Privy Bearer or x-api-key). Response codes
match open-agents: 200 happy path, 401 no auth, 403 not owner, 404 not
found.
Wire-format translation: snake_case Supabase row -> camelCase response,
with account_id surfaced as userId so the existing open-agents frontend
fetches with zero code changes. Translation lives at the route boundary
(toSessionResponse) where it is easy to remove once chat absorbs this
UI and we can switch to schema-natural naming.
Verification:
- pnpm lint:check: clean
- pnpm test: 2379/2379 pass (5 new for this route)
Up next:
- Cutover step (separate PR in open-agents): point the frontend at
api's URL for this single route. Validate end-to-end before porting
the next route.
- Next routes in priority order (still pure DB, no agent/workflow):
GET /api/sessions (list with unread — needs Postgres RPC for the
multi-table aggregation), GET /api/sessions/[id]/chats, GET
/api/sessions/[id]/chats/[chatId].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address PR review — SRP splits + use Tables<\"sessions\"> from regen'd types
Three review comments on PR #514:
1. SRP: extract toSessionResponse to its own file
was: defined inline in app/api/sessions/[sessionId]/route.ts
now: lib/sessions/toSessionResponse.ts (one exported fn per file)
2. SRP: add a handler function (mirroring api convention)
was: GET handler logic inline in route.ts
now: lib/sessions/getSessionByIdHandler.ts contains all the auth +
ownership + DB lookup + response logic; route.ts is a thin
shell that awaits options.params and delegates. Matches the
pattern used by every other api route (e.g. socials/[id]/scrape,
artists/[id]/...).
3. DRY: use existing db schema type
was: hand-typed SessionRow interface in selectSession.ts
now: Tables<\"sessions\"> from types/database.types.ts (regenerated
via npx supabase gen types typescript --project-id ...
--schema public)
The types regen also resolved the preview-build failure
(\"Type instantiation is excessively deep and possibly infinite\") on
the .from(\"sessions\") call — Supabase's type inference was choking
because the table was unknown to the generic.
Files added:
lib/sessions/toSessionResponse.ts
lib/sessions/getSessionByIdHandler.ts
Files modified:
app/api/sessions/[sessionId]/route.ts thin shell now
app/api/sessions/[sessionId]/__tests__/
route.test.ts type alias updated
lib/supabase/sessions/selectSession.ts Tables<\"sessions\">
types/database.types.ts Supabase regen
Verification:
- pnpm lint:check: clean
- pnpm test: 2379/2379 pass (no test changes; same 5 route tests)
- tsc compile clean (the local pnpm build progresses past compile
into page-data collection where it fails on missing local env
vars — Vercel preview will have those set, so the preview rebuild
should now succeed)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): make 404/403 errors emit status:"error" for shape consistency
The 401 returned by validateAuthContext shaped like
{status:"error", error:"..."} but 404/403 from this handler returned
{error:"..."} only. Same endpoint, two error shapes — inconsistent for
clients. Align all error responses on the validateAuthContext shape.
Tests now assert the full error body, not just the status code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): port POST /api/sessions from open-agents
Implements the POST /api/sessions contract documented in
recoupable/docs PR #186 + #187. Creates a session row and an
initial chat row; rolls back the session if chat insert fails so
callers never observe an orphaned session.
Auth: validateAuthContext (Privy Bearer or x-api-key).
Validation: Zod schema + GitHub repo segment regex. Body is
optional — empty body creates a session with sensible defaults
(status=running, lifecycle_state=provisioning, sandbox_state.type=
vercel, title="New session").
Out of scope (will follow once database catches up):
auto_commit_push_override, auto_create_pr_override, pr_number,
pr_status — these columns don't yet exist on api's sessions
table, so the docs spec was trimmed accordingly in docs PR #187.
TDD: 9 handler tests cover 401, 400 (sandboxType / repoOwner /
repoName), 200 happy path, branch generation, title pass-through,
500 (insertSession failure), and 500-with-rollback (insertChat
failure). Plus 1 thin test on the route shell.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): add OPTIONS handler + cache directives to POST route
Match the convention from app/api/sessions/[sessionId]/route.ts:
- OPTIONS handler returning 200 + CORS headers (preflight)
- dynamic="force-dynamic", fetchCache="force-no-store", revalidate=0
POST routes that mutate DB shouldn't be cached, and browsers issuing
preflight checks (POST with JSON body + custom auth headers) need
OPTIONS to respond.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): address PR review feedback
- SRP: extract insert-row construction to lib/sessions/buildSessionInsertRow.ts
- YAGNI: drop generateSessionBranchName + isNewBranch handling (sessions
commit to whatever branch the client provides; auto-generation was
speculative)
- Tighten isValidGitHubRepoOwner: GitHub's actual rules are alphanumeric
+ hyphen only (no `_` or `.`), 1-39 chars, no leading/trailing or
consecutive hyphens
- Tighten isValidGitHubRepoName: reject reserved `.` and `..`, reject
`.git` suffix, cap at 100 chars
- Add unit tests for both validators (15 cases) and for the new
buildSessionInsertRow (4 cases)
- Split createSessionHandler tests into auth/validation + persistence
files; share fixtures via createSessionHandlerFixtures.ts. All test
files now under 100 lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): address second round of PR review
- 500 message: "Failed to create session" → "Internal server error"
(per cubic.dev standardized 500 envelope feedback)
- SRP: extract failedToCreateSession to lib/sessions/failedToCreateSession.ts
- YAGNI: drop repoOwner from request body and remove
isValidGitHubRepoOwner helper entirely (recoupable is the only
owner; no need to validate)
- YAGNI: drop repoName from request body and remove
isValidGitHubRepoName helper (repo identity is derived server-side
from the authenticated account, not accepted from user input)
- Single-export per file: split createSessionHandlerFixtures.ts into
makeCreateSessionReq.ts, baseSessionRow.ts, baseChatRow.ts.
okAuth constant inlined where used.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): port random-city title fallback from open-agents
Generated session titles now match the open-agents UX — names like
"Anchorage", "Vienna", "Philadelphia" — instead of every untitled
session being called "New session". Closes a wire-shape gap with
open-agents production identified by the head-to-head test on PR.
Pieces:
- lib/sessions/cityNames.ts: ~200-city curated list (verbatim port)
- lib/sessions/getRandomCityName.ts: pick a city not in `usedNames`,
numeric-suffix fallback when the curated list is exhausted
- lib/supabase/sessions/selectSessionTitlesByAccountId.ts: Supabase
helper for collision avoidance
- lib/sessions/resolveSessionTitle.ts: orchestrates provided title
(trimmed) > random city fallback. Async. Kept separate from the
insert-row builder so that stays synchronous + pure.
- buildSessionInsertRow now takes `title` as a parameter
- createSessionHandler awaits resolveSessionTitle before building the
row
TDD: 4 tests for getRandomCityName, 4 for resolveSessionTitle. Handler
tests updated to mock resolveSessionTitle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: remove GET-only files (scope this PR to POST)
The GET endpoint + handler + tests live in PR #514 and were
inadvertently brought in when this branch was rebased after #514's
work. This PR is scoped to POST only; GET ships in #514.
Shared infrastructure stays (types/database.types.ts regen +
lib/sessions/toSessionResponse.ts) — both are required by the POST
handler too. When either #514 or this PR merges to test first, the
other will see those files already present and resolve cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): consolidate request validation + DRY supabase select
Two reviewer asks rolled into one commit:
SRP — validateCreateSessionBody now owns the full validation flow.
The handler used to call safeParseJson, validateAuthContext, and the
Zod body schema separately; that was three places to short-circuit
and three places to duplicate the error envelope. Folded them into
validateCreateSessionBody so the handler does one call → success or
NextResponse error. Returns { body, auth } on success.
DRY — replaced lib/supabase/sessions/selectSession.ts and
selectSessionTitlesByAccountId.ts with a single
selectSessions({ id?, accountId? }) that supports both call sites.
resolveSessionTitle now derives titles from the general fetch.
Tests:
- New validateCreateSessionBody.test.ts covers auth-failure / 400 /
success / malformed-JSON tolerance (4 cases)
- Handler tests now mock validateCreateSessionBody (single mock
surface instead of three)
- resolveSessionTitle tests mock selectSessions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): address automated review feedback
Four small fixes from the latest round:
1. Zod v4 migration: { message } → { error } on the sandboxType
literal. v4 unified the error customization API; { message } is
deprecated.
2. Orphan rollback observability: when insertChat fails AND the
session-rollback delete also fails, log the session id so ops
can detect orphaned rows. New persistence test asserts the log.
3. Defensive try/catch in selectSessions so a thrown exception
(network-level rejection, not a Supabase {error} return) doesn't
bubble up and 500 the entire session-creation flow.
4. Deterministic test for getRandomCityName suffix-increment: pin
Math.random instead of looping until the random pick lands on
baseCity. Previous test could pass without ever asserting if the
loop cap was hit.
Skipped: cubic-dev-ai's note about logging raw sessionId in
selectSession.ts — that file was deleted earlier in this PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: prettier format fix on persistence test
The new orphan-session test had a line that exceeded prettier's wrap
width. Auto-format fixed it; format-check now clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rebased onto current main (which now has the POST endpoint + shared
infra from PR #515). Three pieces of GET-specific work:
- app/api/sessions/[sessionId]/route.ts: thin shell delegating to the
handler, plus OPTIONS for CORS preflight + cache directives
- lib/sessions/getSessionByIdHandler.ts: validates auth via
validateAuthContext, reads via selectSessions({id}), enforces
ownership (403 if account_id mismatch), 404 if missing
- app/api/sessions/[sessionId]/__tests__/route.test.ts: 5 cases —
401 / 404 / 403 / 200 happy path / OPTIONS smoke
Uses the new general selectSessions({id}) reader rather than the
deleted single-purpose selectSession helper. All other shared infra
(types, toSessionResponse) is already on main from #515.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sweetmantech

Copy link
Copy Markdown
ContributorAuthor

Preview deployment smoke test (commit e6aaba94)

Tested against https://api-git-feat-port-get-session-route-recoup.vercel.app. Reused an x-api-key issued earlier via POST /api/agents/signup on this preview's environment.

A session created during the POST PR's smoke test is still in the preview DB on the same account (2db5cda8-8b1c-4c6d-87e9-ed8f10a9757a, session id ec6c10d9-8966-4231-93ea-c1b40ba6caf1, title "Osaka"). Used that as the owned-session fixture.

T1 — 401 no auth

GET /api/sessions/ec6c10d9-8966-4231-93ea-c1b40ba6caf1 HTTP/1.1

Response:

HTTP/2 401
{"status":"error","error":"Exactly one of x-api-key or Authorization must be provided"}

✅ Matches validateAuthContext envelope.

T2 — 404 session not found

GET /api/sessions/00000000-0000-0000-0000-000000000000 HTTP/1.1x-api-key: recoup_sk_…

Response:

HTTP/2 404
{"status":"error","error":"Session not found"}

✅ Matches docs PR #185 spec for 404 response.

T3 — 200 owned session (full Session shape)

GET /api/sessions/ec6c10d9-8966-4231-93ea-c1b40ba6caf1 HTTP/1.1x-api-key: recoup_sk_…

Response:

HTTP/2 200
{
"session": {
"id": "ec6c10d9-8966-4231-93ea-c1b40ba6caf1",
"userId": "2db5cda8-8b1c-4c6d-87e9-ed8f10a9757a",
"title": "Osaka",
"status": "running",
"repoOwner": null, "repoName": null, "branch": null, "cloneUrl": null,
"isNewBranch": false,
"globalSkillRefs": [],
"sandboxState": { "type": "vercel" },
"lifecycleState": "provisioning",
"lifecycleVersion": 0,
"lastActivityAt": null, "sandboxExpiresAt": null, "hibernateAfter": null,
"lifecycleRunId": null, "lifecycleError": null,
"linesAdded": 0, "linesRemoved": 0,
"snapshotUrl": null, "snapshotCreatedAt": null, "snapshotSizeBytes": null,
"cachedDiff": null, "cachedDiffUpdatedAt": null,
"createdAt": "2026-05-04T23:23:20.653727+00:00",
"updatedAt": "2026-05-04T23:23:20.653727+00:00"
}
}

Field-by-field check against docs PR #185 spec:

  • userId (UUID) ✓, title ✓, status ✓, all repo fields nullable ✓, isNewBranch
  • globalSkillRefs array, sandboxState object ✓
  • lifecycleState, lifecycleVersion
  • All nullable timestamps + nullable lifecycle fields ✓
  • linesAdded / linesRemoved
  • All snapshot/diff fields ✓
  • createdAt / updatedAt

The 4 trimmed fields (autoCommitPushOverride, autoCreatePrOverride, prNumber, prStatus) correctly absent — matches the trimmed Session schema in docs.

403 cross-account ownership

Couldn't trigger live (would need a session owned by a different account). Covered by unit test app/api/sessions/[sessionId]/__tests__/route.test.ts:100-116 which mocks validateAuthContext to return accountId: "acc-uuid-OTHER" while selectSessions returns a row owned by acc-uuid-1, asserting 403 {status:"error", error:"Forbidden"}.

Unit test summary

29 tests pass (5 added by this PR, 24 existing from the POST PR). Format + lint clean.

@sweetmantech
sweetmantech merged commit b2dd017 into testMay 5, 2026
6 checks passed
@sweetmantech
sweetmantech deleted the feat/port-get-session-route branch May 5, 2026 00:00
sweetmantech added a commit that referenced this pull request May 5, 2026
* refactor(sandbox): callers use open-agents abstraction (Phase 2.2) (#509)
* refactor(sandbox): callers use open-agents abstraction (Phase 2.2)
Replaces direct @vercel/sandbox SDK calls with the open-agents sandbox
abstraction layer (inlined in Phase 2.1) for sandbox lifecycle (create
+ reconnect). HTTP response shapes preserved exactly.
Per the agreed Option B (hybrid): only the lifecycle creator helpers
get refactored. installClaudeCode / runClaudeCode / getSandboxStatus
stay on the SDK directly because the abstraction does not cover their
needs (sudo, stdout/stderr streaming, simple status reads). Those
two install/run files are also dead orphans (defined but never called)
and will be removed entirely after the full migration.
Production refactor:
createSandbox.ts Sandbox.create(...) -> VercelSandbox.create(...)
Input: VercelSandboxConfig (was SDK params)
Snapshot trigger: restoreSnapshotId field
(was source: { type: "snapshot", ... })
Returns VercelSandbox (was SDK Sandbox)
createSandboxWithFallback.ts cascade — passes restoreSnapshotId to createSandbox
createSandboxFromSnapshot.ts type cascade only (Sandbox -> VercelSandbox)
getActiveSandbox.ts Sandbox.get({name}) -> VercelSandbox.connect(name, {})
Status check: sandbox.status -> sandbox.sdkStatus
getOrCreateSandbox.ts no code change — type cascades automatically
processCreateSandbox.ts reads sandbox.sdkStatus instead of sandbox.status
defensive nullish on createdAt
Abstraction extension:
vercel/sandbox/VercelSandbox.ts adds two readonly getters following
the existing host/environmentDetails/expiresAt pattern:
get sdkStatus(): string — raw SDK session status (running/pending/
stopped/failed/aborted/snapshotting),
distinct from the abstraction's normalized
status getter
get createdAt(): Date | undefined — SDK session.createdAt
These give api callers what they need to construct the existing
HTTP response shape without breaking the abstraction's interface.
Tests updated:
createSandbox.test.ts mocks VercelSandbox.create instead of
Sandbox.create; mock object uses
sdkStatus instead of status
createSandboxWithFallback.test.ts asserts restoreSnapshotId pass-through
getActiveSandbox.test.ts mocks VercelSandbox.connect; sdkStatus
on mock objects
processCreateSandbox.test.ts mockSandbox uses sdkStatus
Verification:
- pnpm lint:check: clean
- pnpm test: 2391/2391 pass
- HTTP response shape unchanged: same fields, same enum values for
sandboxStatus (sourced from the SDK now via sdkStatus, was directly
via SDK Sandbox.status before — identical strings either way)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address PR #509 review feedback
Three real issues from CodeRabbit + cubic:
1. createdAt staleness (CodeRabbit minor)
The new `createdAt` getter on VercelSandbox skipped the
`refreshStateFromCurrentSession()` step that `sdkStatus` uses, so
readers right after a reconnect could see stale session metadata.
Add the refresh.
2. Fabricated createdAt (cubic P2)
Both createSandbox.ts and processCreateSandbox.ts had a
`?? new Date().toISOString()` fallback that fabricated creation
metadata when sandbox.createdAt was missing. The SDK guarantees
createdAt is populated for any reachable instance, so the fallback
was both wrong (fabricates data) and unnecessary.
Tighten the getter to return `Date` (not `Date | undefined`) and
throw with an explicit "SDK contract violation" message if the
field is missing — fail-fast surfaces a real contract bug instead
of silently lying.
Drop the `?? new Date()` fallbacks at both call sites.
3. Misleading snapshot-restore branching (CodeRabbit major)
createSandbox.ts had two paths — a "snapshot" branch that omitted
DEFAULT_VCPUS/DEFAULT_RUNTIME (intent: let snapshot dictate), and
a "fresh" branch that applied defaults. But VercelSandbox.create
internally defaults vcpus=4 and runtime="node22" regardless, so
the omission was a no-op — the abstraction always forwarded those
to the SDK.
Drop the misleading branching. Document the actual behavior at
the top of createSandbox: "VercelSandbox.create applies its own
defaults regardless of source — those apply to the runtime
resources of the new sandbox even when restoring from a snapshot."
Updated the snapshot-restore test to assert the actual call shape
(vcpus + runtime + timeout + restoreSnapshotId) instead of just
the original SDK-style truncated args.
Verification:
- pnpm lint:check: clean
- pnpm test: 2391/2391 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(sandbox): delete dead Claude Code helpers (Phase 2.3) (#512)
* chore(sandbox): delete dead Claude Code helpers (Phase 2.3)
installClaudeCode and runClaudeCode were defined but never imported
anywhere in api production code — confirmed by grep on main:
$ grep -rn "installClaudeCode\b\|runClaudeCode\b" lib/ app/
lib/sandbox/installClaudeCode.ts:9: export async function installClaudeCode(...)
lib/sandbox/runClaudeCode.ts:10: export async function runClaudeCode(...)
Both files were skipped during the Phase 2.2 abstraction refactor
(per the agreed Option B — they used SDK features the abstraction
doesn't expose: sudo, stdout/stderr streaming, batched writes). With
the broader migration moving to Vercel Workflow + open-agents' agent
package for sandbox bootstrap, these orphans have no path to being
called again.
Removed:
lib/sandbox/installClaudeCode.ts (32 lines)
lib/sandbox/runClaudeCode.ts (29 lines)
lib/sandbox/__tests__/installClaudeCode.test.ts (4 tests)
lib/sandbox/__tests__/runClaudeCode.test.ts (6 tests)
Verification:
- pnpm lint:check: clean
- pnpm test: 2381/2381 pass (was 2391 — net -10 tests from the
two deleted test files)
Note: getOrCreateSandbox.ts also has zero importers per the audit
and is similarly dead, but is intentionally NOT deleted in this PR
since it was not explicitly flagged as orphan in the migration plan.
Worth a separate follow-up decision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(sandbox): also delete getOrCreateSandbox + getActiveSandbox (YAGNI)
Cascade audit found two more truly-dead helpers per YAGNI:
getOrCreateSandbox.ts 0 importers (self-only references)
getActiveSandbox.ts only called by getOrCreateSandbox — orphan
once that goes
Removed:
lib/sandbox/getOrCreateSandbox.ts (39 lines)
lib/sandbox/getActiveSandbox.ts (33 lines)
lib/sandbox/__tests__/getOrCreateSandbox.test.ts (3 tests)
lib/sandbox/__tests__/getActiveSandbox.test.ts (4 tests)
Live consumers of related helpers preserved:
- createSandboxFromSnapshot still used by processCreateSandbox
- selectAccountSandboxes still used by aggregateAccountSandboxStats,
buildGetSandboxesParams, getSandboxesHandler, validateGetSandboxesRequest
Verification:
- pnpm lint:check: clean
- pnpm test: 2374/2374 pass (was 2381 — net -7 from the two deleted
test files; -3 from getOrCreateSandbox.test.ts + -4 from
getActiveSandbox.test.ts)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): port POST /api/sessions from open-agents (#515)
* feat(sessions): port GET /api/sessions/[sessionId] from open-agents (Phase 2.4 — first route)
First route in the route-by-route cutover plan. Strategy: open-agents
frontend stays unchanged in shape; api ports each route it calls in
priority order (simplest first), and the open-agents frontend gets
cut over to api one route at a time.
Why this route first:
- Pure DB read (single-row select by id) — no agent runner, no Vercel
Workflow, no sandbox runtime
- Hits sessions table already migrated in database PR #20
- Frontend usage: agents-frontend hits /api/sessions/{id} on session
detail page navigation
- Smallest possible blast radius for proving the cutover pattern
Files added:
lib/supabase/sessions/selectSession.ts Single-row helper + SessionRow
type (hand-typed; database.types.ts
regen pending — flagged in code
comment)
app/api/sessions/[sessionId]/route.ts GET handler matching open-agents
response shape exactly (camelCase
fields, "userId" preserved on the
wire even though stored as
account_id internally)
app/api/sessions/[sessionId]/__tests__/route.test.ts (5 tests)
Auth: validateAuthContext (Privy Bearer or x-api-key). Response codes
match open-agents: 200 happy path, 401 no auth, 403 not owner, 404 not
found.
Wire-format translation: snake_case Supabase row -> camelCase response,
with account_id surfaced as userId so the existing open-agents frontend
fetches with zero code changes. Translation lives at the route boundary
(toSessionResponse) where it is easy to remove once chat absorbs this
UI and we can switch to schema-natural naming.
Verification:
- pnpm lint:check: clean
- pnpm test: 2379/2379 pass (5 new for this route)
Up next:
- Cutover step (separate PR in open-agents): point the frontend at
api's URL for this single route. Validate end-to-end before porting
the next route.
- Next routes in priority order (still pure DB, no agent/workflow):
GET /api/sessions (list with unread — needs Postgres RPC for the
multi-table aggregation), GET /api/sessions/[id]/chats, GET
/api/sessions/[id]/chats/[chatId].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address PR review — SRP splits + use Tables<\"sessions\"> from regen'd types
Three review comments on PR #514:
1. SRP: extract toSessionResponse to its own file
was: defined inline in app/api/sessions/[sessionId]/route.ts
now: lib/sessions/toSessionResponse.ts (one exported fn per file)
2. SRP: add a handler function (mirroring api convention)
was: GET handler logic inline in route.ts
now: lib/sessions/getSessionByIdHandler.ts contains all the auth +
ownership + DB lookup + response logic; route.ts is a thin
shell that awaits options.params and delegates. Matches the
pattern used by every other api route (e.g. socials/[id]/scrape,
artists/[id]/...).
3. DRY: use existing db schema type
was: hand-typed SessionRow interface in selectSession.ts
now: Tables<\"sessions\"> from types/database.types.ts (regenerated
via npx supabase gen types typescript --project-id ...
--schema public)
The types regen also resolved the preview-build failure
(\"Type instantiation is excessively deep and possibly infinite\") on
the .from(\"sessions\") call — Supabase's type inference was choking
because the table was unknown to the generic.
Files added:
lib/sessions/toSessionResponse.ts
lib/sessions/getSessionByIdHandler.ts
Files modified:
app/api/sessions/[sessionId]/route.ts thin shell now
app/api/sessions/[sessionId]/__tests__/
route.test.ts type alias updated
lib/supabase/sessions/selectSession.ts Tables<\"sessions\">
types/database.types.ts Supabase regen
Verification:
- pnpm lint:check: clean
- pnpm test: 2379/2379 pass (no test changes; same 5 route tests)
- tsc compile clean (the local pnpm build progresses past compile
into page-data collection where it fails on missing local env
vars — Vercel preview will have those set, so the preview rebuild
should now succeed)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): make 404/403 errors emit status:"error" for shape consistency
The 401 returned by validateAuthContext shaped like
{status:"error", error:"..."} but 404/403 from this handler returned
{error:"..."} only. Same endpoint, two error shapes — inconsistent for
clients. Align all error responses on the validateAuthContext shape.
Tests now assert the full error body, not just the status code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): port POST /api/sessions from open-agents
Implements the POST /api/sessions contract documented in
recoupable/docs PR #186 + #187. Creates a session row and an
initial chat row; rolls back the session if chat insert fails so
callers never observe an orphaned session.
Auth: validateAuthContext (Privy Bearer or x-api-key).
Validation: Zod schema + GitHub repo segment regex. Body is
optional — empty body creates a session with sensible defaults
(status=running, lifecycle_state=provisioning, sandbox_state.type=
vercel, title="New session").
Out of scope (will follow once database catches up):
auto_commit_push_override, auto_create_pr_override, pr_number,
pr_status — these columns don't yet exist on api's sessions
table, so the docs spec was trimmed accordingly in docs PR #187.
TDD: 9 handler tests cover 401, 400 (sandboxType / repoOwner /
repoName), 200 happy path, branch generation, title pass-through,
500 (insertSession failure), and 500-with-rollback (insertChat
failure). Plus 1 thin test on the route shell.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): add OPTIONS handler + cache directives to POST route
Match the convention from app/api/sessions/[sessionId]/route.ts:
- OPTIONS handler returning 200 + CORS headers (preflight)
- dynamic="force-dynamic", fetchCache="force-no-store", revalidate=0
POST routes that mutate DB shouldn't be cached, and browsers issuing
preflight checks (POST with JSON body + custom auth headers) need
OPTIONS to respond.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): address PR review feedback
- SRP: extract insert-row construction to lib/sessions/buildSessionInsertRow.ts
- YAGNI: drop generateSessionBranchName + isNewBranch handling (sessions
commit to whatever branch the client provides; auto-generation was
speculative)
- Tighten isValidGitHubRepoOwner: GitHub's actual rules are alphanumeric
+ hyphen only (no `_` or `.`), 1-39 chars, no leading/trailing or
consecutive hyphens
- Tighten isValidGitHubRepoName: reject reserved `.` and `..`, reject
`.git` suffix, cap at 100 chars
- Add unit tests for both validators (15 cases) and for the new
buildSessionInsertRow (4 cases)
- Split createSessionHandler tests into auth/validation + persistence
files; share fixtures via createSessionHandlerFixtures.ts. All test
files now under 100 lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): address second round of PR review
- 500 message: "Failed to create session" → "Internal server error"
(per cubic.dev standardized 500 envelope feedback)
- SRP: extract failedToCreateSession to lib/sessions/failedToCreateSession.ts
- YAGNI: drop repoOwner from request body and remove
isValidGitHubRepoOwner helper entirely (recoupable is the only
owner; no need to validate)
- YAGNI: drop repoName from request body and remove
isValidGitHubRepoName helper (repo identity is derived server-side
from the authenticated account, not accepted from user input)
- Single-export per file: split createSessionHandlerFixtures.ts into
makeCreateSessionReq.ts, baseSessionRow.ts, baseChatRow.ts.
okAuth constant inlined where used.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): port random-city title fallback from open-agents
Generated session titles now match the open-agents UX — names like
"Anchorage", "Vienna", "Philadelphia" — instead of every untitled
session being called "New session". Closes a wire-shape gap with
open-agents production identified by the head-to-head test on PR.
Pieces:
- lib/sessions/cityNames.ts: ~200-city curated list (verbatim port)
- lib/sessions/getRandomCityName.ts: pick a city not in `usedNames`,
numeric-suffix fallback when the curated list is exhausted
- lib/supabase/sessions/selectSessionTitlesByAccountId.ts: Supabase
helper for collision avoidance
- lib/sessions/resolveSessionTitle.ts: orchestrates provided title
(trimmed) > random city fallback. Async. Kept separate from the
insert-row builder so that stays synchronous + pure.
- buildSessionInsertRow now takes `title` as a parameter
- createSessionHandler awaits resolveSessionTitle before building the
row
TDD: 4 tests for getRandomCityName, 4 for resolveSessionTitle. Handler
tests updated to mock resolveSessionTitle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: remove GET-only files (scope this PR to POST)
The GET endpoint + handler + tests live in PR #514 and were
inadvertently brought in when this branch was rebased after #514's
work. This PR is scoped to POST only; GET ships in #514.
Shared infrastructure stays (types/database.types.ts regen +
lib/sessions/toSessionResponse.ts) — both are required by the POST
handler too. When either #514 or this PR merges to test first, the
other will see those files already present and resolve cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): consolidate request validation + DRY supabase select
Two reviewer asks rolled into one commit:
SRP — validateCreateSessionBody now owns the full validation flow.
The handler used to call safeParseJson, validateAuthContext, and the
Zod body schema separately; that was three places to short-circuit
and three places to duplicate the error envelope. Folded them into
validateCreateSessionBody so the handler does one call → success or
NextResponse error. Returns { body, auth } on success.
DRY — replaced lib/supabase/sessions/selectSession.ts and
selectSessionTitlesByAccountId.ts with a single
selectSessions({ id?, accountId? }) that supports both call sites.
resolveSessionTitle now derives titles from the general fetch.
Tests:
- New validateCreateSessionBody.test.ts covers auth-failure / 400 /
success / malformed-JSON tolerance (4 cases)
- Handler tests now mock validateCreateSessionBody (single mock
surface instead of three)
- resolveSessionTitle tests mock selectSessions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): address automated review feedback
Four small fixes from the latest round:
1. Zod v4 migration: { message } → { error } on the sandboxType
literal. v4 unified the error customization API; { message } is
deprecated.
2. Orphan rollback observability: when insertChat fails AND the
session-rollback delete also fails, log the session id so ops
can detect orphaned rows. New persistence test asserts the log.
3. Defensive try/catch in selectSessions so a thrown exception
(network-level rejection, not a Supabase {error} return) doesn't
bubble up and 500 the entire session-creation flow.
4. Deterministic test for getRandomCityName suffix-increment: pin
Math.random instead of looping until the random pick lands on
baseCity. Previous test could pass without ever asserting if the
loop cap was hit.
Skipped: cubic-dev-ai's note about logging raw sessionId in
selectSession.ts — that file was deleted earlier in this PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: prettier format fix on persistence test
The new orphan-session test had a line that exceeded prettier's wrap
width. Auto-format fixed it; format-check now clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): port GET /api/sessions/[sessionId] from open-agents (#514)
Rebased onto current main (which now has the POST endpoint + shared
infra from PR #515). Three pieces of GET-specific work:
- app/api/sessions/[sessionId]/route.ts: thin shell delegating to the
handler, plus OPTIONS for CORS preflight + cache directives
- lib/sessions/getSessionByIdHandler.ts: validates auth via
validateAuthContext, reads via selectSessions({id}), enforces
ownership (403 if account_id mismatch), 404 if missing
- app/api/sessions/[sessionId]/__tests__/route.test.ts: 5 cases —
401 / 404 / 403 / 200 happy path / OPTIONS smoke
Uses the new general selectSessions({id}) reader rather than the
deleted single-purpose selectSession helper. All other shared infra
(types, toSessionResponse) is already on main from #515.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sweetmantech added a commit that referenced this pull request May 5, 2026
* refactor(sandbox): callers use open-agents abstraction (Phase 2.2) (#509)
* refactor(sandbox): callers use open-agents abstraction (Phase 2.2)
Replaces direct @vercel/sandbox SDK calls with the open-agents sandbox
abstraction layer (inlined in Phase 2.1) for sandbox lifecycle (create
+ reconnect). HTTP response shapes preserved exactly.
Per the agreed Option B (hybrid): only the lifecycle creator helpers
get refactored. installClaudeCode / runClaudeCode / getSandboxStatus
stay on the SDK directly because the abstraction does not cover their
needs (sudo, stdout/stderr streaming, simple status reads). Those
two install/run files are also dead orphans (defined but never called)
and will be removed entirely after the full migration.
Production refactor:
createSandbox.ts Sandbox.create(...) -> VercelSandbox.create(...)
Input: VercelSandboxConfig (was SDK params)
Snapshot trigger: restoreSnapshotId field
(was source: { type: "snapshot", ... })
Returns VercelSandbox (was SDK Sandbox)
createSandboxWithFallback.ts cascade — passes restoreSnapshotId to createSandbox
createSandboxFromSnapshot.ts type cascade only (Sandbox -> VercelSandbox)
getActiveSandbox.ts Sandbox.get({name}) -> VercelSandbox.connect(name, {})
Status check: sandbox.status -> sandbox.sdkStatus
getOrCreateSandbox.ts no code change — type cascades automatically
processCreateSandbox.ts reads sandbox.sdkStatus instead of sandbox.status
defensive nullish on createdAt
Abstraction extension:
vercel/sandbox/VercelSandbox.ts adds two readonly getters following
the existing host/environmentDetails/expiresAt pattern:
get sdkStatus(): string — raw SDK session status (running/pending/
stopped/failed/aborted/snapshotting),
distinct from the abstraction's normalized
status getter
get createdAt(): Date | undefined — SDK session.createdAt
These give api callers what they need to construct the existing
HTTP response shape without breaking the abstraction's interface.
Tests updated:
createSandbox.test.ts mocks VercelSandbox.create instead of
Sandbox.create; mock object uses
sdkStatus instead of status
createSandboxWithFallback.test.ts asserts restoreSnapshotId pass-through
getActiveSandbox.test.ts mocks VercelSandbox.connect; sdkStatus
on mock objects
processCreateSandbox.test.ts mockSandbox uses sdkStatus
Verification:
- pnpm lint:check: clean
- pnpm test: 2391/2391 pass
- HTTP response shape unchanged: same fields, same enum values for
sandboxStatus (sourced from the SDK now via sdkStatus, was directly
via SDK Sandbox.status before — identical strings either way)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address PR #509 review feedback
Three real issues from CodeRabbit + cubic:
1. createdAt staleness (CodeRabbit minor)
The new `createdAt` getter on VercelSandbox skipped the
`refreshStateFromCurrentSession()` step that `sdkStatus` uses, so
readers right after a reconnect could see stale session metadata.
Add the refresh.
2. Fabricated createdAt (cubic P2)
Both createSandbox.ts and processCreateSandbox.ts had a
`?? new Date().toISOString()` fallback that fabricated creation
metadata when sandbox.createdAt was missing. The SDK guarantees
createdAt is populated for any reachable instance, so the fallback
was both wrong (fabricates data) and unnecessary.
Tighten the getter to return `Date` (not `Date | undefined`) and
throw with an explicit "SDK contract violation" message if the
field is missing — fail-fast surfaces a real contract bug instead
of silently lying.
Drop the `?? new Date()` fallbacks at both call sites.
3. Misleading snapshot-restore branching (CodeRabbit major)
createSandbox.ts had two paths — a "snapshot" branch that omitted
DEFAULT_VCPUS/DEFAULT_RUNTIME (intent: let snapshot dictate), and
a "fresh" branch that applied defaults. But VercelSandbox.create
internally defaults vcpus=4 and runtime="node22" regardless, so
the omission was a no-op — the abstraction always forwarded those
to the SDK.
Drop the misleading branching. Document the actual behavior at
the top of createSandbox: "VercelSandbox.create applies its own
defaults regardless of source — those apply to the runtime
resources of the new sandbox even when restoring from a snapshot."
Updated the snapshot-restore test to assert the actual call shape
(vcpus + runtime + timeout + restoreSnapshotId) instead of just
the original SDK-style truncated args.
Verification:
- pnpm lint:check: clean
- pnpm test: 2391/2391 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(sandbox): delete dead Claude Code helpers (Phase 2.3) (#512)
* chore(sandbox): delete dead Claude Code helpers (Phase 2.3)
installClaudeCode and runClaudeCode were defined but never imported
anywhere in api production code — confirmed by grep on main:
$ grep -rn "installClaudeCode\b\|runClaudeCode\b" lib/ app/
lib/sandbox/installClaudeCode.ts:9: export async function installClaudeCode(...)
lib/sandbox/runClaudeCode.ts:10: export async function runClaudeCode(...)
Both files were skipped during the Phase 2.2 abstraction refactor
(per the agreed Option B — they used SDK features the abstraction
doesn't expose: sudo, stdout/stderr streaming, batched writes). With
the broader migration moving to Vercel Workflow + open-agents' agent
package for sandbox bootstrap, these orphans have no path to being
called again.
Removed:
lib/sandbox/installClaudeCode.ts (32 lines)
lib/sandbox/runClaudeCode.ts (29 lines)
lib/sandbox/__tests__/installClaudeCode.test.ts (4 tests)
lib/sandbox/__tests__/runClaudeCode.test.ts (6 tests)
Verification:
- pnpm lint:check: clean
- pnpm test: 2381/2381 pass (was 2391 — net -10 tests from the
two deleted test files)
Note: getOrCreateSandbox.ts also has zero importers per the audit
and is similarly dead, but is intentionally NOT deleted in this PR
since it was not explicitly flagged as orphan in the migration plan.
Worth a separate follow-up decision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(sandbox): also delete getOrCreateSandbox + getActiveSandbox (YAGNI)
Cascade audit found two more truly-dead helpers per YAGNI:
getOrCreateSandbox.ts 0 importers (self-only references)
getActiveSandbox.ts only called by getOrCreateSandbox — orphan
once that goes
Removed:
lib/sandbox/getOrCreateSandbox.ts (39 lines)
lib/sandbox/getActiveSandbox.ts (33 lines)
lib/sandbox/__tests__/getOrCreateSandbox.test.ts (3 tests)
lib/sandbox/__tests__/getActiveSandbox.test.ts (4 tests)
Live consumers of related helpers preserved:
- createSandboxFromSnapshot still used by processCreateSandbox
- selectAccountSandboxes still used by aggregateAccountSandboxStats,
buildGetSandboxesParams, getSandboxesHandler, validateGetSandboxesRequest
Verification:
- pnpm lint:check: clean
- pnpm test: 2374/2374 pass (was 2381 — net -7 from the two deleted
test files; -3 from getOrCreateSandbox.test.ts + -4 from
getActiveSandbox.test.ts)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): port POST /api/sessions from open-agents (#515)
* feat(sessions): port GET /api/sessions/[sessionId] from open-agents (Phase 2.4 — first route)
First route in the route-by-route cutover plan. Strategy: open-agents
frontend stays unchanged in shape; api ports each route it calls in
priority order (simplest first), and the open-agents frontend gets
cut over to api one route at a time.
Why this route first:
- Pure DB read (single-row select by id) — no agent runner, no Vercel
Workflow, no sandbox runtime
- Hits sessions table already migrated in database PR #20
- Frontend usage: agents-frontend hits /api/sessions/{id} on session
detail page navigation
- Smallest possible blast radius for proving the cutover pattern
Files added:
lib/supabase/sessions/selectSession.ts Single-row helper + SessionRow
type (hand-typed; database.types.ts
regen pending — flagged in code
comment)
app/api/sessions/[sessionId]/route.ts GET handler matching open-agents
response shape exactly (camelCase
fields, "userId" preserved on the
wire even though stored as
account_id internally)
app/api/sessions/[sessionId]/__tests__/route.test.ts (5 tests)
Auth: validateAuthContext (Privy Bearer or x-api-key). Response codes
match open-agents: 200 happy path, 401 no auth, 403 not owner, 404 not
found.
Wire-format translation: snake_case Supabase row -> camelCase response,
with account_id surfaced as userId so the existing open-agents frontend
fetches with zero code changes. Translation lives at the route boundary
(toSessionResponse) where it is easy to remove once chat absorbs this
UI and we can switch to schema-natural naming.
Verification:
- pnpm lint:check: clean
- pnpm test: 2379/2379 pass (5 new for this route)
Up next:
- Cutover step (separate PR in open-agents): point the frontend at
api's URL for this single route. Validate end-to-end before porting
the next route.
- Next routes in priority order (still pure DB, no agent/workflow):
GET /api/sessions (list with unread — needs Postgres RPC for the
multi-table aggregation), GET /api/sessions/[id]/chats, GET
/api/sessions/[id]/chats/[chatId].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address PR review — SRP splits + use Tables<\"sessions\"> from regen'd types
Three review comments on PR #514:
1. SRP: extract toSessionResponse to its own file
was: defined inline in app/api/sessions/[sessionId]/route.ts
now: lib/sessions/toSessionResponse.ts (one exported fn per file)
2. SRP: add a handler function (mirroring api convention)
was: GET handler logic inline in route.ts
now: lib/sessions/getSessionByIdHandler.ts contains all the auth +
ownership + DB lookup + response logic; route.ts is a thin
shell that awaits options.params and delegates. Matches the
pattern used by every other api route (e.g. socials/[id]/scrape,
artists/[id]/...).
3. DRY: use existing db schema type
was: hand-typed SessionRow interface in selectSession.ts
now: Tables<\"sessions\"> from types/database.types.ts (regenerated
via npx supabase gen types typescript --project-id ...
--schema public)
The types regen also resolved the preview-build failure
(\"Type instantiation is excessively deep and possibly infinite\") on
the .from(\"sessions\") call — Supabase's type inference was choking
because the table was unknown to the generic.
Files added:
lib/sessions/toSessionResponse.ts
lib/sessions/getSessionByIdHandler.ts
Files modified:
app/api/sessions/[sessionId]/route.ts thin shell now
app/api/sessions/[sessionId]/__tests__/
route.test.ts type alias updated
lib/supabase/sessions/selectSession.ts Tables<\"sessions\">
types/database.types.ts Supabase regen
Verification:
- pnpm lint:check: clean
- pnpm test: 2379/2379 pass (no test changes; same 5 route tests)
- tsc compile clean (the local pnpm build progresses past compile
into page-data collection where it fails on missing local env
vars — Vercel preview will have those set, so the preview rebuild
should now succeed)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): make 404/403 errors emit status:"error" for shape consistency
The 401 returned by validateAuthContext shaped like
{status:"error", error:"..."} but 404/403 from this handler returned
{error:"..."} only. Same endpoint, two error shapes — inconsistent for
clients. Align all error responses on the validateAuthContext shape.
Tests now assert the full error body, not just the status code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): port POST /api/sessions from open-agents
Implements the POST /api/sessions contract documented in
recoupable/docs PR #186 + #187. Creates a session row and an
initial chat row; rolls back the session if chat insert fails so
callers never observe an orphaned session.
Auth: validateAuthContext (Privy Bearer or x-api-key).
Validation: Zod schema + GitHub repo segment regex. Body is
optional — empty body creates a session with sensible defaults
(status=running, lifecycle_state=provisioning, sandbox_state.type=
vercel, title="New session").
Out of scope (will follow once database catches up):
auto_commit_push_override, auto_create_pr_override, pr_number,
pr_status — these columns don't yet exist on api's sessions
table, so the docs spec was trimmed accordingly in docs PR #187.
TDD: 9 handler tests cover 401, 400 (sandboxType / repoOwner /
repoName), 200 happy path, branch generation, title pass-through,
500 (insertSession failure), and 500-with-rollback (insertChat
failure). Plus 1 thin test on the route shell.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): add OPTIONS handler + cache directives to POST route
Match the convention from app/api/sessions/[sessionId]/route.ts:
- OPTIONS handler returning 200 + CORS headers (preflight)
- dynamic="force-dynamic", fetchCache="force-no-store", revalidate=0
POST routes that mutate DB shouldn't be cached, and browsers issuing
preflight checks (POST with JSON body + custom auth headers) need
OPTIONS to respond.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): address PR review feedback
- SRP: extract insert-row construction to lib/sessions/buildSessionInsertRow.ts
- YAGNI: drop generateSessionBranchName + isNewBranch handling (sessions
commit to whatever branch the client provides; auto-generation was
speculative)
- Tighten isValidGitHubRepoOwner: GitHub's actual rules are alphanumeric
+ hyphen only (no `_` or `.`), 1-39 chars, no leading/trailing or
consecutive hyphens
- Tighten isValidGitHubRepoName: reject reserved `.` and `..`, reject
`.git` suffix, cap at 100 chars
- Add unit tests for both validators (15 cases) and for the new
buildSessionInsertRow (4 cases)
- Split createSessionHandler tests into auth/validation + persistence
files; share fixtures via createSessionHandlerFixtures.ts. All test
files now under 100 lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): address second round of PR review
- 500 message: "Failed to create session" → "Internal server error"
(per cubic.dev standardized 500 envelope feedback)
- SRP: extract failedToCreateSession to lib/sessions/failedToCreateSession.ts
- YAGNI: drop repoOwner from request body and remove
isValidGitHubRepoOwner helper entirely (recoupable is the only
owner; no need to validate)
- YAGNI: drop repoName from request body and remove
isValidGitHubRepoName helper (repo identity is derived server-side
from the authenticated account, not accepted from user input)
- Single-export per file: split createSessionHandlerFixtures.ts into
makeCreateSessionReq.ts, baseSessionRow.ts, baseChatRow.ts.
okAuth constant inlined where used.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): port random-city title fallback from open-agents
Generated session titles now match the open-agents UX — names like
"Anchorage", "Vienna", "Philadelphia" — instead of every untitled
session being called "New session". Closes a wire-shape gap with
open-agents production identified by the head-to-head test on PR.
Pieces:
- lib/sessions/cityNames.ts: ~200-city curated list (verbatim port)
- lib/sessions/getRandomCityName.ts: pick a city not in `usedNames`,
numeric-suffix fallback when the curated list is exhausted
- lib/supabase/sessions/selectSessionTitlesByAccountId.ts: Supabase
helper for collision avoidance
- lib/sessions/resolveSessionTitle.ts: orchestrates provided title
(trimmed) > random city fallback. Async. Kept separate from the
insert-row builder so that stays synchronous + pure.
- buildSessionInsertRow now takes `title` as a parameter
- createSessionHandler awaits resolveSessionTitle before building the
row
TDD: 4 tests for getRandomCityName, 4 for resolveSessionTitle. Handler
tests updated to mock resolveSessionTitle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: remove GET-only files (scope this PR to POST)
The GET endpoint + handler + tests live in PR #514 and were
inadvertently brought in when this branch was rebased after #514's
work. This PR is scoped to POST only; GET ships in #514.
Shared infrastructure stays (types/database.types.ts regen +
lib/sessions/toSessionResponse.ts) — both are required by the POST
handler too. When either #514 or this PR merges to test first, the
other will see those files already present and resolve cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): consolidate request validation + DRY supabase select
Two reviewer asks rolled into one commit:
SRP — validateCreateSessionBody now owns the full validation flow.
The handler used to call safeParseJson, validateAuthContext, and the
Zod body schema separately; that was three places to short-circuit
and three places to duplicate the error envelope. Folded them into
validateCreateSessionBody so the handler does one call → success or
NextResponse error. Returns { body, auth } on success.
DRY — replaced lib/supabase/sessions/selectSession.ts and
selectSessionTitlesByAccountId.ts with a single
selectSessions({ id?, accountId? }) that supports both call sites.
resolveSessionTitle now derives titles from the general fetch.
Tests:
- New validateCreateSessionBody.test.ts covers auth-failure / 400 /
success / malformed-JSON tolerance (4 cases)
- Handler tests now mock validateCreateSessionBody (single mock
surface instead of three)
- resolveSessionTitle tests mock selectSessions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sessions): address automated review feedback
Four small fixes from the latest round:
1. Zod v4 migration: { message } → { error } on the sandboxType
literal. v4 unified the error customization API; { message } is
deprecated.
2. Orphan rollback observability: when insertChat fails AND the
session-rollback delete also fails, log the session id so ops
can detect orphaned rows. New persistence test asserts the log.
3. Defensive try/catch in selectSessions so a thrown exception
(network-level rejection, not a Supabase {error} return) doesn't
bubble up and 500 the entire session-creation flow.
4. Deterministic test for getRandomCityName suffix-increment: pin
Math.random instead of looping until the random pick lands on
baseCity. Previous test could pass without ever asserting if the
loop cap was hit.
Skipped: cubic-dev-ai's note about logging raw sessionId in
selectSession.ts — that file was deleted earlier in this PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: prettier format fix on persistence test
The new orphan-session test had a line that exceeded prettier's wrap
width. Auto-format fixed it; format-check now clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sessions): port GET /api/sessions/[sessionId] from open-agents (#514)
Rebased onto current main (which now has the POST endpoint + shared
infra from PR #515). Three pieces of GET-specific work:
- app/api/sessions/[sessionId]/route.ts: thin shell delegating to the
handler, plus OPTIONS for CORS preflight + cache directives
- lib/sessions/getSessionByIdHandler.ts: validates auth via
validateAuthContext, reads via selectSessions({id}), enforces
ownership (403 if account_id mismatch), 404 if missing
- app/api/sessions/[sessionId]/__tests__/route.test.ts: 5 cases —
401 / 404 / 403 / 200 happy path / OPTIONS smoke
Uses the new general selectSessions({id}) reader rather than the
deleted single-purpose selectSession helper. All other shared infra
(types, toSessionResponse) is already on main from #515.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ai/models): enrich response with context_window + cost (#518)
* feat(ai/models): enrich response with context_window + cost from models.dev
api's GET /api/ai/models previously returned just the gateway entries.
Open-agents' frontend depends on two extra fields per model that come
from the public models.dev catalog:
- context_window (integer) — gates model selection in the picker
- cost ({input, output}) — per-million-token pricing for display
Adds three pure helpers (TDD'd individually) plus a small refactor of
the existing fetcher to merge metadata in:
- lib/ai/parseModelsDevMetadata.ts: tolerant unknown→Map parser
- lib/ai/fetchModelsDevMetadata.ts: 750ms-bounded fetch with full
error swallowing (metadata is best-effort, must never gate the
underlying gateway response)
- lib/ai/enrichGatewayModel.ts: pure, non-mutating merge
getAvailableModels now fetches gateway + metadata in parallel and
maps each non-embed model through enrichGatewayModel. If models.dev
is unreachable the response is identical to today (gateway models
unenriched).
Documented in recoupable/docs#188 (merged). Unblocks the eventual
open-agents frontend cutover for the model picker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): extract isRecord into its own lib (SRP)
Per PR feedback: each file should export one primary function.
Pulled isRecord out of parseModelsDevMetadata.ts into
lib/ai/isRecord.ts so the parser file is single-purpose.
Also includes the typecheck fix for enrichGatewayModel — the
`[key: string]: unknown` index signature on its generic constraint
was rejecting `GatewayLanguageModelEntry` and breaking the Vercel
build.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

1 participant

@sweetmantech