Skip to content

Re-point POST /api/chat/generate onto runAgentWorkflow + ephemeral key (chat#1813)#704

Open
sweetmantech wants to merge 6 commits into
testfrom
feat/chat-generate-runagentworkflow
Open

Re-point POST /api/chat/generate onto runAgentWorkflow + ephemeral key (chat#1813)#704
sweetmantech wants to merge 6 commits into
testfrom
feat/chat-generate-runagentworkflow

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

PR 2 (api, the keystone) of the async chat-generation migration tracked in recoupable/chat#1813. Fulfills the contract in docs#249. Merge order: docs#249 → this.

What

POST /api/chat/generate is re-pointed from the synchronous legacy ToolLoopAgent onto the same durable runAgentWorkflow that powers interactive /api/chat. It now:

  1. Provisions a headless session + active sandbox (ensurePersonalRepoinsertSessioninsertChatconnectSandboxupdateSession(active) → discoverSkills).
  2. Mints a short-lived, account-scoped recoup_sk_ key (mintEphemeralAccountKey, re-added from the deferred half of Enforce account_api_keys.expires_at in x-api-key auth (chat#1813) #700 — minting now has its only consumer), injected as the agent's recoupAccessToken so the long-lived service key never enters model-issued bash.
  3. Builds the shared RunAgentWorkflowInput via buildRunAgentInput (Extract shared buildRunAgentInput() (chat#1813) #701) and start()s the run.
  4. Returns { runId } with 202 immediately — generation, assistant-message persistence, the credit charge, and key revocation happen server-side inside the workflow.

Ephemeral key lifecycle

  • Threaded as agentContext.ephemeralKeyId; runAgentWorkflow's finally deletes it on run end (deleteEphemeralKeyStep).
  • The ~15m expires_at TTL (enforced in getApiKeyAccountId, shipped in Enforce account_api_keys.expires_at in x-api-key auth (chat#1813) #700) is the defense-in-depth backstop.
  • If the run fails to start, the handler revokes the key directly (the workflow never ran).

Tests (TDD, red→green)

  • validateGenerateRequest (5), handleChatGenerate orchestration incl. start-failure key revocation (4), mintEphemeralAccountKey re-added (2), runAgentWorkflow ephemeral-key deletion on/off (2). Full lib/chat + lib/keys + app/lib/workflows suites green (571); tsc adds 0 new errors.
  • Preview verification (full scheduled flow → 202 → workflow run → persisted assistant message) posted as a comment once the deployment is Ready.

Notes

  • The legacy generate helpers (setupChatRequest, getGeneralAgent, saveChatCompletion, MCP+Composio tool wiring) are now unused by this path; left in place to keep the diff focused — a follow-up can remove any that are fully orphaned.
  • Composio tools are dropped for scheduled generation (accepted tradeoff, per the issue).

🤖 Generated with Claude Code


Summary by cubic

Re-pointed chat generation onto the durable runAgentWorkflow and renamed the endpoint to POST /api/chat/runs for async, headless runs. Added GET /api/chat/runs/{runId} to check run status. The route provisions a session + active sandbox, mints a short-lived account key, starts the run, and returns 202 with { runId, chatId, sessionId } and a Location header.

  • New Features

    • POST /api/chat/runs (removed /api/chat/generate): provisions session + chat, connects an active sandbox, discovers skills, starts the workflow, and returns 202 { runId, chatId, sessionId } with Location: /api/chat/runs/{runId}.
    • GET /api/chat/runs/{runId}: point-in-time status snapshot { runId, status } normalized to queued|running|completed|failed|cancelled; 404 when unknown.
    • Ephemeral key: mints account-scoped recoup_sk_ via mintEphemeralAccountKey, injects as recoupAccessToken, threads ephemeralKeyId; workflow finally calls deleteEphemeralKeyStep (~15m TTL as backstop); revoked on start failure.
  • Migration

    • Use POST /api/chat/runs, handle 202 + Location, then poll GET /api/chat/runs/{runId} for status. Read results via GET /api/chat/{chatId}/stream or the chat’s persisted messages.
    • Request validation: exactly one of prompt or messages; supports artistId, model (default anthropic/claude-haiku-4.5), optional accountId override with org key; no topic or roomId. Composio tools are not available for scheduled generation.

Written for commit 897cee2. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a new async chat run endpoint that starts generation jobs and returns a run ID, chat ID, and session ID.
    • Added support for headless request validation, including prompt or message-based input and model selection.
    • Introduced short-lived API keys for scheduled runs, with automatic cleanup after completion.
  • Bug Fixes

    • Improved cleanup so temporary credentials are revoked even if a run ends unexpectedly.
    • Added safer handling for invalid or incomplete request payloads.

…813)

Async chat generation now runs on the SAME durable runAgentWorkflow as
interactive /api/chat instead of the synchronous legacy ToolLoopAgent.
POST /api/chat/generate provisions a headless session + active sandbox,
mints a short-lived account-scoped recoup_sk_ key for in-sandbox recoup-api
calls, builds the shared workflow input via buildRunAgentInput, and
start()s the run — returning { runId } with 202 immediately. Generation,
message persistence, the credit charge, and key revocation happen
server-side inside the workflow.

- lib/keys/mintEphemeralAccountKey + insertApiKey expires_at writer (re-added
  from the deferred half of #700; minting now has its only consumer).
- lib/chat/generate/validateGenerateRequest — x-api-key auth + prompt/messages
  normalization to UIMessage[].
- lib/chat/generate/provisionGenerateSession — ensurePersonalRepo → insertSession
  → insertChat → connectSandbox → updateSession(active) → discoverSkills.
- lib/chat/handleChatGenerate — orchestrates provision → mint → start; revokes
  the key if the run never starts.
- Ephemeral key injected as recoupAccessToken + threaded as agentContext.ephemeralKeyId;
  runAgentWorkflow's finally deletes it on run end (deleteEphemeralKeyStep). The
  ~15m expires_at TTL (enforced by #700) is the backstop.
- Matches docs#249 (202 { runId } contract).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jun 24, 2026 7:48pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sweetmantech, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 9 minutes and 33 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e7bf8ebd-4607-4b5c-a587-b6158bf9062f

📥 Commits

Reviewing files that changed from the base of the PR and between f407058 and 897cee2.

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

Walkthrough

The PR removes the legacy /api/chat/generate route and adds /api/chat/runs with CORS handling, request validation, session and sandbox provisioning, ephemeral key minting, workflow startup, and workflow cleanup that revokes the key.

Changes

Chat runs cutover

Layer / File(s) Summary
Route and handler orchestration
app/api/chat/runs/route.ts, lib/chat/handleStartChatRun.ts
The new /api/chat/runs route adds CORS preflight handling, delegates POST requests to handleStartChatRun, returns 202 with Location, and sets dynamic and maxDuration; the legacy generate route is removed.
Request contracts and agent input
lib/chat/generate/validateGenerateRequest.ts, lib/agent/tools/AgentContext.ts, lib/chat/buildRunAgentInput.ts
validateGenerateRequest parses prompt or messages requests, applies auth context, normalizes prompt text into a UIMessage, and threads optional ephemeralKeyId through the shared agent context and workflow input.
Session provisioning
lib/chat/generate/provisionGenerateSession.ts
provisionGenerateSession inserts the session and chat rows, connects the Vercel sandbox, updates session lifecycle fields, and resolves working directory and skills from the sandbox state.
Ephemeral key minting
lib/keys/mintEphemeralAccountKey.ts, lib/supabase/account_api_keys/insertApiKey.ts
mintEphemeralAccountKey generates a recoup_sk_ key, hashes it, stores it with expires_at, and returns the raw key plus row id.
Workflow key cleanup
app/lib/workflows/deleteEphemeralKeyStep.ts, app/lib/workflows/runAgentWorkflow.ts
runAgentWorkflow adds deleteEphemeralKeyStep to finally cleanup when agentContext.ephemeralKeyId is set, and the step suppresses deletion failures.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RunsRoute as POST /api/chat/runs
  participant Handler as handleStartChatRun
  participant Validate as validateGenerateRequest
  participant Provision as provisionGenerateSession
  participant Mint as mintEphemeralAccountKey
  participant Workflow as runAgentWorkflow
  participant Cleanup as deleteEphemeralKeyStep

  Client->>RunsRoute: POST /api/chat/runs
  RunsRoute->>Handler: handleStartChatRun(request)
  Handler->>Validate: validate request body and auth
  Handler->>Provision: create session, chat, sandbox
  Handler->>Mint: mint ephemeral account key
  Handler->>Workflow: start workflow with keyId
  Handler-->>Client: 202 runId, chatId, sessionId
  Workflow-->>Cleanup: finally cleanup when ephemeralKeyId is set
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • recoupable/api#582: Introduces the durable agent workflow that this PR extends with per-run ephemeral-key cleanup.
  • recoupable/api#604: Modifies the same runAgentWorkflow cleanup path that this PR updates with deleteEphemeralKeyStep.
  • recoupable/api#700: Covers account_api_keys.expires_at, which mintEphemeralAccountKey now writes through insertApiKey.

Suggested reviewers

  • cubic-dev-ai

Poem

A route was born where runs take flight,
Sandboxes hum in headless light,
A key appears, then fades from sight,
The workflow ends and cleans up right,
And quiet state returns to night.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning FAIL: several changed files violate SRP/file-function rules—route.ts exports POST/OPTIONS, and validate/provision/handler functions are 40–60+ lines mixing validation, provisioning, cleanup. Split route wrappers from business logic, keep one primary function per file, and break validate/provision/start flows into smaller helpers; also remove duplicated hardcoded titles.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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/chat-generate-runagentworkflow

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

7 issues found across 14 files

Confidence score: 3/5

  • In lib/chat/generate/validateGenerateRequest.ts, messages are type-cast to UIMessage[] without normalization and blank prompt strings pass validation, so malformed/empty inputs can get accepted and fail later in generation flows with user-visible errors—add strict message shape validation plus prompt.trim() presence checks before merging.
  • In lib/chat/handleChatGenerate.ts, the ephemeral-key cleanup path does not surface DB delete failures, which can leave compromised run keys usable until expiry with no operational signal—treat delete failures as actionable (log/alert and fail or retry) before merge.
  • In lib/chat/generate/provisionGenerateSession.ts, provisioned sandboxes are not hooked into lifecycle kick scheduling and mid-flight provisioning failures have no rollback, so sessions can become unsupervised and orphaned resources can accumulate—register generate sessions with lifecycle scheduling and add transactional/compensating cleanup for partial failures.
  • lib/chat/generate/__tests__/validateGenerateRequest.test.ts currently passes via UUID schema failure instead of the intended prompt/messages exclusivity path, and lib/chat/handleChatGenerate.ts returns a non-standard 500 body; this can hide regressions and break contract-based clients/tests—fix the test fixture to hit the intended branch and align 500 text to "Internal server error".
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="lib/chat/handleChatGenerate.ts">

<violation number="1" location="lib/chat/handleChatGenerate.ts:72">
P2: Ephemeral-key cleanup path does not detect DB delete failures. This can leave compromised run keys valid until expiry without any error signal.</violation>
</file>

<file name="lib/chat/generate/validateGenerateRequest.ts">

<violation number="1" location="lib/chat/generate/validateGenerateRequest.ts:80">
P1: `messages` are cast to `UIMessage[]` without normalization/validation, so non-UIMessage payloads can pass validation and break later in the workflow.</violation>
</file>

<file name="lib/chat/generate/provisionGenerateSession.ts">

<violation number="1" location="lib/chat/generate/provisionGenerateSession.ts:67">
P2: Provisioning has no rollback/cleanup on mid-flight failure after creating session/chat. Failed starts can accumulate orphan sessions/chats and potentially a persistent sandbox.</violation>

<violation number="2" location="lib/chat/generate/provisionGenerateSession.ts:88">
P2: Provisioned generate sandboxes are not registered with lifecycle kick scheduling. This can leave scheduled-generation sessions unsupervised for idle auto-pause behavior.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


const uiMessages: UIMessage[] = hasPrompt
? [{ id: generateUUID(), role: "user", parts: [{ type: "text", text: prompt! }] }]
: (messages as UIMessage[]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: messages are cast to UIMessage[] without normalization/validation, so non-UIMessage payloads can pass validation and break later in the workflow.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/chat/generate/validateGenerateRequest.ts, line 80:

<comment>`messages` are cast to `UIMessage[]` without normalization/validation, so non-UIMessage payloads can pass validation and break later in the workflow.</comment>

<file context>
@@ -0,0 +1,90 @@
+
+  const uiMessages: UIMessage[] = hasPrompt
+    ? [{ id: generateUUID(), role: "user", parts: [{ type: "text", text: prompt! }] }]
+    : (messages as UIMessage[]);
+
+  return {
</file context>

// there (provisioning ok, then mint ok, then start threw), the key would
// linger until its TTL. Revoke it now. If mint itself threw, there's no key.
if (ephemeralKeyId) {
try {

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: Ephemeral-key cleanup path does not detect DB delete failures. This can leave compromised run keys valid until expiry without any error signal.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/chat/handleChatGenerate.ts, line 72:

<comment>Ephemeral-key cleanup path does not detect DB delete failures. This can leave compromised run keys valid until expiry without any error signal.</comment>

<file context>
@@ -1,77 +1,81 @@
+    // there (provisioning ok, then mint ok, then start threw), the key would
+    // linger until its TTL. Revoke it now. If mint itself threw, there's no key.
+    if (ephemeralKeyId) {
+      try {
+        await deleteApiKey(ephemeralKeyId);
+      } catch (cleanupError) {
</file context>

Comment thread lib/chat/generate/validateGenerateRequest.ts Outdated

const sandboxName = getSessionSandboxName(session.id);
const gitUser = await resolveGitUser(accountId);
const sandbox = await connectSandbox({

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: Provisioning has no rollback/cleanup on mid-flight failure after creating session/chat. Failed starts can accumulate orphan sessions/chats and potentially a persistent sandbox.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/chat/generate/provisionGenerateSession.ts, line 67:

<comment>Provisioning has no rollback/cleanup on mid-flight failure after creating session/chat. Failed starts can accumulate orphan sessions/chats and potentially a persistent sandbox.</comment>

<file context>
@@ -0,0 +1,109 @@
+
+  const sandboxName = getSessionSandboxName(session.id);
+  const gitUser = await resolveGitUser(accountId);
+  const sandbox = await connectSandbox({
+    state: { type: "vercel", sandboxName, source: { repo: cloneUrl, prebuilt: false } },
+    options: {
</file context>

snapshot_url: null,
snapshot_created_at: null,
});
if (!updated) throw new Error("Failed to activate session sandbox");

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: Provisioned generate sandboxes are not registered with lifecycle kick scheduling. This can leave scheduled-generation sessions unsupervised for idle auto-pause behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/chat/generate/provisionGenerateSession.ts, line 88:

<comment>Provisioned generate sandboxes are not registered with lifecycle kick scheduling. This can leave scheduled-generation sessions unsupervised for idle auto-pause behavior.</comment>

<file context>
@@ -0,0 +1,109 @@
+    snapshot_url: null,
+    snapshot_created_at: null,
+  });
+  if (!updated) throw new Error("Failed to activate session sandbox");
+
+  // Best-effort skill + working-directory discovery from the live handle —
</file context>

Comment thread lib/chat/generate/__tests__/validateGenerateRequest.test.ts Outdated
Comment thread lib/chat/handleChatGenerate.ts Outdated
@sweetmantech

Copy link
Copy Markdown
Contributor Author

✅ Preview test — re-point verified end-to-end on the durable workflow

Preview: https://api-git-feat-chat-generate-runagentworkflow-recoup.vercel.app (head 72b79ab)
CI green: format, lint, test (full suite), and the Vercel build all pass.

Method

Called the endpoint with a real recoup_sk_ key (account fb678396…), then watched the durable run land server-side in the prod DB the preview reads (godremdqwajrwazhbrue).

Results (explicit, non-null)

Step Result
POST /api/chat/generate {prompt:"Reply with exactly: PR704 OK"} 202 {"runId":"wrun_01KVX2RBB3JDZFA0131B3RT1QS"} in 4.3s
Headless session provisioned sessions row "Scheduled generation" + initial chats row created
Workflow ran + persisted output assistant chat_messages row: PR704 OK (instruction followed exactly), gen_01KVX2REZJMP8DBGS7A773TBSR
Ephemeral key minted → revoked on run end account_api_keys where name='ephemeral:chat-generate' = 0 after the run (deleted by the workflow's finally)

What this proves

The scheduled path now runs on the same runAgentWorkflow as interactive /api/chat: provision session + active sandbox → mint short-lived account-scoped key (injected as recoupAccessToken, never the service key) → buildRunAgentInputstart()202 { runId }. Generation, assistant-message persistence, and key revocation all happen server-side after the response. The { runId } matches docs#249's 202 ChatGenerateAcceptedResponse.

Test session/chat/messages + the test key were deleted afterward (all 0 remaining).

The workflow runId alone can't be resolved back to the chat output. Return
the persisted-output identifiers too so a caller can read the result later
(GET /api/chat/{chatId}/stream, or the chat's persisted messages) — turning
the endpoint from fire-and-forget-only into a proper async-job contract.
The scheduled task still ignores the body. (chat#1813, review follow-up.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sweetmantech

Copy link
Copy Markdown
Contributor Author

Contract update — now returns { runId, chatId, sessionId }

Per review on docs#249: the workflow runId alone can't be resolved back to the chat output, so the 202 now also returns chatId + sessionId. This makes the endpoint a proper async-job contract — the caller can read the result later via GET /api/chat/{chatId}/stream (resume) or the chat's persisted messages — instead of fire-and-forget-only. The scheduled task still ignores the body. Matches docs#249.

Re-verified on the rebuilt preview (commit 28edafc):

  • POST /api/chat/generate202 {"runId":"wrun_01KVX9G42Y3E0J4QC5ZTFGHP6C","chatId":"3e9b43f8-…","sessionId":"2c5d6788-…"}
  • The returned chatId resolves to the persisted output: assistant message PR704B OK saved under that chat.
  • Ephemeral key revoked on run end (0 live). Test artifacts deleted.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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

Re-trigger cubic

REST cleanup (chat#1813): the endpoint starts a *run*, so it's modeled as a run
resource, not a `generate` verb. Removes /generate entirely (no alias).

- Route app/api/chat/generate → app/api/chat/runs; handler handleChatGenerate →
  handleStartChatRun. Add a Location header at /api/chat/runs/{runId}.
- Update path strings in comments/JSDoc to /api/chat/runs.

Also addresses cubic review on this PR:
- validateGenerateRequest: trim prompt before the presence check (reject blank).
- handleStartChatRun: standardized 500 body "Internal server error".
- validateGenerateRequest test: use a schema-valid field so the "exactly one of
  prompt/messages" case is exercised for the right reason; add a whitespace-prompt test.

(Internal helper names — validateGenerateRequest/provisionGenerateSession — keep
"generate" as it describes the operation; renaming is out of scope.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 10 files (changes from recent commits).

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="lib/chat/handleStartChatRun.ts">

<violation number="1" location="lib/chat/handleStartChatRun.ts:76">
P2: 202 Location points to a run-status endpoint that is not implemented yet, so clients following the header will hit 404. Either implement `GET /api/chat/runs/{runId}` now or omit the Location header until it exists.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

{ runId: run.runId, chatId: provisioned.chat.id, sessionId: provisioned.session.id },
{
status: 202,
headers: { ...getCorsHeaders(), Location: `/api/chat/runs/${run.runId}` },

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: 202 Location points to a run-status endpoint that is not implemented yet, so clients following the header will hit 404. Either implement GET /api/chat/runs/{runId} now or omit the Location header until it exists.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/chat/handleStartChatRun.ts, line 76:

<comment>202 Location points to a run-status endpoint that is not implemented yet, so clients following the header will hit 404. Either implement `GET /api/chat/runs/{runId}` now or omit the Location header until it exists.</comment>

<file context>
@@ -66,11 +67,14 @@ export async function handleChatGenerate(request: NextRequest): Promise<Response
-      { status: 202, headers: getCorsHeaders() },
+      {
+        status: 202,
+        headers: { ...getCorsHeaders(), Location: `/api/chat/runs/${run.runId}` },
+      },
     );
</file context>
Suggested change
headers: { ...getCorsHeaders(), Location: `/api/chat/runs/${run.runId}` },
headers: getCorsHeaders(),

roomId was accepted-but-ignored on the re-pointed endpoint (it mints its own
session+chat per run and returns chatId/sessionId). Nothing sends it anymore
(tasks#152 stopped), and Zod strips unknown keys regardless — so remove it from
the schema to keep docs↔api in sync. excludeTools was already gone. (chat#1813)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 1 file (changes from recent commits).

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

Re-trigger cubic

/api/chat takes no session-title param, so /api/chat/runs shouldn't either. The
endpoint provisions its own session with a default title; drop topic from the
request schema and the GenerateRequest type. (chat#1813 review)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (3)
lib/chat/buildRunAgentInput.ts (1)

39-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split agent-context construction out of buildRunAgentInput().

This helper now does repo-id derivation, org extraction, and conditional agent-context wiring in one 30+ line function. With headless-only fields like ephemeralKeyId being added, it will keep growing. Extract a dedicated buildAgentContext helper so this file stays focused on composing RunAgentWorkflowInput. As per path instructions, lib functions should keep single responsibility; as per coding guidelines, flag functions longer than 20 lines.

🤖 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 `@lib/chat/buildRunAgentInput.ts` around lines 39 - 73, Split the agent-context
assembly out of buildRunAgentInput so the function only composes
RunAgentWorkflowInput; move the sandbox, recoupOrgId, skills, recoupAccessToken,
and ephemeralKeyId wiring into a new buildAgentContext helper. Keep
buildRunAgentInput focused on repo parsing and top-level field mapping, and have
it call the new helper for the agentContext property to reduce growth and
improve single responsibility.

Sources: Coding guidelines, Path instructions

lib/supabase/account_api_keys/insertApiKey.ts (1)

14-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an explicit typed return contract.

This Supabase wrapper now owns the expires_at insert contract, but its return type is still inferred. Annotate it with the project’s Tables<"account_api_keys"> result shape so callers get a stable API.

As per path instructions, lib/supabase/**/*.ts operations should return typed results using Tables<"table_name">.

🤖 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 `@lib/supabase/account_api_keys/insertApiKey.ts` around lines 14 - 34, The
insertApiKey wrapper currently relies on inferred return typing, so callers do
not get the stable Supabase contract. Add an explicit return annotation on
insertApiKey using the project’s Tables<"account_api_keys"> result shape for the
successful data value and keep the null/error branch typed consistently. Make
sure the function signature and returned object in insertApiKey clearly express
the typed result instead of depending on inference.

Source: Path instructions

lib/chat/generate/provisionGenerateSession.ts (1)

41-109: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Split this provisioning helper into focused steps.

This 69-line function combines repo setup, DB writes, sandbox lifecycle, and skill discovery. Extract focused helpers so failure boundaries and tests stay maintainable.

As per coding guidelines, flag functions longer than 20 lines and keep functions small and focused. As per path instructions, lib/**/*.ts functions should have single responsibility and stay under 50 lines.

🤖 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 `@lib/chat/generate/provisionGenerateSession.ts` around lines 41 - 109,
`provisionGenerateSession` is doing too many unrelated jobs in one place, so
break it into smaller single-purpose helpers to match the function-size
guidelines. Extract the repo provisioning, session/chat creation, sandbox
connection, session activation, and skill/working-directory discovery into
focused functions called from `provisionGenerateSession`, keeping the top-level
function as orchestration only. Use the existing symbols like
`ensurePersonalRepo`, `insertSession`, `insertChat`, `connectSandbox`,
`updateSession`, and the skill-discovery block to guide the split, and preserve
the current failure handling at each step.

Sources: Coding guidelines, Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/chat/runs/route.ts`:
- Around line 28-37: Update the JSDoc for the chat runs route so it matches the
current request contract used by the route handler and validator path. In the
comments near the route entrypoint, replace the stale x-api-key-only auth
description with the actual authentication flow via validateAuthContext(), and
remove the topic field from the documented request body since it is no longer
accepted. Keep the docs aligned with the symbols handling the route contract,
especially validateAuthContext() and the route handler in
app/api/chat/runs/route.ts.

In `@app/lib/workflows/deleteEphemeralKeyStep.ts`:
- Around line 15-18: Scope the deletion in deleteEphemeralKeyStep so it only
targets the minted ephemeral key: thread accountId through
deleteEphemeralKeyStep and into deleteApiKey, and update the delete query to
require an ephemeral-only predicate such as expires_at IS NOT NULL or the
ephemeral:chat-generate name in addition to matching the key identifier. Use the
deleteApiKey path to ensure the row selection is constrained by both account
ownership and ephemeral key criteria before deleting.

In `@lib/chat/generate/provisionGenerateSession.ts`:
- Around line 53-88: The provisioning flow in provisionGenerateSession currently
throws after insertSession succeeds without undoing earlier side effects, so add
compensating cleanup around the insertChat, connectSandbox, and updateSession
steps. If any later step fails, delete the created chat/session rows and tear
down any sandbox created by connectSandbox before rethrowing, using the existing
helpers in provisionGenerateSession and the session/chat/sandbox identifiers to
locate the resources.
- Around line 58-62: The chat row is being created with a hardcoded title
instead of the caller-provided value, which can cause mismatched session labels.
Update provisionGenerateSession’s insertChat call to use the requested title
from the function’s configuration/object arguments rather than "Scheduled
generation", and keep the title wiring consistent with the surrounding
session/chat creation flow.

In `@lib/chat/generate/validateGenerateRequest.ts`:
- Around line 20-25: Remove accountId from the body schema in generateBodySchema
and keep this helper focused only on request body validation. Derive tenancy
exclusively in validateAuthContext() instead of allowing a second account source
here, and update the generate request flow so account selection happens after
auth resolution. If needed, split any mixed responsibilities in
validateGenerateRequest() so body parsing, auth context lookup, and message
normalization are handled by separate helpers with clear single responsibility.
- Around line 20-27: The generateBodySchema currently accepts messages as
z.any(), so malformed arrays bypass request validation and fail later after the
workflow starts. Replace the permissive messages field in
validateGenerateRequest.ts with a real Zod schema for the expected message
structure and use that schema in generateBodySchema so the request is rejected
at the boundary before any cast to UIMessage[] occurs.

In `@lib/chat/handleStartChatRun.ts`:
- Around line 75-80: The 202 response from handleStartChatRun currently
advertises a Location for the run status endpoint even though the corresponding
route is not shipped yet. Update the NextResponse.json call in
handleStartChatRun so it no longer sets the Location header unless the
/api/chat/runs status handler is included in this PR; keep the response body
unchanged and only reference the runId, chatId, and sessionId once the status
resource actually exists.
- Around line 44-94: Rollback the provisioned chat/session/sandbox when
`handleStartChatRun` fails after `provisionGenerateSession()` succeeds but
before `start(runAgentWorkflow, ...)` completes. Move the partial-start flow
into a dedicated helper around `provisionGenerateSession`,
`mintEphemeralAccountKey`, and `start` so the catch path can compensate for all
created resources, not just `deleteApiKey`. Ensure the helper explicitly cleans
up the provisioned session/sandbox on startup failure and keep
`handleStartChatRun` focused and under the line-length guideline.

In `@lib/keys/mintEphemeralAccountKey.ts`:
- Around line 19-45: The mintEphemeralAccountKey helper should not trust ttlMs
directly, since invalid values can produce bad expiration timestamps or weaken
the short-lived key behavior. Add TTL validation in mintEphemeralAccountKey and
extract the expiry computation into a small dedicated helper so the main
function stays focused and under the size guideline; use the existing
DEFAULT_EPHEMERAL_KEY_TTL_MS, expiresAt, and insertApiKey flow as the
integration points.

---

Nitpick comments:
In `@lib/chat/buildRunAgentInput.ts`:
- Around line 39-73: Split the agent-context assembly out of buildRunAgentInput
so the function only composes RunAgentWorkflowInput; move the sandbox,
recoupOrgId, skills, recoupAccessToken, and ephemeralKeyId wiring into a new
buildAgentContext helper. Keep buildRunAgentInput focused on repo parsing and
top-level field mapping, and have it call the new helper for the agentContext
property to reduce growth and improve single responsibility.

In `@lib/chat/generate/provisionGenerateSession.ts`:
- Around line 41-109: `provisionGenerateSession` is doing too many unrelated
jobs in one place, so break it into smaller single-purpose helpers to match the
function-size guidelines. Extract the repo provisioning, session/chat creation,
sandbox connection, session activation, and skill/working-directory discovery
into focused functions called from `provisionGenerateSession`, keeping the
top-level function as orchestration only. Use the existing symbols like
`ensurePersonalRepo`, `insertSession`, `insertChat`, `connectSandbox`,
`updateSession`, and the skill-discovery block to guide the split, and preserve
the current failure handling at each step.

In `@lib/supabase/account_api_keys/insertApiKey.ts`:
- Around line 14-34: The insertApiKey wrapper currently relies on inferred
return typing, so callers do not get the stable Supabase contract. Add an
explicit return annotation on insertApiKey using the project’s
Tables<"account_api_keys"> result shape for the successful data value and keep
the null/error branch typed consistently. Make sure the function signature and
returned object in insertApiKey clearly express the typed result instead of
depending on inference.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f8f33af2-d7dc-4559-8a97-b5bf03b8158a

📥 Commits

Reviewing files that changed from the base of the PR and between 78bd71b and f407058.

⛔ Files ignored due to path filters (5)
  • app/lib/workflows/__tests__/runAgentWorkflow.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • lib/chat/__tests__/handleChatGenerate.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/chat/__tests__/handleStartChatRun.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/chat/generate/__tests__/validateGenerateRequest.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/keys/__tests__/mintEphemeralAccountKey.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (12)
  • app/api/chat/generate/route.ts
  • app/api/chat/runs/route.ts
  • app/lib/workflows/deleteEphemeralKeyStep.ts
  • app/lib/workflows/runAgentWorkflow.ts
  • lib/agent/tools/AgentContext.ts
  • lib/chat/buildRunAgentInput.ts
  • lib/chat/generate/provisionGenerateSession.ts
  • lib/chat/generate/validateGenerateRequest.ts
  • lib/chat/handleChatGenerate.ts
  • lib/chat/handleStartChatRun.ts
  • lib/keys/mintEphemeralAccountKey.ts
  • lib/supabase/account_api_keys/insertApiKey.ts
💤 Files with no reviewable changes (2)
  • lib/chat/handleChatGenerate.ts
  • app/api/chat/generate/route.ts

Comment on lines +28 to +37
* Authentication: x-api-key header required (account inferred from the key;
* org keys may override via body `accountId`).
*
* Request body:
* - prompt: String prompt (mutually exclusive with messages)
* - messages: Array of UIMessages (mutually exclusive with prompt)
* - artistId: Optional UUID of the artist account
* - model: Optional model ID override (default anthropic/claude-haiku-4.5)
* - topic: Optional session title
* - accountId: Optional accountId override (requires org API key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the route JSDoc aligned with the actual contract.

This block still says x-api-key is required and documents topic, but the validator path now supports validateAuthContext() auth and no longer accepts topic. Since this comment is the API contract for the route, stale docs will send callers down the wrong path. As per coding guidelines, API routes should have accurate JSDoc comments and use validateAuthContext() for authentication.

🤖 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/chat/runs/route.ts` around lines 28 - 37, Update the JSDoc for the
chat runs route so it matches the current request contract used by the route
handler and validator path. In the comments near the route entrypoint, replace
the stale x-api-key-only auth description with the actual authentication flow
via validateAuthContext(), and remove the topic field from the documented
request body since it is no longer accepted. Keep the docs aligned with the
symbols handling the route contract, especially validateAuthContext() and the
route handler in app/api/chat/runs/route.ts.

Source: Coding guidelines

Comment on lines +15 to +18
export async function deleteEphemeralKeyStep(keyId: string): Promise<void> {
"use step";
try {
const { error } = await deleteApiKey(keyId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify deleteApiKey enforces account + ephemeral-only predicates.
# Expected: delete query filters by id, account, and an ephemeral marker.
fd -a 'deleteApiKey.ts' . --exec sh -c 'echo "### $1"; sed -n "1,200p" "$1"' sh {}
rg -n -C3 '\bdeleteApiKey\s*\(' .

Repository: recoupable/api

Length of output: 2791


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the delete workflow and related key lifecycle code for scoping and ephemeral markers.
sed -n '1,220p' app/lib/workflows/deleteEphemeralKeyStep.ts
printf '\n---\n'
sed -n '1,220p' lib/supabase/account_api_keys/deleteApiKey.ts
printf '\n---\n'
fd -a 'mint*Key*.ts' lib app . | head -50
printf '\n---\n'
fd -a '*account_api_keys*' lib app . | head -50
printf '\n---\n'
rg -n -C4 'expires_at|ephemeral|account_api_keys|deleteApiKey\(' lib app

Repository: recoupable/api

Length of output: 1909


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the minting and cleanup flow for the ephemeral key.
rg -n -C4 'deleteEphemeralKeyStep|runAgentWorkflow|mint.*Key|expires_at|getApiKeyAccountId|account_api_keys' app lib
printf '\n---\n'
sed -n '1,260p' lib/chat/handleStartChatRun.ts
printf '\n---\n'
fd -a '*getApiKeyAccountId*.ts' lib app . | xargs -r -I{} sh -c 'echo "### {}"; sed -n "1,220p" "{}"'

Repository: recoupable/api

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find every call site for the workflow step and the raw delete helper.
rg -n -C3 'deleteEphemeralKeyStep\(|deleteApiKey\(' app lib --glob '!**/__tests__/**'

Repository: recoupable/api

Length of output: 3035


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find every call site for the workflow step and the raw delete helper.
rg -n -C3 'deleteEphemeralKeyStep\(|deleteApiKey\(' app lib --glob '!**/__tests__/**'

Repository: recoupable/api

Length of output: 3035


Scope the delete to the minted ephemeral key. deleteApiKey(keyId) only filters by id, so a bad keyId can revoke a non-ephemeral row. Thread accountId through this step and add an ephemeral-only predicate (expires_at IS NOT NULL or the ephemeral:chat-generate name) to the delete query.

🤖 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/lib/workflows/deleteEphemeralKeyStep.ts` around lines 15 - 18, Scope the
deletion in deleteEphemeralKeyStep so it only targets the minted ephemeral key:
thread accountId through deleteEphemeralKeyStep and into deleteApiKey, and
update the delete query to require an ephemeral-only predicate such as
expires_at IS NOT NULL or the ephemeral:chat-generate name in addition to
matching the key identifier. Use the deleteApiKey path to ensure the row
selection is constrained by both account ownership and ephemeral key criteria
before deleting.

Comment on lines +53 to +88
const session = await insertSession(
buildSessionInsertRow({ accountId, title, cloneUrl, artistId }),
);
if (!session) throw new Error("Failed to create session");

const chat = await insertChat({
id: generateUUID(),
session_id: session.id,
title: "Scheduled generation",
});
if (!chat) throw new Error("Failed to create chat");

const sandboxName = getSessionSandboxName(session.id);
const gitUser = await resolveGitUser(accountId);
const sandbox = await connectSandbox({
state: { type: "vercel", sandboxName, source: { repo: cloneUrl, prebuilt: false } },
options: {
timeout: SANDBOX_TIMEOUT_MS,
ports: [3000],
githubToken: getServiceGithubToken(),
gitUser,
persistent: true,
resume: true,
createIfMissing: true,
},
});

const sandboxState = sandbox.getState() as Json;
const updated = await updateSession(session.id, {
sandbox_state: sandboxState,
lifecycle_version: session.lifecycle_version + 1,
...buildActiveLifecycleUpdate(sandboxState),
snapshot_url: null,
snapshot_created_at: null,
});
if (!updated) throw new Error("Failed to activate session sandbox");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Add compensating cleanup for partial provisioning failures.

After Line 53 succeeds, later failures in chat insert, sandbox connect, or session activation throw without reverting created rows or the persistent sandbox. This can leave orphaned headless sessions/chats/sandboxes after 5xx responses or retries.

🤖 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 `@lib/chat/generate/provisionGenerateSession.ts` around lines 53 - 88, The
provisioning flow in provisionGenerateSession currently throws after
insertSession succeeds without undoing earlier side effects, so add compensating
cleanup around the insertChat, connectSandbox, and updateSession steps. If any
later step fails, delete the created chat/session rows and tear down any sandbox
created by connectSandbox before rethrowing, using the existing helpers in
provisionGenerateSession and the session/chat/sandbox identifiers to locate the
resources.

Comment on lines +58 to +62
const chat = await insertChat({
id: generateUUID(),
session_id: session.id,
title: "Scheduled generation",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the requested title for the chat row.

Line 61 ignores the title argument and hardcodes "Scheduled generation", so future callers can create sessions and chats with mismatched labels.

Proposed fix
   const chat = await insertChat({
     id: generateUUID(),
     session_id: session.id,
-    title: "Scheduled generation",
+    title,
   });

As per coding guidelines, use configuration objects instead of hardcoded values.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const chat = await insertChat({
id: generateUUID(),
session_id: session.id,
title: "Scheduled generation",
});
const chat = await insertChat({
id: generateUUID(),
session_id: session.id,
title,
});
🤖 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 `@lib/chat/generate/provisionGenerateSession.ts` around lines 58 - 62, The chat
row is being created with a hardcoded title instead of the caller-provided
value, which can cause mismatched session labels. Update
provisionGenerateSession’s insertChat call to use the requested title from the
function’s configuration/object arguments rather than "Scheduled generation",
and keep the title wiring consistent with the surrounding session/chat creation
flow.

Source: Coding guidelines

Comment on lines +20 to +25
export const generateBodySchema = z.object({
prompt: z.string().optional(),
messages: z.array(z.any()).optional(),
artistId: z.string().uuid("artistId must be a valid UUID").optional(),
accountId: z.string().optional(),
organizationId: z.string().optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Derive the account exclusively from auth and split that concern out of this validator.

Publishing accountId in the body schema gives this route two sources of truth for tenancy, and this helper is already doing body parsing, auth resolution, and message normalization in one place. Keep account selection inside validateAuthContext() and leave this file as a pure body validator. As per coding guidelines, always derive the account ID from authentication and flag long functions; as per path instructions, validation/domain helpers should keep single responsibility.

Also applies to: 71-87

🤖 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 `@lib/chat/generate/validateGenerateRequest.ts` around lines 20 - 25, Remove
accountId from the body schema in generateBodySchema and keep this helper
focused only on request body validation. Derive tenancy exclusively in
validateAuthContext() instead of allowing a second account source here, and
update the generate request flow so account selection happens after auth
resolution. If needed, split any mixed responsibilities in
validateGenerateRequest() so body parsing, auth context lookup, and message
normalization are handled by separate helpers with clear single responsibility.

Sources: Coding guidelines, Path instructions

Comment on lines +20 to +27
export const generateBodySchema = z.object({
prompt: z.string().optional(),
messages: z.array(z.any()).optional(),
artistId: z.string().uuid("artistId must be a valid UUID").optional(),
accountId: z.string().optional(),
organizationId: z.string().optional(),
model: z.string().optional(),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate messages structurally instead of accepting z.any().

Any array currently passes here and then gets cast to UIMessage[]. A malformed payload will bypass the 400 path, fail later inside the workflow, and can leave you with invalid persisted input after the async run has already started. Define a real Zod schema for the message shape at this boundary. As per coding guidelines, API input should be parsed with Zod validation; as per path instructions, validation functions should use 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 `@lib/chat/generate/validateGenerateRequest.ts` around lines 20 - 27, The
generateBodySchema currently accepts messages as z.any(), so malformed arrays
bypass request validation and fail later after the workflow starts. Replace the
permissive messages field in validateGenerateRequest.ts with a real Zod schema
for the expected message structure and use that schema in generateBodySchema so
the request is rejected at the boundary before any cast to UIMessage[] occurs.

Sources: Coding guidelines, Path instructions

Comment on lines +44 to +94
const provisioned = await provisionGenerateSession({
accountId,
title: DEFAULT_RUN_SESSION_TITLE,
artistId,
});

const { rawKey, keyId } = await mintEphemeralAccountKey(accountId);
ephemeralKeyId = keyId;

const run = await start(runAgentWorkflow, [
buildRunAgentInput({
messages,
chatId: provisioned.chat.id,
sessionId: provisioned.session.id,
accountId,
modelId,
sessionTitle: provisioned.session.title ?? undefined,
cloneUrl: provisioned.session.clone_url,
sandboxState: provisioned.sandboxState,
workingDirectory: provisioned.workingDirectory,
skills: provisioned.skills,
recoupAccessToken: rawKey,
ephemeralKeyId: keyId,
}),
]);

// Return the run handle plus the persisted-output identifiers so the caller
// can read the result later (the workflow runId alone can't be resolved back
// to the chat): GET /api/chat/{chatId}/stream resumes the stream, and the
// assistant messages persist under chatId. The Location header points at the
// run-status resource. Mirrors the async-job shape of POST /api/content/create.
return NextResponse.json(
{ runId: run.runId, chatId: provisioned.chat.id, sessionId: provisioned.session.id },
{
status: 202,
headers: { ...getCorsHeaders(), Location: `/api/chat/runs/${run.runId}` },
},
);
} catch (error) {
// The workflow's `finally` revokes the key on run end — but if we never got
// there (provisioning ok, then mint ok, then start threw), the key would
// linger until its TTL. Revoke it now. If mint itself threw, there's no key.
if (ephemeralKeyId) {
try {
await deleteApiKey(ephemeralKeyId);
} catch (cleanupError) {
console.error("[handleStartChatRun] failed to revoke ephemeral key:", cleanupError);
}
}
console.error("[handleStartChatRun] failed to start generation run:", error);
return errorResponse("Internal server error", 500);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Rollback the provisioned session/sandbox when workflow startup fails.

If provisionGenerateSession() succeeds and start() throws, this catch only deletes the ephemeral key. The newly created chat/session and active sandbox are left behind even though no run exists, which leaks resources and leaves dangling state. Please move provisioning plus compensating cleanup into a dedicated helper so the whole partial-start path is covered. As per path instructions, domain functions should have proper error handling and single responsibility; as per coding guidelines, flag functions longer than 20 lines.

🤖 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 `@lib/chat/handleStartChatRun.ts` around lines 44 - 94, Rollback the
provisioned chat/session/sandbox when `handleStartChatRun` fails after
`provisionGenerateSession()` succeeds but before `start(runAgentWorkflow, ...)`
completes. Move the partial-start flow into a dedicated helper around
`provisionGenerateSession`, `mintEphemeralAccountKey`, and `start` so the catch
path can compensate for all created resources, not just `deleteApiKey`. Ensure
the helper explicitly cleans up the provisioned session/sandbox on startup
failure and keep `handleStartChatRun` focused and under the line-length
guideline.

Sources: Coding guidelines, Path instructions

Comment on lines +75 to +80
return NextResponse.json(
{ runId: run.runId, chatId: provisioned.chat.id, sessionId: provisioned.session.id },
{
status: 202,
headers: { ...getCorsHeaders(), Location: `/api/chat/runs/${run.runId}` },
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't return a Location header for a status route that isn't shipped yet.

This response advertises /api/chat/runs/${run.runId}, but app/api/chat/runs/route.ts says the status route lands in a follow-up. That gives clients a dead pointer in the 202 contract. Either add the status handler in this PR or omit Location until the resource exists.

🤖 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 `@lib/chat/handleStartChatRun.ts` around lines 75 - 80, The 202 response from
handleStartChatRun currently advertises a Location for the run status endpoint
even though the corresponding route is not shipped yet. Update the
NextResponse.json call in handleStartChatRun so it no longer sets the Location
header unless the /api/chat/runs status handler is included in this PR; keep the
response body unchanged and only reference the runId, chatId, and sessionId once
the status resource actually exists.

Comment on lines +19 to +45
export async function mintEphemeralAccountKey(
accountId: string,
{
ttlMs = DEFAULT_EPHEMERAL_KEY_TTL_MS,
name = "ephemeral:chat-generate",
}: {
ttlMs?: number;
name?: string;
} = {},
): Promise<EphemeralAccountKey> {
const rawKey = generateApiKey("recoup_sk");
const keyHash = hashApiKey(rawKey, PRIVY_PROJECT_SECRET);
const expiresAt = new Date(Date.now() + ttlMs).toISOString();

const { data, error } = await insertApiKey({
name,
account: accountId,
key_hash: keyHash,
expires_at: expiresAt,
});

if (error || !data) {
throw new Error(`Failed to mint ephemeral api key: ${error?.message ?? "no row returned"}`);
}

return { rawKey, keyId: data.id };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Validate TTLs and isolate expiry calculation.

Line 31 trusts ttlMs; negative, zero, non-finite, or oversized values can create unusable keys, throw on toISOString(), or weaken the short-lived-key guarantee. Extracting TTL validation/calculation also keeps this helper under the function-size guideline.

Proposed fix
 export const DEFAULT_EPHEMERAL_KEY_TTL_MS = 15 * 60 * 1000;
+const MAX_EPHEMERAL_KEY_TTL_MS = DEFAULT_EPHEMERAL_KEY_TTL_MS;
+
+function buildEphemeralExpiresAt(ttlMs: number): string {
+  if (!Number.isFinite(ttlMs) || ttlMs <= 0 || ttlMs > MAX_EPHEMERAL_KEY_TTL_MS) {
+    throw new Error("Invalid ephemeral key TTL");
+  }
+
+  return new Date(Date.now() + ttlMs).toISOString();
+}
 
 export type EphemeralAccountKey = { rawKey: string; keyId: string };
@@
   const rawKey = generateApiKey("recoup_sk");
   const keyHash = hashApiKey(rawKey, PRIVY_PROJECT_SECRET);
-  const expiresAt = new Date(Date.now() + ttlMs).toISOString();
+  const expiresAt = buildEphemeralExpiresAt(ttlMs);

As per coding guidelines, flag functions longer than 20 lines and keep functions small and focused.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function mintEphemeralAccountKey(
accountId: string,
{
ttlMs = DEFAULT_EPHEMERAL_KEY_TTL_MS,
name = "ephemeral:chat-generate",
}: {
ttlMs?: number;
name?: string;
} = {},
): Promise<EphemeralAccountKey> {
const rawKey = generateApiKey("recoup_sk");
const keyHash = hashApiKey(rawKey, PRIVY_PROJECT_SECRET);
const expiresAt = new Date(Date.now() + ttlMs).toISOString();
const { data, error } = await insertApiKey({
name,
account: accountId,
key_hash: keyHash,
expires_at: expiresAt,
});
if (error || !data) {
throw new Error(`Failed to mint ephemeral api key: ${error?.message ?? "no row returned"}`);
}
return { rawKey, keyId: data.id };
}
export const DEFAULT_EPHEMERAL_KEY_TTL_MS = 15 * 60 * 1000;
const MAX_EPHEMERAL_KEY_TTL_MS = DEFAULT_EPHEMERAL_KEY_TTL_MS;
function buildEphemeralExpiresAt(ttlMs: number): string {
if (!Number.isFinite(ttlMs) || ttlMs <= 0 || ttlMs > MAX_EPHEMERAL_KEY_TTL_MS) {
throw new Error("Invalid ephemeral key TTL");
}
return new Date(Date.now() + ttlMs).toISOString();
}
export async function mintEphemeralAccountKey(
accountId: string,
{
ttlMs = DEFAULT_EPHEMERAL_KEY_TTL_MS,
name = "ephemeral:chat-generate",
}: {
ttlMs?: number;
name?: string;
} = {},
): Promise<EphemeralAccountKey> {
const rawKey = generateApiKey("recoup_sk");
const keyHash = hashApiKey(rawKey, PRIVY_PROJECT_SECRET);
const expiresAt = buildEphemeralExpiresAt(ttlMs);
const { data, error } = await insertApiKey({
name,
account: accountId,
key_hash: keyHash,
expires_at: expiresAt,
});
if (error || !data) {
throw new Error(`Failed to mint ephemeral api key: ${error?.message ?? "no row returned"}`);
}
return { rawKey, keyId: data.id };
}
🤖 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 `@lib/keys/mintEphemeralAccountKey.ts` around lines 19 - 45, The
mintEphemeralAccountKey helper should not trust ttlMs directly, since invalid
values can produce bad expiration timestamps or weaken the short-lived key
behavior. Add TTL validation in mintEphemeralAccountKey and extract the expiry
computation into a small dedicated helper so the main function stays focused and
under the size guideline; use the existing DEFAULT_EPHEMERAL_KEY_TTL_MS,
expiresAt, and insertApiKey flow as the integration points.

Source: Coding guidelines

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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

Re-trigger cubic

Brings the api to parity with the merged docs#249, which documented the run-
status endpoint. Wraps the durable workflow's getRun(runId).status and returns
{ runId, status } (normalized to queued|running|completed|failed|cancelled).
404 when the run is unknown; x-api-key auth.

Returns { runId, status } rather than the documented chatId/sessionId: getRun
exposes only status, and there's no durable runId→chat mapping (the caller
already holds chatId/sessionId from the 202 start response). Docs reconciled to
match; full chatId/sessionId + per-run ownership would need a chats.last_run_id
column (follow-up). (chat#1813)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

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="lib/chat/generate/handleChatRunStatus.ts">

<violation number="1" location="lib/chat/generate/handleChatRunStatus.ts:59">
P1: Error handling may misclassify non-404 failures as 404, masking real server/workflow errors.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

rawStatus = await getRun(runId).status;
} catch (error) {
console.error(`[handleChatRunStatus] run not found ${runId}:`, error);
return errorResponse("Run not found", 404);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Error handling may misclassify non-404 failures as 404, masking real server/workflow errors.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/chat/generate/handleChatRunStatus.ts, line 59:

<comment>Error handling may misclassify non-404 failures as 404, masking real server/workflow errors.</comment>

<file context>
@@ -0,0 +1,66 @@
+    rawStatus = await getRun(runId).status;
+  } catch (error) {
+    console.error(`[handleChatRunStatus] run not found ${runId}:`, error);
+    return errorResponse("Run not found", 404);
+  }
+
</file context>

@sweetmantech

Copy link
Copy Markdown
Contributor Author

GET /api/chat/runs/{runId} status endpoint — verified on preview

Brings the api to parity with merged docs#249 (which documented the status endpoint). Preview commit 897cee2:

Request Result
POST /api/chat/runs 202 {runId: wrun_01KVXJWT…, chatId, sessionId}
GET /api/chat/runs/{runId} (in-flight) 200 {"runId":"…","status":"running"}
GET …/{runId} (after completion) 200 {"status":"completed"}
GET …/{unknown} 404 Run not found
GET … (no auth) 401

Run went runningcompleted — confirms the status normalizer + the docs enum against real workflow states. The minted ephemeral key was auto-revoked on completion (0 left). Test session/chat/messages + key deleted.

Contract note: returns { runId, status }, not the originally-documented chatId/sessionIdgetRun(runId) exposes only status and there's no durable runId→chat mapping (the caller already has chatId/sessionId from the 202). Docs reconciled in docs#250. Full chatId/sessionId + per-run ownership would need a chats.last_run_id column (follow-up).

* Normalize a Vercel Workflow run state to the documented `ChatRunStatusResponse`
* enum. `pending` is treated as running (the codebase's `RUNNING_STATUSES`).
*/
function normalizeRunStatus(raw: string): ChatRunStatus {

Copy link
Copy Markdown
Contributor Author

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 normalizeRunStatus

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

DRY - are there any duplicate code blocks introduced here which duplicate the existing /api/chat flow? If so, can the blocks be abstracted to a shared lib used in both paths instead of duplicating the code blocks?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant