diff --git a/packages/shared/src/validators/execution-workspace.ts b/packages/shared/src/validators/execution-workspace.ts index d761c528..9914d74e 100644 --- a/packages/shared/src/validators/execution-workspace.ts +++ b/packages/shared/src/validators/execution-workspace.ts @@ -62,6 +62,36 @@ export const executionWorkspaceCloseGitReadinessSchema = z.object({ createdByRuntime: z.boolean(), }).strict(); +export const workspaceRuntimeServiceSchema = z.object({ + id: z.string(), + companyId: z.string().uuid(), + projectId: z.string().uuid().nullable(), + projectWorkspaceId: z.string().uuid().nullable(), + executionWorkspaceId: z.string().uuid().nullable(), + issueId: z.string().uuid().nullable(), + scopeType: z.enum(["project_workspace", "execution_workspace", "run", "agent"]), + scopeId: z.string().nullable(), + serviceName: z.string(), + status: z.enum(["starting", "running", "stopped", "failed"]), + lifecycle: z.enum(["shared", "ephemeral"]), + reuseKey: z.string().nullable(), + command: z.string().nullable(), + cwd: z.string().nullable(), + port: z.number().int().nullable(), + url: z.string().nullable(), + provider: z.enum(["local_process", "adapter_managed"]), + providerRef: z.string().nullable(), + ownerAgentId: z.string().uuid().nullable(), + startedByRunId: z.string().uuid().nullable(), + lastUsedAt: z.coerce.date(), + startedAt: z.coerce.date(), + stoppedAt: z.coerce.date().nullable(), + stopPolicy: z.record(z.unknown()).nullable(), + healthStatus: z.enum(["unknown", "healthy", "unhealthy"]), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), +}).strict(); + export const executionWorkspaceCloseReadinessSchema = z.object({ workspaceId: z.string().uuid(), state: executionWorkspaceCloseReadinessStateSchema, @@ -73,35 +103,7 @@ export const executionWorkspaceCloseReadinessSchema = z.object({ isSharedWorkspace: z.boolean(), isProjectPrimaryWorkspace: z.boolean(), git: executionWorkspaceCloseGitReadinessSchema.nullable(), - runtimeServices: z.array(z.object({ - id: z.string(), - companyId: z.string().uuid(), - projectId: z.string().uuid().nullable(), - projectWorkspaceId: z.string().uuid().nullable(), - executionWorkspaceId: z.string().uuid().nullable(), - issueId: z.string().uuid().nullable(), - scopeType: z.enum(["project_workspace", "execution_workspace", "run", "agent"]), - scopeId: z.string().nullable(), - serviceName: z.string(), - status: z.enum(["starting", "running", "stopped", "failed"]), - lifecycle: z.enum(["shared", "ephemeral"]), - reuseKey: z.string().nullable(), - command: z.string().nullable(), - cwd: z.string().nullable(), - port: z.number().int().nullable(), - url: z.string().nullable(), - provider: z.enum(["local_process", "adapter_managed"]), - providerRef: z.string().nullable(), - ownerAgentId: z.string().uuid().nullable(), - startedByRunId: z.string().uuid().nullable(), - lastUsedAt: z.coerce.date(), - startedAt: z.coerce.date(), - stoppedAt: z.coerce.date().nullable(), - stopPolicy: z.record(z.unknown()).nullable(), - healthStatus: z.enum(["unknown", "healthy", "unhealthy"]), - createdAt: z.coerce.date(), - updatedAt: z.coerce.date(), - }).strict()), + runtimeServices: z.array(workspaceRuntimeServiceSchema), }).strict(); export const updateExecutionWorkspaceSchema = z.object({ diff --git a/server/src/services/workspace-runtime.ts b/server/src/services/workspace-runtime.ts index 3d1ec43f..dfece576 100644 --- a/server/src/services/workspace-runtime.ts +++ b/server/src/services/workspace-runtime.ts @@ -1192,6 +1192,8 @@ export function normalizeAdapterManagedRuntimeServices(input: { async function startLocalRuntimeService(input: { db?: Db; runId: string; + leaseRunId?: string | null; + startedByRunId?: string | null; agent: ExecutionWorkspaceAgentRef; issue: ExecutionWorkspaceIssueRef | null; workspace: RealizedExecutionWorkspace; @@ -1203,6 +1205,8 @@ async function startLocalRuntimeService(input: { scopeType: "project_workspace" | "execution_workspace" | "run" | "agent"; scopeId: string | null; }): Promise { + const leaseRunId = input.leaseRunId === undefined ? input.runId : input.leaseRunId; + const startedByRunId = input.startedByRunId === undefined ? input.runId : input.startedByRunId; const identity = resolveRuntimeServiceReuseIdentity({ service: input.service, workspace: input.workspace, @@ -1299,7 +1303,7 @@ async function startLocalRuntimeService(input: { provider: "local_process", providerRef: String(adoptedRecord.pid), ownerAgentId: input.agent.id ?? null, - startedByRunId: input.runId, + startedByRunId, lastUsedAt: new Date().toISOString(), startedAt: adoptedRecord.startedAt, stoppedAt: null, @@ -1308,7 +1312,7 @@ async function startLocalRuntimeService(input: { reused: true, db: input.db, child: null, - leaseRunIds: new Set([input.runId]), + leaseRunIds: leaseRunId ? new Set([leaseRunId]) : new Set(), idleTimer: null, envFingerprint, serviceKey, @@ -1373,7 +1377,7 @@ async function startLocalRuntimeService(input: { provider: "local_process", providerRef: child.pid ? String(child.pid) : null, ownerAgentId: input.agent.id ?? null, - startedByRunId: input.runId, + startedByRunId, lastUsedAt: new Date().toISOString(), startedAt: new Date().toISOString(), stoppedAt: null, @@ -1382,7 +1386,7 @@ async function startLocalRuntimeService(input: { reused: false, db: input.db, child, - leaseRunIds: new Set([input.runId]), + leaseRunIds: leaseRunId ? new Set([leaseRunId]) : new Set(), idleTimer: null, envFingerprint, serviceKey, @@ -1648,9 +1652,13 @@ export async function startRuntimeServicesForWorkspaceControl(input: { } } + // Manually controlled services are not tied to a heartbeat run lifecycle, so they do not + // retain a run lease and never persist a startedByRunId foreign key. const record = await startLocalRuntimeService({ db: input.db, runId: invocationId, + leaseRunId: null, + startedByRunId: null, agent: input.actor, issue: input.issue, workspace: input.workspace, @@ -1662,7 +1670,6 @@ export async function startRuntimeServicesForWorkspaceControl(input: { scopeType, scopeId, }); - record.startedByRunId = null; registerRuntimeService(input.db, record); await persistRuntimeServiceRecord(input.db, record); refs.push(toRuntimeServiceRef(record)); diff --git a/ui/src/components/ExecutionWorkspaceCloseDialog.tsx b/ui/src/components/ExecutionWorkspaceCloseDialog.tsx index 3b6aef8a..f0547684 100644 --- a/ui/src/components/ExecutionWorkspaceCloseDialog.tsx +++ b/ui/src/components/ExecutionWorkspaceCloseDialog.tsx @@ -149,8 +149,8 @@ export function ExecutionWorkspaceCloseDialog({

Blocking reasons