chore(foundry): migrate to actions (#262)

* feat(foundry): checkpoint actor and workspace refactor

* docs(foundry): add agent handoff context

* wip(foundry): continue actor refactor

* wip(foundry): capture remaining local changes

* Complete Foundry refactor checklist

* Fix Foundry validation fallout

* wip

* wip: convert all actors from workflow to plain run handlers

Workaround for RivetKit bug where c.queue.iter() never yields messages
for actors created via getOrCreate from another actor's context. The
queue accepts messages (visible in inspector) but the iterator hangs.
Sleep/wake fixes it, but actors with active connections never sleep.

Converted organization, github-data, task, and user actors from
run: workflow(...) to plain run: async (c) => { for await ... }.

Also fixes:
- Missing auth tables in org migration (auth_verification etc)
- default_model NOT NULL constraint on org profile upsert
- Nested workflow step in github-data (HistoryDivergedError)
- Removed --force from frontend Dockerfile pnpm install

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

* Convert all actors from queues/workflows to direct actions, lazy task creation

Major refactor replacing all queue-based workflow communication with direct
RivetKit action calls across all actors. This works around a RivetKit bug
where c.queue.iter() deadlocks for actors created from another actor's context.

Key changes:
- All actors (organization, task, user, audit-log, github-data) converted
  from run: workflow(...) to actions-only (no run handler, no queues)
- PR sync creates virtual task entries in org local DB instead of spawning
  task actors — prevents OOM from 200+ actors created simultaneously
- Task actors created lazily on first user interaction via getOrCreate,
  self-initialize from org's getTaskIndexEntry data
- Removed requireRepoExists cross-actor call (caused 500s), replaced with
  local resolveTaskRepoId from org's taskIndex table
- Fixed getOrganizationContext to thread overrides through all sync phases
- Fixed sandbox repo path (/home/user/repo for E2B compatibility)
- Fixed buildSessionDetail to skip transcript fetch for pending sessions
- Added process crash protection (uncaughtException/unhandledRejection)
- Fixed React infinite render loop in mock-layout useEffect dependencies
- Added sandbox listProcesses error handling for expired E2B sandboxes
- Set E2B sandbox timeout to 1 hour (was 5 min default)
- Updated CLAUDE.md with lazy task creation rules, no-silent-catch policy,
  React hook dependency safety rules

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

* Fix E2B sandbox timeout comment, frontend stability, and create-flow improvements

- Add TEMPORARY comment on E2B timeoutMs with pointer to rivetkit sandbox
  resilience proposal for when autoPause lands
- Fix React useEffect dependency stability in mock-layout and
  organization-dashboard to prevent infinite re-render loops
- Fix terminal-pane ref handling
- Improve create-flow service and tests

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nathan Flurry 2026-03-16 15:23:59 -07:00 committed by GitHub
parent 32f3c6c3bc
commit f45a467484
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
139 changed files with 9768 additions and 7204 deletions

View file

@ -1,9 +1,15 @@
import type { WorkbenchModelId } from "./workbench.js";
import type { WorkspaceModelId } from "./workspace.js";
export type FoundryBillingPlanId = "free" | "team";
export type FoundryBillingStatus = "active" | "trialing" | "past_due" | "scheduled_cancel";
export type FoundryGithubInstallationStatus = "connected" | "install_required" | "reconnect_required";
export type FoundryGithubSyncStatus = "pending" | "syncing" | "synced" | "error";
export type FoundryGithubSyncPhase =
| "discovering_repositories"
| "syncing_repositories"
| "syncing_branches"
| "syncing_members"
| "syncing_pull_requests";
export type FoundryOrganizationKind = "personal" | "organization";
export type FoundryStarterRepoStatus = "pending" | "starred" | "skipped";
@ -14,6 +20,7 @@ export interface FoundryUser {
githubLogin: string;
roleLabel: string;
eligibleOrganizationIds: string[];
defaultModel: WorkspaceModelId;
}
export interface FoundryOrganizationMember {
@ -52,6 +59,10 @@ export interface FoundryGithubState {
lastSyncAt: number | null;
lastWebhookAt: number | null;
lastWebhookEvent: string;
syncGeneration?: number;
syncPhase?: FoundryGithubSyncPhase | null;
processedRepositoryCount?: number;
totalRepositoryCount?: number;
}
export interface FoundryOrganizationSettings {
@ -59,7 +70,6 @@ export interface FoundryOrganizationSettings {
slug: string;
primaryDomain: string;
seatAccrualMode: "first_prompt";
defaultModel: WorkbenchModelId;
autoImportRepos: boolean;
}

View file

@ -54,11 +54,24 @@ export const CreateTaskInputSchema = z.object({
explicitTitle: z.string().trim().min(1).optional(),
explicitBranchName: z.string().trim().min(1).optional(),
sandboxProviderId: SandboxProviderIdSchema.optional(),
agentType: AgentTypeSchema.optional(),
onBranch: z.string().trim().min(1).optional(),
});
export type CreateTaskInput = z.infer<typeof CreateTaskInputSchema>;
export const WorkspacePullRequestSummarySchema = z.object({
number: z.number().int(),
status: z.enum(["draft", "ready"]),
title: z.string().min(1),
state: z.string().min(1),
url: z.string().min(1),
headRefName: z.string().min(1),
baseRefName: z.string().min(1),
repoFullName: z.string().min(1),
authorLogin: z.string().nullable(),
isDraft: z.boolean(),
updatedAtMs: z.number().int(),
});
export const TaskRecordSchema = z.object({
organizationId: OrganizationIdSchema,
repoId: z.string().min(1),
@ -69,9 +82,8 @@ export const TaskRecordSchema = z.object({
task: z.string().min(1),
sandboxProviderId: SandboxProviderIdSchema,
status: TaskStatusSchema,
statusMessage: z.string().nullable(),
activeSandboxId: z.string().nullable(),
activeSessionId: z.string().nullable(),
pullRequest: WorkspacePullRequestSummarySchema.nullable(),
sandboxes: z.array(
z.object({
sandboxId: z.string().min(1),
@ -83,17 +95,6 @@ export const TaskRecordSchema = z.object({
updatedAt: z.number().int(),
}),
),
agentType: z.string().nullable(),
prSubmitted: z.boolean(),
diffStat: z.string().nullable(),
prUrl: z.string().nullable(),
prAuthor: z.string().nullable(),
ciStatus: z.string().nullable(),
reviewStatus: z.string().nullable(),
reviewer: z.string().nullable(),
conflictsWithMain: z.string().nullable(),
hasUnpushed: z.string().nullable(),
parentBranch: z.string().nullable(),
createdAt: z.number().int(),
updatedAt: z.number().int(),
});
@ -107,11 +108,13 @@ export const TaskSummarySchema = z.object({
title: z.string().min(1).nullable(),
status: TaskStatusSchema,
updatedAt: z.number().int(),
pullRequest: WorkspacePullRequestSummarySchema.nullable(),
});
export type TaskSummary = z.infer<typeof TaskSummarySchema>;
export const TaskActionInputSchema = z.object({
organizationId: OrganizationIdSchema,
repoId: RepoIdSchema,
taskId: z.string().min(1),
});
export type TaskActionInput = z.infer<typeof TaskActionInputSchema>;
@ -136,12 +139,8 @@ export const RepoBranchRecordSchema = z.object({
taskId: z.string().nullable(),
taskTitle: z.string().nullable(),
taskStatus: TaskStatusSchema.nullable(),
prNumber: z.number().int().nullable(),
prState: z.string().nullable(),
prUrl: z.string().nullable(),
pullRequest: WorkspacePullRequestSummarySchema.nullable(),
ciStatus: z.string().nullable(),
reviewStatus: z.string().nullable(),
reviewer: z.string().nullable(),
updatedAt: z.number().int(),
});
export type RepoBranchRecord = z.infer<typeof RepoBranchRecordSchema>;
@ -174,13 +173,14 @@ export type StarSandboxAgentRepoResult = z.infer<typeof StarSandboxAgentRepoResu
export const HistoryQueryInputSchema = z.object({
organizationId: OrganizationIdSchema,
repoId: z.string().min(1).optional(),
limit: z.number().int().positive().max(500).optional(),
branch: z.string().min(1).optional(),
taskId: z.string().min(1).optional(),
});
export type HistoryQueryInput = z.infer<typeof HistoryQueryInputSchema>;
export const HistoryEventSchema = z.object({
export const AuditLogEventSchema = z.object({
id: z.number().int(),
organizationId: OrganizationIdSchema,
repoId: z.string().nullable(),
@ -190,7 +190,7 @@ export const HistoryEventSchema = z.object({
payloadJson: z.string().min(1),
createdAt: z.number().int(),
});
export type HistoryEvent = z.infer<typeof HistoryEventSchema>;
export type AuditLogEvent = z.infer<typeof AuditLogEventSchema>;
export const PruneInputSchema = z.object({
organizationId: OrganizationIdSchema,
@ -201,6 +201,7 @@ export type PruneInput = z.infer<typeof PruneInputSchema>;
export const KillInputSchema = z.object({
organizationId: OrganizationIdSchema,
repoId: RepoIdSchema,
taskId: z.string().min(1),
deleteBranch: z.boolean(),
abandon: z.boolean(),

View file

@ -2,6 +2,7 @@ export * from "./app-shell.js";
export * from "./contracts.js";
export * from "./config.js";
export * from "./logging.js";
export * from "./models.js";
export * from "./realtime-events.js";
export * from "./workbench.js";
export * from "./workspace.js";
export * from "./organization.js";

View file

@ -1,4 +1,4 @@
import { pino, type Logger, type LoggerOptions } from "pino";
import { pino, type LogFn, type Logger, type LoggerOptions } from "pino";
export interface FoundryLoggerOptions {
service: string;
@ -160,7 +160,7 @@ export function createFoundryLogger(options: FoundryLoggerOptions): Logger {
loggerOptions.timestamp = pino.stdTimeFunctions.isoTime;
if (options.format === "logfmt") {
loggerOptions.hooks = {
logMethod(this: Logger, args, _method, level) {
logMethod(this: Logger, args: Parameters<LogFn>, _method: LogFn, level: number) {
const levelLabel = this.levels.labels[level] ?? "info";
const record = buildLogRecord(levelLabel, this.bindings(), args);
writeLogfmtLine(formatLogfmtLine(record));

View file

@ -0,0 +1,217 @@
import claudeConfig from "../../../../scripts/agent-configs/resources/claude.json" with { type: "json" };
import codexConfig from "../../../../scripts/agent-configs/resources/codex.json" with { type: "json" };
export type WorkspaceAgentKind = string;
export type WorkspaceModelId = string;
export interface WorkspaceModelOption {
id: WorkspaceModelId;
label: string;
}
export interface WorkspaceModelGroup {
provider: string;
agentKind: WorkspaceAgentKind;
sandboxAgentId: string;
models: WorkspaceModelOption[];
}
interface AgentConfigResource {
defaultModel?: string;
models?: Array<{ id?: string; name?: string }>;
}
interface SandboxAgentInfoLike {
id?: unknown;
configOptions?: unknown;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function normalizeModelLabel(model: { id?: string; name?: string }): string {
const name = model.name?.trim();
if (name && name.length > 0) {
return name;
}
return model.id?.trim() || "Unknown";
}
function buildGroup(provider: string, agentKind: WorkspaceAgentKind, sandboxAgentId: string, config: AgentConfigResource): WorkspaceModelGroup {
return {
provider,
agentKind,
sandboxAgentId,
models: (config.models ?? [])
.map((model) => {
const id = model.id?.trim();
if (!id) {
return null;
}
return {
id,
label: normalizeModelLabel(model),
};
})
.filter((model): model is WorkspaceModelOption => model != null),
};
}
function titleCaseIdentifier(value: string): string {
return value
.split(/[\s_-]+/)
.filter(Boolean)
.map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
.join(" ");
}
function workspaceAgentMetadata(agentId: string): { provider: string; agentKind: string } {
const normalized = agentId.trim().toLowerCase();
switch (normalized) {
case "claude":
return { provider: "Claude", agentKind: "Claude" };
case "codex":
return { provider: "Codex", agentKind: "Codex" };
default:
return {
provider: titleCaseIdentifier(agentId),
agentKind: titleCaseIdentifier(agentId),
};
}
}
function normalizeOptionLabel(entry: Record<string, unknown>): string | null {
const name = typeof entry.name === "string" ? entry.name.trim() : "";
if (name) {
return name;
}
const label = typeof entry.label === "string" ? entry.label.trim() : "";
if (label) {
return label;
}
const value = typeof entry.value === "string" ? entry.value.trim() : "";
return value || null;
}
function appendSelectOptionModels(target: WorkspaceModelOption[], options: unknown): void {
if (!Array.isArray(options)) {
return;
}
for (const entry of options) {
if (!isRecord(entry)) {
continue;
}
const value = typeof entry.value === "string" ? entry.value.trim() : "";
if (value) {
target.push({
id: value,
label: normalizeOptionLabel(entry) ?? value,
});
continue;
}
appendSelectOptionModels(target, entry.options);
}
}
function normalizeAgentModels(configOptions: unknown): WorkspaceModelOption[] {
if (!Array.isArray(configOptions)) {
return [];
}
const options = configOptions.find((entry) => isRecord(entry) && entry.category === "model" && entry.type === "select");
if (!isRecord(options)) {
return [];
}
const models: WorkspaceModelOption[] = [];
appendSelectOptionModels(models, options.options);
const seen = new Set<string>();
return models.filter((model) => {
if (!model.id || seen.has(model.id)) {
return false;
}
seen.add(model.id);
return true;
});
}
export function workspaceModelGroupsFromSandboxAgents(agents: SandboxAgentInfoLike[]): WorkspaceModelGroup[] {
return agents
.map((agent) => {
const sandboxAgentId = typeof agent.id === "string" ? agent.id.trim() : "";
if (!sandboxAgentId) {
return null;
}
const models = normalizeAgentModels(agent.configOptions);
if (models.length === 0) {
return null;
}
const metadata = workspaceAgentMetadata(sandboxAgentId);
return {
provider: metadata.provider,
agentKind: metadata.agentKind,
sandboxAgentId,
models,
} satisfies WorkspaceModelGroup;
})
.filter((group): group is WorkspaceModelGroup => group != null);
}
export const DEFAULT_WORKSPACE_MODEL_GROUPS: WorkspaceModelGroup[] = [
buildGroup("Claude", "Claude", "claude", claudeConfig as AgentConfigResource),
buildGroup("Codex", "Codex", "codex", codexConfig as AgentConfigResource),
].filter((group) => group.models.length > 0);
export const DEFAULT_WORKSPACE_MODEL_ID: WorkspaceModelId =
((codexConfig as AgentConfigResource).defaultModel ?? DEFAULT_WORKSPACE_MODEL_GROUPS[0]?.models[0]?.id ?? "default").trim();
export function workspaceProviderAgent(
provider: string,
groups: WorkspaceModelGroup[] = DEFAULT_WORKSPACE_MODEL_GROUPS,
): WorkspaceAgentKind {
return groups.find((group) => group.provider === provider)?.agentKind ?? provider;
}
export function workspaceModelGroupForId(
id: WorkspaceModelId,
groups: WorkspaceModelGroup[] = DEFAULT_WORKSPACE_MODEL_GROUPS,
): WorkspaceModelGroup | null {
return groups.find((group) => group.models.some((model) => model.id === id)) ?? null;
}
export function workspaceModelLabel(
id: WorkspaceModelId,
groups: WorkspaceModelGroup[] = DEFAULT_WORKSPACE_MODEL_GROUPS,
): string {
const group = workspaceModelGroupForId(id, groups);
const model = group?.models.find((candidate) => candidate.id === id);
return model && group ? `${group.provider} ${model.label}` : id;
}
export function workspaceAgentForModel(
id: WorkspaceModelId,
groups: WorkspaceModelGroup[] = DEFAULT_WORKSPACE_MODEL_GROUPS,
): WorkspaceAgentKind {
const group = workspaceModelGroupForId(id, groups);
if (group) {
return group.agentKind;
}
return groups[0]?.agentKind ?? "Claude";
}
export function workspaceSandboxAgentIdForModel(
id: WorkspaceModelId,
groups: WorkspaceModelGroup[] = DEFAULT_WORKSPACE_MODEL_GROUPS,
): string {
const group = workspaceModelGroupForId(id, groups);
return group?.sandboxAgentId ?? groups[0]?.sandboxAgentId ?? "claude";
}

View file

@ -1,5 +1,5 @@
import type { FoundryAppSnapshot } from "./app-shell.js";
import type { WorkbenchOpenPrSummary, WorkbenchRepositorySummary, WorkbenchSessionDetail, WorkbenchTaskDetail, WorkbenchTaskSummary } from "./workbench.js";
import type { OrganizationSummarySnapshot, WorkspaceSessionDetail, WorkspaceTaskDetail } from "./workspace.js";
export interface SandboxProcessSnapshot {
id: string;
@ -16,20 +16,13 @@ export interface SandboxProcessSnapshot {
}
/** Organization-level events broadcast by the organization actor. */
export type OrganizationEvent =
| { type: "taskSummaryUpdated"; taskSummary: WorkbenchTaskSummary }
| { type: "taskRemoved"; taskId: string }
| { type: "repoAdded"; repo: WorkbenchRepositorySummary }
| { type: "repoUpdated"; repo: WorkbenchRepositorySummary }
| { type: "repoRemoved"; repoId: string }
| { type: "pullRequestUpdated"; pullRequest: WorkbenchOpenPrSummary }
| { type: "pullRequestRemoved"; prId: string };
export type OrganizationEvent = { type: "organizationUpdated"; snapshot: OrganizationSummarySnapshot };
/** Task-level events broadcast by the task actor. */
export type TaskEvent = { type: "taskDetailUpdated"; detail: WorkbenchTaskDetail };
export type TaskEvent = { type: "taskUpdated"; detail: WorkspaceTaskDetail };
/** Session-level events broadcast by the task actor and filtered by sessionId on the client. */
export type SessionEvent = { type: "sessionUpdated"; session: WorkbenchSessionDetail };
export type SessionEvent = { type: "sessionUpdated"; session: WorkspaceSessionDetail };
/** App-level events broadcast by the app organization actor. */
export type AppEvent = { type: "appUpdated"; snapshot: FoundryAppSnapshot };

View file

@ -1,296 +0,0 @@
import type { AgentType, SandboxProviderId, TaskStatus } from "./contracts.js";
export type WorkbenchTaskStatus = TaskStatus | "new";
export type WorkbenchAgentKind = "Claude" | "Codex" | "Cursor";
export type WorkbenchModelId =
| "claude-sonnet-4"
| "claude-opus-4"
| "gpt-5.3-codex"
| "gpt-5.4"
| "gpt-5.2-codex"
| "gpt-5.1-codex-max"
| "gpt-5.2"
| "gpt-5.1-codex-mini";
export type WorkbenchSessionStatus = "pending_provision" | "pending_session_create" | "ready" | "running" | "idle" | "error";
export interface WorkbenchTranscriptEvent {
id: string;
eventIndex: number;
sessionId: string;
createdAt: number;
connectionId: string;
sender: "client" | "agent";
payload: unknown;
}
export interface WorkbenchComposerDraft {
text: string;
attachments: WorkbenchLineAttachment[];
updatedAtMs: number | null;
}
/** Session metadata without transcript content. */
export interface WorkbenchSessionSummary {
id: string;
/** Stable UI session id used for routing and task-local identity. */
sessionId: string;
/** Underlying sandbox session id when provisioning has completed. */
sandboxSessionId?: string | null;
sessionName: string;
agent: WorkbenchAgentKind;
model: WorkbenchModelId;
status: WorkbenchSessionStatus;
thinkingSinceMs: number | null;
unread: boolean;
created: boolean;
errorMessage?: string | null;
}
/** Full session content — only fetched when viewing a specific session. */
export interface WorkbenchSessionDetail {
/** Stable UI session id used for the session topic key and routing. */
sessionId: string;
sandboxSessionId: string | null;
sessionName: string;
agent: WorkbenchAgentKind;
model: WorkbenchModelId;
status: WorkbenchSessionStatus;
thinkingSinceMs: number | null;
unread: boolean;
created: boolean;
errorMessage?: string | null;
draft: WorkbenchComposerDraft;
transcript: WorkbenchTranscriptEvent[];
}
export interface WorkbenchFileChange {
path: string;
added: number;
removed: number;
type: "M" | "A" | "D";
}
export interface WorkbenchFileTreeNode {
name: string;
path: string;
isDir: boolean;
children?: WorkbenchFileTreeNode[];
}
export interface WorkbenchLineAttachment {
id: string;
filePath: string;
lineNumber: number;
lineContent: string;
}
export interface WorkbenchHistoryEvent {
id: string;
messageId: string;
preview: string;
sessionName: string;
sessionId: string;
createdAtMs: number;
detail: string;
}
export type WorkbenchDiffLineKind = "context" | "add" | "remove" | "hunk";
export interface WorkbenchParsedDiffLine {
kind: WorkbenchDiffLineKind;
lineNumber: number;
text: string;
}
export interface WorkbenchPullRequestSummary {
number: number;
status: "draft" | "ready";
}
export interface WorkbenchOpenPrSummary {
prId: string;
repoId: string;
repoFullName: string;
number: number;
title: string;
state: string;
url: string;
headRefName: string;
baseRefName: string;
authorLogin: string | null;
isDraft: boolean;
updatedAtMs: number;
}
export interface WorkbenchSandboxSummary {
sandboxProviderId: SandboxProviderId;
sandboxId: string;
cwd: string | null;
}
/** Sidebar-level task data. Materialized in the organization actor's SQLite. */
export interface WorkbenchTaskSummary {
id: string;
repoId: string;
title: string;
status: WorkbenchTaskStatus;
repoName: string;
updatedAtMs: number;
branch: string | null;
pullRequest: WorkbenchPullRequestSummary | null;
/** Summary of sessions — no transcript content. */
sessionsSummary: WorkbenchSessionSummary[];
}
/** Full task detail — only fetched when viewing a specific task. */
export interface WorkbenchTaskDetail extends WorkbenchTaskSummary {
/** Original task prompt/instructions shown in the detail view. */
task: string;
/** Agent choice used when creating new sandbox sessions for this task. */
agentType: AgentType | null;
/** Underlying task runtime status preserved for detail views and error handling. */
runtimeStatus: TaskStatus;
statusMessage: string | null;
activeSessionId: string | null;
diffStat: string | null;
prUrl: string | null;
reviewStatus: string | null;
fileChanges: WorkbenchFileChange[];
diffs: Record<string, string>;
fileTree: WorkbenchFileTreeNode[];
minutesUsed: number;
/** Sandbox info for this task. */
sandboxes: WorkbenchSandboxSummary[];
activeSandboxId: string | null;
}
/** Repo-level summary for organization sidebar. */
export interface WorkbenchRepositorySummary {
id: string;
label: string;
/** Aggregated branch/task overview state (replaces getRepoOverview polling). */
taskCount: number;
latestActivityMs: number;
}
/** Organization-level snapshot — initial fetch for the organization topic. */
export interface OrganizationSummarySnapshot {
organizationId: string;
repos: WorkbenchRepositorySummary[];
taskSummaries: WorkbenchTaskSummary[];
openPullRequests: WorkbenchOpenPrSummary[];
}
export interface WorkbenchSession extends WorkbenchSessionSummary {
draft: WorkbenchComposerDraft;
transcript: WorkbenchTranscriptEvent[];
}
export interface WorkbenchTask {
id: string;
repoId: string;
title: string;
status: WorkbenchTaskStatus;
runtimeStatus?: TaskStatus;
statusMessage?: string | null;
repoName: string;
updatedAtMs: number;
branch: string | null;
pullRequest: WorkbenchPullRequestSummary | null;
sessions: WorkbenchSession[];
fileChanges: WorkbenchFileChange[];
diffs: Record<string, string>;
fileTree: WorkbenchFileTreeNode[];
minutesUsed: number;
activeSandboxId?: string | null;
}
export interface WorkbenchRepo {
id: string;
label: string;
}
export interface WorkbenchRepositorySection {
id: string;
label: string;
updatedAtMs: number;
tasks: WorkbenchTask[];
}
export interface TaskWorkbenchSnapshot {
organizationId: string;
repos: WorkbenchRepo[];
repositories: WorkbenchRepositorySection[];
tasks: WorkbenchTask[];
}
export interface WorkbenchModelOption {
id: WorkbenchModelId;
label: string;
}
export interface WorkbenchModelGroup {
provider: string;
models: WorkbenchModelOption[];
}
export interface TaskWorkbenchSelectInput {
taskId: string;
}
export interface TaskWorkbenchCreateTaskInput {
repoId: string;
task: string;
title?: string;
branch?: string;
onBranch?: string;
model?: WorkbenchModelId;
}
export interface TaskWorkbenchRenameInput {
taskId: string;
value: string;
}
export interface TaskWorkbenchSendMessageInput {
taskId: string;
sessionId: string;
text: string;
attachments: WorkbenchLineAttachment[];
}
export interface TaskWorkbenchSessionInput {
taskId: string;
sessionId: string;
}
export interface TaskWorkbenchRenameSessionInput extends TaskWorkbenchSessionInput {
title: string;
}
export interface TaskWorkbenchChangeModelInput extends TaskWorkbenchSessionInput {
model: WorkbenchModelId;
}
export interface TaskWorkbenchUpdateDraftInput extends TaskWorkbenchSessionInput {
text: string;
attachments: WorkbenchLineAttachment[];
}
export interface TaskWorkbenchSetSessionUnreadInput extends TaskWorkbenchSessionInput {
unread: boolean;
}
export interface TaskWorkbenchDiffInput {
taskId: string;
path: string;
}
export interface TaskWorkbenchCreateTaskResponse {
taskId: string;
sessionId?: string;
}
export interface TaskWorkbenchAddSessionResponse {
sessionId: string;
}

View file

@ -0,0 +1,311 @@
import type { SandboxProviderId, TaskStatus } from "./contracts.js";
import type { WorkspaceAgentKind, WorkspaceModelGroup, WorkspaceModelId, WorkspaceModelOption } from "./models.js";
export type WorkspaceTaskStatus = TaskStatus;
export type WorkspaceSessionStatus = "pending_provision" | "pending_session_create" | "ready" | "running" | "idle" | "error";
export type { WorkspaceAgentKind, WorkspaceModelGroup, WorkspaceModelId, WorkspaceModelOption } from "./models.js";
export interface WorkspaceTranscriptEvent {
id: string;
eventIndex: number;
sessionId: string;
createdAt: number;
connectionId: string;
sender: "client" | "agent";
payload: unknown;
}
export interface WorkspaceComposerDraft {
text: string;
attachments: WorkspaceLineAttachment[];
updatedAtMs: number | null;
}
/** Session metadata without transcript content. */
export interface WorkspaceSessionSummary {
id: string;
/** Stable UI session id used for routing and task-local identity. */
sessionId: string;
/** Underlying sandbox session id when provisioning has completed. */
sandboxSessionId?: string | null;
sessionName: string;
agent: WorkspaceAgentKind;
model: WorkspaceModelId;
status: WorkspaceSessionStatus;
thinkingSinceMs: number | null;
unread: boolean;
created: boolean;
errorMessage?: string | null;
}
/** Full session content — only fetched when viewing a specific session. */
export interface WorkspaceSessionDetail {
/** Stable UI session id used for the session topic key and routing. */
sessionId: string;
sandboxSessionId: string | null;
sessionName: string;
agent: WorkspaceAgentKind;
model: WorkspaceModelId;
status: WorkspaceSessionStatus;
thinkingSinceMs: number | null;
unread: boolean;
created: boolean;
errorMessage?: string | null;
draft: WorkspaceComposerDraft;
transcript: WorkspaceTranscriptEvent[];
}
export interface WorkspaceFileChange {
path: string;
added: number;
removed: number;
type: "M" | "A" | "D";
}
export interface WorkspaceFileTreeNode {
name: string;
path: string;
isDir: boolean;
children?: WorkspaceFileTreeNode[];
}
export interface WorkspaceLineAttachment {
id: string;
filePath: string;
lineNumber: number;
lineContent: string;
}
export interface WorkspaceHistoryEvent {
id: string;
messageId: string;
preview: string;
sessionName: string;
sessionId: string;
createdAtMs: number;
detail: string;
}
export type WorkspaceDiffLineKind = "context" | "add" | "remove" | "hunk";
export interface WorkspaceParsedDiffLine {
kind: WorkspaceDiffLineKind;
lineNumber: number;
text: string;
}
export interface WorkspacePullRequestSummary {
number: number;
status: "draft" | "ready";
title?: string;
state?: string;
url?: string;
headRefName?: string;
baseRefName?: string;
repoFullName?: string;
authorLogin?: string | null;
isDraft?: boolean;
updatedAtMs?: number;
}
export interface WorkspaceSandboxSummary {
sandboxProviderId: SandboxProviderId;
sandboxId: string;
cwd: string | null;
}
/** Sidebar-level task data. Materialized in the organization actor's SQLite. */
export interface WorkspaceTaskSummary {
id: string;
repoId: string;
title: string;
status: WorkspaceTaskStatus;
repoName: string;
updatedAtMs: number;
branch: string | null;
pullRequest: WorkspacePullRequestSummary | null;
activeSessionId: string | null;
/** Summary of sessions — no transcript content. */
sessionsSummary: WorkspaceSessionSummary[];
}
/** Full task detail — only fetched when viewing a specific task. */
export interface WorkspaceTaskDetail extends WorkspaceTaskSummary {
/** Original task prompt/instructions shown in the detail view. */
task: string;
fileChanges: WorkspaceFileChange[];
diffs: Record<string, string>;
fileTree: WorkspaceFileTreeNode[];
minutesUsed: number;
/** Sandbox info for this task. */
sandboxes: WorkspaceSandboxSummary[];
activeSandboxId: string | null;
}
/** Repo-level summary for organization sidebar. */
export interface WorkspaceRepositorySummary {
id: string;
label: string;
/** Aggregated branch/task overview state (replaces getRepoOverview polling). */
taskCount: number;
latestActivityMs: number;
}
export type OrganizationGithubSyncPhase =
| "discovering_repositories"
| "syncing_repositories"
| "syncing_branches"
| "syncing_members"
| "syncing_pull_requests";
export interface OrganizationGithubSummary {
connectedAccount: string;
installationStatus: "connected" | "install_required" | "reconnect_required";
syncStatus: "pending" | "syncing" | "synced" | "error";
importedRepoCount: number;
lastSyncLabel: string;
lastSyncAt: number | null;
lastWebhookAt: number | null;
lastWebhookEvent: string;
syncGeneration: number;
syncPhase: OrganizationGithubSyncPhase | null;
processedRepositoryCount: number;
totalRepositoryCount: number;
}
export interface WorkspaceOpenPullRequest {
repoId: string;
repoFullName: string;
number: number;
title: string;
status: string;
state: string;
url: string;
headRefName: string;
baseRefName: string;
authorLogin: string | null;
isDraft: boolean;
}
/** Organization-level snapshot — initial fetch for the organization topic. */
export interface OrganizationSummarySnapshot {
organizationId: string;
github: OrganizationGithubSummary;
repos: WorkspaceRepositorySummary[];
taskSummaries: WorkspaceTaskSummary[];
openPullRequests?: WorkspaceOpenPullRequest[];
}
export interface WorkspaceSession extends WorkspaceSessionSummary {
draft: WorkspaceComposerDraft;
transcript: WorkspaceTranscriptEvent[];
}
export interface WorkspaceTask {
id: string;
repoId: string;
title: string;
status: WorkspaceTaskStatus;
repoName: string;
updatedAtMs: number;
branch: string | null;
pullRequest: WorkspacePullRequestSummary | null;
activeSessionId?: string | null;
sessions: WorkspaceSession[];
fileChanges: WorkspaceFileChange[];
diffs: Record<string, string>;
fileTree: WorkspaceFileTreeNode[];
minutesUsed: number;
activeSandboxId?: string | null;
}
export interface WorkspaceRepo {
id: string;
label: string;
}
export interface WorkspaceRepositorySection {
id: string;
label: string;
updatedAtMs: number;
tasks: WorkspaceTask[];
}
export interface TaskWorkspaceSnapshot {
organizationId: string;
repos: WorkspaceRepo[];
repositories: WorkspaceRepositorySection[];
tasks: WorkspaceTask[];
}
export interface TaskWorkspaceSelectInput {
repoId: string;
taskId: string;
authSessionId?: string;
}
export interface TaskWorkspaceCreateTaskInput {
repoId: string;
task: string;
title?: string;
branch?: string;
onBranch?: string;
model?: WorkspaceModelId;
authSessionId?: string;
}
export interface TaskWorkspaceRenameInput {
repoId: string;
taskId: string;
value: string;
authSessionId?: string;
}
export interface TaskWorkspaceSendMessageInput {
repoId: string;
taskId: string;
sessionId: string;
text: string;
attachments: WorkspaceLineAttachment[];
authSessionId?: string;
}
export interface TaskWorkspaceSessionInput {
repoId: string;
taskId: string;
sessionId: string;
authSessionId?: string;
}
export interface TaskWorkspaceRenameSessionInput extends TaskWorkspaceSessionInput {
title: string;
}
export interface TaskWorkspaceChangeModelInput extends TaskWorkspaceSessionInput {
model: WorkspaceModelId;
}
export interface TaskWorkspaceUpdateDraftInput extends TaskWorkspaceSessionInput {
text: string;
attachments: WorkspaceLineAttachment[];
}
export interface TaskWorkspaceSetSessionUnreadInput extends TaskWorkspaceSessionInput {
unread: boolean;
}
export interface TaskWorkspaceDiffInput {
repoId: string;
taskId: string;
path: string;
}
export interface TaskWorkspaceCreateTaskResponse {
taskId: string;
sessionId?: string;
}
export interface TaskWorkspaceAddSessionResponse {
sessionId: string;
}