Fix workspace runtime state reconciliation

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-04 13:15:46 -05:00
parent 5a9a2a9112
commit f515f2aa12
9 changed files with 477 additions and 64 deletions

View file

@ -14,6 +14,10 @@ import type {
WorkspaceRuntimeService,
} from "@paperclipai/shared";
import { parseProjectExecutionWorkspacePolicy } from "./execution-workspace-policy.js";
import {
listCurrentRuntimeServicesForExecutionWorkspaces,
listCurrentRuntimeServicesForProjectWorkspaces,
} from "./workspace-runtime-read-model.js";
type ExecutionWorkspaceRow = typeof executionWorkspaces.$inferSelect;
type WorkspaceRuntimeServiceRow = typeof workspaceRuntimeServices.$inferSelect;
@ -317,6 +321,41 @@ function toExecutionWorkspace(
};
}
function usesInheritedProjectRuntimeServices(row: ExecutionWorkspaceRow) {
if (row.mode !== "shared_workspace" || !row.projectWorkspaceId) return false;
return !readExecutionWorkspaceConfig((row.metadata as Record<string, unknown> | null) ?? null)?.workspaceRuntime;
}
async function loadEffectiveRuntimeServicesByExecutionWorkspace(
db: Db,
companyId: string,
rows: ExecutionWorkspaceRow[],
) {
const executionRuntimeServices = await listCurrentRuntimeServicesForExecutionWorkspaces(
db,
companyId,
rows.map((row) => row.id),
);
const projectWorkspaceIds = rows
.filter((row) => usesInheritedProjectRuntimeServices(row))
.map((row) => row.projectWorkspaceId)
.filter((value): value is string => Boolean(value));
const projectRuntimeServices = await listCurrentRuntimeServicesForProjectWorkspaces(
db,
companyId,
[...new Set(projectWorkspaceIds)],
);
return new Map(
rows.map((row) => [
row.id,
usesInheritedProjectRuntimeServices(row)
? (projectRuntimeServices.get(row.projectWorkspaceId!) ?? [])
: (executionRuntimeServices.get(row.id) ?? []),
]),
);
}
export function executionWorkspaceService(db: Db) {
return {
list: async (companyId: string, filters?: {
@ -346,7 +385,13 @@ export function executionWorkspaceService(db: Db) {
.from(executionWorkspaces)
.where(and(...conditions))
.orderBy(desc(executionWorkspaces.lastUsedAt), desc(executionWorkspaces.createdAt));
return rows.map((row) => toExecutionWorkspace(row));
const runtimeServicesByWorkspaceId = await loadEffectiveRuntimeServicesByExecutionWorkspace(db, companyId, rows);
return rows.map((row) =>
toExecutionWorkspace(
row,
(runtimeServicesByWorkspaceId.get(row.id) ?? []).map(toRuntimeService),
),
);
},
getById: async (id: string) => {
@ -356,12 +401,11 @@ export function executionWorkspaceService(db: Db) {
.where(eq(executionWorkspaces.id, id))
.then((rows) => rows[0] ?? null);
if (!row) return null;
const runtimeServiceRows = await db
.select()
.from(workspaceRuntimeServices)
.where(eq(workspaceRuntimeServices.executionWorkspaceId, row.id))
.orderBy(desc(workspaceRuntimeServices.updatedAt), desc(workspaceRuntimeServices.createdAt));
return toExecutionWorkspace(row, runtimeServiceRows.map(toRuntimeService));
const runtimeServicesByWorkspaceId = await loadEffectiveRuntimeServicesByExecutionWorkspace(db, row.companyId, [row]);
return toExecutionWorkspace(
row,
(runtimeServicesByWorkspaceId.get(row.id) ?? []).map(toRuntimeService),
);
},
getCloseReadiness: async (id: string): Promise<ExecutionWorkspaceCloseReadiness | null> => {
@ -372,12 +416,8 @@ export function executionWorkspaceService(db: Db) {
.then((rows) => rows[0] ?? null);
if (!workspace) return null;
const runtimeServiceRows = await db
.select()
.from(workspaceRuntimeServices)
.where(eq(workspaceRuntimeServices.executionWorkspaceId, workspace.id))
.orderBy(desc(workspaceRuntimeServices.updatedAt), desc(workspaceRuntimeServices.createdAt));
const runtimeServices = runtimeServiceRows.map(toRuntimeService);
const runtimeServicesByWorkspaceId = await loadEffectiveRuntimeServicesByExecutionWorkspace(db, workspace.companyId, [workspace]);
const runtimeServices = (runtimeServicesByWorkspaceId.get(workspace.id) ?? []).map(toRuntimeService);
const linkedIssues = await db
.select({