Refactor Foundry GitHub state and sandbox runtime (#247)

* Move Foundry HTTP APIs out of /api/rivet

* Move Foundry HTTP APIs onto /v1

* Fix Foundry Rivet base path and frontend endpoint fallback

* Configure Foundry Rivet runner pool for /v1

* Remove Foundry Rivet runner override

* Serve Foundry Rivet routes directly from Bun

* Log Foundry RivetKit deployment friction

* Add actor display metadata

* Tighten actor schema constraints

* Reset actor persistence baseline

* Remove temporary actor key version prefix

Railway has no persistent volumes so stale actors are wiped on
each deploy. The v2 key rotation is no longer needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Cache app workspace actor handle across requests

Every request was calling getOrCreate on the Rivet engine API
to resolve the workspace actor, even though it's always the same
actor. Cache the handle and invalidate on error so retries
re-resolve. This eliminates redundant cross-region round-trips
to api.rivet.dev on every request.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add temporary debug logging to GitHub OAuth exchange

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Make squashed baseline migrations idempotent

Use CREATE TABLE IF NOT EXISTS and CREATE UNIQUE INDEX IF NOT
EXISTS so the squashed baseline can run against actors that
already have tables from the pre-squash migration sequence.
This fixes the "table already exists" error when org workspace
actors wake up with stale migration journals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Revert "Make squashed baseline migrations idempotent"

This reverts commit 356c146035.

* Fix GitHub OAuth callback by removing retry wrapper

OAuth authorization codes are single-use. The appWorkspaceAction wrapper
retries failed calls up to 20 times, but if the code exchange succeeds
and a later step fails, every retry sends the already-consumed code,
producing "bad_verification_code" from GitHub.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add runner versioning to RivetKit registry

Uses Date.now() so each process start gets a unique version.
This ensures Rivet Cloud migrates actors to the new runner on
deploy instead of routing requests to stale runners.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add backend request and workspace logging

* Log callback request headers

* Make GitHub OAuth callback idempotent against duplicate requests

Clear oauthState before exchangeCode so duplicate callback requests
fail the state check instead of hitting GitHub with a consumed code.
Marked as HACK — root cause of duplicate HTTP requests is unknown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add temporary header dump on GitHub OAuth callback

Log all request headers on the callback endpoint to diagnose
the source of duplicate requests (Railway proxy, Cloudflare, browser).
Remove once root cause is identified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Defer slow GitHub org sync to workflow queue for fast OAuth callback

Split syncGithubSessionFromToken into a fast path (initGithubSession:
exchange code, get viewer, store token+identity) and a slow path
(syncGithubOrganizations: list orgs/installations, sync workspaces).

completeAppGithubAuth now returns the 302 redirect in ~2s instead of
~18s by enqueuing the org sync to the workspace workflow queue
(fire-and-forget). This eliminates the proxy timeout window that was
causing duplicate callback requests.

bootstrapAppGithubSession (dev-only) still calls the full synchronous
sync since proxy timeouts are not a concern and it needs the session
fully populated before returning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* foundry: async app repo import on org select

* foundry: parallelize app snapshot org reads

* repo: push all current workspace changes

* foundry: update runner version and snapshot logging

* Refactor Foundry GitHub state and sandbox runtime

Refactors Foundry around organization/repository ownership and adds an organization-scoped GitHub state actor plus a user-scoped GitHub auth actor, removing the old project PR/branch sync actors and repo PR cache.

Updates sandbox provisioning to rely on sandbox-agent for in-sandbox work, hardens Daytona startup and image-build behavior, and surfaces runtime and task-startup errors more clearly in the UI.

Extends workbench and GitHub state handling to track merged PR state, adds runtime-issue tracking, refreshes client/test/config wiring, and documents the main live Foundry test flow plus actor coordination rules.

Also updates the remaining Sandbox Agent install-version references in docs/examples to the current pinned minor channel.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nathan Flurry 2026-03-13 02:45:07 -07:00 committed by GitHub
parent 436eb4a3a3
commit ae191d1ae1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
102 changed files with 3490 additions and 2003 deletions

View file

@ -0,0 +1,50 @@
CREATE TABLE `task` (
`id` integer PRIMARY KEY NOT NULL,
`branch_name` text,
`title` text,
`task` text NOT NULL,
`provider_id` text NOT NULL,
`status` text NOT NULL,
`agent_type` text DEFAULT 'claude',
`pr_submitted` integer DEFAULT 0,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
CONSTRAINT "task_singleton_id_check" CHECK("task"."id" = 1)
);
--> statement-breakpoint
CREATE TABLE `task_runtime` (
`id` integer PRIMARY KEY NOT NULL,
`active_sandbox_id` text,
`active_session_id` text,
`active_switch_target` text,
`active_cwd` text,
`status_message` text,
`updated_at` integer NOT NULL,
CONSTRAINT "task_runtime_singleton_id_check" CHECK("task_runtime"."id" = 1)
);
--> statement-breakpoint
CREATE TABLE `task_sandboxes` (
`sandbox_id` text PRIMARY KEY NOT NULL,
`provider_id` text NOT NULL,
`sandbox_actor_id` text,
`switch_target` text NOT NULL,
`cwd` text,
`status_message` text,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE `task_workbench_sessions` (
`session_id` text PRIMARY KEY NOT NULL,
`session_name` text NOT NULL,
`model` text NOT NULL,
`unread` integer DEFAULT 0 NOT NULL,
`draft_text` text DEFAULT '' NOT NULL,
`draft_attachments_json` text DEFAULT '[]' NOT NULL,
`draft_updated_at` integer,
`created` integer DEFAULT 1 NOT NULL,
`closed` integer DEFAULT 0 NOT NULL,
`thinking_since_ms` integer,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);

View file

@ -1,24 +0,0 @@
CREATE TABLE `task` (
`id` integer PRIMARY KEY NOT NULL,
`branch_name` text NOT NULL,
`title` text NOT NULL,
`task` text NOT NULL,
`provider_id` text NOT NULL,
`status` text NOT NULL,
`agent_type` text DEFAULT 'claude',
`auto_committed` integer DEFAULT 0,
`pushed` integer DEFAULT 0,
`pr_submitted` integer DEFAULT 0,
`needs_push` integer DEFAULT 0,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE `task_runtime` (
`id` integer PRIMARY KEY NOT NULL,
`sandbox_id` text,
`session_id` text,
`switch_target` text,
`status_message` text,
`updated_at` integer NOT NULL
);

View file

@ -1,3 +0,0 @@
ALTER TABLE `task` DROP COLUMN `auto_committed`;--> statement-breakpoint
ALTER TABLE `task` DROP COLUMN `pushed`;--> statement-breakpoint
ALTER TABLE `task` DROP COLUMN `needs_push`;

View file

@ -1,38 +0,0 @@
ALTER TABLE `task_runtime` RENAME COLUMN "sandbox_id" TO "active_sandbox_id";--> statement-breakpoint
ALTER TABLE `task_runtime` RENAME COLUMN "session_id" TO "active_session_id";--> statement-breakpoint
ALTER TABLE `task_runtime` RENAME COLUMN "switch_target" TO "active_switch_target";--> statement-breakpoint
CREATE TABLE `task_sandboxes` (
`sandbox_id` text PRIMARY KEY NOT NULL,
`provider_id` text NOT NULL,
`switch_target` text NOT NULL,
`cwd` text,
`status_message` text,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
--> statement-breakpoint
ALTER TABLE `task_runtime` ADD `active_cwd` text;
--> statement-breakpoint
INSERT INTO `task_sandboxes` (
`sandbox_id`,
`provider_id`,
`switch_target`,
`cwd`,
`status_message`,
`created_at`,
`updated_at`
)
SELECT
r.`active_sandbox_id`,
(SELECT h.`provider_id` FROM `task` h WHERE h.`id` = 1),
r.`active_switch_target`,
r.`active_cwd`,
r.`status_message`,
COALESCE((SELECT h.`created_at` FROM `task` h WHERE h.`id` = 1), r.`updated_at`),
r.`updated_at`
FROM `task_runtime` r
WHERE
r.`id` = 1
AND r.`active_sandbox_id` IS NOT NULL
AND r.`active_switch_target` IS NOT NULL
ON CONFLICT(`sandbox_id`) DO NOTHING;

View file

@ -1,48 +0,0 @@
-- Allow tasks to exist before their branch/title are determined.
-- Drizzle doesn't support altering column nullability in SQLite directly, so rebuild the table.
PRAGMA foreign_keys=off;
CREATE TABLE `task__new` (
`id` integer PRIMARY KEY NOT NULL,
`branch_name` text,
`title` text,
`task` text NOT NULL,
`provider_id` text NOT NULL,
`status` text NOT NULL,
`agent_type` text DEFAULT 'claude',
`pr_submitted` integer DEFAULT 0,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
INSERT INTO `task__new` (
`id`,
`branch_name`,
`title`,
`task`,
`provider_id`,
`status`,
`agent_type`,
`pr_submitted`,
`created_at`,
`updated_at`
)
SELECT
`id`,
`branch_name`,
`title`,
`task`,
`provider_id`,
`status`,
`agent_type`,
`pr_submitted`,
`created_at`,
`updated_at`
FROM `task`;
DROP TABLE `task`;
ALTER TABLE `task__new` RENAME TO `task`;
PRAGMA foreign_keys=on;

View file

@ -1,57 +0,0 @@
-- Fix: make branch_name/title nullable during initial "naming" stage.
-- 0003 was missing statement breakpoints, so drizzle's migrator marked it applied without executing all statements.
-- Rebuild the table again with proper statement breakpoints.
PRAGMA foreign_keys=off;
--> statement-breakpoint
DROP TABLE IF EXISTS `task__new`;
--> statement-breakpoint
CREATE TABLE `task__new` (
`id` integer PRIMARY KEY NOT NULL,
`branch_name` text,
`title` text,
`task` text NOT NULL,
`provider_id` text NOT NULL,
`status` text NOT NULL,
`agent_type` text DEFAULT 'claude',
`pr_submitted` integer DEFAULT 0,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
--> statement-breakpoint
INSERT INTO `task__new` (
`id`,
`branch_name`,
`title`,
`task`,
`provider_id`,
`status`,
`agent_type`,
`pr_submitted`,
`created_at`,
`updated_at`
)
SELECT
`id`,
`branch_name`,
`title`,
`task`,
`provider_id`,
`status`,
`agent_type`,
`pr_submitted`,
`created_at`,
`updated_at`
FROM `task`;
--> statement-breakpoint
DROP TABLE `task`;
--> statement-breakpoint
ALTER TABLE `task__new` RENAME TO `task`;
--> statement-breakpoint
PRAGMA foreign_keys=on;

View file

@ -1 +0,0 @@
ALTER TABLE `task_sandboxes` ADD `sandbox_actor_id` text;

View file

@ -1,14 +0,0 @@
CREATE TABLE `task_workbench_sessions` (
`session_id` text PRIMARY KEY NOT NULL,
`session_name` text NOT NULL,
`model` text NOT NULL,
`unread` integer DEFAULT 0 NOT NULL,
`draft_text` text DEFAULT '' NOT NULL,
`draft_attachments_json` text DEFAULT '[]' NOT NULL,
`draft_updated_at` integer,
`created` integer DEFAULT 1 NOT NULL,
`closed` integer DEFAULT 0 NOT NULL,
`thinking_since_ms` integer,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);

View file

@ -1,7 +1,7 @@
{
"version": "6",
"dialect": "sqlite",
"id": "9b004d3b-0722-4bb5-a410-d47635db7df3",
"id": "6daaa6d5-3280-46fe-9261-40cabeba1b49",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"task": {
@ -18,14 +18,14 @@
"name": "branch_name",
"type": "text",
"primaryKey": false,
"notNull": true,
"notNull": false,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"notNull": false,
"autoincrement": false
},
"task": {
@ -57,22 +57,6 @@
"autoincrement": false,
"default": "'claude'"
},
"auto_committed": {
"name": "auto_committed",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"pushed": {
"name": "pushed",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"pr_submitted": {
"name": "pr_submitted",
"type": "integer",
@ -81,14 +65,6 @@
"autoincrement": false,
"default": 0
},
"needs_push": {
"name": "needs_push",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "integer",
@ -108,7 +84,12 @@
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
"checkConstraints": {
"task_singleton_id_check": {
"name": "task_singleton_id_check",
"value": "\"task\".\"id\" = 1"
}
}
},
"task_runtime": {
"name": "task_runtime",
@ -120,22 +101,29 @@
"notNull": true,
"autoincrement": false
},
"sandbox_id": {
"name": "sandbox_id",
"active_sandbox_id": {
"name": "active_sandbox_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"session_id": {
"name": "session_id",
"active_session_id": {
"name": "active_session_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"switch_target": {
"name": "switch_target",
"active_switch_target": {
"name": "active_switch_target",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"active_cwd": {
"name": "active_cwd",
"type": "text",
"primaryKey": false,
"notNull": false,
@ -160,6 +148,176 @@
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {
"task_runtime_singleton_id_check": {
"name": "task_runtime_singleton_id_check",
"value": "\"task_runtime\".\"id\" = 1"
}
}
},
"task_sandboxes": {
"name": "task_sandboxes",
"columns": {
"sandbox_id": {
"name": "sandbox_id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sandbox_actor_id": {
"name": "sandbox_actor_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"switch_target": {
"name": "switch_target",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"cwd": {
"name": "cwd",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status_message": {
"name": "status_message",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"task_workbench_sessions": {
"name": "task_workbench_sessions",
"columns": {
"session_id": {
"name": "session_id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"session_name": {
"name": "session_name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"model": {
"name": "model",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"unread": {
"name": "unread",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"draft_text": {
"name": "draft_text",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"draft_attachments_json": {
"name": "draft_attachments_json",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'[]'"
},
"draft_updated_at": {
"name": "draft_updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created": {
"name": "created",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 1
},
"closed": {
"name": "closed",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"thinking_since_ms": {
"name": "thinking_since_ms",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},

View file

@ -1,152 +0,0 @@
{
"version": "6",
"dialect": "sqlite",
"id": "0fca0f14-69df-4fca-bc52-29e902247909",
"prevId": "9b004d3b-0722-4bb5-a410-d47635db7df3",
"tables": {
"task": {
"name": "task",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"branch_name": {
"name": "branch_name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"task": {
"name": "task",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"agent_type": {
"name": "agent_type",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'claude'"
},
"pr_submitted": {
"name": "pr_submitted",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"task_runtime": {
"name": "task_runtime",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"sandbox_id": {
"name": "sandbox_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"session_id": {
"name": "session_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"switch_target": {
"name": "switch_target",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status_message": {
"name": "status_message",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View file

@ -1,222 +0,0 @@
{
"version": "6",
"dialect": "sqlite",
"id": "72cef919-e545-48be-a7c0-7ac74cfcf9e6",
"prevId": "0fca0f14-69df-4fca-bc52-29e902247909",
"tables": {
"task": {
"name": "task",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"branch_name": {
"name": "branch_name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"task": {
"name": "task",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"agent_type": {
"name": "agent_type",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'claude'"
},
"pr_submitted": {
"name": "pr_submitted",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"task_runtime": {
"name": "task_runtime",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"active_sandbox_id": {
"name": "active_sandbox_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"active_session_id": {
"name": "active_session_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"active_switch_target": {
"name": "active_switch_target",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"active_cwd": {
"name": "active_cwd",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status_message": {
"name": "status_message",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"task_sandboxes": {
"name": "task_sandboxes",
"columns": {
"sandbox_id": {
"name": "sandbox_id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"switch_target": {
"name": "switch_target",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"cwd": {
"name": "cwd",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status_message": {
"name": "status_message",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {
"\"task_runtime\".\"sandbox_id\"": "\"task_runtime\".\"active_sandbox_id\"",
"\"task_runtime\".\"session_id\"": "\"task_runtime\".\"active_session_id\"",
"\"task_runtime\".\"switch_target\"": "\"task_runtime\".\"active_switch_target\""
}
},
"internal": {
"indexes": {}
}
}

View file

@ -5,43 +5,8 @@
{
"idx": 0,
"version": "6",
"when": 1770924374665,
"tag": "0000_condemned_maria_hill",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1770947251055,
"tag": "0001_rapid_eddie_brock",
"breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1770948428907,
"tag": "0002_lazy_moira_mactaggert",
"breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1771027535276,
"tag": "0003_plucky_bran",
"breakpoints": true
},
{
"idx": 4,
"version": "6",
"when": 1771097651912,
"tag": "0004_focused_shuri",
"breakpoints": true
},
{
"idx": 5,
"version": "6",
"when": 1771370000000,
"tag": "0005_sandbox_actor_id",
"when": 1773376222525,
"tag": "0000_charming_maestro",
"breakpoints": true
}
]

View file

@ -6,44 +6,8 @@ const journal = {
entries: [
{
idx: 0,
when: 1770924374665,
tag: "0000_condemned_maria_hill",
breakpoints: true,
},
{
idx: 1,
when: 1770947251055,
tag: "0001_rapid_eddie_brock",
breakpoints: true,
},
{
idx: 2,
when: 1770948428907,
tag: "0002_lazy_moira_mactaggert",
breakpoints: true,
},
{
idx: 3,
when: 1771027535276,
tag: "0003_plucky_bran",
breakpoints: true,
},
{
idx: 4,
when: 1771097651912,
tag: "0004_focused_shuri",
breakpoints: true,
},
{
idx: 5,
when: 1771370000000,
tag: "0005_sandbox_actor_id",
breakpoints: true,
},
{
idx: 6,
when: 1773020000000,
tag: "0006_workbench_sessions",
when: 1773376222525,
tag: "0000_charming_maestro",
breakpoints: true,
},
],
@ -53,78 +17,6 @@ export default {
journal,
migrations: {
m0000: `CREATE TABLE \`task\` (
\`id\` integer PRIMARY KEY NOT NULL,
\`branch_name\` text NOT NULL,
\`title\` text NOT NULL,
\`task\` text NOT NULL,
\`provider_id\` text NOT NULL,
\`status\` text NOT NULL,
\`agent_type\` text DEFAULT 'claude',
\`auto_committed\` integer DEFAULT 0,
\`pushed\` integer DEFAULT 0,
\`pr_submitted\` integer DEFAULT 0,
\`needs_push\` integer DEFAULT 0,
\`created_at\` integer NOT NULL,
\`updated_at\` integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE \`task_runtime\` (
\`id\` integer PRIMARY KEY NOT NULL,
\`sandbox_id\` text,
\`session_id\` text,
\`switch_target\` text,
\`status_message\` text,
\`updated_at\` integer NOT NULL
);
`,
m0001: `ALTER TABLE \`task\` DROP COLUMN \`auto_committed\`;--> statement-breakpoint
ALTER TABLE \`task\` DROP COLUMN \`pushed\`;--> statement-breakpoint
ALTER TABLE \`task\` DROP COLUMN \`needs_push\`;`,
m0002: `ALTER TABLE \`task_runtime\` RENAME COLUMN "sandbox_id" TO "active_sandbox_id";--> statement-breakpoint
ALTER TABLE \`task_runtime\` RENAME COLUMN "session_id" TO "active_session_id";--> statement-breakpoint
ALTER TABLE \`task_runtime\` RENAME COLUMN "switch_target" TO "active_switch_target";--> statement-breakpoint
CREATE TABLE \`task_sandboxes\` (
\`sandbox_id\` text PRIMARY KEY NOT NULL,
\`provider_id\` text NOT NULL,
\`switch_target\` text NOT NULL,
\`cwd\` text,
\`status_message\` text,
\`created_at\` integer NOT NULL,
\`updated_at\` integer NOT NULL
);
--> statement-breakpoint
ALTER TABLE \`task_runtime\` ADD \`active_cwd\` text;
--> statement-breakpoint
INSERT INTO \`task_sandboxes\` (
\`sandbox_id\`,
\`provider_id\`,
\`switch_target\`,
\`cwd\`,
\`status_message\`,
\`created_at\`,
\`updated_at\`
)
SELECT
r.\`active_sandbox_id\`,
(SELECT h.\`provider_id\` FROM \`task\` h WHERE h.\`id\` = 1),
r.\`active_switch_target\`,
r.\`active_cwd\`,
r.\`status_message\`,
COALESCE((SELECT h.\`created_at\` FROM \`task\` h WHERE h.\`id\` = 1), r.\`updated_at\`),
r.\`updated_at\`
FROM \`task_runtime\` r
WHERE
r.\`id\` = 1
AND r.\`active_sandbox_id\` IS NOT NULL
AND r.\`active_switch_target\` IS NOT NULL
ON CONFLICT(\`sandbox_id\`) DO NOTHING;
`,
m0003: `-- Allow tasks to exist before their branch/title are determined.
-- Drizzle doesn't support altering column nullability in SQLite directly, so rebuild the table.
PRAGMA foreign_keys=off;
CREATE TABLE \`task__new\` (
\`id\` integer PRIMARY KEY NOT NULL,
\`branch_name\` text,
\`title\` text,
@ -134,100 +26,33 @@ CREATE TABLE \`task__new\` (
\`agent_type\` text DEFAULT 'claude',
\`pr_submitted\` integer DEFAULT 0,
\`created_at\` integer NOT NULL,
\`updated_at\` integer NOT NULL,
CONSTRAINT "task_singleton_id_check" CHECK("task"."id" = 1)
);
--> statement-breakpoint
CREATE TABLE \`task_runtime\` (
\`id\` integer PRIMARY KEY NOT NULL,
\`active_sandbox_id\` text,
\`active_session_id\` text,
\`active_switch_target\` text,
\`active_cwd\` text,
\`status_message\` text,
\`updated_at\` integer NOT NULL,
CONSTRAINT "task_runtime_singleton_id_check" CHECK("task_runtime"."id" = 1)
);
--> statement-breakpoint
CREATE TABLE \`task_sandboxes\` (
\`sandbox_id\` text PRIMARY KEY NOT NULL,
\`provider_id\` text NOT NULL,
\`sandbox_actor_id\` text,
\`switch_target\` text NOT NULL,
\`cwd\` text,
\`status_message\` text,
\`created_at\` integer NOT NULL,
\`updated_at\` integer NOT NULL
);
INSERT INTO \`task__new\` (
\`id\`,
\`branch_name\`,
\`title\`,
\`task\`,
\`provider_id\`,
\`status\`,
\`agent_type\`,
\`pr_submitted\`,
\`created_at\`,
\`updated_at\`
)
SELECT
\`id\`,
\`branch_name\`,
\`title\`,
\`task\`,
\`provider_id\`,
\`status\`,
\`agent_type\`,
\`pr_submitted\`,
\`created_at\`,
\`updated_at\`
FROM \`task\`;
DROP TABLE \`task\`;
ALTER TABLE \`task__new\` RENAME TO \`task\`;
PRAGMA foreign_keys=on;
`,
m0004: `-- Fix: make branch_name/title nullable during initial "naming" stage.
-- 0003 was missing statement breakpoints, so drizzle's migrator marked it applied without executing all statements.
-- Rebuild the table again with proper statement breakpoints.
PRAGMA foreign_keys=off;
--> statement-breakpoint
DROP TABLE IF EXISTS \`task__new\`;
--> statement-breakpoint
CREATE TABLE \`task__new\` (
\`id\` integer PRIMARY KEY NOT NULL,
\`branch_name\` text,
\`title\` text,
\`task\` text NOT NULL,
\`provider_id\` text NOT NULL,
\`status\` text NOT NULL,
\`agent_type\` text DEFAULT 'claude',
\`pr_submitted\` integer DEFAULT 0,
\`created_at\` integer NOT NULL,
\`updated_at\` integer NOT NULL
);
--> statement-breakpoint
INSERT INTO \`task__new\` (
\`id\`,
\`branch_name\`,
\`title\`,
\`task\`,
\`provider_id\`,
\`status\`,
\`agent_type\`,
\`pr_submitted\`,
\`created_at\`,
\`updated_at\`
)
SELECT
\`id\`,
\`branch_name\`,
\`title\`,
\`task\`,
\`provider_id\`,
\`status\`,
\`agent_type\`,
\`pr_submitted\`,
\`created_at\`,
\`updated_at\`
FROM \`task\`;
--> statement-breakpoint
DROP TABLE \`task\`;
--> statement-breakpoint
ALTER TABLE \`task__new\` RENAME TO \`task\`;
--> statement-breakpoint
PRAGMA foreign_keys=on;
`,
m0005: `ALTER TABLE \`task_sandboxes\` ADD \`sandbox_actor_id\` text;`,
m0006: `CREATE TABLE \`task_workbench_sessions\` (
CREATE TABLE \`task_workbench_sessions\` (
\`session_id\` text PRIMARY KEY NOT NULL,
\`session_name\` text NOT NULL,
\`model\` text NOT NULL,
@ -240,6 +65,7 @@ PRAGMA foreign_keys=on;
\`thinking_since_ms\` integer,
\`created_at\` integer NOT NULL,
\`updated_at\` integer NOT NULL
);`,
);
`,
} as const,
};

View file

@ -1,28 +1,37 @@
import { integer, sqliteTable, text } from "rivetkit/db/drizzle";
import { check, integer, sqliteTable, text } from "rivetkit/db/drizzle";
import { sql } from "drizzle-orm";
// SQLite is per task actor instance, so these tables only ever store one row (id=1).
export const task = sqliteTable("task", {
id: integer("id").primaryKey(),
branchName: text("branch_name"),
title: text("title"),
task: text("task").notNull(),
providerId: text("provider_id").notNull(),
status: text("status").notNull(),
agentType: text("agent_type").default("claude"),
prSubmitted: integer("pr_submitted").default(0),
createdAt: integer("created_at").notNull(),
updatedAt: integer("updated_at").notNull(),
});
export const task = sqliteTable(
"task",
{
id: integer("id").primaryKey(),
branchName: text("branch_name"),
title: text("title"),
task: text("task").notNull(),
providerId: text("provider_id").notNull(),
status: text("status").notNull(),
agentType: text("agent_type").default("claude"),
prSubmitted: integer("pr_submitted").default(0),
createdAt: integer("created_at").notNull(),
updatedAt: integer("updated_at").notNull(),
},
(table) => [check("task_singleton_id_check", sql`${table.id} = 1`)],
);
export const taskRuntime = sqliteTable("task_runtime", {
id: integer("id").primaryKey(),
activeSandboxId: text("active_sandbox_id"),
activeSessionId: text("active_session_id"),
activeSwitchTarget: text("active_switch_target"),
activeCwd: text("active_cwd"),
statusMessage: text("status_message"),
updatedAt: integer("updated_at").notNull(),
});
export const taskRuntime = sqliteTable(
"task_runtime",
{
id: integer("id").primaryKey(),
activeSandboxId: text("active_sandbox_id"),
activeSessionId: text("active_session_id"),
activeSwitchTarget: text("active_switch_target"),
activeCwd: text("active_cwd"),
statusMessage: text("status_message"),
updatedAt: integer("updated_at").notNull(),
},
(table) => [check("task_runtime_singleton_id_check", sql`${table.id} = 1`)],
);
export const taskSandboxes = sqliteTable("task_sandboxes", {
sandboxId: text("sandbox_id").notNull().primaryKey(),
@ -41,6 +50,7 @@ export const taskWorkbenchSessions = sqliteTable("task_workbench_sessions", {
model: text("model").notNull(),
unread: integer("unread").notNull().default(0),
draftText: text("draft_text").notNull().default(""),
// Structured by the workbench composer attachment payload format.
draftAttachmentsJson: text("draft_attachments_json").notNull().default("[]"),
draftUpdatedAt: integer("draft_updated_at"),
created: integer("created").notNull().default(1),

View file

@ -111,6 +111,8 @@ export const task = actor({
db: taskDb,
queues: Object.fromEntries(TASK_QUEUE_NAMES.map((name) => [name, queue()])),
options: {
name: "Task",
icon: "wrench",
actionTimeout: 5 * 60_000,
},
createState: (_c, input: TaskInput) => ({