feat(sessions): ensure personal repo on POST /api/sessions
Summary
Unblocks chat.recoupable.com's Path C cutover (recoupable/chat#1748) by moving personal-repo provisioning into recoup-api. When POST /api/sessions is called without a cloneUrl and without an org bound to the caller's auth context, the handler now provisions (or reuses) the user's personal Recoupable workspace repo before returning the session row.
Previously this code lived only in open-agents (apps/web/lib/recoupable/ensure-personal-repo.ts), so chat.recoupable.com had no path to a personal repo without round-tripping through open-agents. Chat tried to build the cloneUrl client-side from account_info.name — but that field is the user's display name (e.g. sweetman.eth → kebabed to sweetman-eth), not the open-agents-canonical name (sweetman). Result: POST /api/sandbox 502'd because the constructed repo URL didn't exist on GitHub.
What changed
Ported from open-agents (matching naming + behavior, but using recoup-api's plain-fetch style instead of Octokit to avoid a new dep):
lib/recoupable/githubOwner.ts—RECOUPABLE_GITHUB_OWNERconstantlib/string/toKebabCase.ts— kebab helperlib/recoupable/buildPersonalRepoIdentifier.ts—{ owner, repo }pairlib/recoupable/buildPersonalRepoUrl.ts— full GitHub URLlib/github/repositoryExists.ts—GET /repos/:owner/:repo(true / false / null)lib/github/createRepository.ts—POST /orgs/:owner/reposwithauto_init: truelib/recoupable/ensurePersonalRepo.ts— idempotent check-then-create
New session-side helper:
lib/sessions/resolveSessionCloneUrl.ts— picksbodyCloneUrl> org-no-op >ensurePersonalRepo. Falls back toemail.split("@")[0]ifaccount_info.nameis missing (matches open-agents'fetchOrCreateAccountfallback).
Modified:
lib/sessions/buildSessionInsertRow.ts— accepts the resolvedcloneUrlinstead of readingbody.cloneUrldirectlylib/sessions/createSessionHandler.ts— callsresolveSessionCloneUrl, returns 502 if it fails
Tests
lib/string/__tests__/toKebabCase.test.ts(6 tests — including thesweetman.eth → sweetman-ethregression case)lib/recoupable/__tests__/buildPersonalRepoIdentifier.test.ts(3 tests)lib/github/__tests__/repositoryExists.test.ts(4 tests — 200 / 404 / 403 / network)lib/github/__tests__/createRepository.test.ts(5 tests — bad name / 201 / 422 / 403 / network)lib/recoupable/__tests__/ensurePersonalRepo.test.ts(5 tests — missing token / already-exists / check-failed / create-success / create-failed)lib/sessions/__tests__/resolveSessionCloneUrl.test.ts(7 tests — body cloneUrl / org / personal / email fallback / no-name+no-email / missing account / ensure-failed)lib/sessions/__tests__/buildSessionInsertRow.test.ts(updated for newcloneUrlprop)lib/sessions/__tests__/createSessionHandler.persistence.test.ts(added 502 + cloneUrl-forwarding cases)- Full suite: 3,331 / 3,331 pass
Test plan
- On preview deployment, log in as a user without an existing repo, call
POST /api/sessionswith body{}— verify arecoupable/<slug>-<account_id>repo is created and the response session'sclone_urlmatches it - Re-call with the same account — verify idempotency: no new repo, same
clone_urlreturned - Call with body
{ cloneUrl: "..." }— verify the provided URL is used verbatim (no repo-creation side effects) - Call from a user with an org bound (Bearer token resolves to
orgId != null) and no body cloneUrl — verifyclone_urlisnull(narrow scope; org-URL derivation stays client-side for now) - If
GITHUB_TOKENis unset — verify the handler returns 502 with a clean error, not a 500
Follow-ups (not in this PR)
- Once this ships, chat.recoupable.com PR #1748 can stop constructing the personal
cloneUrlclient-side and instead omitcloneUrlfrom itsPOST /api/sessionsbody — the api will resolve it correctly. lib/github/findOrgReposByAccountId.tsand friends useprocess.env.GITHUB_TOKENdirectly. They could migrate togetServiceGithubToken()for consistency. Out of scope here.
🤖 Generated with Claude Code
Summary by cubic
Automatically provisions or reuses a workspace repo at github.com/recoupable/ on POST /api/sessions when cloneUrl is absent, for both personal and org-bound sessions. Standardizes repo names to the account UUID and returns the resolved clone_url; legacy <slug>-<accountId> URLs keep working via GitHub redirects.
-
New Features
resolveSessionCloneUrlprovisions for org sessions too (accountId = auth.orgId ?? auth.accountId), persists the URL, andcreateSessionHandlerreturns 502 on provisioning failure.- GitHub helpers:
repositoryExistsandcreateRepository(plainfetch, ownerrecoupable,private: true,auto_init: true, token viagetServiceGithubToken);createRepositorynow returns{ success, repoUrl, error }. ensurePersonalReporeturns the final URL string;extractOrgIdaccepts both legacy and new repo shapes;buildSessionInsertRowtakes the resolvedcloneUrl.- Flattened
ResolveSessionCloneUrlResultto{ ok, cloneUrl, error? }to fix Next.js 16 type-narrowing build errors.
-
Migration
- One-time repo rename to
<accountId>was run; the migration script was removed. No action needed.
- One-time repo rename to
Summary by CodeRabbit
Release Notes
-
New Features
- Sessions now automatically provision workspace repositories and resolve appropriate clone URLs during creation.
-
Improvements
- Enhanced repository validation, existence checking, and error handling with improved fallback mechanisms for better reliability.