mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 02:40:39 +09:00
Add workspace runtime controls
Expose project and execution workspace runtime defaults, control endpoints, startup recovery, and operator UI for start/stop/restart flows. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
f1ad07616c
commit
1f1fe9c989
25 changed files with 1133 additions and 51 deletions
|
|
@ -41,6 +41,7 @@ describe("execution workspace config helpers", () => {
|
|||
provisionCommand: "bash ./scripts/provision-worktree.sh",
|
||||
teardownCommand: "bash ./scripts/teardown-worktree.sh",
|
||||
cleanupCommand: "pkill -f vite || true",
|
||||
desiredState: null,
|
||||
workspaceRuntime: {
|
||||
services: [{ name: "web", command: "pnpm dev", port: 3100 }],
|
||||
},
|
||||
|
|
@ -70,6 +71,7 @@ describe("execution workspace config helpers", () => {
|
|||
provisionCommand: "bash ./scripts/provision-worktree.sh",
|
||||
teardownCommand: "bash ./scripts/teardown-worktree.sh",
|
||||
cleanupCommand: "pkill -f vite || true",
|
||||
desiredState: null,
|
||||
workspaceRuntime: {
|
||||
services: [{ name: "web", command: "pnpm dev" }],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { createApp } from "./app.js";
|
|||
import { loadConfig } from "./config.js";
|
||||
import { logger } from "./middleware/logger.js";
|
||||
import { setupLiveEventsWebSocketServer } from "./realtime/live-events-ws.js";
|
||||
import { heartbeatService, reconcilePersistedRuntimeServicesOnStartup, routineService } from "./services/index.js";
|
||||
import { heartbeatService, reconcilePersistedRuntimeServicesOnStartup, restartDesiredRuntimeServicesOnStartup, routineService } from "./services/index.js";
|
||||
import { createStorageServiceFromConfig } from "./storage/index.js";
|
||||
import { printStartupBanner } from "./startup-banner.js";
|
||||
import { getBoardClaimWarningUrl, initializeBoardClaimChallenge } from "./board-claim.js";
|
||||
|
|
@ -557,6 +557,15 @@ export async function startServer(): Promise<StartedServer> {
|
|||
"reconciled persisted runtime services from a previous server process",
|
||||
);
|
||||
}
|
||||
return restartDesiredRuntimeServicesOnStartup(db as any);
|
||||
})
|
||||
.then((result) => {
|
||||
if (result && result.restarted > 0) {
|
||||
logger.warn(
|
||||
{ restarted: result.restarted, failed: result.failed },
|
||||
"restarted desired workspace runtime services on startup",
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error({ err }, "startup reconciliation of persisted runtime services failed");
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import { validate } from "../middleware/validate.js";
|
|||
import { executionWorkspaceService, logActivity, workspaceOperationService } from "../services/index.js";
|
||||
import { mergeExecutionWorkspaceConfig, readExecutionWorkspaceConfig } from "../services/execution-workspaces.js";
|
||||
import { parseProjectExecutionWorkspacePolicy } from "../services/execution-workspace-policy.js";
|
||||
import { readProjectWorkspaceRuntimeConfig } from "../services/project-workspace-runtime-config.js";
|
||||
import {
|
||||
cleanupExecutionWorkspaceArtifacts,
|
||||
startRuntimeServicesForWorkspaceControl,
|
||||
stopRuntimeServicesForExecutionWorkspace,
|
||||
} from "../services/workspace-runtime.js";
|
||||
import { assertCompanyAccess, getActorInfo } from "./authz.js";
|
||||
|
|
@ -58,6 +60,186 @@ export function executionWorkspaceRoutes(db: Db) {
|
|||
res.json(readiness);
|
||||
});
|
||||
|
||||
router.get("/execution-workspaces/:id/workspace-operations", async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const workspace = await svc.getById(id);
|
||||
if (!workspace) {
|
||||
res.status(404).json({ error: "Execution workspace not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, workspace.companyId);
|
||||
const operations = await workspaceOperationsSvc.listForExecutionWorkspace(id);
|
||||
res.json(operations);
|
||||
});
|
||||
|
||||
router.post("/execution-workspaces/:id/runtime-services/:action", async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const action = String(req.params.action ?? "").trim().toLowerCase();
|
||||
if (action !== "start" && action !== "stop" && action !== "restart") {
|
||||
res.status(404).json({ error: "Runtime service action not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = await svc.getById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Execution workspace not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, existing.companyId);
|
||||
|
||||
const workspaceCwd = existing.cwd;
|
||||
if (!workspaceCwd) {
|
||||
res.status(422).json({ error: "Execution workspace needs a local path before Paperclip can manage local runtime services" });
|
||||
return;
|
||||
}
|
||||
|
||||
const projectWorkspace = existing.projectWorkspaceId
|
||||
? await db
|
||||
.select({
|
||||
id: projectWorkspaces.id,
|
||||
cwd: projectWorkspaces.cwd,
|
||||
repoUrl: projectWorkspaces.repoUrl,
|
||||
repoRef: projectWorkspaces.repoRef,
|
||||
defaultRef: projectWorkspaces.defaultRef,
|
||||
metadata: projectWorkspaces.metadata,
|
||||
})
|
||||
.from(projectWorkspaces)
|
||||
.where(
|
||||
and(
|
||||
eq(projectWorkspaces.id, existing.projectWorkspaceId),
|
||||
eq(projectWorkspaces.companyId, existing.companyId),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows[0] ?? null)
|
||||
: null;
|
||||
const projectWorkspaceRuntime = readProjectWorkspaceRuntimeConfig(
|
||||
(projectWorkspace?.metadata as Record<string, unknown> | null) ?? null,
|
||||
)?.workspaceRuntime ?? null;
|
||||
const effectiveRuntimeConfig = existing.config?.workspaceRuntime ?? projectWorkspaceRuntime ?? null;
|
||||
|
||||
if ((action === "start" || action === "restart") && !effectiveRuntimeConfig) {
|
||||
res.status(422).json({ error: "Execution workspace has no runtime service configuration or inherited project workspace default" });
|
||||
return;
|
||||
}
|
||||
|
||||
const actor = getActorInfo(req);
|
||||
const recorder = workspaceOperationsSvc.createRecorder({
|
||||
companyId: existing.companyId,
|
||||
executionWorkspaceId: existing.id,
|
||||
});
|
||||
let runtimeServiceCount = existing.runtimeServices?.length ?? 0;
|
||||
const stdout: string[] = [];
|
||||
const stderr: string[] = [];
|
||||
|
||||
const operation = await recorder.recordOperation({
|
||||
phase: action === "stop" ? "workspace_teardown" : "workspace_provision",
|
||||
command: `workspace runtime ${action}`,
|
||||
cwd: existing.cwd,
|
||||
metadata: {
|
||||
action,
|
||||
executionWorkspaceId: existing.id,
|
||||
},
|
||||
run: async () => {
|
||||
const onLog = async (stream: "stdout" | "stderr", chunk: string) => {
|
||||
if (stream === "stdout") stdout.push(chunk);
|
||||
else stderr.push(chunk);
|
||||
};
|
||||
|
||||
if (action === "stop" || action === "restart") {
|
||||
await stopRuntimeServicesForExecutionWorkspace({
|
||||
db,
|
||||
executionWorkspaceId: existing.id,
|
||||
workspaceCwd,
|
||||
});
|
||||
}
|
||||
|
||||
if (action === "start" || action === "restart") {
|
||||
const startedServices = await startRuntimeServicesForWorkspaceControl({
|
||||
db,
|
||||
actor: {
|
||||
id: actor.agentId ?? null,
|
||||
name: actor.actorType === "user" ? "Board" : "Agent",
|
||||
companyId: existing.companyId,
|
||||
},
|
||||
issue: existing.sourceIssueId
|
||||
? {
|
||||
id: existing.sourceIssueId,
|
||||
identifier: null,
|
||||
title: existing.name,
|
||||
}
|
||||
: null,
|
||||
workspace: {
|
||||
baseCwd: workspaceCwd,
|
||||
source: existing.mode === "shared_workspace" ? "project_primary" : "task_session",
|
||||
projectId: existing.projectId,
|
||||
workspaceId: existing.projectWorkspaceId,
|
||||
repoUrl: existing.repoUrl,
|
||||
repoRef: existing.baseRef,
|
||||
strategy: existing.strategyType === "git_worktree" ? "git_worktree" : "project_primary",
|
||||
cwd: workspaceCwd,
|
||||
branchName: existing.branchName,
|
||||
worktreePath: existing.strategyType === "git_worktree" ? workspaceCwd : null,
|
||||
warnings: [],
|
||||
created: false,
|
||||
},
|
||||
executionWorkspaceId: existing.id,
|
||||
config: { workspaceRuntime: effectiveRuntimeConfig },
|
||||
adapterEnv: {},
|
||||
onLog,
|
||||
});
|
||||
runtimeServiceCount = startedServices.length;
|
||||
} else {
|
||||
runtimeServiceCount = 0;
|
||||
}
|
||||
|
||||
const metadata = mergeExecutionWorkspaceConfig(existing.metadata as Record<string, unknown> | null, {
|
||||
desiredState: action === "stop" ? "stopped" : "running",
|
||||
});
|
||||
await svc.update(existing.id, { metadata });
|
||||
|
||||
return {
|
||||
status: "succeeded",
|
||||
stdout: stdout.join(""),
|
||||
stderr: stderr.join(""),
|
||||
system:
|
||||
action === "stop"
|
||||
? "Stopped execution workspace runtime services.\n"
|
||||
: action === "restart"
|
||||
? "Restarted execution workspace runtime services.\n"
|
||||
: "Started execution workspace runtime services.\n",
|
||||
metadata: {
|
||||
runtimeServiceCount,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const workspace = await svc.getById(id);
|
||||
if (!workspace) {
|
||||
res.status(404).json({ error: "Execution workspace not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
await logActivity(db, {
|
||||
companyId: existing.companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: `execution_workspace.runtime_${action}`,
|
||||
entityType: "execution_workspace",
|
||||
entityId: existing.id,
|
||||
details: {
|
||||
runtimeServiceCount,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
workspace,
|
||||
operation,
|
||||
});
|
||||
});
|
||||
|
||||
router.patch("/execution-workspaces/:id", validate(updateExecutionWorkspaceSchema), async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const existing = await svc.getById(id);
|
||||
|
|
|
|||
|
|
@ -8,13 +8,15 @@ import {
|
|||
updateProjectWorkspaceSchema,
|
||||
} from "@paperclipai/shared";
|
||||
import { validate } from "../middleware/validate.js";
|
||||
import { projectService, logActivity } from "../services/index.js";
|
||||
import { projectService, logActivity, workspaceOperationService } from "../services/index.js";
|
||||
import { conflict } from "../errors.js";
|
||||
import { assertCompanyAccess, getActorInfo } from "./authz.js";
|
||||
import { startRuntimeServicesForWorkspaceControl, stopRuntimeServicesForProjectWorkspace } from "../services/workspace-runtime.js";
|
||||
|
||||
export function projectRoutes(db: Db) {
|
||||
const router = Router();
|
||||
const svc = projectService(db);
|
||||
const workspaceOperations = workspaceOperationService(db);
|
||||
|
||||
async function resolveCompanyIdForProjectReference(req: Request) {
|
||||
const companyIdQuery = req.query.companyId;
|
||||
|
|
@ -229,6 +231,145 @@ export function projectRoutes(db: Db) {
|
|||
},
|
||||
);
|
||||
|
||||
router.post("/projects/:id/workspaces/:workspaceId/runtime-services/:action", async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const workspaceId = req.params.workspaceId as string;
|
||||
const action = String(req.params.action ?? "").trim().toLowerCase();
|
||||
if (action !== "start" && action !== "stop" && action !== "restart") {
|
||||
res.status(404).json({ error: "Runtime service action not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const project = await svc.getById(id);
|
||||
if (!project) {
|
||||
res.status(404).json({ error: "Project not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, project.companyId);
|
||||
|
||||
const workspace = project.workspaces.find((entry) => entry.id === workspaceId) ?? null;
|
||||
if (!workspace) {
|
||||
res.status(404).json({ error: "Project workspace not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceCwd = workspace.cwd;
|
||||
if (!workspaceCwd) {
|
||||
res.status(422).json({ error: "Project workspace needs a local path before Paperclip can manage local runtime services" });
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeConfig = workspace.runtimeConfig?.workspaceRuntime ?? null;
|
||||
if ((action === "start" || action === "restart") && !runtimeConfig) {
|
||||
res.status(422).json({ error: "Project workspace has no runtime service configuration" });
|
||||
return;
|
||||
}
|
||||
|
||||
const actor = getActorInfo(req);
|
||||
const recorder = workspaceOperations.createRecorder({ companyId: project.companyId });
|
||||
let runtimeServiceCount = workspace.runtimeServices?.length ?? 0;
|
||||
const stdout: string[] = [];
|
||||
const stderr: string[] = [];
|
||||
|
||||
const operation = await recorder.recordOperation({
|
||||
phase: action === "stop" ? "workspace_teardown" : "workspace_provision",
|
||||
command: `workspace runtime ${action}`,
|
||||
cwd: workspace.cwd,
|
||||
metadata: {
|
||||
action,
|
||||
projectId: project.id,
|
||||
projectWorkspaceId: workspace.id,
|
||||
},
|
||||
run: async () => {
|
||||
const onLog = async (stream: "stdout" | "stderr", chunk: string) => {
|
||||
if (stream === "stdout") stdout.push(chunk);
|
||||
else stderr.push(chunk);
|
||||
};
|
||||
|
||||
if (action === "stop" || action === "restart") {
|
||||
await stopRuntimeServicesForProjectWorkspace({
|
||||
db,
|
||||
projectWorkspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (action === "start" || action === "restart") {
|
||||
const startedServices = await startRuntimeServicesForWorkspaceControl({
|
||||
db,
|
||||
actor: {
|
||||
id: actor.agentId ?? null,
|
||||
name: actor.actorType === "user" ? "Board" : "Agent",
|
||||
companyId: project.companyId,
|
||||
},
|
||||
issue: null,
|
||||
workspace: {
|
||||
baseCwd: workspaceCwd,
|
||||
source: "project_primary",
|
||||
projectId: project.id,
|
||||
workspaceId: workspace.id,
|
||||
repoUrl: workspace.repoUrl,
|
||||
repoRef: workspace.repoRef,
|
||||
strategy: "project_primary",
|
||||
cwd: workspaceCwd,
|
||||
branchName: workspace.defaultRef ?? workspace.repoRef ?? null,
|
||||
worktreePath: null,
|
||||
warnings: [],
|
||||
created: false,
|
||||
},
|
||||
config: { workspaceRuntime: runtimeConfig },
|
||||
adapterEnv: {},
|
||||
onLog,
|
||||
});
|
||||
runtimeServiceCount = startedServices.length;
|
||||
} else {
|
||||
runtimeServiceCount = 0;
|
||||
}
|
||||
|
||||
await svc.updateWorkspace(project.id, workspace.id, {
|
||||
runtimeConfig: {
|
||||
desiredState: action === "stop" ? "stopped" : "running",
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
status: "succeeded",
|
||||
stdout: stdout.join(""),
|
||||
stderr: stderr.join(""),
|
||||
system:
|
||||
action === "stop"
|
||||
? "Stopped project workspace runtime services.\n"
|
||||
: action === "restart"
|
||||
? "Restarted project workspace runtime services.\n"
|
||||
: "Started project workspace runtime services.\n",
|
||||
metadata: {
|
||||
runtimeServiceCount,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const updatedWorkspace = (await svc.listWorkspaces(project.id)).find((entry) => entry.id === workspace.id) ?? workspace;
|
||||
|
||||
await logActivity(db, {
|
||||
companyId: project.companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
action: `project.workspace_runtime_${action}`,
|
||||
entityType: "project",
|
||||
entityId: project.id,
|
||||
details: {
|
||||
projectWorkspaceId: workspace.id,
|
||||
runtimeServiceCount,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
workspace: updatedWorkspace,
|
||||
operation,
|
||||
});
|
||||
});
|
||||
|
||||
router.delete("/projects/:id/workspaces/:workspaceId", async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const workspaceId = req.params.workspaceId as string;
|
||||
|
|
|
|||
|
|
@ -187,6 +187,7 @@ export function readExecutionWorkspaceConfig(metadata: Record<string, unknown> |
|
|||
teardownCommand: readNullableString(raw.teardownCommand),
|
||||
cleanupCommand: readNullableString(raw.cleanupCommand),
|
||||
workspaceRuntime: cloneRecord(raw.workspaceRuntime),
|
||||
desiredState: raw.desiredState === "running" || raw.desiredState === "stopped" ? raw.desiredState : null,
|
||||
};
|
||||
|
||||
const hasConfig = Object.values(config).some((value) => {
|
||||
|
|
@ -208,6 +209,7 @@ export function mergeExecutionWorkspaceConfig(
|
|||
teardownCommand: null,
|
||||
cleanupCommand: null,
|
||||
workspaceRuntime: null,
|
||||
desiredState: null,
|
||||
};
|
||||
|
||||
if (patch === null) {
|
||||
|
|
@ -220,6 +222,12 @@ export function mergeExecutionWorkspaceConfig(
|
|||
teardownCommand: patch.teardownCommand !== undefined ? readNullableString(patch.teardownCommand) : current.teardownCommand,
|
||||
cleanupCommand: patch.cleanupCommand !== undefined ? readNullableString(patch.cleanupCommand) : current.cleanupCommand,
|
||||
workspaceRuntime: patch.workspaceRuntime !== undefined ? cloneRecord(patch.workspaceRuntime) : current.workspaceRuntime,
|
||||
desiredState:
|
||||
patch.desiredState !== undefined
|
||||
? patch.desiredState === "running" || patch.desiredState === "stopped"
|
||||
? patch.desiredState
|
||||
: null
|
||||
: current.desiredState,
|
||||
};
|
||||
|
||||
const hasConfig = Object.values(nextConfig).some((value) => {
|
||||
|
|
@ -234,6 +242,7 @@ export function mergeExecutionWorkspaceConfig(
|
|||
teardownCommand: nextConfig.teardownCommand,
|
||||
cleanupCommand: nextConfig.cleanupCommand,
|
||||
workspaceRuntime: nextConfig.workspaceRuntime,
|
||||
desiredState: nextConfig.desiredState,
|
||||
};
|
||||
} else {
|
||||
delete nextMetadata.config;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import {
|
|||
resolveExecutionWorkspaceMode,
|
||||
} from "./execution-workspace-policy.js";
|
||||
import { instanceSettingsService } from "./instance-settings.js";
|
||||
import { readProjectWorkspaceRuntimeConfig } from "./project-workspace-runtime-config.js";
|
||||
import { redactCurrentUserText, redactCurrentUserValue } from "../log-redaction.js";
|
||||
import {
|
||||
hasSessionCompactionThresholds,
|
||||
|
|
@ -79,21 +80,22 @@ const SESSIONED_LOCAL_ADAPTERS = new Set([
|
|||
function applyPersistedExecutionWorkspaceConfig(input: {
|
||||
config: Record<string, unknown>;
|
||||
workspaceConfig: ExecutionWorkspaceConfig | null;
|
||||
projectWorkspaceRuntime: Record<string, unknown> | null;
|
||||
mode: ReturnType<typeof resolveExecutionWorkspaceMode>;
|
||||
}) {
|
||||
if (!input.workspaceConfig) return input.config;
|
||||
|
||||
const nextConfig = { ...input.config };
|
||||
|
||||
if (input.mode !== "agent_default") {
|
||||
if (input.workspaceConfig.workspaceRuntime === null) {
|
||||
if (input.workspaceConfig?.workspaceRuntime === null) {
|
||||
delete nextConfig.workspaceRuntime;
|
||||
} else {
|
||||
} else if (input.workspaceConfig?.workspaceRuntime) {
|
||||
nextConfig.workspaceRuntime = { ...input.workspaceConfig.workspaceRuntime };
|
||||
} else if (input.projectWorkspaceRuntime) {
|
||||
nextConfig.workspaceRuntime = { ...input.projectWorkspaceRuntime };
|
||||
}
|
||||
}
|
||||
|
||||
if (input.mode === "isolated_workspace") {
|
||||
if (input.workspaceConfig && input.mode === "isolated_workspace") {
|
||||
const nextStrategy = parseObject(nextConfig.workspaceStrategy);
|
||||
if (input.workspaceConfig.provisionCommand === null) delete nextStrategy.provisionCommand;
|
||||
else nextStrategy.provisionCommand = input.workspaceConfig.provisionCommand;
|
||||
|
|
@ -2112,14 +2114,32 @@ export function heartbeatService(db: Db) {
|
|||
: null;
|
||||
const existingExecutionWorkspace =
|
||||
issueRef?.executionWorkspaceId ? await executionWorkspacesSvc.getById(issueRef.executionWorkspaceId) : null;
|
||||
const resolvedProjectWorkspace =
|
||||
resolvedWorkspace.workspaceId
|
||||
? await db
|
||||
.select({ metadata: projectWorkspaces.metadata })
|
||||
.from(projectWorkspaces)
|
||||
.where(
|
||||
and(
|
||||
eq(projectWorkspaces.id, resolvedWorkspace.workspaceId),
|
||||
eq(projectWorkspaces.companyId, agent.companyId),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows[0] ?? null)
|
||||
: null;
|
||||
const projectWorkspaceRuntimeConfig = readProjectWorkspaceRuntimeConfig(
|
||||
(resolvedProjectWorkspace?.metadata as Record<string, unknown> | null) ?? null,
|
||||
);
|
||||
const persistedWorkspaceManagedConfig = applyPersistedExecutionWorkspaceConfig({
|
||||
config: workspaceManagedConfig,
|
||||
workspaceConfig: existingExecutionWorkspace?.config ?? null,
|
||||
projectWorkspaceRuntime: projectWorkspaceRuntimeConfig?.workspaceRuntime ?? null,
|
||||
mode: executionWorkspaceMode,
|
||||
});
|
||||
const mergedConfig = issueAssigneeOverrides?.adapterConfig
|
||||
? { ...persistedWorkspaceManagedConfig, ...issueAssigneeOverrides.adapterConfig }
|
||||
: persistedWorkspaceManagedConfig;
|
||||
const configSnapshot = buildExecutionWorkspaceConfigSnapshot(mergedConfig);
|
||||
const { config: resolvedConfig, secretKeys } = await secretsSvc.resolveAdapterConfigForRuntime(
|
||||
agent.companyId,
|
||||
mergedConfig,
|
||||
|
|
@ -2129,7 +2149,6 @@ export function heartbeatService(db: Db) {
|
|||
...resolvedConfig,
|
||||
paperclipRuntimeSkills: runtimeSkillEntries,
|
||||
};
|
||||
const configSnapshot = buildExecutionWorkspaceConfigSnapshot(resolvedConfig);
|
||||
const workspaceOperationRecorder = workspaceOperationsSvc.createRecorder({
|
||||
companyId: agent.companyId,
|
||||
heartbeatRunId: run.id,
|
||||
|
|
|
|||
|
|
@ -28,5 +28,5 @@ export { workProductService } from "./work-products.js";
|
|||
export { logActivity, type LogActivityInput } from "./activity-log.js";
|
||||
export { notifyHireApproved, type NotifyHireApprovedInput } from "./hire-hook.js";
|
||||
export { publishLiveEvent, subscribeCompanyLiveEvents } from "./live-events.js";
|
||||
export { reconcilePersistedRuntimeServicesOnStartup } from "./workspace-runtime.js";
|
||||
export { reconcilePersistedRuntimeServicesOnStartup, restartDesiredRuntimeServicesOnStartup } from "./workspace-runtime.js";
|
||||
export { createStorageServiceFromConfig, getStorageService } from "../storage/index.js";
|
||||
|
|
|
|||
59
server/src/services/project-workspace-runtime-config.ts
Normal file
59
server/src/services/project-workspace-runtime-config.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import type { ProjectWorkspaceRuntimeConfig } from "@paperclipai/shared";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function cloneRecord(value: unknown): Record<string, unknown> | null {
|
||||
return isRecord(value) ? { ...value } : null;
|
||||
}
|
||||
|
||||
function readDesiredState(value: unknown): ProjectWorkspaceRuntimeConfig["desiredState"] {
|
||||
return value === "running" || value === "stopped" ? value : null;
|
||||
}
|
||||
|
||||
export function readProjectWorkspaceRuntimeConfig(
|
||||
metadata: Record<string, unknown> | null | undefined,
|
||||
): ProjectWorkspaceRuntimeConfig | null {
|
||||
const raw = isRecord(metadata?.runtimeConfig) ? metadata.runtimeConfig : null;
|
||||
if (!raw) return null;
|
||||
|
||||
const config: ProjectWorkspaceRuntimeConfig = {
|
||||
workspaceRuntime: cloneRecord(raw.workspaceRuntime),
|
||||
desiredState: readDesiredState(raw.desiredState),
|
||||
};
|
||||
|
||||
const hasConfig = config.workspaceRuntime !== null || config.desiredState !== null;
|
||||
return hasConfig ? config : null;
|
||||
}
|
||||
|
||||
export function mergeProjectWorkspaceRuntimeConfig(
|
||||
metadata: Record<string, unknown> | null | undefined,
|
||||
patch: Partial<ProjectWorkspaceRuntimeConfig> | null,
|
||||
): Record<string, unknown> | null {
|
||||
const nextMetadata = isRecord(metadata) ? { ...metadata } : {};
|
||||
const current = readProjectWorkspaceRuntimeConfig(metadata) ?? {
|
||||
workspaceRuntime: null,
|
||||
desiredState: null,
|
||||
};
|
||||
|
||||
if (patch === null) {
|
||||
delete nextMetadata.runtimeConfig;
|
||||
return Object.keys(nextMetadata).length > 0 ? nextMetadata : null;
|
||||
}
|
||||
|
||||
const nextConfig: ProjectWorkspaceRuntimeConfig = {
|
||||
workspaceRuntime:
|
||||
patch.workspaceRuntime !== undefined ? cloneRecord(patch.workspaceRuntime) : current.workspaceRuntime,
|
||||
desiredState:
|
||||
patch.desiredState !== undefined ? readDesiredState(patch.desiredState) : current.desiredState,
|
||||
};
|
||||
|
||||
if (nextConfig.workspaceRuntime === null && nextConfig.desiredState === null) {
|
||||
delete nextMetadata.runtimeConfig;
|
||||
} else {
|
||||
nextMetadata.runtimeConfig = nextConfig;
|
||||
}
|
||||
|
||||
return Object.keys(nextMetadata).length > 0 ? nextMetadata : null;
|
||||
}
|
||||
|
|
@ -9,11 +9,13 @@ import {
|
|||
type ProjectCodebase,
|
||||
type ProjectExecutionWorkspacePolicy,
|
||||
type ProjectGoalRef,
|
||||
type ProjectWorkspaceRuntimeConfig,
|
||||
type ProjectWorkspace,
|
||||
type WorkspaceRuntimeService,
|
||||
} from "@paperclipai/shared";
|
||||
import { listWorkspaceRuntimeServicesForProjectWorkspaces } from "./workspace-runtime.js";
|
||||
import { parseProjectExecutionWorkspacePolicy } from "./execution-workspace-policy.js";
|
||||
import { mergeProjectWorkspaceRuntimeConfig, readProjectWorkspaceRuntimeConfig } from "./project-workspace-runtime-config.js";
|
||||
import { resolveManagedProjectWorkspaceDir } from "../home-paths.js";
|
||||
|
||||
type ProjectRow = typeof projects.$inferSelect;
|
||||
|
|
@ -34,6 +36,7 @@ type CreateWorkspaceInput = {
|
|||
remoteWorkspaceRef?: string | null;
|
||||
sharedWorkspaceKey?: string | null;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
runtimeConfig?: Partial<ProjectWorkspaceRuntimeConfig> | null;
|
||||
isPrimary?: boolean;
|
||||
};
|
||||
type UpdateWorkspaceInput = Partial<CreateWorkspaceInput>;
|
||||
|
|
@ -149,6 +152,7 @@ function toWorkspace(
|
|||
remoteWorkspaceRef: row.remoteWorkspaceRef ?? null,
|
||||
sharedWorkspaceKey: row.sharedWorkspaceKey ?? null,
|
||||
metadata: (row.metadata as Record<string, unknown> | null) ?? null,
|
||||
runtimeConfig: readProjectWorkspaceRuntimeConfig((row.metadata as Record<string, unknown> | null) ?? null),
|
||||
isPrimary: row.isPrimary,
|
||||
runtimeServices,
|
||||
createdAt: row.createdAt,
|
||||
|
|
@ -611,7 +615,13 @@ export function projectService(db: Db) {
|
|||
remoteProvider: readNonEmptyString(data.remoteProvider),
|
||||
remoteWorkspaceRef,
|
||||
sharedWorkspaceKey: readNonEmptyString(data.sharedWorkspaceKey),
|
||||
metadata: (data.metadata as Record<string, unknown> | null | undefined) ?? null,
|
||||
metadata:
|
||||
data.runtimeConfig !== undefined
|
||||
? mergeProjectWorkspaceRuntimeConfig(
|
||||
(data.metadata as Record<string, unknown> | null | undefined) ?? null,
|
||||
data.runtimeConfig ?? null,
|
||||
)
|
||||
: (data.metadata as Record<string, unknown> | null | undefined) ?? null,
|
||||
isPrimary: shouldBePrimary,
|
||||
})
|
||||
.returning()
|
||||
|
|
@ -681,7 +691,17 @@ export function projectService(db: Db) {
|
|||
if (data.remoteProvider !== undefined) patch.remoteProvider = readNonEmptyString(data.remoteProvider);
|
||||
if (data.remoteWorkspaceRef !== undefined) patch.remoteWorkspaceRef = nextRemoteWorkspaceRef;
|
||||
if (data.sharedWorkspaceKey !== undefined) patch.sharedWorkspaceKey = readNonEmptyString(data.sharedWorkspaceKey);
|
||||
if (data.metadata !== undefined) patch.metadata = data.metadata;
|
||||
if (data.metadata !== undefined || data.runtimeConfig !== undefined) {
|
||||
patch.metadata =
|
||||
data.runtimeConfig !== undefined
|
||||
? mergeProjectWorkspaceRuntimeConfig(
|
||||
data.metadata !== undefined
|
||||
? (data.metadata as Record<string, unknown> | null | undefined)
|
||||
: ((existing.metadata as Record<string, unknown> | null | undefined) ?? null),
|
||||
data.runtimeConfig ?? null,
|
||||
)
|
||||
: data.metadata;
|
||||
}
|
||||
|
||||
const updated = await db.transaction(async (tx) => {
|
||||
if (data.isPrimary === true) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import path from "node:path";
|
|||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import type { AdapterRuntimeServiceReport } from "@paperclipai/adapter-utils";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { workspaceRuntimeServices } from "@paperclipai/db";
|
||||
import { executionWorkspaces, projectWorkspaces, workspaceRuntimeServices } from "@paperclipai/db";
|
||||
import { and, desc, eq, inArray } from "drizzle-orm";
|
||||
import { asNumber, asString, parseObject, renderTemplate } from "../adapters/utils.js";
|
||||
import { resolveHomeAwarePath } from "../home-paths.js";
|
||||
|
|
@ -21,6 +21,8 @@ import {
|
|||
writeLocalServiceRegistryRecord,
|
||||
} from "./local-service-supervisor.js";
|
||||
import type { WorkspaceOperationRecorder } from "./workspace-operations.js";
|
||||
import { readExecutionWorkspaceConfig } from "./execution-workspaces.js";
|
||||
import { readProjectWorkspaceRuntimeConfig } from "./project-workspace-runtime-config.js";
|
||||
|
||||
export interface ExecutionWorkspaceInput {
|
||||
baseCwd: string;
|
||||
|
|
@ -38,7 +40,7 @@ export interface ExecutionWorkspaceIssueRef {
|
|||
}
|
||||
|
||||
export interface ExecutionWorkspaceAgentRef {
|
||||
id: string;
|
||||
id: string | null;
|
||||
name: string;
|
||||
companyId: string;
|
||||
}
|
||||
|
|
@ -211,7 +213,7 @@ function renderWorkspaceTemplate(template: string, input: {
|
|||
title: input.issue?.title ?? "",
|
||||
},
|
||||
agent: {
|
||||
id: input.agent.id,
|
||||
id: input.agent.id ?? "",
|
||||
name: input.agent.name,
|
||||
},
|
||||
project: {
|
||||
|
|
@ -334,7 +336,7 @@ function buildWorkspaceCommandEnv(input: {
|
|||
env.PAPERCLIP_WORKSPACE_CREATED = input.created ? "true" : "false";
|
||||
env.PAPERCLIP_PROJECT_ID = input.base.projectId ?? "";
|
||||
env.PAPERCLIP_PROJECT_WORKSPACE_ID = input.base.workspaceId ?? "";
|
||||
env.PAPERCLIP_AGENT_ID = input.agent.id;
|
||||
env.PAPERCLIP_AGENT_ID = input.agent.id ?? "";
|
||||
env.PAPERCLIP_AGENT_NAME = input.agent.name;
|
||||
env.PAPERCLIP_COMPANY_ID = input.agent.companyId;
|
||||
env.PAPERCLIP_ISSUE_ID = input.issue?.id ?? "";
|
||||
|
|
@ -903,7 +905,7 @@ function buildTemplateData(input: {
|
|||
title: input.issue?.title ?? "",
|
||||
},
|
||||
agent: {
|
||||
id: input.agent.id,
|
||||
id: input.agent.id ?? "",
|
||||
name: input.agent.name,
|
||||
},
|
||||
port: input.port ?? "",
|
||||
|
|
@ -1091,7 +1093,7 @@ export function normalizeAdapterManagedRuntimeServices(input: {
|
|||
url: report.url ?? null,
|
||||
provider: "adapter_managed",
|
||||
providerRef: report.providerRef ?? null,
|
||||
ownerAgentId: report.ownerAgentId ?? input.agent.id,
|
||||
ownerAgentId: report.ownerAgentId ?? input.agent.id ?? null,
|
||||
startedByRunId: input.runId,
|
||||
lastUsedAt: nowIso,
|
||||
startedAt: nowIso,
|
||||
|
|
@ -1203,7 +1205,7 @@ async function startLocalRuntimeService(input: {
|
|||
url: adoptedRecord.url ?? url,
|
||||
provider: "local_process",
|
||||
providerRef: String(adoptedRecord.pid),
|
||||
ownerAgentId: input.agent.id,
|
||||
ownerAgentId: input.agent.id ?? null,
|
||||
startedByRunId: input.runId,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
startedAt: adoptedRecord.startedAt,
|
||||
|
|
@ -1277,7 +1279,7 @@ async function startLocalRuntimeService(input: {
|
|||
url,
|
||||
provider: "local_process",
|
||||
providerRef: child.pid ? String(child.pid) : null,
|
||||
ownerAgentId: input.agent.id,
|
||||
ownerAgentId: input.agent.id ?? null,
|
||||
startedByRunId: input.runId,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
startedAt: new Date().toISOString(),
|
||||
|
|
@ -1345,7 +1347,10 @@ async function stopRuntimeService(serviceId: string) {
|
|||
record.lastUsedAt = new Date().toISOString();
|
||||
record.stoppedAt = new Date().toISOString();
|
||||
if (record.child && record.child.pid) {
|
||||
terminateChildProcess(record.child);
|
||||
await terminateLocalService({
|
||||
pid: record.child.pid,
|
||||
processGroupId: record.processGroupId ?? record.child.pid,
|
||||
});
|
||||
} else if (record.providerRef) {
|
||||
const pid = Number.parseInt(record.providerRef, 10);
|
||||
if (Number.isInteger(pid) && pid > 0) {
|
||||
|
|
@ -1409,6 +1414,13 @@ function registerRuntimeService(db: Db | undefined, record: RuntimeServiceRecord
|
|||
});
|
||||
}
|
||||
|
||||
function readRuntimeServiceEntries(config: Record<string, unknown>) {
|
||||
const runtime = parseObject(config.workspaceRuntime);
|
||||
return Array.isArray(runtime.services)
|
||||
? runtime.services.filter((entry): entry is Record<string, unknown> => typeof entry === "object" && entry !== null)
|
||||
: [];
|
||||
}
|
||||
|
||||
export async function ensureRuntimeServicesForRun(input: {
|
||||
db?: Db;
|
||||
runId: string;
|
||||
|
|
@ -1420,10 +1432,7 @@ export async function ensureRuntimeServicesForRun(input: {
|
|||
adapterEnv: Record<string, string>;
|
||||
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
||||
}): Promise<RuntimeServiceRef[]> {
|
||||
const runtime = parseObject(input.config.workspaceRuntime);
|
||||
const rawServices = Array.isArray(runtime.services)
|
||||
? runtime.services.filter((entry): entry is Record<string, unknown> => typeof entry === "object" && entry !== null)
|
||||
: [];
|
||||
const rawServices = readRuntimeServiceEntries(input.config);
|
||||
const acquiredServiceIds: string[] = [];
|
||||
const refs: RuntimeServiceRef[] = [];
|
||||
runtimeServiceLeasesByRun.set(input.runId, acquiredServiceIds);
|
||||
|
|
@ -1493,6 +1502,79 @@ export async function ensureRuntimeServicesForRun(input: {
|
|||
return refs;
|
||||
}
|
||||
|
||||
export async function startRuntimeServicesForWorkspaceControl(input: {
|
||||
db?: Db;
|
||||
invocationId?: string;
|
||||
actor: ExecutionWorkspaceAgentRef;
|
||||
issue: ExecutionWorkspaceIssueRef | null;
|
||||
workspace: RealizedExecutionWorkspace;
|
||||
executionWorkspaceId?: string | null;
|
||||
config: Record<string, unknown>;
|
||||
adapterEnv: Record<string, string>;
|
||||
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
||||
}): Promise<RuntimeServiceRef[]> {
|
||||
const rawServices = readRuntimeServiceEntries(input.config);
|
||||
const refs: RuntimeServiceRef[] = [];
|
||||
const invocationId = input.invocationId ?? randomUUID();
|
||||
|
||||
for (const service of rawServices) {
|
||||
const lifecycle = asString(service.lifecycle, "shared") === "ephemeral" ? "ephemeral" : "shared";
|
||||
const { scopeType, scopeId } = resolveServiceScopeId({
|
||||
service,
|
||||
workspace: input.workspace,
|
||||
executionWorkspaceId: input.executionWorkspaceId,
|
||||
issue: input.issue,
|
||||
runId: invocationId,
|
||||
agent: input.actor,
|
||||
});
|
||||
const envConfig = parseObject(service.env);
|
||||
const envFingerprint = createHash("sha256").update(stableStringify(envConfig)).digest("hex");
|
||||
const serviceName = asString(service.name, "service");
|
||||
const reuseKey =
|
||||
lifecycle === "shared"
|
||||
? [scopeType, scopeId ?? "", serviceName, envFingerprint].join(":")
|
||||
: null;
|
||||
|
||||
if (reuseKey) {
|
||||
const existingId = runtimeServicesByReuseKey.get(reuseKey);
|
||||
const existing = existingId ? runtimeServicesById.get(existingId) : null;
|
||||
if (existing && existing.status === "running") {
|
||||
existing.lastUsedAt = new Date().toISOString();
|
||||
existing.stoppedAt = null;
|
||||
clearIdleTimer(existing);
|
||||
void touchLocalServiceRegistryRecord(existing.serviceKey, {
|
||||
runtimeServiceId: existing.id,
|
||||
lastSeenAt: existing.lastUsedAt,
|
||||
});
|
||||
await persistRuntimeServiceRecord(input.db, existing);
|
||||
refs.push(toRuntimeServiceRef(existing, { reused: true }));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const record = await startLocalRuntimeService({
|
||||
db: input.db,
|
||||
runId: invocationId,
|
||||
agent: input.actor,
|
||||
issue: input.issue,
|
||||
workspace: input.workspace,
|
||||
executionWorkspaceId: input.executionWorkspaceId,
|
||||
adapterEnv: input.adapterEnv,
|
||||
service,
|
||||
onLog: input.onLog,
|
||||
reuseKey,
|
||||
scopeType,
|
||||
scopeId,
|
||||
});
|
||||
record.startedByRunId = null;
|
||||
registerRuntimeService(input.db, record);
|
||||
await persistRuntimeServiceRecord(input.db, record);
|
||||
refs.push(toRuntimeServiceRef(record));
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
export async function releaseRuntimeServicesForRun(runId: string) {
|
||||
const acquired = runtimeServiceLeasesByRun.get(runId) ?? [];
|
||||
runtimeServiceLeasesByRun.delete(runId);
|
||||
|
|
@ -1543,6 +1625,39 @@ export async function stopRuntimeServicesForExecutionWorkspace(input: {
|
|||
}
|
||||
}
|
||||
|
||||
export async function stopRuntimeServicesForProjectWorkspace(input: {
|
||||
db?: Db;
|
||||
projectWorkspaceId: string;
|
||||
}) {
|
||||
const matchingServiceIds = Array.from(runtimeServicesById.values())
|
||||
.filter((record) => record.projectWorkspaceId === input.projectWorkspaceId && record.scopeType === "project_workspace")
|
||||
.map((record) => record.id);
|
||||
|
||||
for (const serviceId of matchingServiceIds) {
|
||||
await stopRuntimeService(serviceId);
|
||||
}
|
||||
|
||||
if (input.db) {
|
||||
const now = new Date();
|
||||
await input.db
|
||||
.update(workspaceRuntimeServices)
|
||||
.set({
|
||||
status: "stopped",
|
||||
healthStatus: "unknown",
|
||||
stoppedAt: now,
|
||||
lastUsedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceRuntimeServices.projectWorkspaceId, input.projectWorkspaceId),
|
||||
eq(workspaceRuntimeServices.scopeType, "project_workspace"),
|
||||
inArray(workspaceRuntimeServices.status, ["starting", "running"]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function listWorkspaceRuntimeServicesForProjectWorkspaces(
|
||||
db: Db,
|
||||
companyId: string,
|
||||
|
|
@ -1556,6 +1671,7 @@ export async function listWorkspaceRuntimeServicesForProjectWorkspaces(
|
|||
and(
|
||||
eq(workspaceRuntimeServices.companyId, companyId),
|
||||
inArray(workspaceRuntimeServices.projectWorkspaceId, projectWorkspaceIds),
|
||||
eq(workspaceRuntimeServices.scopeType, "project_workspace"),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(workspaceRuntimeServices.updatedAt), desc(workspaceRuntimeServices.createdAt));
|
||||
|
|
@ -1661,6 +1777,93 @@ export async function reconcilePersistedRuntimeServicesOnStartup(db: Db) {
|
|||
return { reconciled: rows.length, adopted, stopped };
|
||||
}
|
||||
|
||||
export async function restartDesiredRuntimeServicesOnStartup(db: Db) {
|
||||
let restarted = 0;
|
||||
let failed = 0;
|
||||
|
||||
const projectWorkspaceRows = await db
|
||||
.select()
|
||||
.from(projectWorkspaces);
|
||||
|
||||
for (const row of projectWorkspaceRows) {
|
||||
const runtimeConfig = readProjectWorkspaceRuntimeConfig((row.metadata as Record<string, unknown> | null) ?? null);
|
||||
if (runtimeConfig?.desiredState !== "running" || !runtimeConfig.workspaceRuntime || !row.cwd) continue;
|
||||
|
||||
try {
|
||||
const refs = await startRuntimeServicesForWorkspaceControl({
|
||||
db,
|
||||
actor: { id: null, name: "Paperclip", companyId: row.companyId },
|
||||
issue: null,
|
||||
workspace: {
|
||||
baseCwd: row.cwd,
|
||||
source: "project_primary",
|
||||
projectId: row.projectId,
|
||||
workspaceId: row.id,
|
||||
repoUrl: row.repoUrl ?? null,
|
||||
repoRef: row.repoRef ?? null,
|
||||
strategy: "project_primary",
|
||||
cwd: row.cwd,
|
||||
branchName: row.defaultRef ?? row.repoRef ?? null,
|
||||
worktreePath: null,
|
||||
warnings: [],
|
||||
created: false,
|
||||
},
|
||||
config: { workspaceRuntime: runtimeConfig.workspaceRuntime },
|
||||
adapterEnv: {},
|
||||
});
|
||||
if (refs.length > 0) restarted += refs.filter((ref) => !ref.reused).length;
|
||||
} catch {
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const executionWorkspaceRows = await db
|
||||
.select()
|
||||
.from(executionWorkspaces)
|
||||
.where(inArray(executionWorkspaces.status, ["active", "idle", "in_review", "cleanup_failed"]));
|
||||
|
||||
for (const row of executionWorkspaceRows) {
|
||||
const config = readExecutionWorkspaceConfig((row.metadata as Record<string, unknown> | null) ?? null);
|
||||
if (config?.desiredState !== "running" || !config.workspaceRuntime || !row.cwd) continue;
|
||||
|
||||
try {
|
||||
const refs = await startRuntimeServicesForWorkspaceControl({
|
||||
db,
|
||||
actor: { id: null, name: "Paperclip", companyId: row.companyId },
|
||||
issue: row.sourceIssueId
|
||||
? {
|
||||
id: row.sourceIssueId,
|
||||
identifier: null,
|
||||
title: row.name,
|
||||
}
|
||||
: null,
|
||||
workspace: {
|
||||
baseCwd: row.cwd,
|
||||
source: row.mode === "shared_workspace" ? "project_primary" : "task_session",
|
||||
projectId: row.projectId,
|
||||
workspaceId: row.projectWorkspaceId ?? null,
|
||||
repoUrl: row.repoUrl ?? null,
|
||||
repoRef: row.baseRef ?? null,
|
||||
strategy: row.strategyType === "git_worktree" ? "git_worktree" : "project_primary",
|
||||
cwd: row.cwd,
|
||||
branchName: row.branchName ?? null,
|
||||
worktreePath: row.strategyType === "git_worktree" ? row.cwd : null,
|
||||
warnings: [],
|
||||
created: false,
|
||||
},
|
||||
executionWorkspaceId: row.id,
|
||||
config: { workspaceRuntime: config.workspaceRuntime },
|
||||
adapterEnv: {},
|
||||
});
|
||||
if (refs.length > 0) restarted += refs.filter((ref) => !ref.reused).length;
|
||||
} catch {
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { restarted, failed };
|
||||
}
|
||||
|
||||
export async function persistAdapterManagedRuntimeServices(input: {
|
||||
db: Db;
|
||||
adapterType: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue