mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 19:04:40 +00:00
Fix Foundry UI bugs: org names, sessions, and repo selection (#250)
* Fix Foundry auth: migrate to Better Auth adapter, fix access token retrieval - Remove @ts-nocheck from better-auth.ts, auth-user/index.ts, app-shell.ts and fix all type errors - Fix getAccessTokenForSession: read GitHub token directly from account record instead of calling Better Auth's internal /get-access-token endpoint which returns 403 on server-side calls - Re-implement workspaceAuth helper functions (workspaceAuthColumn, normalizeAuthValue, workspaceAuthClause, workspaceAuthWhere) that were accidentally deleted - Remove all retry logic (withRetries, isRetryableAppActorError) - Implement CORS origin allowlist from configured environment - Document cachedAppWorkspace singleton pattern - Add inline org sync fallback in buildAppSnapshot for post-OAuth flow - Add no-retry rule to CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add Foundry dev panel from fix-git-data branch Port the dev panel component that was left out when PR #243 was replaced by PR #247. Adapted to remove runtime/mock-debug references that don't exist on the current branch. - Toggle with Shift+D, persists visibility to localStorage - Shows context, session, GitHub sync status sections - Dev-only (import.meta.env.DEV) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add full Docker image defaults, fix actor deadlocks, and improve dev experience - Add Dockerfile.full and --all flag to install-agent CLI for pre-built images - Centralize Docker image constant (FULL_IMAGE) pinned to 0.3.1-full - Remove examples/shared/Dockerfile{,.dev} and daytona snapshot example - Expand Docker docs with full runnable Dockerfile - Fix self-deadlock in createWorkbenchSession (fire-and-forget provisioning) - Audit and convert 12 task actions from wait:true to wait:false - Add bun --hot for dev backend hot reload - Remove --force from pnpm install in dev Dockerfile for faster startup - Add env_file support to compose.dev.yaml for automatic credential loading - Add mock frontend compose config and dev panel - Update CLAUDE.md with wait:true policy and dev environment setup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * WIP: async action fixes and interest manager Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix Foundry UI bugs: org names, hanging sessions, and wrong repo creation - Fix org display name using GitHub description instead of name field - Fix createWorkbenchSession hanging when sandbox is provisioning - Fix auto-session creation retry storm on errors - Fix task creation using wrong repo due to React state race conditions - Remove Bun hot-reload from backend Dockerfile (causes port drift) - Add GitHub sync/install status to dev panel 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
58c54156f1
commit
d8b8b49f37
88 changed files with 9252 additions and 1933 deletions
|
|
@ -10,7 +10,7 @@ import { foundryRepoClonePath } from "../../services/foundry-paths.js";
|
|||
import { resolveWorkspaceGithubAuth } from "../../services/github-auth.js";
|
||||
import { expectQueueResponse } from "../../services/queue.js";
|
||||
import { withRepoGitLock } from "../../services/repo-git-lock.js";
|
||||
import { branches, taskIndex, prCache, repoMeta } from "./db/schema.js";
|
||||
import { branches, taskIndex, prCache, repoActionJobs, repoMeta } from "./db/schema.js";
|
||||
import { deriveFallbackTitle } from "../../services/create-flow.js";
|
||||
import { normalizeBaseBranchName } from "../../integrations/git-spice/index.js";
|
||||
import { sortBranchesForOverview } from "./stack-model.js";
|
||||
|
|
@ -87,6 +87,7 @@ interface BranchSyncResult {
|
|||
interface RepoOverviewCommand {}
|
||||
|
||||
interface RunRepoStackActionCommand {
|
||||
jobId?: string;
|
||||
action: RepoStackAction;
|
||||
branchName?: string;
|
||||
parentBranch?: string;
|
||||
|
|
@ -133,6 +134,90 @@ async function ensureProjectSyncActors(c: any, localPath: string): Promise<void>
|
|||
c.state.syncActorsStarted = true;
|
||||
}
|
||||
|
||||
async function ensureRepoActionJobsTable(c: any): Promise<void> {
|
||||
await c.db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS repo_action_jobs (
|
||||
job_id text PRIMARY KEY NOT NULL,
|
||||
action text NOT NULL,
|
||||
branch_name text,
|
||||
parent_branch text,
|
||||
status text NOT NULL,
|
||||
message text NOT NULL,
|
||||
created_at integer NOT NULL,
|
||||
updated_at integer NOT NULL,
|
||||
completed_at integer
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
async function writeRepoActionJob(
|
||||
c: any,
|
||||
input: {
|
||||
jobId: string;
|
||||
action: RepoStackAction;
|
||||
branchName: string | null;
|
||||
parentBranch: string | null;
|
||||
status: "queued" | "running" | "completed" | "error";
|
||||
message: string;
|
||||
createdAt?: number;
|
||||
completedAt?: number | null;
|
||||
},
|
||||
): Promise<void> {
|
||||
await ensureRepoActionJobsTable(c);
|
||||
const now = Date.now();
|
||||
await c.db
|
||||
.insert(repoActionJobs)
|
||||
.values({
|
||||
jobId: input.jobId,
|
||||
action: input.action,
|
||||
branchName: input.branchName,
|
||||
parentBranch: input.parentBranch,
|
||||
status: input.status,
|
||||
message: input.message,
|
||||
createdAt: input.createdAt ?? now,
|
||||
updatedAt: now,
|
||||
completedAt: input.completedAt ?? null,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: repoActionJobs.jobId,
|
||||
set: {
|
||||
status: input.status,
|
||||
message: input.message,
|
||||
updatedAt: now,
|
||||
completedAt: input.completedAt ?? null,
|
||||
},
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
async function listRepoActionJobRows(c: any): Promise<
|
||||
Array<{
|
||||
jobId: string;
|
||||
action: RepoStackAction;
|
||||
branchName: string | null;
|
||||
parentBranch: string | null;
|
||||
status: "queued" | "running" | "completed" | "error";
|
||||
message: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
completedAt: number | null;
|
||||
}>
|
||||
> {
|
||||
await ensureRepoActionJobsTable(c);
|
||||
const rows = await c.db.select().from(repoActionJobs).orderBy(desc(repoActionJobs.updatedAt)).limit(20).all();
|
||||
return rows.map((row: any) => ({
|
||||
jobId: row.jobId,
|
||||
action: row.action,
|
||||
branchName: row.branchName ?? null,
|
||||
parentBranch: row.parentBranch ?? null,
|
||||
status: row.status,
|
||||
message: row.message,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
completedAt: row.completedAt ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
async function deleteStaleTaskIndexRow(c: any, taskId: string): Promise<void> {
|
||||
try {
|
||||
await c.db.delete(taskIndex).where(eq(taskIndex.taskId, taskId)).run();
|
||||
|
|
@ -359,8 +444,6 @@ async function createTaskMutation(c: any, cmd: CreateTaskCommand): Promise<TaskR
|
|||
const taskId = randomUUID();
|
||||
|
||||
if (onBranch) {
|
||||
await forceProjectSync(c, localPath);
|
||||
|
||||
const branchRow = await c.db.select({ branchName: branches.branchName }).from(branches).where(eq(branches.branchName, onBranch)).get();
|
||||
if (!branchRow) {
|
||||
throw new Error(`Branch not found in repo snapshot: ${onBranch}`);
|
||||
|
|
@ -573,14 +656,37 @@ async function runRepoStackActionMutation(c: any, cmd: RunRepoStackActionCommand
|
|||
|
||||
const { driver } = getActorRuntimeContext();
|
||||
const at = Date.now();
|
||||
const jobId = cmd.jobId ?? randomUUID();
|
||||
const action = cmd.action;
|
||||
const branchName = cmd.branchName?.trim() || null;
|
||||
const parentBranch = cmd.parentBranch?.trim() || null;
|
||||
|
||||
await writeRepoActionJob(c, {
|
||||
jobId,
|
||||
action,
|
||||
branchName,
|
||||
parentBranch,
|
||||
status: "running",
|
||||
message: `Running ${action}`,
|
||||
createdAt: at,
|
||||
});
|
||||
|
||||
if (!(await driver.stack.available(localPath).catch(() => false))) {
|
||||
await writeRepoActionJob(c, {
|
||||
jobId,
|
||||
action,
|
||||
branchName,
|
||||
parentBranch,
|
||||
status: "error",
|
||||
message: "git-spice is not available for this repo",
|
||||
createdAt: at,
|
||||
completedAt: Date.now(),
|
||||
});
|
||||
return {
|
||||
jobId,
|
||||
action,
|
||||
executed: false,
|
||||
status: "error",
|
||||
message: "git-spice is not available for this repo",
|
||||
at,
|
||||
};
|
||||
|
|
@ -615,48 +721,77 @@ async function runRepoStackActionMutation(c: any, cmd: RunRepoStackActionCommand
|
|||
}
|
||||
}
|
||||
|
||||
await withRepoGitLock(localPath, async () => {
|
||||
if (action === "sync_repo") {
|
||||
await driver.stack.syncRepo(localPath);
|
||||
} else if (action === "restack_repo") {
|
||||
await driver.stack.restackRepo(localPath);
|
||||
} else if (action === "restack_subtree") {
|
||||
await driver.stack.restackSubtree(localPath, branchName!);
|
||||
} else if (action === "rebase_branch") {
|
||||
await driver.stack.rebaseBranch(localPath, branchName!);
|
||||
} else if (action === "reparent_branch") {
|
||||
await driver.stack.reparentBranch(localPath, branchName!, parentBranch!);
|
||||
} else {
|
||||
throw new Error(`Unsupported repo stack action: ${action}`);
|
||||
}
|
||||
});
|
||||
|
||||
await forceProjectSync(c, localPath);
|
||||
|
||||
try {
|
||||
const history = await getOrCreateHistory(c, c.state.workspaceId, c.state.repoId);
|
||||
await history.append({
|
||||
kind: "repo.stack_action",
|
||||
branchName: branchName ?? null,
|
||||
payload: {
|
||||
action,
|
||||
await withRepoGitLock(localPath, async () => {
|
||||
if (action === "sync_repo") {
|
||||
await driver.stack.syncRepo(localPath);
|
||||
} else if (action === "restack_repo") {
|
||||
await driver.stack.restackRepo(localPath);
|
||||
} else if (action === "restack_subtree") {
|
||||
await driver.stack.restackSubtree(localPath, branchName!);
|
||||
} else if (action === "rebase_branch") {
|
||||
await driver.stack.rebaseBranch(localPath, branchName!);
|
||||
} else if (action === "reparent_branch") {
|
||||
await driver.stack.reparentBranch(localPath, branchName!, parentBranch!);
|
||||
} else {
|
||||
throw new Error(`Unsupported repo stack action: ${action}`);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const history = await getOrCreateHistory(c, c.state.workspaceId, c.state.repoId);
|
||||
await history.append({
|
||||
kind: "repo.stack_action",
|
||||
branchName: branchName ?? null,
|
||||
parentBranch: parentBranch ?? null,
|
||||
},
|
||||
payload: {
|
||||
action,
|
||||
branchName: branchName ?? null,
|
||||
parentBranch: parentBranch ?? null,
|
||||
jobId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logActorWarning("project", "failed appending repo stack history event", {
|
||||
workspaceId: c.state.workspaceId,
|
||||
repoId: c.state.repoId,
|
||||
action,
|
||||
error: resolveErrorMessage(error),
|
||||
});
|
||||
}
|
||||
|
||||
await forceProjectSync(c, localPath);
|
||||
|
||||
await writeRepoActionJob(c, {
|
||||
jobId,
|
||||
action,
|
||||
branchName,
|
||||
parentBranch,
|
||||
status: "completed",
|
||||
message: `Completed ${action}`,
|
||||
createdAt: at,
|
||||
completedAt: Date.now(),
|
||||
});
|
||||
} catch (error) {
|
||||
logActorWarning("project", "failed appending repo stack history event", {
|
||||
workspaceId: c.state.workspaceId,
|
||||
repoId: c.state.repoId,
|
||||
const message = resolveErrorMessage(error);
|
||||
await writeRepoActionJob(c, {
|
||||
jobId,
|
||||
action,
|
||||
error: resolveErrorMessage(error),
|
||||
branchName,
|
||||
parentBranch,
|
||||
status: "error",
|
||||
message,
|
||||
createdAt: at,
|
||||
completedAt: Date.now(),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
jobId,
|
||||
action,
|
||||
executed: true,
|
||||
message: `stack action executed: ${action}`,
|
||||
status: "completed",
|
||||
message: `Completed ${action}`,
|
||||
at,
|
||||
};
|
||||
}
|
||||
|
|
@ -999,7 +1134,6 @@ export const projectActions = {
|
|||
async getRepoOverview(c: any, _cmd?: RepoOverviewCommand): Promise<RepoOverview> {
|
||||
const localPath = await ensureProjectReadyForRead(c);
|
||||
await ensureTaskIndexHydratedForRead(c);
|
||||
await forceProjectSync(c, localPath);
|
||||
|
||||
const { driver } = getActorRuntimeContext();
|
||||
const now = Date.now();
|
||||
|
|
@ -1118,6 +1252,9 @@ export const projectActions = {
|
|||
};
|
||||
});
|
||||
|
||||
const latestBranchSync = await c.db.select({ updatedAt: branches.updatedAt }).from(branches).orderBy(desc(branches.updatedAt)).limit(1).get();
|
||||
const latestPrSync = await c.db.select({ updatedAt: prCache.updatedAt }).from(prCache).orderBy(desc(prCache.updatedAt)).limit(1).get();
|
||||
|
||||
return {
|
||||
workspaceId: c.state.workspaceId,
|
||||
repoId: c.state.repoId,
|
||||
|
|
@ -1125,6 +1262,11 @@ export const projectActions = {
|
|||
baseRef,
|
||||
stackAvailable,
|
||||
fetchedAt: now,
|
||||
branchSyncAt: latestBranchSync?.updatedAt ?? null,
|
||||
prSyncAt: latestPrSync?.updatedAt ?? null,
|
||||
branchSyncStatus: latestBranchSync ? "synced" : "pending",
|
||||
prSyncStatus: latestPrSync ? "synced" : "pending",
|
||||
repoActionJobs: await listRepoActionJobRows(c),
|
||||
branches: branchRows,
|
||||
};
|
||||
},
|
||||
|
|
@ -1156,12 +1298,41 @@ export const projectActions = {
|
|||
|
||||
async runRepoStackAction(c: any, cmd: RunRepoStackActionCommand): Promise<RepoStackActionResult> {
|
||||
const self = selfProject(c);
|
||||
return expectQueueResponse<RepoStackActionResult>(
|
||||
await self.send(projectWorkflowQueueName("project.command.runRepoStackAction"), cmd, {
|
||||
wait: true,
|
||||
timeout: 12 * 60_000,
|
||||
}),
|
||||
const jobId = randomUUID();
|
||||
const at = Date.now();
|
||||
const action = cmd.action;
|
||||
const branchName = cmd.branchName?.trim() || null;
|
||||
const parentBranch = cmd.parentBranch?.trim() || null;
|
||||
|
||||
await writeRepoActionJob(c, {
|
||||
jobId,
|
||||
action,
|
||||
branchName,
|
||||
parentBranch,
|
||||
status: "queued",
|
||||
message: `Queued ${action}`,
|
||||
createdAt: at,
|
||||
});
|
||||
|
||||
await self.send(
|
||||
projectWorkflowQueueName("project.command.runRepoStackAction"),
|
||||
{
|
||||
...cmd,
|
||||
jobId,
|
||||
},
|
||||
{
|
||||
wait: false,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
jobId,
|
||||
action,
|
||||
executed: true,
|
||||
status: "queued",
|
||||
message: `Queued ${action}`,
|
||||
at,
|
||||
};
|
||||
},
|
||||
|
||||
async applyPrSyncResult(c: any, body: PrSyncResult): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -42,3 +42,15 @@ export const taskIndex = sqliteTable("task_index", {
|
|||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
});
|
||||
|
||||
export const repoActionJobs = sqliteTable("repo_action_jobs", {
|
||||
jobId: text("job_id").notNull().primaryKey(),
|
||||
action: text("action").notNull(),
|
||||
branchName: text("branch_name"),
|
||||
parentBranch: text("parent_branch"),
|
||||
status: text("status").notNull(),
|
||||
message: text("message").notNull(),
|
||||
createdAt: integer("created_at").notNull(),
|
||||
updatedAt: integer("updated_at").notNull(),
|
||||
completedAt: integer("completed_at"),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue