mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 06:04:43 +00:00
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:
parent
436eb4a3a3
commit
ae191d1ae1
102 changed files with 3490 additions and 2003 deletions
|
|
@ -29,6 +29,7 @@ WorkspaceActor
|
|||
- Branch rename is a real git operation, not just metadata.
|
||||
- `SandboxInstanceActor` stays separate from `TaskActor`; tasks/sessions reference it by identity.
|
||||
- Sync actors are polling workers only. They feed parent actors and should not become the source of truth.
|
||||
- When a backend request path must aggregate multiple independent actor calls or reads, prefer bounded parallelism over sequential fan-out when correctness permits. Do not serialize independent work by default.
|
||||
|
||||
## Maintenance
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { createErrorContext, createFoundryLogger } from "@sandbox-agent/foundry-shared";
|
||||
|
||||
type Journal = {
|
||||
entries?: Array<{
|
||||
|
|
@ -11,6 +12,10 @@ type Journal = {
|
|||
}>;
|
||||
};
|
||||
|
||||
const logger = createFoundryLogger({
|
||||
service: "foundry-backend-migrations",
|
||||
});
|
||||
|
||||
function padMigrationKey(idx: number): string {
|
||||
return `m${String(idx).padStart(4, "0")}`;
|
||||
}
|
||||
|
|
@ -128,8 +133,6 @@ async function main(): Promise<void> {
|
|||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message);
|
||||
logger.error(createErrorContext(error), "generate_actor_migrations_failed");
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "9d9ebe3c-8341-449c-bd14-2b6fd62853a1",
|
||||
"id": "e592c829-141f-4740-88b7-09cf957a4405",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"events": {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1770924375133,
|
||||
"tag": "0000_watery_bushwacker",
|
||||
"when": 1773376223815,
|
||||
"tag": "0000_fluffy_kid_colt",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ const journal = {
|
|||
entries: [
|
||||
{
|
||||
idx: 0,
|
||||
when: 1770924375133,
|
||||
tag: "0000_watery_bushwacker",
|
||||
when: 1773376223815,
|
||||
tag: "0000_fluffy_kid_colt",
|
||||
breakpoints: true,
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export const events = sqliteTable("events", {
|
|||
taskId: text("task_id"),
|
||||
branchName: text("branch_name"),
|
||||
kind: text("kind").notNull(),
|
||||
// Structured by the history event kind definitions in application code.
|
||||
payloadJson: text("payload_json").notNull(),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ export const history = actor({
|
|||
queues: {
|
||||
"history.command.append": queue(),
|
||||
},
|
||||
options: {
|
||||
name: "History",
|
||||
icon: "database",
|
||||
},
|
||||
createState: (_c, input: HistoryInput) => ({
|
||||
workspaceId: input.workspaceId,
|
||||
repoId: input.repoId,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,20 @@ import { projectPrSync } from "./project-pr-sync/index.js";
|
|||
import { project } from "./project/index.js";
|
||||
import { sandboxInstance } from "./sandbox-instance/index.js";
|
||||
import { workspace } from "./workspace/index.js";
|
||||
import { logger } from "../logging.js";
|
||||
|
||||
const RUNNER_VERSION = Math.floor(Date.now() / 1000);
|
||||
|
||||
export const registry = setup({
|
||||
serverless: {
|
||||
basePath: "/v1/rivet",
|
||||
},
|
||||
runner: {
|
||||
version: RUNNER_VERSION,
|
||||
},
|
||||
logging: {
|
||||
baseLogger: logger,
|
||||
},
|
||||
use: {
|
||||
workspace,
|
||||
project,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { logger } from "../logging.js";
|
||||
|
||||
export function resolveErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
|
|
@ -17,11 +19,11 @@ export function resolveErrorStack(error: unknown): string | undefined {
|
|||
}
|
||||
|
||||
export function logActorWarning(scope: string, message: string, context?: Record<string, unknown>): void {
|
||||
const payload = {
|
||||
scope,
|
||||
logger.warn(
|
||||
{
|
||||
scope,
|
||||
...(context ?? {}),
|
||||
},
|
||||
message,
|
||||
...(context ?? {}),
|
||||
};
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("[foundry][actor:warn]", payload);
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,8 @@ export const projectBranchSync = actor({
|
|||
[CONTROL.force]: queue(),
|
||||
},
|
||||
options: {
|
||||
name: "Project Branch Sync",
|
||||
icon: "code-branch",
|
||||
// Polling actors rely on timer-based wakeups; sleeping would pause the timer and stop polling.
|
||||
noSleep: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ export const projectPrSync = actor({
|
|||
[CONTROL.force]: queue(),
|
||||
},
|
||||
options: {
|
||||
name: "Project PR Sync",
|
||||
icon: "code-merge",
|
||||
// Polling actors rely on timer-based wakeups; sleeping would pause the timer and stop polling.
|
||||
noSleep: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
CREATE TABLE `branches` (
|
||||
`branch_name` text PRIMARY KEY NOT NULL,
|
||||
`commit_sha` text NOT NULL,
|
||||
`worktree_path` text,
|
||||
`parent_branch` text,
|
||||
`tracked_in_stack` integer DEFAULT 0 NOT NULL,
|
||||
`diff_stat` text,
|
||||
`has_unpushed` integer,
|
||||
`conflicts_with_main` integer,
|
||||
`has_unpushed` integer DEFAULT 0 NOT NULL,
|
||||
`conflicts_with_main` integer DEFAULT 0 NOT NULL,
|
||||
`first_seen_at` integer,
|
||||
`last_seen_at` integer,
|
||||
`updated_at` integer NOT NULL
|
||||
|
|
@ -18,10 +18,23 @@ CREATE TABLE `pr_cache` (
|
|||
`title` text NOT NULL,
|
||||
`pr_url` text,
|
||||
`pr_author` text,
|
||||
`is_draft` integer,
|
||||
`is_draft` integer DEFAULT 0 NOT NULL,
|
||||
`ci_status` text,
|
||||
`review_status` text,
|
||||
`reviewer` text,
|
||||
`fetched_at` integer,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `repo_meta` (
|
||||
`id` integer PRIMARY KEY NOT NULL,
|
||||
`remote_url` text NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `task_index` (
|
||||
`task_id` text PRIMARY KEY NOT NULL,
|
||||
`branch_name` text,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
CREATE TABLE `repo_meta` (
|
||||
`id` integer PRIMARY KEY NOT NULL,
|
||||
`remote_url` text NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `branches` DROP COLUMN `worktree_path`;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
CREATE TABLE `task_index` (
|
||||
`task_id` text PRIMARY KEY NOT NULL,
|
||||
`branch_name` text,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
|
|
@ -1 +0,0 @@
|
|||
ALTER TABLE `branches` ADD `tracked_in_stack` integer;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "03d97613-0108-4197-8660-5f2af5409fe6",
|
||||
"id": "6ffd6acb-e737-46ee-a8fe-fcfddcdd6ea9",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"branches": {
|
||||
|
|
@ -21,13 +21,6 @@
|
|||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"worktree_path": {
|
||||
"name": "worktree_path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"parent_branch": {
|
||||
"name": "parent_branch",
|
||||
"type": "text",
|
||||
|
|
@ -35,6 +28,14 @@
|
|||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"tracked_in_stack": {
|
||||
"name": "tracked_in_stack",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"diff_stat": {
|
||||
"name": "diff_stat",
|
||||
"type": "text",
|
||||
|
|
@ -46,15 +47,17 @@
|
|||
"name": "has_unpushed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"conflicts_with_main": {
|
||||
"name": "conflicts_with_main",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"first_seen_at": {
|
||||
"name": "first_seen_at",
|
||||
|
|
@ -133,8 +136,9 @@
|
|||
"name": "is_draft",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"ci_status": {
|
||||
"name": "ci_status",
|
||||
|
|
@ -177,6 +181,75 @@
|
|||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"repo_meta": {
|
||||
"name": "repo_meta",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"remote_url": {
|
||||
"name": "remote_url",
|
||||
"type": "text",
|
||||
"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_index": {
|
||||
"name": "task_index",
|
||||
"columns": {
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"branch_name": {
|
||||
"name": "branch_name",
|
||||
"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": {},
|
||||
|
|
|
|||
|
|
@ -1,216 +0,0 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "e6d294b6-27ce-424b-a3b3-c100b42e628b",
|
||||
"prevId": "03d97613-0108-4197-8660-5f2af5409fe6",
|
||||
"tables": {
|
||||
"branches": {
|
||||
"name": "branches",
|
||||
"columns": {
|
||||
"branch_name": {
|
||||
"name": "branch_name",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"commit_sha": {
|
||||
"name": "commit_sha",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"parent_branch": {
|
||||
"name": "parent_branch",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"diff_stat": {
|
||||
"name": "diff_stat",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"has_unpushed": {
|
||||
"name": "has_unpushed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"conflicts_with_main": {
|
||||
"name": "conflicts_with_main",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"first_seen_at": {
|
||||
"name": "first_seen_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_seen_at": {
|
||||
"name": "last_seen_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"pr_cache": {
|
||||
"name": "pr_cache",
|
||||
"columns": {
|
||||
"branch_name": {
|
||||
"name": "branch_name",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pr_number": {
|
||||
"name": "pr_number",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pr_url": {
|
||||
"name": "pr_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pr_author": {
|
||||
"name": "pr_author",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_draft": {
|
||||
"name": "is_draft",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ci_status": {
|
||||
"name": "ci_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"review_status": {
|
||||
"name": "review_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reviewer": {
|
||||
"name": "reviewer",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fetched_at": {
|
||||
"name": "fetched_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"repo_meta": {
|
||||
"name": "repo_meta",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"remote_url": {
|
||||
"name": "remote_url",
|
||||
"type": "text",
|
||||
"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": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "ac89870f-1630-4a16-9606-7b1225f6da8a",
|
||||
"prevId": "e6d294b6-27ce-424b-a3b3-c100b42e628b",
|
||||
"tables": {
|
||||
"branches": {
|
||||
"name": "branches",
|
||||
"columns": {
|
||||
"branch_name": {
|
||||
"name": "branch_name",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"commit_sha": {
|
||||
"name": "commit_sha",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"parent_branch": {
|
||||
"name": "parent_branch",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"diff_stat": {
|
||||
"name": "diff_stat",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"has_unpushed": {
|
||||
"name": "has_unpushed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"conflicts_with_main": {
|
||||
"name": "conflicts_with_main",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"first_seen_at": {
|
||||
"name": "first_seen_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_seen_at": {
|
||||
"name": "last_seen_at",
|
||||
"type": "integer",
|
||||
"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_index": {
|
||||
"name": "task_index",
|
||||
"columns": {
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"branch_name": {
|
||||
"name": "branch_name",
|
||||
"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": {}
|
||||
},
|
||||
"pr_cache": {
|
||||
"name": "pr_cache",
|
||||
"columns": {
|
||||
"branch_name": {
|
||||
"name": "branch_name",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pr_number": {
|
||||
"name": "pr_number",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pr_url": {
|
||||
"name": "pr_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pr_author": {
|
||||
"name": "pr_author",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_draft": {
|
||||
"name": "is_draft",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ci_status": {
|
||||
"name": "ci_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"review_status": {
|
||||
"name": "review_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reviewer": {
|
||||
"name": "reviewer",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fetched_at": {
|
||||
"name": "fetched_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"repo_meta": {
|
||||
"name": "repo_meta",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"remote_url": {
|
||||
"name": "remote_url",
|
||||
"type": "text",
|
||||
"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": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,29 +5,8 @@
|
|||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1770924376062,
|
||||
"tag": "0000_stormy_the_hunter",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1770947252449,
|
||||
"tag": "0001_wild_carlie_cooper",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1771276338465,
|
||||
"tag": "0002_far_war_machine",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "6",
|
||||
"when": 1771369000000,
|
||||
"tag": "0003_busy_legacy",
|
||||
"when": 1773376221848,
|
||||
"tag": "0000_useful_la_nuit",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,26 +6,8 @@ const journal = {
|
|||
entries: [
|
||||
{
|
||||
idx: 0,
|
||||
when: 1770924376062,
|
||||
tag: "0000_stormy_the_hunter",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 1,
|
||||
when: 1770947252449,
|
||||
tag: "0001_wild_carlie_cooper",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 2,
|
||||
when: 1771276338465,
|
||||
tag: "0002_far_war_machine",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 3,
|
||||
when: 1771369000000,
|
||||
tag: "0003_busy_legacy",
|
||||
when: 1773376221848,
|
||||
tag: "0000_useful_la_nuit",
|
||||
breakpoints: true,
|
||||
},
|
||||
],
|
||||
|
|
@ -37,11 +19,11 @@ export default {
|
|||
m0000: `CREATE TABLE \`branches\` (
|
||||
\`branch_name\` text PRIMARY KEY NOT NULL,
|
||||
\`commit_sha\` text NOT NULL,
|
||||
\`worktree_path\` text,
|
||||
\`parent_branch\` text,
|
||||
\`tracked_in_stack\` integer DEFAULT 0 NOT NULL,
|
||||
\`diff_stat\` text,
|
||||
\`has_unpushed\` integer,
|
||||
\`conflicts_with_main\` integer,
|
||||
\`has_unpushed\` integer DEFAULT 0 NOT NULL,
|
||||
\`conflicts_with_main\` integer DEFAULT 0 NOT NULL,
|
||||
\`first_seen_at\` integer,
|
||||
\`last_seen_at\` integer,
|
||||
\`updated_at\` integer NOT NULL
|
||||
|
|
@ -54,28 +36,26 @@ CREATE TABLE \`pr_cache\` (
|
|||
\`title\` text NOT NULL,
|
||||
\`pr_url\` text,
|
||||
\`pr_author\` text,
|
||||
\`is_draft\` integer,
|
||||
\`is_draft\` integer DEFAULT 0 NOT NULL,
|
||||
\`ci_status\` text,
|
||||
\`review_status\` text,
|
||||
\`reviewer\` text,
|
||||
\`fetched_at\` integer,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0001: `CREATE TABLE \`repo_meta\` (
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`repo_meta\` (
|
||||
\`id\` integer PRIMARY KEY NOT NULL,
|
||||
\`remote_url\` text NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE \`branches\` DROP COLUMN \`worktree_path\`;`,
|
||||
m0002: `CREATE TABLE \`task_index\` (
|
||||
CREATE TABLE \`task_index\` (
|
||||
\`task_id\` text PRIMARY KEY NOT NULL,
|
||||
\`branch_name\` text,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0003: `ALTER TABLE \`branches\` ADD \`tracked_in_stack\` integer;`,
|
||||
} as const,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ export const branches = sqliteTable("branches", {
|
|||
branchName: text("branch_name").notNull().primaryKey(),
|
||||
commitSha: text("commit_sha").notNull(),
|
||||
parentBranch: text("parent_branch"),
|
||||
trackedInStack: integer("tracked_in_stack"),
|
||||
trackedInStack: integer("tracked_in_stack").notNull().default(0),
|
||||
diffStat: text("diff_stat"),
|
||||
hasUnpushed: integer("has_unpushed"),
|
||||
conflictsWithMain: integer("conflicts_with_main"),
|
||||
hasUnpushed: integer("has_unpushed").notNull().default(0),
|
||||
conflictsWithMain: integer("conflicts_with_main").notNull().default(0),
|
||||
firstSeenAt: integer("first_seen_at"),
|
||||
lastSeenAt: integer("last_seen_at"),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
|
|
@ -28,7 +28,7 @@ export const prCache = sqliteTable("pr_cache", {
|
|||
title: text("title").notNull(),
|
||||
prUrl: text("pr_url"),
|
||||
prAuthor: text("pr_author"),
|
||||
isDraft: integer("is_draft"),
|
||||
isDraft: integer("is_draft").notNull().default(0),
|
||||
ciStatus: text("ci_status"),
|
||||
reviewStatus: text("review_status"),
|
||||
reviewer: text("reviewer"),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ export const project = actor({
|
|||
db: projectDb,
|
||||
queues: Object.fromEntries(PROJECT_QUEUE_NAMES.map((name) => [name, queue()])),
|
||||
options: {
|
||||
name: "Project",
|
||||
icon: "folder",
|
||||
actionTimeout: 5 * 60_000,
|
||||
},
|
||||
createState: (_c, input: ProjectInput) => ({
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
CREATE TABLE `sandbox_instance` (
|
||||
`id` integer PRIMARY KEY NOT NULL,
|
||||
`metadata_json` text NOT NULL,
|
||||
`status` text NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
CREATE TABLE `sandbox_sessions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`agent` text NOT NULL,
|
||||
`agent_session_id` text NOT NULL,
|
||||
`last_connection_id` text NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`destroyed_at` integer,
|
||||
`session_init_json` text
|
||||
CREATE TABLE `sandbox_instance` (
|
||||
`id` integer PRIMARY KEY NOT NULL,
|
||||
`metadata_json` text NOT NULL,
|
||||
`status` text NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE TABLE `sandbox_session_events` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`session_id` text NOT NULL,
|
||||
|
|
@ -19,9 +15,13 @@ CREATE TABLE `sandbox_session_events` (
|
|||
`payload_json` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE INDEX `sandbox_sessions_created_at_idx` ON `sandbox_sessions` (`created_at`);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `sandbox_session_events_session_id_event_index_idx` ON `sandbox_session_events` (`session_id`,`event_index`);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `sandbox_session_events_session_id_created_at_idx` ON `sandbox_session_events` (`session_id`,`created_at`);
|
||||
CREATE UNIQUE INDEX `sandbox_session_events_session_id_event_index_unique` ON `sandbox_session_events` (`session_id`,`event_index`);--> statement-breakpoint
|
||||
CREATE TABLE `sandbox_sessions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`agent` text NOT NULL,
|
||||
`agent_session_id` text NOT NULL,
|
||||
`last_connection_id` text NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`destroyed_at` integer,
|
||||
`session_init_json` text
|
||||
);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "ef8a919c-64f0-46d9-b8ed-a15f039e6ba7",
|
||||
"id": "130486c5-6208-4d00-b367-e02b9def953a",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"sandbox_instance": {
|
||||
|
|
@ -41,6 +41,130 @@
|
|||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sandbox_session_events": {
|
||||
"name": "sandbox_session_events",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"session_id": {
|
||||
"name": "session_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"event_index": {
|
||||
"name": "event_index",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"connection_id": {
|
||||
"name": "connection_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"sender": {
|
||||
"name": "sender",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payload_json": {
|
||||
"name": "payload_json",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"sandbox_session_events_session_id_event_index_unique": {
|
||||
"name": "sandbox_session_events_session_id_event_index_unique",
|
||||
"columns": ["session_id", "event_index"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sandbox_sessions": {
|
||||
"name": "sandbox_sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"agent": {
|
||||
"name": "agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"agent_session_id": {
|
||||
"name": "agent_session_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_connection_id": {
|
||||
"name": "last_connection_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"destroyed_at": {
|
||||
"name": "destroyed_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"session_init_json": {
|
||||
"name": "session_init_json",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
|
|
|
|||
|
|
@ -5,15 +5,8 @@
|
|||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1770924375604,
|
||||
"tag": "0000_broad_tyrannus",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1776482400000,
|
||||
"tag": "0001_sandbox_sessions",
|
||||
"when": 1773376224446,
|
||||
"tag": "0000_smooth_sauron",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,14 +6,8 @@ const journal = {
|
|||
entries: [
|
||||
{
|
||||
idx: 0,
|
||||
when: 1770924375604,
|
||||
tag: "0000_broad_tyrannus",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 1,
|
||||
when: 1776482400000,
|
||||
tag: "0001_sandbox_sessions",
|
||||
when: 1773376224446,
|
||||
tag: "0000_smooth_sauron",
|
||||
breakpoints: true,
|
||||
},
|
||||
],
|
||||
|
|
@ -28,18 +22,7 @@ export default {
|
|||
\`status\` text NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0001: `CREATE TABLE \`sandbox_sessions\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
\`agent\` text NOT NULL,
|
||||
\`agent_session_id\` text NOT NULL,
|
||||
\`last_connection_id\` text NOT NULL,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`destroyed_at\` integer,
|
||||
\`session_init_json\` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE TABLE \`sandbox_session_events\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
\`session_id\` text NOT NULL,
|
||||
|
|
@ -50,12 +33,16 @@ CREATE TABLE \`sandbox_session_events\` (
|
|||
\`payload_json\` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE INDEX \`sandbox_sessions_created_at_idx\` ON \`sandbox_sessions\` (\`created_at\`);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX \`sandbox_session_events_session_id_event_index_idx\` ON \`sandbox_session_events\` (\`session_id\`,\`event_index\`);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX \`sandbox_session_events_session_id_created_at_idx\` ON \`sandbox_session_events\` (\`session_id\`,\`created_at\`);
|
||||
CREATE UNIQUE INDEX \`sandbox_session_events_session_id_event_index_unique\` ON \`sandbox_session_events\` (\`session_id\`,\`event_index\`);--> statement-breakpoint
|
||||
CREATE TABLE \`sandbox_sessions\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
\`agent\` text NOT NULL,
|
||||
\`agent_session_id\` text NOT NULL,
|
||||
\`last_connection_id\` text NOT NULL,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`destroyed_at\` integer,
|
||||
\`session_init_json\` text
|
||||
);
|
||||
`,
|
||||
} as const,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { integer, sqliteTable, text } from "rivetkit/db/drizzle";
|
||||
import { integer, sqliteTable, text, uniqueIndex } from "rivetkit/db/drizzle";
|
||||
|
||||
// SQLite is per sandbox-instance actor instance.
|
||||
export const sandboxInstance = sqliteTable("sandbox_instance", {
|
||||
id: integer("id").primaryKey(),
|
||||
// Structured by the provider/runtime metadata serializer for this actor.
|
||||
metadataJson: text("metadata_json").notNull(),
|
||||
status: text("status").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
|
|
@ -17,15 +18,21 @@ export const sandboxSessions = sqliteTable("sandbox_sessions", {
|
|||
lastConnectionId: text("last_connection_id").notNull(),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
destroyedAt: integer("destroyed_at"),
|
||||
// Structured by the sandbox-agent ACP session bootstrap payload.
|
||||
sessionInitJson: text("session_init_json"),
|
||||
});
|
||||
|
||||
export const sandboxSessionEvents = sqliteTable("sandbox_session_events", {
|
||||
id: text("id").notNull().primaryKey(),
|
||||
sessionId: text("session_id").notNull(),
|
||||
eventIndex: integer("event_index").notNull(),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
connectionId: text("connection_id").notNull(),
|
||||
sender: text("sender").notNull(),
|
||||
payloadJson: text("payload_json").notNull(),
|
||||
});
|
||||
export const sandboxSessionEvents = sqliteTable(
|
||||
"sandbox_session_events",
|
||||
{
|
||||
id: text("id").notNull().primaryKey(),
|
||||
sessionId: text("session_id").notNull(),
|
||||
eventIndex: integer("event_index").notNull(),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
connectionId: text("connection_id").notNull(),
|
||||
sender: text("sender").notNull(),
|
||||
// Structured by the sandbox-agent session event envelope.
|
||||
payloadJson: text("payload_json").notNull(),
|
||||
},
|
||||
(table) => [uniqueIndex("sandbox_session_events_session_id_event_index_unique").on(table.sessionId, table.eventIndex)],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -458,6 +458,8 @@ export const sandboxInstance = actor({
|
|||
db: sandboxInstanceDb,
|
||||
queues: Object.fromEntries(SANDBOX_INSTANCE_QUEUE_NAMES.map((name) => [name, queue()])),
|
||||
options: {
|
||||
name: "Sandbox Instance",
|
||||
icon: "box",
|
||||
actionTimeout: 5 * 60_000,
|
||||
},
|
||||
createState: (_c, input: SandboxInstanceInput) => ({
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ export const taskStatusSync = actor({
|
|||
[CONTROL.force]: queue(),
|
||||
},
|
||||
options: {
|
||||
name: "Task Status Sync",
|
||||
icon: "signal",
|
||||
// Polling actors rely on timer-based wakeups; sleeping would pause the timer and stop polling.
|
||||
noSleep: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -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`;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
ALTER TABLE `task_sandboxes` ADD `sandbox_actor_id` text;
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -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": {}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -62,7 +62,13 @@ interface RepoOverviewInput {
|
|||
repoId: string;
|
||||
}
|
||||
|
||||
const WORKSPACE_QUEUE_NAMES = ["workspace.command.addRepo", "workspace.command.createTask", "workspace.command.refreshProviderProfiles"] as const;
|
||||
const WORKSPACE_QUEUE_NAMES = [
|
||||
"workspace.command.addRepo",
|
||||
"workspace.command.createTask",
|
||||
"workspace.command.refreshProviderProfiles",
|
||||
"workspace.command.syncGithubOrganizationRepos",
|
||||
"workspace.command.syncGithubSession",
|
||||
] as const;
|
||||
const SANDBOX_AGENT_REPO = "rivet-dev/sandbox-agent";
|
||||
|
||||
type WorkspaceQueueName = (typeof WORKSPACE_QUEUE_NAMES)[number];
|
||||
|
|
@ -366,6 +372,33 @@ export async function runWorkspaceWorkflow(ctx: any): Promise<void> {
|
|||
refreshProviderProfilesMutation(loopCtx, msg.body as RefreshProviderProfilesCommand),
|
||||
);
|
||||
await msg.complete({ ok: true });
|
||||
return Loop.continue(undefined);
|
||||
}
|
||||
|
||||
if (msg.name === "workspace.command.syncGithubSession") {
|
||||
await loopCtx.step({
|
||||
name: "workspace-sync-github-session",
|
||||
timeout: 60_000,
|
||||
run: async () => {
|
||||
const { syncGithubOrganizations } = await import("./app-shell.js");
|
||||
await syncGithubOrganizations(loopCtx, msg.body as { sessionId: string; accessToken: string });
|
||||
},
|
||||
});
|
||||
await msg.complete({ ok: true });
|
||||
return Loop.continue(undefined);
|
||||
}
|
||||
|
||||
if (msg.name === "workspace.command.syncGithubOrganizationRepos") {
|
||||
await loopCtx.step({
|
||||
name: "workspace-sync-github-organization-repos",
|
||||
timeout: 60_000,
|
||||
run: async () => {
|
||||
const { syncGithubOrganizationRepos } = await import("./app-shell.js");
|
||||
await syncGithubOrganizationRepos(loopCtx, msg.body as { sessionId: string; organizationId: string });
|
||||
},
|
||||
});
|
||||
await msg.complete({ ok: true });
|
||||
return Loop.continue(undefined);
|
||||
}
|
||||
|
||||
return Loop.continue(undefined);
|
||||
|
|
|
|||
|
|
@ -11,16 +11,25 @@ import type {
|
|||
UpdateFoundryOrganizationProfileInput,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { getActorRuntimeContext } from "../context.js";
|
||||
import { getOrCreateWorkspace } from "../handles.js";
|
||||
import { getOrCreateWorkspace, selfWorkspace } from "../handles.js";
|
||||
import { GitHubAppError } from "../../services/app-github.js";
|
||||
import { repoIdFromRemote, repoLabelFromRemote } from "../../services/repo.js";
|
||||
import { logger } from "../../logging.js";
|
||||
import { appSessions, invoices, organizationMembers, organizationProfile, repos, seatAssignments, stripeLookup } from "./db/schema.js";
|
||||
|
||||
export const APP_SHELL_WORKSPACE_ID = "app";
|
||||
|
||||
const githubWebhookLogger = logger.child({
|
||||
scope: "github-webhook",
|
||||
});
|
||||
|
||||
const PROFILE_ROW_ID = "profile";
|
||||
const OAUTH_TTL_MS = 10 * 60_000;
|
||||
|
||||
function roundDurationMs(start: number): number {
|
||||
return Math.round((performance.now() - start) * 100) / 100;
|
||||
}
|
||||
|
||||
function assertAppWorkspace(c: any): void {
|
||||
if (c.state.workspaceId !== APP_SHELL_WORKSPACE_ID) {
|
||||
throw new Error(`App shell action requires workspace ${APP_SHELL_WORKSPACE_ID}, got ${c.state.workspaceId}`);
|
||||
|
|
@ -222,22 +231,67 @@ async function getOrganizationState(workspace: any) {
|
|||
|
||||
async function buildAppSnapshot(c: any, sessionId: string): Promise<FoundryAppSnapshot> {
|
||||
assertAppWorkspace(c);
|
||||
const startedAt = performance.now();
|
||||
const session = await requireAppSessionRow(c, sessionId);
|
||||
const eligibleOrganizationIds = parseEligibleOrganizationIds(session.eligibleOrganizationIdsJson);
|
||||
|
||||
const organizations: FoundryOrganization[] = [];
|
||||
for (const organizationId of eligibleOrganizationIds) {
|
||||
try {
|
||||
const workspace = await getOrCreateWorkspace(c, organizationId);
|
||||
const organizationState = await getOrganizationState(workspace);
|
||||
organizations.push(organizationState.snapshot);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("Actor not found")) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info(
|
||||
{
|
||||
sessionId,
|
||||
workspaceId: c.state.workspaceId,
|
||||
eligibleOrganizationCount: eligibleOrganizationIds.length,
|
||||
eligibleOrganizationIds,
|
||||
},
|
||||
"build_app_snapshot_started",
|
||||
);
|
||||
|
||||
const organizations = (
|
||||
await Promise.all(
|
||||
eligibleOrganizationIds.map(async (organizationId) => {
|
||||
const organizationStartedAt = performance.now();
|
||||
try {
|
||||
const workspace = await getOrCreateWorkspace(c, organizationId);
|
||||
const organizationState = await getOrganizationState(workspace);
|
||||
logger.info(
|
||||
{
|
||||
sessionId,
|
||||
workspaceId: c.state.workspaceId,
|
||||
organizationId,
|
||||
durationMs: roundDurationMs(organizationStartedAt),
|
||||
},
|
||||
"build_app_snapshot_organization_completed",
|
||||
);
|
||||
return organizationState.snapshot;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("Actor not found")) {
|
||||
logger.error(
|
||||
{
|
||||
sessionId,
|
||||
workspaceId: c.state.workspaceId,
|
||||
organizationId,
|
||||
durationMs: roundDurationMs(organizationStartedAt),
|
||||
errorMessage: message,
|
||||
errorStack: error instanceof Error ? error.stack : undefined,
|
||||
},
|
||||
"build_app_snapshot_organization_failed",
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
logger.info(
|
||||
{
|
||||
sessionId,
|
||||
workspaceId: c.state.workspaceId,
|
||||
organizationId,
|
||||
durationMs: roundDurationMs(organizationStartedAt),
|
||||
},
|
||||
"build_app_snapshot_organization_missing",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
)
|
||||
).filter((organization): organization is FoundryOrganization => organization !== null);
|
||||
|
||||
const currentUser: FoundryUser | null = session.currentUserId
|
||||
? {
|
||||
|
|
@ -257,7 +311,7 @@ async function buildAppSnapshot(c: any, sessionId: string): Promise<FoundryAppSn
|
|||
? (organizations[0]?.id ?? null)
|
||||
: null;
|
||||
|
||||
return {
|
||||
const snapshot = {
|
||||
auth: {
|
||||
status: currentUser ? "signed_in" : "signed_out",
|
||||
currentUserId: currentUser?.id ?? null,
|
||||
|
|
@ -275,6 +329,19 @@ async function buildAppSnapshot(c: any, sessionId: string): Promise<FoundryAppSn
|
|||
users: currentUser ? [currentUser] : [],
|
||||
organizations,
|
||||
};
|
||||
|
||||
logger.info(
|
||||
{
|
||||
sessionId,
|
||||
workspaceId: c.state.workspaceId,
|
||||
eligibleOrganizationCount: eligibleOrganizationIds.length,
|
||||
organizationCount: organizations.length,
|
||||
durationMs: roundDurationMs(startedAt),
|
||||
},
|
||||
"build_app_snapshot_completed",
|
||||
);
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
async function requireSignedInSession(c: any, sessionId: string) {
|
||||
|
|
@ -364,11 +431,50 @@ async function safeListInstallations(accessToken: string): Promise<any[]> {
|
|||
}
|
||||
}
|
||||
|
||||
async function syncGithubSessionFromToken(c: any, sessionId: string, accessToken: string): Promise<{ sessionId: string; redirectTo: string }> {
|
||||
/**
|
||||
* Fast path: resolve viewer identity, store user + token in the session,
|
||||
* and return the redirect URL. Does NOT sync organizations — that work is
|
||||
* deferred to `syncGithubOrganizations` via the workflow queue so the HTTP
|
||||
* callback can respond before any proxy timeout triggers a retry.
|
||||
*/
|
||||
async function initGithubSession(c: any, sessionId: string, accessToken: string, scopes: string[]): Promise<{ sessionId: string; redirectTo: string }> {
|
||||
assertAppWorkspace(c);
|
||||
const { appShell } = getActorRuntimeContext();
|
||||
const viewer = await appShell.github.getViewer(accessToken);
|
||||
const userId = `user-${slugify(viewer.login)}`;
|
||||
|
||||
await updateAppSession(c, sessionId, {
|
||||
currentUserId: userId,
|
||||
currentUserName: viewer.name || viewer.login,
|
||||
currentUserEmail: viewer.email ?? `${viewer.login}@users.noreply.github.com`,
|
||||
currentUserGithubLogin: viewer.login,
|
||||
currentUserRoleLabel: "GitHub user",
|
||||
githubAccessToken: accessToken,
|
||||
githubScope: scopes.join(","),
|
||||
oauthState: null,
|
||||
oauthStateExpiresAt: null,
|
||||
});
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
redirectTo: `${appShell.appUrl}/organizations?foundrySession=${encodeURIComponent(sessionId)}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Slow path: list GitHub orgs + installations, sync each org workspace,
|
||||
* and update the session's eligible organization list. Called from the
|
||||
* workflow queue so it runs in the background after the callback has
|
||||
* already returned a redirect to the browser.
|
||||
*
|
||||
* Also used synchronously by bootstrapAppGithubSession (dev-only) where
|
||||
* proxy timeouts are not a concern.
|
||||
*/
|
||||
export async function syncGithubOrganizations(c: any, input: { sessionId: string; accessToken: string }): Promise<void> {
|
||||
assertAppWorkspace(c);
|
||||
const { appShell } = getActorRuntimeContext();
|
||||
const { sessionId, accessToken } = input;
|
||||
const session = await requireAppSessionRow(c, sessionId);
|
||||
const token = { accessToken, scopes: splitScopes(session.githubScope) };
|
||||
const viewer = await appShell.github.getViewer(accessToken);
|
||||
const organizations = await safeListOrganizations(accessToken);
|
||||
const installations = await safeListInstallations(accessToken);
|
||||
|
|
@ -419,24 +525,75 @@ async function syncGithubSessionFromToken(c: any, sessionId: string, accessToken
|
|||
? (linkedOrganizationIds[0] ?? null)
|
||||
: null;
|
||||
|
||||
await updateAppSession(c, session.id, {
|
||||
currentUserId: userId,
|
||||
currentUserName: viewer.name || viewer.login,
|
||||
currentUserEmail: viewer.email ?? `${viewer.login}@users.noreply.github.com`,
|
||||
currentUserGithubLogin: viewer.login,
|
||||
currentUserRoleLabel: "GitHub user",
|
||||
await updateAppSession(c, sessionId, {
|
||||
eligibleOrganizationIdsJson: encodeEligibleOrganizationIds(linkedOrganizationIds),
|
||||
activeOrganizationId,
|
||||
githubAccessToken: accessToken,
|
||||
githubScope: token.scopes.join(","),
|
||||
oauthState: null,
|
||||
oauthStateExpiresAt: null,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: session.id,
|
||||
redirectTo: `${appShell.appUrl}/organizations?foundrySession=${encodeURIComponent(session.id)}`,
|
||||
};
|
||||
export async function syncGithubOrganizationRepos(c: any, input: { sessionId: string; organizationId: string }): Promise<void> {
|
||||
assertAppWorkspace(c);
|
||||
const session = await requireSignedInSession(c, input.sessionId);
|
||||
requireEligibleOrganization(session, input.organizationId);
|
||||
|
||||
const { appShell } = getActorRuntimeContext();
|
||||
const workspace = await getOrCreateWorkspace(c, input.organizationId);
|
||||
const organization = await getOrganizationState(workspace);
|
||||
|
||||
try {
|
||||
let repositories;
|
||||
let installationStatus = organization.snapshot.github.installationStatus;
|
||||
|
||||
if (organization.snapshot.kind === "personal") {
|
||||
repositories = await appShell.github.listUserRepositories(session.githubAccessToken);
|
||||
installationStatus = "connected";
|
||||
} else if (organization.githubInstallationId) {
|
||||
try {
|
||||
repositories = await appShell.github.listInstallationRepositories(organization.githubInstallationId);
|
||||
} catch (error) {
|
||||
if (!(error instanceof GitHubAppError) || (error.status !== 403 && error.status !== 404)) {
|
||||
throw error;
|
||||
}
|
||||
repositories = (await appShell.github.listUserRepositories(session.githubAccessToken)).filter((repository) =>
|
||||
repository.fullName.startsWith(`${organization.githubLogin}/`),
|
||||
);
|
||||
installationStatus = "reconnect_required";
|
||||
}
|
||||
} else {
|
||||
repositories = (await appShell.github.listUserRepositories(session.githubAccessToken)).filter((repository) =>
|
||||
repository.fullName.startsWith(`${organization.githubLogin}/`),
|
||||
);
|
||||
installationStatus = "reconnect_required";
|
||||
}
|
||||
|
||||
await workspace.applyOrganizationSyncCompleted({
|
||||
repositories,
|
||||
installationStatus,
|
||||
lastSyncLabel: repositories.length > 0 ? "Synced just now" : "No repositories available",
|
||||
});
|
||||
} catch (error) {
|
||||
const installationStatus =
|
||||
error instanceof GitHubAppError && (error.status === 403 || error.status === 404)
|
||||
? "reconnect_required"
|
||||
: organization.snapshot.github.installationStatus;
|
||||
await workspace.markOrganizationSyncFailed({
|
||||
message: error instanceof Error ? error.message : "GitHub import failed",
|
||||
installationStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full synchronous sync: init session + sync orgs in one call.
|
||||
* Used by bootstrapAppGithubSession (dev-only) where there is no proxy
|
||||
* timeout concern and we want the session fully populated before returning.
|
||||
*/
|
||||
async function syncGithubSessionFromToken(c: any, sessionId: string, accessToken: string): Promise<{ sessionId: string; redirectTo: string }> {
|
||||
const session = await requireAppSessionRow(c, sessionId);
|
||||
const scopes = splitScopes(session.githubScope);
|
||||
const result = await initGithubSession(c, sessionId, accessToken, scopes);
|
||||
await syncGithubOrganizations(c, { sessionId, accessToken });
|
||||
return result;
|
||||
}
|
||||
|
||||
async function readOrganizationProfileRow(c: any) {
|
||||
|
|
@ -489,13 +646,14 @@ async function listOrganizationRepoCatalog(c: any): Promise<string[]> {
|
|||
}
|
||||
|
||||
async function buildOrganizationState(c: any) {
|
||||
const startedAt = performance.now();
|
||||
const row = await requireOrganizationProfileRow(c);
|
||||
const repoCatalog = await listOrganizationRepoCatalog(c);
|
||||
const members = await listOrganizationMembers(c);
|
||||
const seatAssignmentEmails = await listOrganizationSeatAssignments(c);
|
||||
const invoiceRows = await listOrganizationInvoices(c);
|
||||
|
||||
return {
|
||||
const state = {
|
||||
id: c.state.workspaceId,
|
||||
workspaceId: c.state.workspaceId,
|
||||
kind: row.kind,
|
||||
|
|
@ -540,6 +698,21 @@ async function buildOrganizationState(c: any) {
|
|||
repoCatalog,
|
||||
},
|
||||
};
|
||||
|
||||
logger.info(
|
||||
{
|
||||
workspaceId: c.state.workspaceId,
|
||||
githubLogin: row.githubLogin,
|
||||
repoCount: repoCatalog.length,
|
||||
memberCount: members.length,
|
||||
seatAssignmentCount: seatAssignmentEmails.length,
|
||||
invoiceCount: invoiceRows.length,
|
||||
durationMs: roundDurationMs(startedAt),
|
||||
},
|
||||
"build_organization_state_completed",
|
||||
);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
async function applySubscriptionState(
|
||||
|
|
@ -621,11 +794,37 @@ export const workspaceAppActions = {
|
|||
throw new Error("GitHub OAuth state is invalid or expired");
|
||||
}
|
||||
|
||||
const token = await appShell.github.exchangeCode(input.code);
|
||||
// Clear state before exchangeCode — GitHub codes are single-use and
|
||||
// duplicate callback requests (from proxy retries or user refresh)
|
||||
// must fail the state check rather than attempt a second exchange.
|
||||
// See research/friction/general.mdx 2026-03-13 entry.
|
||||
await updateAppSession(c, session.id, {
|
||||
githubScope: token.scopes.join(","),
|
||||
oauthState: null,
|
||||
oauthStateExpiresAt: null,
|
||||
});
|
||||
return await syncGithubSessionFromToken(c, session.id, token.accessToken);
|
||||
|
||||
const token = await appShell.github.exchangeCode(input.code);
|
||||
|
||||
// Fast path: store token + user identity and return the redirect
|
||||
// immediately. The slow org sync (list orgs, list installations,
|
||||
// sync each workspace) runs in the workflow queue so the HTTP
|
||||
// response lands before any proxy/infra timeout triggers a retry.
|
||||
// The frontend already polls when it sees syncStatus === "syncing".
|
||||
const result = await initGithubSession(c, session.id, token.accessToken, token.scopes);
|
||||
|
||||
// Enqueue the slow org sync to the workflow. fire-and-forget (wait: false)
|
||||
// because the redirect does not depend on org data — the frontend will
|
||||
// poll getAppSnapshot until organizations are populated.
|
||||
const self = selfWorkspace(c);
|
||||
await self.send(
|
||||
"workspace.command.syncGithubSession",
|
||||
{ sessionId: session.id, accessToken: token.accessToken },
|
||||
{
|
||||
wait: false,
|
||||
},
|
||||
);
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
async bootstrapAppGithubSession(c: any, input: { accessToken: string; sessionId?: string | null }): Promise<{ sessionId: string; redirectTo: string }> {
|
||||
|
|
@ -697,7 +896,22 @@ export const workspaceAppActions = {
|
|||
const workspace = await getOrCreateWorkspace(c, input.organizationId);
|
||||
const organization = await getOrganizationState(workspace);
|
||||
if (organization.snapshot.github.syncStatus !== "synced") {
|
||||
return await workspaceAppActions.triggerAppRepoImport(c, input);
|
||||
if (organization.snapshot.github.syncStatus !== "syncing") {
|
||||
await workspace.markOrganizationSyncStarted({
|
||||
label: "Importing repository catalog...",
|
||||
});
|
||||
|
||||
const self = selfWorkspace(c);
|
||||
await self.send(
|
||||
"workspace.command.syncGithubOrganizationRepos",
|
||||
{ sessionId: input.sessionId, organizationId: input.organizationId },
|
||||
{
|
||||
wait: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return await buildAppSnapshot(c, input.sessionId);
|
||||
}
|
||||
return await buildAppSnapshot(c, input.sessionId);
|
||||
},
|
||||
|
|
@ -723,55 +937,24 @@ export const workspaceAppActions = {
|
|||
const session = await requireSignedInSession(c, input.sessionId);
|
||||
requireEligibleOrganization(session, input.organizationId);
|
||||
|
||||
const { appShell } = getActorRuntimeContext();
|
||||
const workspace = await getOrCreateWorkspace(c, input.organizationId);
|
||||
const organization = await getOrganizationState(workspace);
|
||||
if (organization.snapshot.github.syncStatus === "syncing") {
|
||||
return await buildAppSnapshot(c, input.sessionId);
|
||||
}
|
||||
|
||||
await workspace.markOrganizationSyncStarted({
|
||||
label: "Importing repository catalog...",
|
||||
});
|
||||
|
||||
try {
|
||||
let repositories;
|
||||
let installationStatus = organization.snapshot.github.installationStatus;
|
||||
|
||||
if (organization.snapshot.kind === "personal") {
|
||||
repositories = await appShell.github.listUserRepositories(session.githubAccessToken);
|
||||
installationStatus = "connected";
|
||||
} else if (organization.githubInstallationId) {
|
||||
try {
|
||||
repositories = await appShell.github.listInstallationRepositories(organization.githubInstallationId);
|
||||
} catch (error) {
|
||||
if (!(error instanceof GitHubAppError) || (error.status !== 403 && error.status !== 404)) {
|
||||
throw error;
|
||||
}
|
||||
repositories = (await appShell.github.listUserRepositories(session.githubAccessToken)).filter((repository) =>
|
||||
repository.fullName.startsWith(`${organization.githubLogin}/`),
|
||||
);
|
||||
installationStatus = "reconnect_required";
|
||||
}
|
||||
} else {
|
||||
repositories = (await appShell.github.listUserRepositories(session.githubAccessToken)).filter((repository) =>
|
||||
repository.fullName.startsWith(`${organization.githubLogin}/`),
|
||||
);
|
||||
installationStatus = "reconnect_required";
|
||||
}
|
||||
|
||||
await workspace.applyOrganizationSyncCompleted({
|
||||
repositories,
|
||||
installationStatus,
|
||||
lastSyncLabel: repositories.length > 0 ? "Synced just now" : "No repositories available",
|
||||
});
|
||||
} catch (error) {
|
||||
const installationStatus =
|
||||
error instanceof GitHubAppError && (error.status === 403 || error.status === 404)
|
||||
? "reconnect_required"
|
||||
: organization.snapshot.github.installationStatus;
|
||||
await workspace.markOrganizationSyncFailed({
|
||||
message: error instanceof Error ? error.message : "GitHub import failed",
|
||||
installationStatus,
|
||||
});
|
||||
}
|
||||
const self = selfWorkspace(c);
|
||||
await self.send(
|
||||
"workspace.command.syncGithubOrganizationRepos",
|
||||
{ sessionId: input.sessionId, organizationId: input.organizationId },
|
||||
{
|
||||
wait: false,
|
||||
},
|
||||
);
|
||||
|
||||
return await buildAppSnapshot(c, input.sessionId);
|
||||
},
|
||||
|
|
@ -832,7 +1015,7 @@ export const workspaceAppActions = {
|
|||
customerId,
|
||||
customerEmail: session.currentUserEmail,
|
||||
planId: input.planId,
|
||||
successUrl: `${appShell.appUrl}/api/rivet/app/billing/checkout/complete?organizationId=${encodeURIComponent(
|
||||
successUrl: `${appShell.apiUrl}/v1/billing/checkout/complete?organizationId=${encodeURIComponent(
|
||||
input.organizationId,
|
||||
)}&foundrySession=${encodeURIComponent(input.sessionId)}&session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancelUrl: `${appShell.appUrl}/organizations/${input.organizationId}/billing?foundrySession=${encodeURIComponent(input.sessionId)}`,
|
||||
|
|
@ -1014,7 +1197,14 @@ export const workspaceAppActions = {
|
|||
const accountLogin = body.installation?.account?.login;
|
||||
const accountType = body.installation?.account?.type;
|
||||
if (!accountLogin) {
|
||||
console.log(`[github-webhook] Ignoring ${event}.${body.action ?? ""}: no installation account`);
|
||||
githubWebhookLogger.info(
|
||||
{
|
||||
event,
|
||||
action: body.action ?? null,
|
||||
reason: "missing_installation_account",
|
||||
},
|
||||
"ignored",
|
||||
);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
|
|
@ -1022,7 +1212,15 @@ export const workspaceAppActions = {
|
|||
const organizationId = organizationWorkspaceId(kind, accountLogin);
|
||||
|
||||
if (event === "installation" && (body.action === "created" || body.action === "deleted" || body.action === "suspend" || body.action === "unsuspend")) {
|
||||
console.log(`[github-webhook] ${event}.${body.action} for ${accountLogin} (org=${organizationId})`);
|
||||
githubWebhookLogger.info(
|
||||
{
|
||||
event,
|
||||
action: body.action,
|
||||
accountLogin,
|
||||
organizationId,
|
||||
},
|
||||
"installation_event",
|
||||
);
|
||||
if (body.action === "deleted") {
|
||||
const workspace = await getOrCreateWorkspace(c, organizationId);
|
||||
await workspace.applyGithubInstallationRemoved({});
|
||||
|
|
@ -1036,8 +1234,16 @@ export const workspaceAppActions = {
|
|||
}
|
||||
|
||||
if (event === "installation_repositories") {
|
||||
console.log(
|
||||
`[github-webhook] ${event}.${body.action} for ${accountLogin}: +${body.repositories_added?.length ?? 0} -${body.repositories_removed?.length ?? 0}`,
|
||||
githubWebhookLogger.info(
|
||||
{
|
||||
event,
|
||||
action: body.action ?? null,
|
||||
accountLogin,
|
||||
organizationId,
|
||||
repositoriesAdded: body.repositories_added?.length ?? 0,
|
||||
repositoriesRemoved: body.repositories_removed?.length ?? 0,
|
||||
},
|
||||
"repository_membership_changed",
|
||||
);
|
||||
const workspace = await getOrCreateWorkspace(c, organizationId);
|
||||
await workspace.applyGithubRepositoryChanges({
|
||||
|
|
@ -1063,13 +1269,30 @@ export const workspaceAppActions = {
|
|||
) {
|
||||
const repoFullName = body.repository?.full_name;
|
||||
if (repoFullName) {
|
||||
console.log(`[github-webhook] ${event}.${body.action ?? ""} for ${repoFullName}`);
|
||||
githubWebhookLogger.info(
|
||||
{
|
||||
event,
|
||||
action: body.action ?? null,
|
||||
accountLogin,
|
||||
organizationId,
|
||||
repoFullName,
|
||||
},
|
||||
"repository_event",
|
||||
);
|
||||
// TODO: Dispatch to GitHubStateActor / downstream actors
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
console.log(`[github-webhook] Unhandled event: ${event}.${body.action ?? ""}`);
|
||||
githubWebhookLogger.info(
|
||||
{
|
||||
event,
|
||||
action: body.action ?? null,
|
||||
accountLogin,
|
||||
organizationId,
|
||||
},
|
||||
"unhandled_event",
|
||||
);
|
||||
return { ok: true };
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
CREATE TABLE `app_sessions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`current_user_id` text,
|
||||
`current_user_name` text,
|
||||
`current_user_email` text,
|
||||
`current_user_github_login` text,
|
||||
`current_user_role_label` text,
|
||||
`eligible_organization_ids_json` text NOT NULL,
|
||||
`active_organization_id` text,
|
||||
`github_access_token` text,
|
||||
`github_scope` text NOT NULL,
|
||||
`starter_repo_status` text NOT NULL,
|
||||
`starter_repo_starred_at` integer,
|
||||
`starter_repo_skipped_at` integer,
|
||||
`oauth_state` text,
|
||||
`oauth_state_expires_at` integer,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `invoices` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`label` text NOT NULL,
|
||||
`issued_at` text NOT NULL,
|
||||
`amount_usd` integer NOT NULL,
|
||||
`status` text NOT NULL,
|
||||
`created_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `organization_members` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`email` text NOT NULL,
|
||||
`role` text NOT NULL,
|
||||
`state` text NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `organization_profile` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`kind` text NOT NULL,
|
||||
`github_account_id` text NOT NULL,
|
||||
`github_login` text NOT NULL,
|
||||
`github_account_type` text NOT NULL,
|
||||
`display_name` text NOT NULL,
|
||||
`slug` text NOT NULL,
|
||||
`primary_domain` text NOT NULL,
|
||||
`default_model` text NOT NULL,
|
||||
`auto_import_repos` integer NOT NULL,
|
||||
`repo_import_status` text NOT NULL,
|
||||
`github_connected_account` text NOT NULL,
|
||||
`github_installation_status` text NOT NULL,
|
||||
`github_sync_status` text NOT NULL,
|
||||
`github_installation_id` integer,
|
||||
`github_last_sync_label` text NOT NULL,
|
||||
`github_last_sync_at` integer,
|
||||
`stripe_customer_id` text,
|
||||
`stripe_subscription_id` text,
|
||||
`stripe_price_id` text,
|
||||
`billing_plan_id` text NOT NULL,
|
||||
`billing_status` text NOT NULL,
|
||||
`billing_seats_included` integer NOT NULL,
|
||||
`billing_trial_ends_at` text,
|
||||
`billing_renewal_at` text,
|
||||
`billing_payment_method_label` text NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `provider_profiles` (
|
||||
`provider_id` text PRIMARY KEY NOT NULL,
|
||||
`profile_json` text NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `repos` (
|
||||
`repo_id` text PRIMARY KEY NOT NULL,
|
||||
`remote_url` text NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `seat_assignments` (
|
||||
`email` text PRIMARY KEY NOT NULL,
|
||||
`created_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `stripe_lookup` (
|
||||
`lookup_key` text PRIMARY KEY NOT NULL,
|
||||
`organization_id` text NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `task_lookup` (
|
||||
`task_id` text PRIMARY KEY NOT NULL,
|
||||
`repo_id` text NOT NULL
|
||||
);
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
CREATE TABLE `provider_profiles` (
|
||||
`provider_id` text PRIMARY KEY NOT NULL,
|
||||
`profile_json` text NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
CREATE TABLE `repos` (
|
||||
`repo_id` text PRIMARY KEY NOT NULL,
|
||||
`remote_url` text NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
CREATE TABLE `task_lookup` (
|
||||
`task_id` text PRIMARY KEY NOT NULL,
|
||||
`repo_id` text NOT NULL
|
||||
);
|
||||
|
|
@ -1,9 +1,448 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "a85809c0-65c2-4f99-92ed-34357c9f83d7",
|
||||
"id": "0bef30e4-148a-4fe1-b2ca-a9721893c3ac",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"app_sessions": {
|
||||
"name": "app_sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"current_user_id": {
|
||||
"name": "current_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"current_user_name": {
|
||||
"name": "current_user_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"current_user_email": {
|
||||
"name": "current_user_email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"current_user_github_login": {
|
||||
"name": "current_user_github_login",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"current_user_role_label": {
|
||||
"name": "current_user_role_label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"eligible_organization_ids_json": {
|
||||
"name": "eligible_organization_ids_json",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"active_organization_id": {
|
||||
"name": "active_organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"github_access_token": {
|
||||
"name": "github_access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"github_scope": {
|
||||
"name": "github_scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"starter_repo_status": {
|
||||
"name": "starter_repo_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"starter_repo_starred_at": {
|
||||
"name": "starter_repo_starred_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"starter_repo_skipped_at": {
|
||||
"name": "starter_repo_skipped_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"oauth_state": {
|
||||
"name": "oauth_state",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"oauth_state_expires_at": {
|
||||
"name": "oauth_state_expires_at",
|
||||
"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": {}
|
||||
},
|
||||
"invoices": {
|
||||
"name": "invoices",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"issued_at": {
|
||||
"name": "issued_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"amount_usd": {
|
||||
"name": "amount_usd",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"organization_members": {
|
||||
"name": "organization_members",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"organization_profile": {
|
||||
"name": "organization_profile",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"kind": {
|
||||
"name": "kind",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"github_account_id": {
|
||||
"name": "github_account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"github_login": {
|
||||
"name": "github_login",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"github_account_type": {
|
||||
"name": "github_account_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"display_name": {
|
||||
"name": "display_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"primary_domain": {
|
||||
"name": "primary_domain",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"default_model": {
|
||||
"name": "default_model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"auto_import_repos": {
|
||||
"name": "auto_import_repos",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"repo_import_status": {
|
||||
"name": "repo_import_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"github_connected_account": {
|
||||
"name": "github_connected_account",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"github_installation_status": {
|
||||
"name": "github_installation_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"github_sync_status": {
|
||||
"name": "github_sync_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"github_installation_id": {
|
||||
"name": "github_installation_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"github_last_sync_label": {
|
||||
"name": "github_last_sync_label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"github_last_sync_at": {
|
||||
"name": "github_last_sync_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"stripe_customer_id": {
|
||||
"name": "stripe_customer_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"stripe_subscription_id": {
|
||||
"name": "stripe_subscription_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"stripe_price_id": {
|
||||
"name": "stripe_price_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"billing_plan_id": {
|
||||
"name": "billing_plan_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"billing_status": {
|
||||
"name": "billing_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"billing_seats_included": {
|
||||
"name": "billing_seats_included",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"billing_trial_ends_at": {
|
||||
"name": "billing_trial_ends_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"billing_renewal_at": {
|
||||
"name": "billing_renewal_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"billing_payment_method_label": {
|
||||
"name": "billing_payment_method_label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"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": {}
|
||||
},
|
||||
"provider_profiles": {
|
||||
"name": "provider_profiles",
|
||||
"columns": {
|
||||
|
|
@ -34,6 +473,123 @@
|
|||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"repos": {
|
||||
"name": "repos",
|
||||
"columns": {
|
||||
"repo_id": {
|
||||
"name": "repo_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"remote_url": {
|
||||
"name": "remote_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"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": {}
|
||||
},
|
||||
"seat_assignments": {
|
||||
"name": "seat_assignments",
|
||||
"columns": {
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"stripe_lookup": {
|
||||
"name": "stripe_lookup",
|
||||
"columns": {
|
||||
"lookup_key": {
|
||||
"name": "lookup_key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"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_lookup": {
|
||||
"name": "task_lookup",
|
||||
"columns": {
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"repo_id": {
|
||||
"name": "repo_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
|
|
|
|||
|
|
@ -1,87 +0,0 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "450e2fdf-6349-482f-8a68-5bc0f0a9718a",
|
||||
"prevId": "a85809c0-65c2-4f99-92ed-34357c9f83d7",
|
||||
"tables": {
|
||||
"provider_profiles": {
|
||||
"name": "provider_profiles",
|
||||
"columns": {
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"profile_json": {
|
||||
"name": "profile_json",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"repos": {
|
||||
"name": "repos",
|
||||
"columns": {
|
||||
"repo_id": {
|
||||
"name": "repo_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"remote_url": {
|
||||
"name": "remote_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"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": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,22 +5,8 @@
|
|||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1770924376525,
|
||||
"tag": "0000_rare_iron_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1770947252912,
|
||||
"tag": "0001_sleepy_lady_deathstrike",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1772668800000,
|
||||
"tag": "0002_tiny_silver_surfer",
|
||||
"when": 1773376221152,
|
||||
"tag": "0000_melted_viper",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,68 +6,8 @@ const journal = {
|
|||
entries: [
|
||||
{
|
||||
idx: 0,
|
||||
when: 1770924376525,
|
||||
tag: "0000_rare_iron_man",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 1,
|
||||
when: 1770947252912,
|
||||
tag: "0001_sleepy_lady_deathstrike",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 2,
|
||||
when: 1772668800000,
|
||||
tag: "0002_tiny_silver_surfer",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 3,
|
||||
when: 1773100800000,
|
||||
tag: "0003_app_shell_organization_profile",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 4,
|
||||
when: 1773100800001,
|
||||
tag: "0004_app_shell_organization_members",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 5,
|
||||
when: 1773100800002,
|
||||
tag: "0005_app_shell_seat_assignments",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 6,
|
||||
when: 1773100800003,
|
||||
tag: "0006_app_shell_invoices",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 7,
|
||||
when: 1773100800004,
|
||||
tag: "0007_app_shell_sessions",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 8,
|
||||
when: 1773100800005,
|
||||
tag: "0008_app_shell_stripe_lookup",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 9,
|
||||
when: 1773100800006,
|
||||
tag: "0009_github_sync_status",
|
||||
breakpoints: true,
|
||||
},
|
||||
{
|
||||
idx: 10,
|
||||
when: 1772928000000,
|
||||
tag: "0010_app_session_starter_repo",
|
||||
when: 1773376221152,
|
||||
tag: "0000_melted_viper",
|
||||
breakpoints: true,
|
||||
},
|
||||
],
|
||||
|
|
@ -76,77 +16,7 @@ const journal = {
|
|||
export default {
|
||||
journal,
|
||||
migrations: {
|
||||
m0000: `CREATE TABLE \`provider_profiles\` (
|
||||
\`provider_id\` text PRIMARY KEY NOT NULL,
|
||||
\`profile_json\` text NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0001: `CREATE TABLE \`repos\` (
|
||||
\`repo_id\` text PRIMARY KEY NOT NULL,
|
||||
\`remote_url\` text NOT NULL,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0002: `CREATE TABLE \`task_lookup\` (
|
||||
\`task_id\` text PRIMARY KEY NOT NULL,
|
||||
\`repo_id\` text NOT NULL
|
||||
);
|
||||
`,
|
||||
m0003: `CREATE TABLE \`organization_profile\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
\`kind\` text NOT NULL,
|
||||
\`github_account_id\` text NOT NULL,
|
||||
\`github_login\` text NOT NULL,
|
||||
\`github_account_type\` text NOT NULL,
|
||||
\`display_name\` text NOT NULL,
|
||||
\`slug\` text NOT NULL,
|
||||
\`primary_domain\` text NOT NULL,
|
||||
\`default_model\` text NOT NULL,
|
||||
\`auto_import_repos\` integer NOT NULL,
|
||||
\`repo_import_status\` text NOT NULL,
|
||||
\`github_connected_account\` text NOT NULL,
|
||||
\`github_installation_status\` text NOT NULL,
|
||||
\`github_installation_id\` integer,
|
||||
\`github_last_sync_label\` text NOT NULL,
|
||||
\`stripe_customer_id\` text,
|
||||
\`stripe_subscription_id\` text,
|
||||
\`stripe_price_id\` text,
|
||||
\`billing_plan_id\` text NOT NULL,
|
||||
\`billing_status\` text NOT NULL,
|
||||
\`billing_seats_included\` integer NOT NULL,
|
||||
\`billing_trial_ends_at\` text,
|
||||
\`billing_renewal_at\` text,
|
||||
\`billing_payment_method_label\` text NOT NULL,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0004: `CREATE TABLE \`organization_members\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
\`name\` text NOT NULL,
|
||||
\`email\` text NOT NULL,
|
||||
\`role\` text NOT NULL,
|
||||
\`state\` text NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0005: `CREATE TABLE \`seat_assignments\` (
|
||||
\`email\` text PRIMARY KEY NOT NULL,
|
||||
\`created_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0006: `CREATE TABLE \`invoices\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
\`label\` text NOT NULL,
|
||||
\`issued_at\` text NOT NULL,
|
||||
\`amount_usd\` integer NOT NULL,
|
||||
\`status\` text NOT NULL,
|
||||
\`created_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0007: `CREATE TABLE \`app_sessions\` (
|
||||
m0000: `CREATE TABLE \`app_sessions\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
\`current_user_id\` text,
|
||||
\`current_user_name\` text,
|
||||
|
|
@ -165,23 +35,84 @@ export default {
|
|||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0008: `CREATE TABLE \`stripe_lookup\` (
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`invoices\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
\`label\` text NOT NULL,
|
||||
\`issued_at\` text NOT NULL,
|
||||
\`amount_usd\` integer NOT NULL,
|
||||
\`status\` text NOT NULL,
|
||||
\`created_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`organization_members\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
\`name\` text NOT NULL,
|
||||
\`email\` text NOT NULL,
|
||||
\`role\` text NOT NULL,
|
||||
\`state\` text NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`organization_profile\` (
|
||||
\`id\` text PRIMARY KEY NOT NULL,
|
||||
\`kind\` text NOT NULL,
|
||||
\`github_account_id\` text NOT NULL,
|
||||
\`github_login\` text NOT NULL,
|
||||
\`github_account_type\` text NOT NULL,
|
||||
\`display_name\` text NOT NULL,
|
||||
\`slug\` text NOT NULL,
|
||||
\`primary_domain\` text NOT NULL,
|
||||
\`default_model\` text NOT NULL,
|
||||
\`auto_import_repos\` integer NOT NULL,
|
||||
\`repo_import_status\` text NOT NULL,
|
||||
\`github_connected_account\` text NOT NULL,
|
||||
\`github_installation_status\` text NOT NULL,
|
||||
\`github_sync_status\` text NOT NULL,
|
||||
\`github_installation_id\` integer,
|
||||
\`github_last_sync_label\` text NOT NULL,
|
||||
\`github_last_sync_at\` integer,
|
||||
\`stripe_customer_id\` text,
|
||||
\`stripe_subscription_id\` text,
|
||||
\`stripe_price_id\` text,
|
||||
\`billing_plan_id\` text NOT NULL,
|
||||
\`billing_status\` text NOT NULL,
|
||||
\`billing_seats_included\` integer NOT NULL,
|
||||
\`billing_trial_ends_at\` text,
|
||||
\`billing_renewal_at\` text,
|
||||
\`billing_payment_method_label\` text NOT NULL,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`provider_profiles\` (
|
||||
\`provider_id\` text PRIMARY KEY NOT NULL,
|
||||
\`profile_json\` text NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`repos\` (
|
||||
\`repo_id\` text PRIMARY KEY NOT NULL,
|
||||
\`remote_url\` text NOT NULL,
|
||||
\`created_at\` integer NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`seat_assignments\` (
|
||||
\`email\` text PRIMARY KEY NOT NULL,
|
||||
\`created_at\` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`stripe_lookup\` (
|
||||
\`lookup_key\` text PRIMARY KEY NOT NULL,
|
||||
\`organization_id\` text NOT NULL,
|
||||
\`updated_at\` integer NOT NULL
|
||||
);
|
||||
`,
|
||||
m0009: `ALTER TABLE \`organization_profile\` ADD COLUMN \`github_sync_status\` text NOT NULL DEFAULT 'pending';
|
||||
ALTER TABLE \`organization_profile\` ADD COLUMN \`github_last_sync_at\` integer;
|
||||
UPDATE \`organization_profile\`
|
||||
SET \`github_sync_status\` = CASE
|
||||
WHEN \`repo_import_status\` = 'ready' THEN 'synced'
|
||||
WHEN \`repo_import_status\` = 'importing' THEN 'syncing'
|
||||
ELSE 'pending'
|
||||
END;
|
||||
`,
|
||||
m0010: `-- no-op: starter_repo_* columns are already present in m0007 app_sessions
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE \`task_lookup\` (
|
||||
\`task_id\` text PRIMARY KEY NOT NULL,
|
||||
\`repo_id\` text NOT NULL
|
||||
);
|
||||
`,
|
||||
} as const,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { integer, sqliteTable, text } from "rivetkit/db/drizzle";
|
|||
// SQLite is per workspace actor instance, so no workspaceId column needed.
|
||||
export const providerProfiles = sqliteTable("provider_profiles", {
|
||||
providerId: text("provider_id").notNull().primaryKey(),
|
||||
// Structured by the provider profile snapshot returned by provider integrations.
|
||||
profileJson: text("profile_json").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
|
@ -80,6 +81,7 @@ export const appSessions = sqliteTable("app_sessions", {
|
|||
currentUserEmail: text("current_user_email"),
|
||||
currentUserGithubLogin: text("current_user_github_login"),
|
||||
currentUserRoleLabel: text("current_user_role_label"),
|
||||
// Structured as a JSON array of eligible organization ids for the session.
|
||||
eligibleOrganizationIdsJson: text("eligible_organization_ids_json").notNull(),
|
||||
activeOrganizationId: text("active_organization_id"),
|
||||
githubAccessToken: text("github_access_token"),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ export const workspace = actor({
|
|||
db: workspaceDb,
|
||||
queues: Object.fromEntries(WORKSPACE_QUEUE_NAMES.map((name) => [name, queue()])),
|
||||
options: {
|
||||
name: "Workspace",
|
||||
icon: "compass",
|
||||
actionTimeout: 5 * 60_000,
|
||||
},
|
||||
createState: (_c, workspaceId: string) => ({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Hono } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { initActorRuntimeContext } from "./actors/context.js";
|
||||
import { registry } from "./actors/index.js";
|
||||
import { workspaceKey } from "./actors/keys.js";
|
||||
|
|
@ -11,12 +12,38 @@ import { createClient } from "rivetkit/client";
|
|||
import type { FoundryBillingPlanId } from "@sandbox-agent/foundry-shared";
|
||||
import { createDefaultAppShellServices } from "./services/app-shell-runtime.js";
|
||||
import { APP_SHELL_WORKSPACE_ID } from "./actors/workspace/app-shell.js";
|
||||
import { logger } from "./logging.js";
|
||||
|
||||
export interface BackendStartOptions {
|
||||
host?: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
interface AppWorkspaceLogContext {
|
||||
action?: string;
|
||||
cfConnectingIp?: string;
|
||||
cfRay?: string;
|
||||
forwardedFor?: string;
|
||||
forwardedHost?: string;
|
||||
forwardedProto?: string;
|
||||
method?: string;
|
||||
path?: string;
|
||||
requestId?: string;
|
||||
referer?: string;
|
||||
secFetchDest?: string;
|
||||
secFetchMode?: string;
|
||||
secFetchSite?: string;
|
||||
secFetchUser?: string;
|
||||
sessionId?: string;
|
||||
userAgent?: string;
|
||||
xRealIp?: string;
|
||||
}
|
||||
|
||||
function isRivetRequest(request: Request): boolean {
|
||||
const { pathname } = new URL(request.url);
|
||||
return pathname === "/v1/rivet" || pathname.startsWith("/v1/rivet/");
|
||||
}
|
||||
|
||||
function isRetryableAppActorError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return message.includes("Actor not ready") || message.includes("socket connection was closed unexpectedly");
|
||||
|
|
@ -70,11 +97,26 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
|
|||
initActorRuntimeContext(config, providers, notifications, driver, createDefaultAppShellServices());
|
||||
|
||||
const actorClient = createClient({
|
||||
endpoint: `http://127.0.0.1:${config.backend.port}/api/rivet`,
|
||||
endpoint: `http://127.0.0.1:${config.backend.port}/v1/rivet`,
|
||||
}) as any;
|
||||
|
||||
// Wrap RivetKit and app routes in a single Hono app mounted at /api/rivet.
|
||||
const app = new Hono();
|
||||
const requestHeaderContext = (c: any): AppWorkspaceLogContext => ({
|
||||
cfConnectingIp: c.req.header("cf-connecting-ip") ?? undefined,
|
||||
cfRay: c.req.header("cf-ray") ?? undefined,
|
||||
forwardedFor: c.req.header("x-forwarded-for") ?? undefined,
|
||||
forwardedHost: c.req.header("x-forwarded-host") ?? undefined,
|
||||
forwardedProto: c.req.header("x-forwarded-proto") ?? undefined,
|
||||
referer: c.req.header("referer") ?? undefined,
|
||||
secFetchDest: c.req.header("sec-fetch-dest") ?? undefined,
|
||||
secFetchMode: c.req.header("sec-fetch-mode") ?? undefined,
|
||||
secFetchSite: c.req.header("sec-fetch-site") ?? undefined,
|
||||
secFetchUser: c.req.header("sec-fetch-user") ?? undefined,
|
||||
userAgent: c.req.header("user-agent") ?? undefined,
|
||||
xRealIp: c.req.header("x-real-ip") ?? undefined,
|
||||
});
|
||||
|
||||
// Serve custom Foundry HTTP APIs alongside the RivetKit registry.
|
||||
const app = new Hono<{ Variables: { requestId: string } }>();
|
||||
const allowHeaders = [
|
||||
"Content-Type",
|
||||
"Authorization",
|
||||
|
|
@ -93,7 +135,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
|
|||
];
|
||||
const exposeHeaders = ["Content-Type", "x-foundry-session", "x-rivet-ray-id"];
|
||||
app.use(
|
||||
"/api/rivet/*",
|
||||
"/v1/*",
|
||||
cors({
|
||||
origin: (origin) => origin ?? "*",
|
||||
credentials: true,
|
||||
|
|
@ -103,7 +145,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
|
|||
}),
|
||||
);
|
||||
app.use(
|
||||
"/api/rivet",
|
||||
"/v1",
|
||||
cors({
|
||||
origin: (origin) => origin ?? "*",
|
||||
credentials: true,
|
||||
|
|
@ -112,92 +154,208 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
|
|||
exposeHeaders,
|
||||
}),
|
||||
);
|
||||
app.use("*", async (c, next) => {
|
||||
const requestId = c.req.header("x-request-id")?.trim() || randomUUID();
|
||||
const start = performance.now();
|
||||
c.set("requestId", requestId);
|
||||
c.header("x-request-id", requestId);
|
||||
|
||||
const appWorkspace = async () =>
|
||||
await withRetries(
|
||||
async () =>
|
||||
await actorClient.workspace.getOrCreate(workspaceKey(APP_SHELL_WORKSPACE_ID), {
|
||||
createWithInput: APP_SHELL_WORKSPACE_ID,
|
||||
}),
|
||||
try {
|
||||
await next();
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{
|
||||
...requestHeaderContext(c),
|
||||
requestId,
|
||||
method: c.req.method,
|
||||
path: c.req.path,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
errorStack: error instanceof Error ? error.stack : undefined,
|
||||
},
|
||||
"http_request_failed",
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
...requestHeaderContext(c),
|
||||
requestId,
|
||||
method: c.req.method,
|
||||
path: c.req.path,
|
||||
status: c.res.status,
|
||||
durationMs: Math.round((performance.now() - start) * 100) / 100,
|
||||
},
|
||||
"http_request",
|
||||
);
|
||||
});
|
||||
|
||||
const appWorkspaceAction = async <T>(run: (workspace: any) => Promise<T>): Promise<T> => await withRetries(async () => await run(await appWorkspace()));
|
||||
let cachedAppWorkspace: any | null = null;
|
||||
|
||||
const appWorkspace = async (context: AppWorkspaceLogContext = {}) => {
|
||||
if (cachedAppWorkspace) return cachedAppWorkspace;
|
||||
|
||||
const start = performance.now();
|
||||
try {
|
||||
const handle = await withRetries(
|
||||
async () =>
|
||||
await actorClient.workspace.getOrCreate(workspaceKey(APP_SHELL_WORKSPACE_ID), {
|
||||
createWithInput: APP_SHELL_WORKSPACE_ID,
|
||||
}),
|
||||
);
|
||||
cachedAppWorkspace = handle;
|
||||
logger.info(
|
||||
{
|
||||
...context,
|
||||
cache: "miss",
|
||||
durationMs: Math.round((performance.now() - start) * 100) / 100,
|
||||
},
|
||||
"app_workspace_resolve",
|
||||
);
|
||||
return handle;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{
|
||||
...context,
|
||||
cache: "miss",
|
||||
durationMs: Math.round((performance.now() - start) * 100) / 100,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
errorStack: error instanceof Error ? error.stack : undefined,
|
||||
},
|
||||
"app_workspace_resolve_failed",
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const appWorkspaceAction = async <T>(action: string, run: (workspace: any) => Promise<T>, context: AppWorkspaceLogContext = {}): Promise<T> => {
|
||||
try {
|
||||
return await run(await appWorkspace({ ...context, action }));
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{
|
||||
...context,
|
||||
action,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
errorStack: error instanceof Error ? error.stack : undefined,
|
||||
},
|
||||
"app_workspace_action_failed",
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const requestLogContext = (c: any, sessionId?: string): AppWorkspaceLogContext => ({
|
||||
...requestHeaderContext(c),
|
||||
method: c.req.method,
|
||||
path: c.req.path,
|
||||
requestId: c.get("requestId"),
|
||||
sessionId,
|
||||
});
|
||||
|
||||
const resolveSessionId = async (c: any): Promise<string> => {
|
||||
const requested = c.req.header("x-foundry-session");
|
||||
const { sessionId } = await appWorkspaceAction(
|
||||
"ensureAppSession",
|
||||
async (workspace) => await workspace.ensureAppSession(requested && requested.trim().length > 0 ? { requestedSessionId: requested } : {}),
|
||||
requestLogContext(c),
|
||||
);
|
||||
c.header("x-foundry-session", sessionId);
|
||||
return sessionId;
|
||||
};
|
||||
|
||||
app.get("/api/rivet/app/snapshot", async (c) => {
|
||||
app.get("/v1/app/snapshot", async (c) => {
|
||||
const sessionId = await resolveSessionId(c);
|
||||
return c.json(await appWorkspaceAction(async (workspace) => await workspace.getAppSnapshot({ sessionId })));
|
||||
return c.json(
|
||||
await appWorkspaceAction("getAppSnapshot", async (workspace) => await workspace.getAppSnapshot({ sessionId }), requestLogContext(c, sessionId)),
|
||||
);
|
||||
});
|
||||
|
||||
app.get("/api/rivet/app/auth/github/start", async (c) => {
|
||||
app.get("/v1/auth/github/start", async (c) => {
|
||||
const sessionId = await resolveSessionId(c);
|
||||
const result = await appWorkspaceAction(async (workspace) => await workspace.startAppGithubAuth({ sessionId }));
|
||||
const result = await appWorkspaceAction(
|
||||
"startAppGithubAuth",
|
||||
async (workspace) => await workspace.startAppGithubAuth({ sessionId }),
|
||||
requestLogContext(c, sessionId),
|
||||
);
|
||||
return Response.redirect(result.url, 302);
|
||||
});
|
||||
|
||||
const handleGithubAuthCallback = async (c: any) => {
|
||||
// TEMPORARY: dump all request headers to diagnose duplicate callback requests
|
||||
// (Railway nginx proxy_next_upstream? Cloudflare retry? browser?)
|
||||
// Remove once root cause is identified.
|
||||
const allHeaders: Record<string, string> = {};
|
||||
c.req.raw.headers.forEach((value: string, key: string) => {
|
||||
allHeaders[key] = value;
|
||||
});
|
||||
logger.info({ headers: allHeaders, url: c.req.url }, "github_callback_headers");
|
||||
|
||||
const code = c.req.query("code");
|
||||
const state = c.req.query("state");
|
||||
if (!code || !state) {
|
||||
return c.text("Missing GitHub OAuth callback parameters", 400);
|
||||
}
|
||||
const result = await appWorkspaceAction(async (workspace) => await workspace.completeAppGithubAuth({ code, state }));
|
||||
const result = await appWorkspaceAction(
|
||||
"completeAppGithubAuth",
|
||||
async (workspace) => await workspace.completeAppGithubAuth({ code, state }),
|
||||
requestLogContext(c),
|
||||
);
|
||||
c.header("x-foundry-session", result.sessionId);
|
||||
return Response.redirect(result.redirectTo, 302);
|
||||
};
|
||||
|
||||
app.get("/api/rivet/app/auth/github/callback", handleGithubAuthCallback);
|
||||
app.get("/v1/auth/github/callback", handleGithubAuthCallback);
|
||||
app.get("/api/auth/callback/github", handleGithubAuthCallback);
|
||||
|
||||
app.post("/api/rivet/app/sign-out", async (c) => {
|
||||
app.post("/v1/app/sign-out", async (c) => {
|
||||
const sessionId = await resolveSessionId(c);
|
||||
return c.json(await appWorkspaceAction(async (workspace) => await workspace.signOutApp({ sessionId })));
|
||||
return c.json(await appWorkspaceAction("signOutApp", async (workspace) => await workspace.signOutApp({ sessionId }), requestLogContext(c, sessionId)));
|
||||
});
|
||||
|
||||
app.post("/api/rivet/app/onboarding/starter-repo/skip", async (c) => {
|
||||
app.post("/v1/app/onboarding/starter-repo/skip", async (c) => {
|
||||
const sessionId = await resolveSessionId(c);
|
||||
return c.json(await appWorkspaceAction(async (workspace) => await workspace.skipAppStarterRepo({ sessionId })));
|
||||
return c.json(
|
||||
await appWorkspaceAction("skipAppStarterRepo", async (workspace) => await workspace.skipAppStarterRepo({ sessionId }), requestLogContext(c, sessionId)),
|
||||
);
|
||||
});
|
||||
|
||||
app.post("/api/rivet/app/organizations/:organizationId/starter-repo/star", async (c) => {
|
||||
app.post("/v1/app/organizations/:organizationId/starter-repo/star", async (c) => {
|
||||
const sessionId = await resolveSessionId(c);
|
||||
return c.json(
|
||||
await appWorkspaceAction(
|
||||
"starAppStarterRepo",
|
||||
async (workspace) =>
|
||||
await workspace.starAppStarterRepo({
|
||||
sessionId,
|
||||
organizationId: c.req.param("organizationId"),
|
||||
}),
|
||||
requestLogContext(c, sessionId),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
app.post("/api/rivet/app/organizations/:organizationId/select", async (c) => {
|
||||
app.post("/v1/app/organizations/:organizationId/select", async (c) => {
|
||||
const sessionId = await resolveSessionId(c);
|
||||
return c.json(
|
||||
await appWorkspaceAction(
|
||||
"selectAppOrganization",
|
||||
async (workspace) =>
|
||||
await workspace.selectAppOrganization({
|
||||
sessionId,
|
||||
organizationId: c.req.param("organizationId"),
|
||||
}),
|
||||
requestLogContext(c, sessionId),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
app.patch("/api/rivet/app/organizations/:organizationId/profile", async (c) => {
|
||||
app.patch("/v1/app/organizations/:organizationId/profile", async (c) => {
|
||||
const sessionId = await resolveSessionId(c);
|
||||
const body = await c.req.json();
|
||||
return c.json(
|
||||
await appWorkspaceAction(
|
||||
"updateAppOrganizationProfile",
|
||||
async (workspace) =>
|
||||
await workspace.updateAppOrganizationProfile({
|
||||
sessionId,
|
||||
|
|
@ -206,42 +364,47 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
|
|||
slug: typeof body?.slug === "string" ? body.slug : "",
|
||||
primaryDomain: typeof body?.primaryDomain === "string" ? body.primaryDomain : "",
|
||||
}),
|
||||
requestLogContext(c, sessionId),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
app.post("/api/rivet/app/organizations/:organizationId/import", async (c) => {
|
||||
app.post("/v1/app/organizations/:organizationId/import", async (c) => {
|
||||
const sessionId = await resolveSessionId(c);
|
||||
return c.json(
|
||||
await appWorkspaceAction(
|
||||
"triggerAppRepoImport",
|
||||
async (workspace) =>
|
||||
await workspace.triggerAppRepoImport({
|
||||
sessionId,
|
||||
organizationId: c.req.param("organizationId"),
|
||||
}),
|
||||
requestLogContext(c, sessionId),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
app.post("/api/rivet/app/organizations/:organizationId/reconnect", async (c) => {
|
||||
app.post("/v1/app/organizations/:organizationId/reconnect", async (c) => {
|
||||
const sessionId = await resolveSessionId(c);
|
||||
return c.json(
|
||||
await appWorkspaceAction(
|
||||
"beginAppGithubInstall",
|
||||
async (workspace) =>
|
||||
await workspace.beginAppGithubInstall({
|
||||
sessionId,
|
||||
organizationId: c.req.param("organizationId"),
|
||||
}),
|
||||
requestLogContext(c, sessionId),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
app.post("/api/rivet/app/organizations/:organizationId/billing/checkout", async (c) => {
|
||||
app.post("/v1/app/organizations/:organizationId/billing/checkout", async (c) => {
|
||||
const sessionId = await resolveSessionId(c);
|
||||
const body = await c.req.json().catch(() => ({}));
|
||||
const planId = body?.planId === "free" || body?.planId === "team" ? (body.planId as FoundryBillingPlanId) : "team";
|
||||
return c.json(
|
||||
await (await appWorkspace()).createAppCheckoutSession({
|
||||
await (await appWorkspace(requestLogContext(c, sessionId))).createAppCheckoutSession({
|
||||
sessionId,
|
||||
organizationId: c.req.param("organizationId"),
|
||||
planId,
|
||||
|
|
@ -249,14 +412,14 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
|
|||
);
|
||||
});
|
||||
|
||||
app.get("/api/rivet/app/billing/checkout/complete", async (c) => {
|
||||
app.get("/v1/billing/checkout/complete", async (c) => {
|
||||
const organizationId = c.req.query("organizationId");
|
||||
const sessionId = c.req.query("foundrySession");
|
||||
const checkoutSessionId = c.req.query("session_id");
|
||||
if (!organizationId || !sessionId || !checkoutSessionId) {
|
||||
return c.text("Missing Stripe checkout completion parameters", 400);
|
||||
}
|
||||
const result = await (await appWorkspace()).finalizeAppCheckoutSession({
|
||||
const result = await (await appWorkspace(requestLogContext(c, sessionId))).finalizeAppCheckoutSession({
|
||||
organizationId,
|
||||
sessionId,
|
||||
checkoutSessionId,
|
||||
|
|
@ -264,40 +427,40 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
|
|||
return Response.redirect(result.redirectTo, 302);
|
||||
});
|
||||
|
||||
app.post("/api/rivet/app/organizations/:organizationId/billing/portal", async (c) => {
|
||||
app.post("/v1/app/organizations/:organizationId/billing/portal", async (c) => {
|
||||
const sessionId = await resolveSessionId(c);
|
||||
return c.json(
|
||||
await (await appWorkspace()).createAppBillingPortalSession({
|
||||
await (await appWorkspace(requestLogContext(c, sessionId))).createAppBillingPortalSession({
|
||||
sessionId,
|
||||
organizationId: c.req.param("organizationId"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
app.post("/api/rivet/app/organizations/:organizationId/billing/cancel", async (c) => {
|
||||
app.post("/v1/app/organizations/:organizationId/billing/cancel", async (c) => {
|
||||
const sessionId = await resolveSessionId(c);
|
||||
return c.json(
|
||||
await (await appWorkspace()).cancelAppScheduledRenewal({
|
||||
await (await appWorkspace(requestLogContext(c, sessionId))).cancelAppScheduledRenewal({
|
||||
sessionId,
|
||||
organizationId: c.req.param("organizationId"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
app.post("/api/rivet/app/organizations/:organizationId/billing/resume", async (c) => {
|
||||
app.post("/v1/app/organizations/:organizationId/billing/resume", async (c) => {
|
||||
const sessionId = await resolveSessionId(c);
|
||||
return c.json(
|
||||
await (await appWorkspace()).resumeAppSubscription({
|
||||
await (await appWorkspace(requestLogContext(c, sessionId))).resumeAppSubscription({
|
||||
sessionId,
|
||||
organizationId: c.req.param("organizationId"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
app.post("/api/rivet/app/workspaces/:workspaceId/seat-usage", async (c) => {
|
||||
app.post("/v1/app/workspaces/:workspaceId/seat-usage", async (c) => {
|
||||
const sessionId = await resolveSessionId(c);
|
||||
return c.json(
|
||||
await (await appWorkspace()).recordAppSeatUsage({
|
||||
await (await appWorkspace(requestLogContext(c, sessionId))).recordAppSeatUsage({
|
||||
sessionId,
|
||||
workspaceId: c.req.param("workspaceId"),
|
||||
}),
|
||||
|
|
@ -306,19 +469,18 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
|
|||
|
||||
const handleStripeWebhook = async (c: any) => {
|
||||
const payload = await c.req.text();
|
||||
await (await appWorkspace()).handleAppStripeWebhook({
|
||||
await (await appWorkspace(requestLogContext(c))).handleAppStripeWebhook({
|
||||
payload,
|
||||
signatureHeader: c.req.header("stripe-signature") ?? null,
|
||||
});
|
||||
return c.json({ ok: true });
|
||||
};
|
||||
|
||||
app.post("/api/rivet/app/webhooks/stripe", handleStripeWebhook);
|
||||
app.post("/api/rivet/app/stripe/webhook", handleStripeWebhook);
|
||||
app.post("/v1/webhooks/stripe", handleStripeWebhook);
|
||||
|
||||
app.post("/api/rivet/app/webhooks/github", async (c) => {
|
||||
app.post("/v1/webhooks/github", async (c) => {
|
||||
const payload = await c.req.text();
|
||||
await (await appWorkspace()).handleAppGithubWebhook({
|
||||
await (await appWorkspace(requestLogContext(c))).handleAppGithubWebhook({
|
||||
payload,
|
||||
signatureHeader: c.req.header("x-hub-signature-256") ?? null,
|
||||
eventHeader: c.req.header("x-github-event") ?? null,
|
||||
|
|
@ -326,15 +488,25 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
|
|||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
app.all("/api/rivet", (c) => registry.handler(c.req.raw));
|
||||
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
|
||||
|
||||
const server = Bun.serve({
|
||||
fetch: app.fetch,
|
||||
fetch: (request) => {
|
||||
if (isRivetRequest(request)) {
|
||||
return registry.handler(request);
|
||||
}
|
||||
return app.fetch(request);
|
||||
},
|
||||
hostname: config.backend.host,
|
||||
port: config.backend.port,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
{
|
||||
host: config.backend.host,
|
||||
port: config.backend.port,
|
||||
},
|
||||
"backend_started",
|
||||
);
|
||||
|
||||
process.on("SIGINT", async () => {
|
||||
server.stop();
|
||||
process.exit(0);
|
||||
|
|
@ -382,8 +554,13 @@ async function main(): Promise<void> {
|
|||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch((err: unknown) => {
|
||||
const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
||||
console.error(message);
|
||||
logger.fatal(
|
||||
{
|
||||
errorMessage: err instanceof Error ? err.message : String(err),
|
||||
errorStack: err instanceof Error ? err.stack : undefined,
|
||||
},
|
||||
"backend_start_failed",
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
5
foundry/packages/backend/src/logging.ts
Normal file
5
foundry/packages/backend/src/logging.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { createFoundryLogger } from "@sandbox-agent/foundry-shared";
|
||||
|
||||
export const logger = createFoundryLogger({
|
||||
service: "foundry-backend",
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { createHmac, createPrivateKey, createSign, timingSafeEqual } from "node:crypto";
|
||||
import { logger } from "../logging.js";
|
||||
|
||||
export class GitHubAppError extends Error {
|
||||
readonly status: number;
|
||||
|
|
@ -51,6 +52,10 @@ interface GitHubPageResponse<T> {
|
|||
nextUrl: string | null;
|
||||
}
|
||||
|
||||
const githubOAuthLogger = logger.child({
|
||||
scope: "github-oauth",
|
||||
});
|
||||
|
||||
export interface GitHubWebhookEvent {
|
||||
action?: string;
|
||||
installation?: { id: number; account?: { login?: string; type?: string; id?: number } | null };
|
||||
|
|
@ -161,21 +166,40 @@ export class GitHubAppClient {
|
|||
throw new GitHubAppError("GitHub OAuth is not configured", 500);
|
||||
}
|
||||
|
||||
const exchangeBody = {
|
||||
client_id: this.clientId,
|
||||
client_secret: this.clientSecret,
|
||||
code,
|
||||
redirect_uri: this.redirectUri,
|
||||
};
|
||||
githubOAuthLogger.debug(
|
||||
{
|
||||
url: `${this.authBaseUrl}/login/oauth/access_token`,
|
||||
clientId: this.clientId,
|
||||
redirectUri: this.redirectUri,
|
||||
codeLength: code.length,
|
||||
codePrefix: code.slice(0, 6),
|
||||
},
|
||||
"exchange_code_request",
|
||||
);
|
||||
|
||||
const response = await fetch(`${this.authBaseUrl}/login/oauth/access_token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: this.clientId,
|
||||
client_secret: this.clientSecret,
|
||||
code,
|
||||
redirect_uri: this.redirectUri,
|
||||
}),
|
||||
body: JSON.stringify(exchangeBody),
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
githubOAuthLogger.debug(
|
||||
{
|
||||
status: response.status,
|
||||
bodyPreview: responseText.slice(0, 300),
|
||||
},
|
||||
"exchange_code_response",
|
||||
);
|
||||
let payload: GitHubTokenResponse;
|
||||
try {
|
||||
payload = JSON.parse(responseText) as GitHubTokenResponse;
|
||||
|
|
|
|||
|
|
@ -47,12 +47,14 @@ export type AppShellStripeClient = Pick<
|
|||
|
||||
export interface AppShellServices {
|
||||
appUrl: string;
|
||||
apiUrl: string;
|
||||
github: AppShellGithubClient;
|
||||
stripe: AppShellStripeClient;
|
||||
}
|
||||
|
||||
export interface CreateAppShellServicesOptions {
|
||||
appUrl?: string;
|
||||
apiUrl?: string;
|
||||
github?: AppShellGithubClient;
|
||||
stripe?: AppShellStripeClient;
|
||||
}
|
||||
|
|
@ -60,6 +62,7 @@ export interface CreateAppShellServicesOptions {
|
|||
export function createDefaultAppShellServices(options: CreateAppShellServicesOptions = {}): AppShellServices {
|
||||
return {
|
||||
appUrl: (options.appUrl ?? process.env.APP_URL ?? "http://localhost:4173").replace(/\/$/, ""),
|
||||
apiUrl: (options.apiUrl ?? process.env.BETTER_AUTH_URL ?? process.env.APP_URL ?? "http://localhost:7741").replace(/\/$/, ""),
|
||||
github: options.github ?? new GitHubAppClient(),
|
||||
stripe: options.stripe ?? new StripeAppClient(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|||
import { checkBackendHealth } from "@sandbox-agent/foundry-client";
|
||||
import type { AppConfig } from "@sandbox-agent/foundry-shared";
|
||||
import { CLI_BUILD_ID } from "../build-id.js";
|
||||
import { logger } from "../logging.js";
|
||||
|
||||
const HEALTH_TIMEOUT_MS = 1_500;
|
||||
const START_TIMEOUT_MS = 30_000;
|
||||
|
|
@ -132,7 +133,7 @@ function removeStateFiles(host: string, port: number): void {
|
|||
|
||||
async function checkHealth(host: string, port: number): Promise<boolean> {
|
||||
return await checkBackendHealth({
|
||||
endpoint: `http://${host}:${port}/api/rivet`,
|
||||
endpoint: `http://${host}:${port}/v1/rivet`,
|
||||
timeoutMs: HEALTH_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
|
@ -237,7 +238,17 @@ async function startBackend(host: string, port: number): Promise<void> {
|
|||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
console.error(`failed to launch backend: ${String(error)}`);
|
||||
logger.error(
|
||||
{
|
||||
host,
|
||||
port,
|
||||
command: launch.command,
|
||||
args: launch.args,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
errorStack: error instanceof Error ? error.stack : undefined,
|
||||
},
|
||||
"failed_to_launch_backend",
|
||||
);
|
||||
});
|
||||
|
||||
child.unref();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { homedir } from "node:os";
|
|||
import { AgentTypeSchema, CreateTaskInputSchema, type TaskRecord } from "@sandbox-agent/foundry-shared";
|
||||
import { readBackendMetadata, createBackendClientFromConfig, formatRelativeAge, groupTaskStatus, summarizeTasks } from "@sandbox-agent/foundry-client";
|
||||
import { ensureBackendRunning, getBackendStatus, parseBackendPort, stopBackend } from "./backend/manager.js";
|
||||
import { writeStderr, writeStdout } from "./io.js";
|
||||
import { openEditorForTask } from "./task-editor.js";
|
||||
import { spawnCreateTmuxWindow } from "./tmux.js";
|
||||
import { loadConfig, resolveWorkspace, saveConfig } from "./workspace/config.js";
|
||||
|
|
@ -87,7 +88,7 @@ function positionals(args: string[]): string[] {
|
|||
}
|
||||
|
||||
function printUsage(): void {
|
||||
console.log(`
|
||||
writeStdout(`
|
||||
Usage:
|
||||
hf backend start [--host HOST] [--port PORT]
|
||||
hf backend stop [--host HOST] [--port PORT]
|
||||
|
|
@ -120,7 +121,7 @@ Tips:
|
|||
}
|
||||
|
||||
function printStatusUsage(): void {
|
||||
console.log(`
|
||||
writeStdout(`
|
||||
Usage:
|
||||
hf status [--workspace WS] [--json]
|
||||
|
||||
|
|
@ -146,7 +147,7 @@ JSON Output:
|
|||
}
|
||||
|
||||
function printHistoryUsage(): void {
|
||||
console.log(`
|
||||
writeStdout(`
|
||||
Usage:
|
||||
hf history [--workspace WS] [--limit N] [--branch NAME] [--task ID] [--json]
|
||||
|
||||
|
|
@ -195,13 +196,13 @@ async function handleBackend(args: string[]): Promise<void> {
|
|||
const pid = status.pid ?? "unknown";
|
||||
const version = status.version ?? "unknown";
|
||||
const stale = status.running && !status.versionCurrent ? " [outdated]" : "";
|
||||
console.log(`running=true pid=${pid} version=${version}${stale} log=${status.logPath}`);
|
||||
writeStdout(`running=true pid=${pid} version=${version}${stale} log=${status.logPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "stop") {
|
||||
await stopBackend(host, port);
|
||||
console.log(`running=false host=${host} port=${port}`);
|
||||
writeStdout(`running=false host=${host} port=${port}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -210,21 +211,21 @@ async function handleBackend(args: string[]): Promise<void> {
|
|||
const pid = status.pid ?? "unknown";
|
||||
const version = status.version ?? "unknown";
|
||||
const stale = status.running && !status.versionCurrent ? " [outdated]" : "";
|
||||
console.log(`running=${status.running} pid=${pid} version=${version}${stale} host=${host} port=${port} log=${status.logPath}`);
|
||||
writeStdout(`running=${status.running} pid=${pid} version=${version}${stale} host=${host} port=${port} log=${status.logPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "inspect") {
|
||||
await ensureBackendRunning(backendConfig);
|
||||
const metadata = await readBackendMetadata({
|
||||
endpoint: `http://${host}:${port}/api/rivet`,
|
||||
endpoint: `http://${host}:${port}/v1/rivet`,
|
||||
timeoutMs: 4_000,
|
||||
});
|
||||
const managerEndpoint = metadata.clientEndpoint ?? `http://${host}:${port}`;
|
||||
const inspectorUrl = `https://inspect.rivet.dev?u=${encodeURIComponent(managerEndpoint)}`;
|
||||
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
||||
spawnSync(openCmd, [inspectorUrl], { stdio: "ignore" });
|
||||
console.log(inspectorUrl);
|
||||
writeStdout(inspectorUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -253,7 +254,7 @@ async function handleWorkspace(args: string[]): Promise<void> {
|
|||
// Backend may not be running yet. Config is already updated.
|
||||
}
|
||||
|
||||
console.log(`workspace=${name}`);
|
||||
writeStdout(`workspace=${name}`);
|
||||
}
|
||||
|
||||
async function handleList(args: string[]): Promise<void> {
|
||||
|
|
@ -265,12 +266,12 @@ async function handleList(args: string[]): Promise<void> {
|
|||
const rows = await client.listTasks(workspaceId);
|
||||
|
||||
if (format === "json") {
|
||||
console.log(JSON.stringify(rows, null, 2));
|
||||
writeStdout(JSON.stringify(rows, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
console.log("no tasks");
|
||||
writeStdout("no tasks");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -281,7 +282,7 @@ async function handleList(args: string[]): Promise<void> {
|
|||
const task = row.task.length > 60 ? `${row.task.slice(0, 57)}...` : row.task;
|
||||
line += `\t${row.title}\t${task}\t${row.activeSessionId ?? "-"}\t${row.activeSandboxId ?? "-"}`;
|
||||
}
|
||||
console.log(line);
|
||||
writeStdout(line);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -294,7 +295,7 @@ async function handlePush(args: string[]): Promise<void> {
|
|||
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
|
||||
const client = createBackendClientFromConfig(config);
|
||||
await client.runAction(workspaceId, taskId, "push");
|
||||
console.log("ok");
|
||||
writeStdout("ok");
|
||||
}
|
||||
|
||||
async function handleSync(args: string[]): Promise<void> {
|
||||
|
|
@ -306,7 +307,7 @@ async function handleSync(args: string[]): Promise<void> {
|
|||
const workspaceId = resolveWorkspace(readOption(args, "--workspace"), config);
|
||||
const client = createBackendClientFromConfig(config);
|
||||
await client.runAction(workspaceId, taskId, "sync");
|
||||
console.log("ok");
|
||||
writeStdout("ok");
|
||||
}
|
||||
|
||||
async function handleKill(args: string[]): Promise<void> {
|
||||
|
|
@ -320,15 +321,15 @@ async function handleKill(args: string[]): Promise<void> {
|
|||
const abandon = hasFlag(args, "--abandon");
|
||||
|
||||
if (deleteBranch) {
|
||||
console.log("info: --delete-branch flag set, branch will be deleted after kill");
|
||||
writeStdout("info: --delete-branch flag set, branch will be deleted after kill");
|
||||
}
|
||||
if (abandon) {
|
||||
console.log("info: --abandon flag set, Graphite abandon will be attempted");
|
||||
writeStdout("info: --abandon flag set, Graphite abandon will be attempted");
|
||||
}
|
||||
|
||||
const client = createBackendClientFromConfig(config);
|
||||
await client.runAction(workspaceId, taskId, "kill");
|
||||
console.log("ok");
|
||||
writeStdout("ok");
|
||||
}
|
||||
|
||||
async function handlePrune(args: string[]): Promise<void> {
|
||||
|
|
@ -341,26 +342,26 @@ async function handlePrune(args: string[]): Promise<void> {
|
|||
const prunable = rows.filter((r) => r.status === "archived" || r.status === "killed");
|
||||
|
||||
if (prunable.length === 0) {
|
||||
console.log("nothing to prune");
|
||||
writeStdout("nothing to prune");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of prunable) {
|
||||
const age = formatRelativeAge(row.updatedAt);
|
||||
console.log(`${dryRun ? "[dry-run] " : ""}${row.taskId}\t${row.branchName}\t${row.status}\t${age}`);
|
||||
writeStdout(`${dryRun ? "[dry-run] " : ""}${row.taskId}\t${row.branchName}\t${row.status}\t${age}`);
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`\n${prunable.length} task(s) would be pruned`);
|
||||
writeStdout(`\n${prunable.length} task(s) would be pruned`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!yes) {
|
||||
console.log("\nnot yet implemented: auto-pruning requires confirmation");
|
||||
writeStdout("\nnot yet implemented: auto-pruning requires confirmation");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n${prunable.length} task(s) would be pruned (pruning not yet implemented)`);
|
||||
writeStdout(`\n${prunable.length} task(s) would be pruned (pruning not yet implemented)`);
|
||||
}
|
||||
|
||||
async function handleStatusline(args: string[]): Promise<void> {
|
||||
|
|
@ -375,11 +376,11 @@ async function handleStatusline(args: string[]): Promise<void> {
|
|||
const errorCount = summary.byStatus.error;
|
||||
|
||||
if (format === "claude-code") {
|
||||
console.log(`hf:${running}R/${idle}I/${errorCount}E`);
|
||||
writeStdout(`hf:${running}R/${idle}I/${errorCount}E`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`running=${running} idle=${idle} error=${errorCount}`);
|
||||
writeStdout(`running=${running} idle=${idle} error=${errorCount}`);
|
||||
}
|
||||
|
||||
async function handleDb(args: string[]): Promise<void> {
|
||||
|
|
@ -387,12 +388,12 @@ async function handleDb(args: string[]): Promise<void> {
|
|||
if (sub === "path") {
|
||||
const config = loadConfig();
|
||||
const dbPath = config.backend.dbPath.replace(/^~/, homedir());
|
||||
console.log(dbPath);
|
||||
writeStdout(dbPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "nuke") {
|
||||
console.log("WARNING: hf db nuke would delete the entire database. This is a placeholder and does not delete anything.");
|
||||
writeStdout("WARNING: hf db nuke would delete the entire database. This is a placeholder and does not delete anything.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -465,12 +466,12 @@ async function handleCreate(args: string[]): Promise<void> {
|
|||
const switched = await client.switchTask(workspaceId, task.taskId);
|
||||
const attached = await client.attachTask(workspaceId, task.taskId);
|
||||
|
||||
console.log(`Branch: ${task.branchName ?? "-"}`);
|
||||
console.log(`Task: ${task.taskId}`);
|
||||
console.log(`Provider: ${task.providerId}`);
|
||||
console.log(`Session: ${attached.sessionId ?? "none"}`);
|
||||
console.log(`Target: ${switched.switchTarget || attached.target}`);
|
||||
console.log(`Title: ${task.title ?? "-"}`);
|
||||
writeStdout(`Branch: ${task.branchName ?? "-"}`);
|
||||
writeStdout(`Task: ${task.taskId}`);
|
||||
writeStdout(`Provider: ${task.providerId}`);
|
||||
writeStdout(`Session: ${attached.sessionId ?? "none"}`);
|
||||
writeStdout(`Target: ${switched.switchTarget || attached.target}`);
|
||||
writeStdout(`Title: ${task.title ?? "-"}`);
|
||||
|
||||
const tmuxResult = spawnCreateTmuxWindow({
|
||||
branchName: task.branchName ?? task.taskId,
|
||||
|
|
@ -479,14 +480,14 @@ async function handleCreate(args: string[]): Promise<void> {
|
|||
});
|
||||
|
||||
if (tmuxResult.created) {
|
||||
console.log(`Window: created (${task.branchName})`);
|
||||
writeStdout(`Window: created (${task.branchName})`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log(`Run: hf switch ${task.taskId}`);
|
||||
writeStdout("");
|
||||
writeStdout(`Run: hf switch ${task.taskId}`);
|
||||
if ((switched.switchTarget || attached.target).startsWith("/")) {
|
||||
console.log(`cd ${switched.switchTarget || attached.target}`);
|
||||
writeStdout(`cd ${switched.switchTarget || attached.target}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -510,7 +511,7 @@ async function handleStatus(args: string[]): Promise<void> {
|
|||
const summary = summarizeTasks(rows);
|
||||
|
||||
if (hasFlag(args, "--json")) {
|
||||
console.log(
|
||||
writeStdout(
|
||||
JSON.stringify(
|
||||
{
|
||||
workspaceId,
|
||||
|
|
@ -528,16 +529,16 @@ async function handleStatus(args: string[]): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
console.log(`workspace=${workspaceId}`);
|
||||
console.log(`backend running=${backendStatus.running} pid=${backendStatus.pid ?? "unknown"} version=${backendStatus.version ?? "unknown"}`);
|
||||
console.log(`tasks total=${summary.total}`);
|
||||
console.log(
|
||||
writeStdout(`workspace=${workspaceId}`);
|
||||
writeStdout(`backend running=${backendStatus.running} pid=${backendStatus.pid ?? "unknown"} version=${backendStatus.version ?? "unknown"}`);
|
||||
writeStdout(`tasks total=${summary.total}`);
|
||||
writeStdout(
|
||||
`status queued=${summary.byStatus.queued} running=${summary.byStatus.running} idle=${summary.byStatus.idle} archived=${summary.byStatus.archived} killed=${summary.byStatus.killed} error=${summary.byStatus.error}`,
|
||||
);
|
||||
const providerSummary = Object.entries(summary.byProvider)
|
||||
.map(([provider, count]) => `${provider}=${count}`)
|
||||
.join(" ");
|
||||
console.log(`providers ${providerSummary || "-"}`);
|
||||
writeStdout(`providers ${providerSummary || "-"}`);
|
||||
}
|
||||
|
||||
async function handleHistory(args: string[]): Promise<void> {
|
||||
|
|
@ -560,12 +561,12 @@ async function handleHistory(args: string[]): Promise<void> {
|
|||
});
|
||||
|
||||
if (hasFlag(args, "--json")) {
|
||||
console.log(JSON.stringify(rows, null, 2));
|
||||
writeStdout(JSON.stringify(rows, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
console.log("no events");
|
||||
writeStdout("no events");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -576,7 +577,7 @@ async function handleHistory(args: string[]): Promise<void> {
|
|||
if (payload.length > 120) {
|
||||
payload = `${payload.slice(0, 117)}...`;
|
||||
}
|
||||
console.log(`${ts}\t${row.kind}\t${target}\t${payload}`);
|
||||
writeStdout(`${ts}\t${row.kind}\t${target}\t${payload}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -611,19 +612,19 @@ async function handleSwitchLike(cmd: string, args: string[]): Promise<void> {
|
|||
|
||||
if (cmd === "switch") {
|
||||
const result = await client.switchTask(workspaceId, taskId);
|
||||
console.log(`cd ${result.switchTarget}`);
|
||||
writeStdout(`cd ${result.switchTarget}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === "attach") {
|
||||
const result = await client.attachTask(workspaceId, taskId);
|
||||
console.log(`target=${result.target} session=${result.sessionId ?? "none"}`);
|
||||
writeStdout(`target=${result.target} session=${result.sessionId ?? "none"}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === "merge" || cmd === "archive") {
|
||||
await client.runAction(workspaceId, taskId, cmd);
|
||||
console.log("ok");
|
||||
writeStdout("ok");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -726,6 +727,6 @@ async function main(): Promise<void> {
|
|||
|
||||
main().catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
||||
console.error(msg);
|
||||
writeStderr(msg);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
7
foundry/packages/cli/src/io.ts
Normal file
7
foundry/packages/cli/src/io.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export function writeStdout(message = ""): void {
|
||||
process.stdout.write(`${message}\n`);
|
||||
}
|
||||
|
||||
export function writeStderr(message = ""): void {
|
||||
process.stderr.write(`${message}\n`);
|
||||
}
|
||||
5
foundry/packages/cli/src/logging.ts
Normal file
5
foundry/packages/cli/src/logging.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { createFoundryLogger } from "@sandbox-agent/foundry-shared";
|
||||
|
||||
export const logger = createFoundryLogger({
|
||||
service: "foundry-cli",
|
||||
});
|
||||
|
|
@ -2,6 +2,7 @@ import type { AppConfig, TaskRecord } from "@sandbox-agent/foundry-shared";
|
|||
import { spawnSync } from "node:child_process";
|
||||
import { createBackendClientFromConfig, filterTasks, formatRelativeAge, groupTaskStatus } from "@sandbox-agent/foundry-client";
|
||||
import { CLI_BUILD_ID } from "./build-id.js";
|
||||
import { writeStdout } from "./io.js";
|
||||
import { resolveTuiTheme, type TuiTheme } from "./theme.js";
|
||||
|
||||
interface KeyEventLike {
|
||||
|
|
@ -412,7 +413,7 @@ export async function runTui(config: AppConfig, workspaceId: string): Promise<vo
|
|||
process.off("SIGTERM", handleSignal);
|
||||
renderer.destroy();
|
||||
if (output) {
|
||||
console.log(output);
|
||||
writeStdout(output);
|
||||
}
|
||||
resolveDone();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ export interface BackendClient {
|
|||
}
|
||||
|
||||
export function rivetEndpoint(config: AppConfig): string {
|
||||
return `http://${config.backend.host}:${config.backend.port}/api/rivet`;
|
||||
return `http://${config.backend.host}:${config.backend.port}/v1/rivet`;
|
||||
}
|
||||
|
||||
export function createBackendClientFromConfig(config: AppConfig): BackendClient {
|
||||
|
|
@ -269,6 +269,32 @@ export function createBackendClientFromConfig(config: AppConfig): BackendClient
|
|||
});
|
||||
}
|
||||
|
||||
function stripTrailingSlash(value: string): string {
|
||||
return value.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
function normalizeLegacyBackendEndpoint(endpoint: string): string {
|
||||
const normalized = stripTrailingSlash(endpoint);
|
||||
if (normalized.endsWith("/api/rivet")) {
|
||||
return `${normalized.slice(0, -"/api/rivet".length)}/v1/rivet`;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function deriveBackendEndpoints(endpoint: string): { appEndpoint: string; rivetEndpoint: string } {
|
||||
const normalized = normalizeLegacyBackendEndpoint(endpoint);
|
||||
if (normalized.endsWith("/rivet")) {
|
||||
return {
|
||||
appEndpoint: normalized.slice(0, -"/rivet".length),
|
||||
rivetEndpoint: normalized,
|
||||
};
|
||||
}
|
||||
return {
|
||||
appEndpoint: normalized,
|
||||
rivetEndpoint: `${normalized}/rivet`,
|
||||
};
|
||||
}
|
||||
|
||||
function isLoopbackHost(hostname: string): boolean {
|
||||
const h = hostname.toLowerCase();
|
||||
return h === "127.0.0.1" || h === "localhost" || h === "0.0.0.0" || h === "::1";
|
||||
|
|
@ -386,6 +412,9 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
return createMockBackendClient(options.defaultWorkspaceId);
|
||||
}
|
||||
|
||||
const endpoints = deriveBackendEndpoints(options.endpoint);
|
||||
const rivetApiEndpoint = endpoints.rivetEndpoint;
|
||||
const appApiEndpoint = endpoints.appEndpoint;
|
||||
let clientPromise: Promise<RivetClient> | null = null;
|
||||
let appSessionId = typeof window !== "undefined" ? window.localStorage.getItem("sandbox-agent-foundry:remote-app-session") : null;
|
||||
const workbenchSubscriptions = new Map<
|
||||
|
|
@ -434,7 +463,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
headers.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
const res = await fetch(`${options.endpoint.replace(/\/$/, "")}${path}`, {
|
||||
const res = await fetch(`${appApiEndpoint}${path}`, {
|
||||
...init,
|
||||
headers,
|
||||
credentials: "include",
|
||||
|
|
@ -465,22 +494,22 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
// Use the serverless /metadata endpoint to discover the manager endpoint.
|
||||
// If the server reports a loopback clientEndpoint (127.0.0.1), rewrite to the same host
|
||||
// as the configured endpoint so remote browsers/clients can connect.
|
||||
const configured = new URL(options.endpoint);
|
||||
const configured = new URL(rivetApiEndpoint);
|
||||
const configuredOrigin = `${configured.protocol}//${configured.host}`;
|
||||
|
||||
const initialNamespace = undefined;
|
||||
const metadata = await fetchMetadataWithRetry(options.endpoint, initialNamespace, {
|
||||
const metadata = await fetchMetadataWithRetry(rivetApiEndpoint, initialNamespace, {
|
||||
timeoutMs: 30_000,
|
||||
requestTimeoutMs: 8_000,
|
||||
});
|
||||
|
||||
// Candidate endpoint: manager endpoint if provided, otherwise stick to the configured endpoint.
|
||||
const candidateEndpoint = metadata.clientEndpoint ? rewriteLoopbackClientEndpoint(metadata.clientEndpoint, configuredOrigin) : options.endpoint;
|
||||
const candidateEndpoint = metadata.clientEndpoint ? rewriteLoopbackClientEndpoint(metadata.clientEndpoint, configuredOrigin) : rivetApiEndpoint;
|
||||
|
||||
// If the manager port isn't reachable from this client (common behind reverse proxies),
|
||||
// fall back to the configured serverless endpoint to avoid hanging requests.
|
||||
const shouldUseCandidate = metadata.clientEndpoint ? await probeMetadataEndpoint(candidateEndpoint, metadata.clientNamespace, 1_500) : true;
|
||||
const resolvedEndpoint = shouldUseCandidate ? candidateEndpoint : options.endpoint;
|
||||
const resolvedEndpoint = shouldUseCandidate ? candidateEndpoint : rivetApiEndpoint;
|
||||
|
||||
return createClient({
|
||||
endpoint: resolvedEndpoint,
|
||||
|
|
@ -676,10 +705,10 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
|
||||
async signInWithGithub(): Promise<void> {
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.assign(`${options.endpoint.replace(/\/$/, "")}/app/auth/github/start`);
|
||||
window.location.assign(`${appApiEndpoint}/auth/github/start`);
|
||||
return;
|
||||
}
|
||||
await redirectTo("/app/auth/github/start");
|
||||
await redirectTo("/auth/github/start");
|
||||
},
|
||||
|
||||
async signOutApp(): Promise<FoundryAppSnapshot> {
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ async function ensureRemoteBranchExists(token: string, fullName: string, branchN
|
|||
|
||||
describe("e2e(client): full integration stack workflow", () => {
|
||||
it.skipIf(!RUN_FULL_E2E)("adds repo, loads branch graph, and executes a stack restack action", { timeout: 8 * 60_000 }, async () => {
|
||||
const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/api/rivet";
|
||||
const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/v1/rivet";
|
||||
const workspaceId = process.env.HF_E2E_WORKSPACE?.trim() || "default";
|
||||
const repoRemote = requiredEnv("HF_E2E_GITHUB_REPO");
|
||||
const githubToken = requiredEnv("GITHUB_TOKEN");
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ async function githubApi(token: string, path: string, init?: RequestInit): Promi
|
|||
|
||||
describe("e2e: backend -> sandbox-agent -> git -> PR", () => {
|
||||
it.skipIf(!RUN_E2E)("creates a task, waits for agent to implement, and opens a PR", { timeout: 15 * 60_000 }, async () => {
|
||||
const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/api/rivet";
|
||||
const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/v1/rivet";
|
||||
const workspaceId = process.env.HF_E2E_WORKSPACE?.trim() || "default";
|
||||
const repoRemote = requiredEnv("HF_E2E_GITHUB_REPO");
|
||||
const githubToken = requiredEnv("GITHUB_TOKEN");
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ function transcriptIncludesAgentText(transcript: WorkbenchTranscriptEvent[], exp
|
|||
|
||||
describe("e2e(client): workbench flows", () => {
|
||||
it.skipIf(!RUN_WORKBENCH_E2E)("creates a task, adds sessions, exchanges messages, and manages workbench state", { timeout: 20 * 60_000 }, async () => {
|
||||
const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/api/rivet";
|
||||
const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/v1/rivet";
|
||||
const workspaceId = process.env.HF_E2E_WORKSPACE?.trim() || "default";
|
||||
const repoRemote = requiredEnv("HF_E2E_GITHUB_REPO");
|
||||
const model = workbenchModelEnv("HF_E2E_MODEL", "gpt-4o");
|
||||
|
|
|
|||
|
|
@ -1,8 +1,21 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { TaskWorkbenchSnapshot, WorkbenchAgentTab, WorkbenchTask, WorkbenchModelId, WorkbenchTranscriptEvent } from "@sandbox-agent/foundry-shared";
|
||||
import {
|
||||
createFoundryLogger,
|
||||
type TaskWorkbenchSnapshot,
|
||||
type WorkbenchAgentTab,
|
||||
type WorkbenchTask,
|
||||
type WorkbenchModelId,
|
||||
type WorkbenchTranscriptEvent,
|
||||
} from "@sandbox-agent/foundry-shared";
|
||||
import { createBackendClient } from "../../src/backend-client.js";
|
||||
|
||||
const RUN_WORKBENCH_LOAD_E2E = process.env.HF_ENABLE_DAEMON_WORKBENCH_LOAD_E2E === "1";
|
||||
const logger = createFoundryLogger({
|
||||
service: "foundry-client-e2e",
|
||||
bindings: {
|
||||
suite: "workbench-load",
|
||||
},
|
||||
});
|
||||
|
||||
function requiredEnv(name: string): string {
|
||||
const value = process.env[name]?.trim();
|
||||
|
|
@ -175,7 +188,7 @@ async function measureWorkbenchSnapshot(
|
|||
|
||||
describe("e2e(client): workbench load", () => {
|
||||
it.skipIf(!RUN_WORKBENCH_LOAD_E2E)("runs a simple sequential load profile against the real backend", { timeout: 30 * 60_000 }, async () => {
|
||||
const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/api/rivet";
|
||||
const endpoint = process.env.HF_E2E_BACKEND_ENDPOINT?.trim() || "http://127.0.0.1:7741/v1/rivet";
|
||||
const workspaceId = process.env.HF_E2E_WORKSPACE?.trim() || "default";
|
||||
const repoRemote = requiredEnv("HF_E2E_GITHUB_REPO");
|
||||
const model = workbenchModelEnv("HF_E2E_MODEL", "gpt-4o");
|
||||
|
|
@ -269,12 +282,12 @@ describe("e2e(client): workbench load", () => {
|
|||
|
||||
const snapshotMetrics = await measureWorkbenchSnapshot(client, workspaceId, 3);
|
||||
snapshotSeries.push(snapshotMetrics);
|
||||
console.info(
|
||||
"[workbench-load-snapshot]",
|
||||
JSON.stringify({
|
||||
logger.info(
|
||||
{
|
||||
taskIndex: taskIndex + 1,
|
||||
...snapshotMetrics,
|
||||
}),
|
||||
},
|
||||
"workbench_load_snapshot",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -296,7 +309,7 @@ describe("e2e(client): workbench load", () => {
|
|||
snapshotTranscriptFinalCount: lastSnapshot.transcriptEventCount,
|
||||
};
|
||||
|
||||
console.info("[workbench-load-summary]", JSON.stringify(summary));
|
||||
logger.info(summary, "workbench_load_summary");
|
||||
|
||||
expect(createTaskLatencies.length).toBe(taskCount);
|
||||
expect(provisionLatencies.length).toBe(taskCount);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"tsx": "^4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sandbox-agent/foundry-shared": "workspace:*",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-shell": "^2"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,22 @@ import { execSync } from "node:child_process";
|
|||
import { cpSync, readFileSync, writeFileSync, rmSync, existsSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createFoundryLogger } from "@sandbox-agent/foundry-shared";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const desktopRoot = resolve(__dirname, "..");
|
||||
const repoRoot = resolve(desktopRoot, "../../..");
|
||||
const frontendDist = resolve(desktopRoot, "../frontend/dist");
|
||||
const destDir = resolve(desktopRoot, "frontend-dist");
|
||||
const logger = createFoundryLogger({
|
||||
service: "foundry-desktop-build",
|
||||
bindings: {
|
||||
script: "build-frontend",
|
||||
},
|
||||
});
|
||||
|
||||
function run(cmd: string, opts?: { cwd?: string; env?: NodeJS.ProcessEnv }) {
|
||||
console.log(`> ${cmd}`);
|
||||
logger.info({ command: cmd, cwd: opts?.cwd ?? repoRoot }, "run_command");
|
||||
execSync(cmd, {
|
||||
stdio: "inherit",
|
||||
cwd: opts?.cwd ?? repoRoot,
|
||||
|
|
@ -19,15 +26,15 @@ function run(cmd: string, opts?: { cwd?: string; env?: NodeJS.ProcessEnv }) {
|
|||
}
|
||||
|
||||
// Step 1: Build the frontend with the desktop-specific backend endpoint
|
||||
console.log("\n=== Building frontend for desktop ===\n");
|
||||
logger.info("building_frontend");
|
||||
run("pnpm --filter @sandbox-agent/foundry-frontend build", {
|
||||
env: {
|
||||
VITE_HF_BACKEND_ENDPOINT: "http://127.0.0.1:7741/api/rivet",
|
||||
VITE_HF_BACKEND_ENDPOINT: "http://127.0.0.1:7741/v1/rivet",
|
||||
},
|
||||
});
|
||||
|
||||
// Step 2: Copy dist to frontend-dist/
|
||||
console.log("\n=== Copying frontend build output ===\n");
|
||||
logger.info({ frontendDist, destDir }, "copying_frontend_dist");
|
||||
if (existsSync(destDir)) {
|
||||
rmSync(destDir, { recursive: true });
|
||||
}
|
||||
|
|
@ -39,4 +46,4 @@ let html = readFileSync(indexPath, "utf-8");
|
|||
html = html.replace(/<script\s+src="https:\/\/unpkg\.com\/react-scan\/dist\/auto\.global\.js"[^>]*><\/script>\s*/g, "");
|
||||
writeFileSync(indexPath, html);
|
||||
|
||||
console.log("\n=== Frontend build complete ===\n");
|
||||
logger.info({ indexPath }, "frontend_build_complete");
|
||||
|
|
|
|||
|
|
@ -2,10 +2,17 @@ import { execSync } from "node:child_process";
|
|||
import { mkdirSync, existsSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createFoundryLogger } from "@sandbox-agent/foundry-shared";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const desktopRoot = resolve(__dirname, "..");
|
||||
const sidecarDir = resolve(desktopRoot, "src-tauri/sidecars");
|
||||
const logger = createFoundryLogger({
|
||||
service: "foundry-desktop-build",
|
||||
bindings: {
|
||||
script: "build-sidecar",
|
||||
},
|
||||
});
|
||||
|
||||
const isDev = process.argv.includes("--dev");
|
||||
|
||||
|
|
@ -35,7 +42,7 @@ const targets: Array<{ bunTarget: string; tripleTarget: string }> = isDev
|
|||
];
|
||||
|
||||
function run(cmd: string, opts?: { cwd?: string; env?: NodeJS.ProcessEnv }) {
|
||||
console.log(`> ${cmd}`);
|
||||
logger.info({ command: cmd, cwd: opts?.cwd ?? desktopRoot }, "run_command");
|
||||
execSync(cmd, {
|
||||
stdio: "inherit",
|
||||
cwd: opts?.cwd ?? desktopRoot,
|
||||
|
|
@ -44,7 +51,7 @@ function run(cmd: string, opts?: { cwd?: string; env?: NodeJS.ProcessEnv }) {
|
|||
}
|
||||
|
||||
// Step 1: Build the backend with tsup
|
||||
console.log("\n=== Building backend with tsup ===\n");
|
||||
logger.info("building_backend");
|
||||
run("pnpm --filter @sandbox-agent/foundry-backend build", {
|
||||
cwd: resolve(desktopRoot, "../../.."),
|
||||
});
|
||||
|
|
@ -55,14 +62,14 @@ mkdirSync(sidecarDir, { recursive: true });
|
|||
const backendEntry = resolve(desktopRoot, "../backend/dist/index.js");
|
||||
|
||||
if (!existsSync(backendEntry)) {
|
||||
console.error(`Backend build output not found at ${backendEntry}`);
|
||||
logger.error({ backendEntry }, "backend_build_output_not_found");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const { bunTarget, tripleTarget } of targets) {
|
||||
const outfile = resolve(sidecarDir, `foundry-backend-${tripleTarget}`);
|
||||
console.log(`\n=== Compiling sidecar for ${tripleTarget} ===\n`);
|
||||
logger.info({ bunTarget, tripleTarget, outfile }, "compiling_sidecar");
|
||||
run(`bun build --compile --target ${bunTarget} ${backendEntry} --outfile ${outfile}`);
|
||||
}
|
||||
|
||||
console.log("\n=== Sidecar build complete ===\n");
|
||||
logger.info({ targets: targets.map((target) => target.tripleTarget) }, "sidecar_build_complete");
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ fn get_backend_url() -> String {
|
|||
|
||||
#[tauri::command]
|
||||
async fn backend_health() -> Result<bool, String> {
|
||||
match reqwest::get("http://127.0.0.1:7741/api/rivet/metadata").await {
|
||||
match reqwest::get("http://127.0.0.1:7741/v1/rivet/metadata").await {
|
||||
Ok(resp) => Ok(resp.status().is_success()),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ async fn wait_for_backend(timeout_secs: u64) -> Result<(), String> {
|
|||
));
|
||||
}
|
||||
|
||||
match reqwest::get("http://127.0.0.1:7741/api/rivet/metadata").await {
|
||||
match reqwest::get("http://127.0.0.1:7741/v1/rivet/metadata").await {
|
||||
Ok(resp) if resp.status().is_success() => return Ok(()),
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useStyletron } from "baseui";
|
||||
import { createErrorContext } from "@sandbox-agent/foundry-shared";
|
||||
|
||||
import { PanelLeft, PanelRight } from "lucide-react";
|
||||
import { useFoundryTokens } from "../app/theme";
|
||||
import { logger } from "../logging.js";
|
||||
|
||||
import { DiffContent } from "./mock-layout/diff-content";
|
||||
import { MessageList } from "./mock-layout/message-list";
|
||||
|
|
@ -437,7 +439,13 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
await window.navigator.clipboard.writeText(message.text);
|
||||
setCopiedMessageId(message.id);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy transcript message", error);
|
||||
logger.error(
|
||||
{
|
||||
messageId: message.id,
|
||||
...createErrorContext(error),
|
||||
},
|
||||
"failed_to_copy_transcript_message",
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
@ -1108,7 +1116,13 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
|||
const { tabId } = await taskWorkbenchClient.addTab({ taskId: activeTask.id });
|
||||
syncRouteSession(activeTask.id, tabId, true);
|
||||
} catch (error) {
|
||||
console.error("failed to auto-create workbench session", error);
|
||||
logger.error(
|
||||
{
|
||||
taskId: activeTask.id,
|
||||
...createErrorContext(error),
|
||||
},
|
||||
"failed_to_auto_create_workbench_session",
|
||||
);
|
||||
} finally {
|
||||
autoCreatingSessionForTaskRef.current.delete(activeTask.id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { LabelSmall } from "baseui/typography";
|
|||
import { Archive, ArrowUpFromLine, ChevronRight, FileCode, FilePlus, FileX, FolderOpen, GitPullRequest, PanelRight } from "lucide-react";
|
||||
|
||||
import { useFoundryTokens } from "../../app/theme";
|
||||
import { createErrorContext } from "@sandbox-agent/foundry-shared";
|
||||
import { logger } from "../../logging.js";
|
||||
import { type ContextMenuItem, ContextMenuOverlay, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
|
||||
import { type FileTreeNode, type Task, diffTabId } from "./view-model";
|
||||
|
||||
|
|
@ -131,7 +133,13 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
|
||||
await window.navigator.clipboard.writeText(path);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy file path", error);
|
||||
logger.error(
|
||||
{
|
||||
path,
|
||||
...createErrorContext(error),
|
||||
},
|
||||
"failed_to_copy_file_path",
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ declare global {
|
|||
|
||||
function resolveDefaultBackendEndpoint(): string {
|
||||
if (typeof window !== "undefined" && window.location?.origin) {
|
||||
return `${window.location.origin}/api/rivet`;
|
||||
return `${window.location.origin}/v1/rivet`;
|
||||
}
|
||||
return "http://127.0.0.1:7741/api/rivet";
|
||||
return "http://127.0.0.1:7741/v1/rivet";
|
||||
}
|
||||
|
||||
type FrontendImportMetaEnv = ImportMetaEnv & {
|
||||
|
|
|
|||
5
foundry/packages/frontend/src/logging.ts
Normal file
5
foundry/packages/frontend/src/logging.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { createFoundryLogger } from "@sandbox-agent/foundry-shared";
|
||||
|
||||
export const logger = createFoundryLogger({
|
||||
service: "foundry-frontend",
|
||||
});
|
||||
|
|
@ -19,7 +19,7 @@ export default defineConfig({
|
|||
server: {
|
||||
port: 4173,
|
||||
proxy: {
|
||||
"/api/rivet": {
|
||||
"/v1": {
|
||||
target: backendProxyTarget,
|
||||
changeOrigin: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"pino": "^10.3.1",
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export * from "./app-shell.js";
|
||||
export * from "./contracts.js";
|
||||
export * from "./config.js";
|
||||
export * from "./logging.js";
|
||||
export * from "./workbench.js";
|
||||
export * from "./workspace.js";
|
||||
|
|
|
|||
63
foundry/packages/shared/src/logging.ts
Normal file
63
foundry/packages/shared/src/logging.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { pino, type Logger, type LoggerOptions } from "pino";
|
||||
|
||||
export interface FoundryLoggerOptions {
|
||||
service: string;
|
||||
bindings?: Record<string, unknown>;
|
||||
level?: string;
|
||||
}
|
||||
|
||||
type ProcessLike = {
|
||||
env?: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
function resolveEnvVar(name: string): string | undefined {
|
||||
const value = (globalThis as { process?: ProcessLike }).process?.env?.[name];
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function defaultLevel(): string {
|
||||
return resolveEnvVar("FOUNDRY_LOG_LEVEL") ?? resolveEnvVar("LOG_LEVEL") ?? resolveEnvVar("RIVET_LOG_LEVEL") ?? "info";
|
||||
}
|
||||
|
||||
function isBrowserRuntime(): boolean {
|
||||
return typeof window !== "undefined" && typeof document !== "undefined";
|
||||
}
|
||||
|
||||
export function createFoundryLogger(options: FoundryLoggerOptions): Logger {
|
||||
const browser = isBrowserRuntime();
|
||||
const loggerOptions: LoggerOptions = {
|
||||
level: options.level ?? defaultLevel(),
|
||||
base: {
|
||||
service: options.service,
|
||||
...(options.bindings ?? {}),
|
||||
},
|
||||
};
|
||||
|
||||
if (browser) {
|
||||
loggerOptions.browser = {
|
||||
asObject: true,
|
||||
};
|
||||
} else {
|
||||
loggerOptions.timestamp = pino.stdTimeFunctions.isoTime;
|
||||
}
|
||||
|
||||
return pino(loggerOptions);
|
||||
}
|
||||
|
||||
export function createErrorContext(error: unknown): { errorMessage: string; errorStack?: string } {
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
errorMessage: error.message,
|
||||
errorStack: error.stack,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
errorMessage: String(error),
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue