Merge public-gh/master into paperclip-company-import-export
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { execFile as execFileCallback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { and, asc, desc, eq, gt, inArray, sql } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import type { BillingType } from "@paperclipai/shared";
|
||||
@@ -26,22 +28,28 @@ import { costService } from "./costs.js";
|
||||
import { companySkillService } from "./company-skills.js";
|
||||
import { budgetService, type BudgetEnforcementScope } from "./budgets.js";
|
||||
import { secretService } from "./secrets.js";
|
||||
import { resolveDefaultAgentWorkspaceDir } from "../home-paths.js";
|
||||
import { resolveDefaultAgentWorkspaceDir, resolveManagedProjectWorkspaceDir } from "../home-paths.js";
|
||||
import { summarizeHeartbeatRunResultJson } from "./heartbeat-run-summary.js";
|
||||
import {
|
||||
buildWorkspaceReadyComment,
|
||||
cleanupExecutionWorkspaceArtifacts,
|
||||
ensureRuntimeServicesForRun,
|
||||
persistAdapterManagedRuntimeServices,
|
||||
realizeExecutionWorkspace,
|
||||
releaseRuntimeServicesForRun,
|
||||
sanitizeRuntimeServiceBaseEnv,
|
||||
} from "./workspace-runtime.js";
|
||||
import { issueService } from "./issues.js";
|
||||
import { executionWorkspaceService } from "./execution-workspaces.js";
|
||||
import { workspaceOperationService } from "./workspace-operations.js";
|
||||
import {
|
||||
buildExecutionWorkspaceAdapterConfig,
|
||||
gateProjectExecutionWorkspacePolicy,
|
||||
parseIssueExecutionWorkspaceSettings,
|
||||
parseProjectExecutionWorkspacePolicy,
|
||||
resolveExecutionWorkspaceMode,
|
||||
} from "./execution-workspace-policy.js";
|
||||
import { instanceSettingsService } from "./instance-settings.js";
|
||||
import { redactCurrentUserText, redactCurrentUserValue } from "../log-redaction.js";
|
||||
import {
|
||||
hasSessionCompactionThresholds,
|
||||
@@ -55,6 +63,80 @@ const HEARTBEAT_MAX_CONCURRENT_RUNS_MAX = 10;
|
||||
const DEFERRED_WAKE_CONTEXT_KEY = "_paperclipWakeContext";
|
||||
const startLocksByAgent = new Map<string, Promise<void>>();
|
||||
const REPO_ONLY_CWD_SENTINEL = "/__paperclip_repo_only__";
|
||||
const MANAGED_WORKSPACE_GIT_CLONE_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
const execFile = promisify(execFileCallback);
|
||||
const SESSIONED_LOCAL_ADAPTERS = new Set([
|
||||
"claude_local",
|
||||
"codex_local",
|
||||
"cursor",
|
||||
"gemini_local",
|
||||
"opencode_local",
|
||||
"pi_local",
|
||||
]);
|
||||
|
||||
function deriveRepoNameFromRepoUrl(repoUrl: string | null): string | null {
|
||||
const trimmed = repoUrl?.trim() ?? "";
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
const cleanedPath = parsed.pathname.replace(/\/+$/, "");
|
||||
const repoName = cleanedPath.split("/").filter(Boolean).pop()?.replace(/\.git$/i, "") ?? "";
|
||||
return repoName || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureManagedProjectWorkspace(input: {
|
||||
companyId: string;
|
||||
projectId: string;
|
||||
repoUrl: string | null;
|
||||
}): Promise<{ cwd: string; warning: string | null }> {
|
||||
const cwd = resolveManagedProjectWorkspaceDir({
|
||||
companyId: input.companyId,
|
||||
projectId: input.projectId,
|
||||
repoName: deriveRepoNameFromRepoUrl(input.repoUrl),
|
||||
});
|
||||
await fs.mkdir(path.dirname(cwd), { recursive: true });
|
||||
const stats = await fs.stat(cwd).catch(() => null);
|
||||
|
||||
if (!input.repoUrl) {
|
||||
if (!stats) {
|
||||
await fs.mkdir(cwd, { recursive: true });
|
||||
}
|
||||
return { cwd, warning: null };
|
||||
}
|
||||
|
||||
const gitDirExists = await fs
|
||||
.stat(path.resolve(cwd, ".git"))
|
||||
.then((entry) => entry.isDirectory())
|
||||
.catch(() => false);
|
||||
if (gitDirExists) {
|
||||
return { cwd, warning: null };
|
||||
}
|
||||
|
||||
if (stats) {
|
||||
const entries = await fs.readdir(cwd).catch(() => []);
|
||||
if (entries.length > 0) {
|
||||
return {
|
||||
cwd,
|
||||
warning: `Managed workspace path "${cwd}" already exists but is not a git checkout. Using it as-is.`,
|
||||
};
|
||||
}
|
||||
await fs.rm(cwd, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
try {
|
||||
await execFile("git", ["clone", input.repoUrl, cwd], {
|
||||
env: sanitizeRuntimeServiceBaseEnv(process.env),
|
||||
timeout: MANAGED_WORKSPACE_GIT_CLONE_TIMEOUT_MS,
|
||||
});
|
||||
return { cwd, warning: null };
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to prepare managed checkout for "${input.repoUrl}" at "${cwd}": ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
const heartbeatRunListColumns = {
|
||||
id: heartbeatRuns.id,
|
||||
@@ -159,6 +241,20 @@ export type ResolvedWorkspaceForRun = {
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
type ProjectWorkspaceCandidate = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export function prioritizeProjectWorkspaceCandidatesForRun<T extends ProjectWorkspaceCandidate>(
|
||||
rows: T[],
|
||||
preferredWorkspaceId: string | null | undefined,
|
||||
): T[] {
|
||||
if (!preferredWorkspaceId) return rows;
|
||||
const preferredIndex = rows.findIndex((row) => row.id === preferredWorkspaceId);
|
||||
if (preferredIndex <= 0) return rows;
|
||||
return [rows[preferredIndex]!, ...rows.slice(0, preferredIndex), ...rows.slice(preferredIndex + 1)];
|
||||
}
|
||||
|
||||
function readNonEmptyString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
||||
}
|
||||
@@ -592,10 +688,14 @@ function resolveNextSessionState(input: {
|
||||
}
|
||||
|
||||
export function heartbeatService(db: Db) {
|
||||
const instanceSettings = instanceSettingsService(db);
|
||||
|
||||
const runLogStore = getRunLogStore();
|
||||
const secretsSvc = secretService(db);
|
||||
const companySkills = companySkillService(db);
|
||||
const issuesSvc = issueService(db);
|
||||
const executionWorkspacesSvc = executionWorkspaceService(db);
|
||||
const workspaceOperationsSvc = workspaceOperationService(db);
|
||||
const activeRunExecutions = new Set<string>();
|
||||
const budgetHooks = {
|
||||
cancelWorkForScope: cancelBudgetScopeWork,
|
||||
@@ -850,18 +950,25 @@ export function heartbeatService(db: Db) {
|
||||
): Promise<ResolvedWorkspaceForRun> {
|
||||
const issueId = readNonEmptyString(context.issueId);
|
||||
const contextProjectId = readNonEmptyString(context.projectId);
|
||||
const issueProjectId = issueId
|
||||
const contextProjectWorkspaceId = readNonEmptyString(context.projectWorkspaceId);
|
||||
const issueProjectRef = issueId
|
||||
? await db
|
||||
.select({ projectId: issues.projectId })
|
||||
.select({
|
||||
projectId: issues.projectId,
|
||||
projectWorkspaceId: issues.projectWorkspaceId,
|
||||
})
|
||||
.from(issues)
|
||||
.where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)))
|
||||
.then((rows) => rows[0]?.projectId ?? null)
|
||||
.then((rows) => rows[0] ?? null)
|
||||
: null;
|
||||
const issueProjectId = issueProjectRef?.projectId ?? null;
|
||||
const preferredProjectWorkspaceId =
|
||||
issueProjectRef?.projectWorkspaceId ?? contextProjectWorkspaceId ?? null;
|
||||
const resolvedProjectId = issueProjectId ?? contextProjectId;
|
||||
const useProjectWorkspace = opts?.useProjectWorkspace !== false;
|
||||
const workspaceProjectId = useProjectWorkspace ? resolvedProjectId : null;
|
||||
|
||||
const projectWorkspaceRows = workspaceProjectId
|
||||
const unorderedProjectWorkspaceRows = workspaceProjectId
|
||||
? await db
|
||||
.select()
|
||||
.from(projectWorkspaces)
|
||||
@@ -873,6 +980,10 @@ export function heartbeatService(db: Db) {
|
||||
)
|
||||
.orderBy(asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id))
|
||||
: [];
|
||||
const projectWorkspaceRows = prioritizeProjectWorkspaceCandidatesForRun(
|
||||
unorderedProjectWorkspaceRows,
|
||||
preferredProjectWorkspaceId,
|
||||
);
|
||||
|
||||
const workspaceHints = projectWorkspaceRows.map((workspace) => ({
|
||||
workspaceId: workspace.id,
|
||||
@@ -882,12 +993,34 @@ export function heartbeatService(db: Db) {
|
||||
}));
|
||||
|
||||
if (projectWorkspaceRows.length > 0) {
|
||||
const preferredWorkspace = preferredProjectWorkspaceId
|
||||
? projectWorkspaceRows.find((workspace) => workspace.id === preferredProjectWorkspaceId) ?? null
|
||||
: null;
|
||||
const missingProjectCwds: string[] = [];
|
||||
let hasConfiguredProjectCwd = false;
|
||||
let preferredWorkspaceWarning: string | null = null;
|
||||
if (preferredProjectWorkspaceId && !preferredWorkspace) {
|
||||
preferredWorkspaceWarning =
|
||||
`Selected project workspace "${preferredProjectWorkspaceId}" is not available on this project.`;
|
||||
}
|
||||
for (const workspace of projectWorkspaceRows) {
|
||||
const projectCwd = readNonEmptyString(workspace.cwd);
|
||||
let projectCwd = readNonEmptyString(workspace.cwd);
|
||||
let managedWorkspaceWarning: string | null = null;
|
||||
if (!projectCwd || projectCwd === REPO_ONLY_CWD_SENTINEL) {
|
||||
continue;
|
||||
try {
|
||||
const managedWorkspace = await ensureManagedProjectWorkspace({
|
||||
companyId: agent.companyId,
|
||||
projectId: workspaceProjectId ?? resolvedProjectId ?? workspace.projectId,
|
||||
repoUrl: readNonEmptyString(workspace.repoUrl),
|
||||
});
|
||||
projectCwd = managedWorkspace.cwd;
|
||||
managedWorkspaceWarning = managedWorkspace.warning;
|
||||
} catch (error) {
|
||||
if (preferredWorkspace?.id === workspace.id) {
|
||||
preferredWorkspaceWarning = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
hasConfiguredProjectCwd = true;
|
||||
const projectCwdExists = await fs
|
||||
@@ -903,15 +1036,24 @@ export function heartbeatService(db: Db) {
|
||||
repoUrl: workspace.repoUrl,
|
||||
repoRef: workspace.repoRef,
|
||||
workspaceHints,
|
||||
warnings: [],
|
||||
warnings: [preferredWorkspaceWarning, managedWorkspaceWarning].filter(
|
||||
(value): value is string => Boolean(value),
|
||||
),
|
||||
};
|
||||
}
|
||||
if (preferredWorkspace?.id === workspace.id) {
|
||||
preferredWorkspaceWarning =
|
||||
`Selected project workspace path "${projectCwd}" is not available yet.`;
|
||||
}
|
||||
missingProjectCwds.push(projectCwd);
|
||||
}
|
||||
|
||||
const fallbackCwd = resolveDefaultAgentWorkspaceDir(agent.id);
|
||||
await fs.mkdir(fallbackCwd, { recursive: true });
|
||||
const warnings: string[] = [];
|
||||
if (preferredWorkspaceWarning) {
|
||||
warnings.push(preferredWorkspaceWarning);
|
||||
}
|
||||
if (missingProjectCwds.length > 0) {
|
||||
const firstMissing = missingProjectCwds[0];
|
||||
const extraMissingCount = Math.max(0, missingProjectCwds.length - 1);
|
||||
@@ -937,6 +1079,24 @@ export function heartbeatService(db: Db) {
|
||||
};
|
||||
}
|
||||
|
||||
if (workspaceProjectId) {
|
||||
const managedWorkspace = await ensureManagedProjectWorkspace({
|
||||
companyId: agent.companyId,
|
||||
projectId: workspaceProjectId,
|
||||
repoUrl: null,
|
||||
});
|
||||
return {
|
||||
cwd: managedWorkspace.cwd,
|
||||
source: "project_primary" as const,
|
||||
projectId: resolvedProjectId,
|
||||
workspaceId: null,
|
||||
repoUrl: null,
|
||||
repoRef: null,
|
||||
workspaceHints,
|
||||
warnings: managedWorkspace.warning ? [managedWorkspace.warning] : [],
|
||||
};
|
||||
}
|
||||
|
||||
const sessionCwd = readNonEmptyString(previousSessionParams?.cwd);
|
||||
if (sessionCwd) {
|
||||
const sessionCwdExists = await fs
|
||||
@@ -1475,10 +1635,16 @@ export function heartbeatService(db: Db) {
|
||||
const taskKey = deriveTaskKey(context, null);
|
||||
const sessionCodec = getAdapterSessionCodec(agent.adapterType);
|
||||
const issueId = readNonEmptyString(context.issueId);
|
||||
const issueAssigneeConfig = issueId
|
||||
const issueContext = issueId
|
||||
? await db
|
||||
.select({
|
||||
id: issues.id,
|
||||
identifier: issues.identifier,
|
||||
title: issues.title,
|
||||
projectId: issues.projectId,
|
||||
projectWorkspaceId: issues.projectWorkspaceId,
|
||||
executionWorkspaceId: issues.executionWorkspaceId,
|
||||
executionWorkspacePreference: issues.executionWorkspacePreference,
|
||||
assigneeAgentId: issues.assigneeAgentId,
|
||||
assigneeAdapterOverrides: issues.assigneeAdapterOverrides,
|
||||
executionWorkspaceSettings: issues.executionWorkspaceSettings,
|
||||
@@ -1488,22 +1654,27 @@ export function heartbeatService(db: Db) {
|
||||
.then((rows) => rows[0] ?? null)
|
||||
: null;
|
||||
const issueAssigneeOverrides =
|
||||
issueAssigneeConfig && issueAssigneeConfig.assigneeAgentId === agent.id
|
||||
issueContext && issueContext.assigneeAgentId === agent.id
|
||||
? parseIssueAssigneeAdapterOverrides(
|
||||
issueAssigneeConfig.assigneeAdapterOverrides,
|
||||
issueContext.assigneeAdapterOverrides,
|
||||
)
|
||||
: null;
|
||||
const issueExecutionWorkspaceSettings = parseIssueExecutionWorkspaceSettings(
|
||||
issueAssigneeConfig?.executionWorkspaceSettings,
|
||||
);
|
||||
const isolatedWorkspacesEnabled = (await instanceSettings.getExperimental()).enableIsolatedWorkspaces;
|
||||
const issueExecutionWorkspaceSettings = isolatedWorkspacesEnabled
|
||||
? parseIssueExecutionWorkspaceSettings(issueContext?.executionWorkspaceSettings)
|
||||
: null;
|
||||
const contextProjectId = readNonEmptyString(context.projectId);
|
||||
const executionProjectId = issueAssigneeConfig?.projectId ?? contextProjectId;
|
||||
const executionProjectId = issueContext?.projectId ?? contextProjectId;
|
||||
const projectExecutionWorkspacePolicy = executionProjectId
|
||||
? await db
|
||||
.select({ executionWorkspacePolicy: projects.executionWorkspacePolicy })
|
||||
.from(projects)
|
||||
.where(and(eq(projects.id, executionProjectId), eq(projects.companyId, agent.companyId)))
|
||||
.then((rows) => parseProjectExecutionWorkspacePolicy(rows[0]?.executionWorkspacePolicy))
|
||||
.then((rows) =>
|
||||
gateProjectExecutionWorkspacePolicy(
|
||||
parseProjectExecutionWorkspacePolicy(rows[0]?.executionWorkspacePolicy),
|
||||
isolatedWorkspacesEnabled,
|
||||
))
|
||||
: null;
|
||||
const taskSession = taskKey
|
||||
? await getTaskSession(agent.companyId, agent.id, agent.adapterType, taskKey)
|
||||
@@ -1545,17 +1716,24 @@ export function heartbeatService(db: Db) {
|
||||
...resolvedConfig,
|
||||
paperclipRuntimeSkills: runtimeSkillEntries,
|
||||
};
|
||||
const issueRef = issueId
|
||||
? await db
|
||||
.select({
|
||||
id: issues.id,
|
||||
identifier: issues.identifier,
|
||||
title: issues.title,
|
||||
})
|
||||
.from(issues)
|
||||
.where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)))
|
||||
.then((rows) => rows[0] ?? null)
|
||||
const issueRef = issueContext
|
||||
? {
|
||||
id: issueContext.id,
|
||||
identifier: issueContext.identifier,
|
||||
title: issueContext.title,
|
||||
projectId: issueContext.projectId,
|
||||
projectWorkspaceId: issueContext.projectWorkspaceId,
|
||||
executionWorkspaceId: issueContext.executionWorkspaceId,
|
||||
executionWorkspacePreference: issueContext.executionWorkspacePreference,
|
||||
}
|
||||
: null;
|
||||
const existingExecutionWorkspace =
|
||||
issueRef?.executionWorkspaceId ? await executionWorkspacesSvc.getById(issueRef.executionWorkspaceId) : null;
|
||||
const workspaceOperationRecorder = workspaceOperationsSvc.createRecorder({
|
||||
companyId: agent.companyId,
|
||||
heartbeatRunId: run.id,
|
||||
executionWorkspaceId: existingExecutionWorkspace?.id ?? null,
|
||||
});
|
||||
const executionWorkspace = await realizeExecutionWorkspace({
|
||||
base: {
|
||||
baseCwd: resolvedWorkspace.cwd,
|
||||
@@ -1565,14 +1743,139 @@ export function heartbeatService(db: Db) {
|
||||
repoUrl: resolvedWorkspace.repoUrl,
|
||||
repoRef: resolvedWorkspace.repoRef,
|
||||
},
|
||||
config: resolvedConfig,
|
||||
config: runtimeConfig,
|
||||
issue: issueRef,
|
||||
agent: {
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
companyId: agent.companyId,
|
||||
},
|
||||
recorder: workspaceOperationRecorder,
|
||||
});
|
||||
const resolvedProjectId = executionWorkspace.projectId ?? issueRef?.projectId ?? executionProjectId ?? null;
|
||||
const resolvedProjectWorkspaceId = issueRef?.projectWorkspaceId ?? resolvedWorkspace.workspaceId ?? null;
|
||||
const shouldReuseExisting =
|
||||
issueRef?.executionWorkspacePreference === "reuse_existing" &&
|
||||
existingExecutionWorkspace &&
|
||||
existingExecutionWorkspace.status !== "archived";
|
||||
let persistedExecutionWorkspace = null;
|
||||
try {
|
||||
persistedExecutionWorkspace = shouldReuseExisting && existingExecutionWorkspace
|
||||
? await executionWorkspacesSvc.update(existingExecutionWorkspace.id, {
|
||||
cwd: executionWorkspace.cwd,
|
||||
repoUrl: executionWorkspace.repoUrl,
|
||||
baseRef: executionWorkspace.repoRef,
|
||||
branchName: executionWorkspace.branchName,
|
||||
providerType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "local_fs",
|
||||
providerRef: executionWorkspace.worktreePath,
|
||||
status: "active",
|
||||
lastUsedAt: new Date(),
|
||||
metadata: {
|
||||
...(existingExecutionWorkspace.metadata ?? {}),
|
||||
source: executionWorkspace.source,
|
||||
createdByRuntime: executionWorkspace.created,
|
||||
},
|
||||
})
|
||||
: resolvedProjectId
|
||||
? await executionWorkspacesSvc.create({
|
||||
companyId: agent.companyId,
|
||||
projectId: resolvedProjectId,
|
||||
projectWorkspaceId: resolvedProjectWorkspaceId,
|
||||
sourceIssueId: issueRef?.id ?? null,
|
||||
mode:
|
||||
executionWorkspaceMode === "isolated_workspace"
|
||||
? "isolated_workspace"
|
||||
: executionWorkspaceMode === "operator_branch"
|
||||
? "operator_branch"
|
||||
: executionWorkspaceMode === "agent_default"
|
||||
? "adapter_managed"
|
||||
: "shared_workspace",
|
||||
strategyType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "project_primary",
|
||||
name: executionWorkspace.branchName ?? issueRef?.identifier ?? `workspace-${agent.id.slice(0, 8)}`,
|
||||
status: "active",
|
||||
cwd: executionWorkspace.cwd,
|
||||
repoUrl: executionWorkspace.repoUrl,
|
||||
baseRef: executionWorkspace.repoRef,
|
||||
branchName: executionWorkspace.branchName,
|
||||
providerType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "local_fs",
|
||||
providerRef: executionWorkspace.worktreePath,
|
||||
lastUsedAt: new Date(),
|
||||
openedAt: new Date(),
|
||||
metadata: {
|
||||
source: executionWorkspace.source,
|
||||
createdByRuntime: executionWorkspace.created,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
} catch (error) {
|
||||
if (executionWorkspace.created) {
|
||||
try {
|
||||
await cleanupExecutionWorkspaceArtifacts({
|
||||
workspace: {
|
||||
id: existingExecutionWorkspace?.id ?? `transient-${run.id}`,
|
||||
cwd: executionWorkspace.cwd,
|
||||
providerType: executionWorkspace.strategy === "git_worktree" ? "git_worktree" : "local_fs",
|
||||
providerRef: executionWorkspace.worktreePath,
|
||||
branchName: executionWorkspace.branchName,
|
||||
repoUrl: executionWorkspace.repoUrl,
|
||||
baseRef: executionWorkspace.repoRef,
|
||||
projectId: resolvedProjectId,
|
||||
projectWorkspaceId: resolvedProjectWorkspaceId,
|
||||
sourceIssueId: issueRef?.id ?? null,
|
||||
metadata: {
|
||||
createdByRuntime: true,
|
||||
source: executionWorkspace.source,
|
||||
},
|
||||
},
|
||||
projectWorkspace: {
|
||||
cwd: resolvedWorkspace.cwd,
|
||||
cleanupCommand: null,
|
||||
},
|
||||
teardownCommand: projectExecutionWorkspacePolicy?.workspaceStrategy?.teardownCommand ?? null,
|
||||
recorder: workspaceOperationRecorder,
|
||||
});
|
||||
} catch (cleanupError) {
|
||||
logger.warn(
|
||||
{
|
||||
runId: run.id,
|
||||
issueId,
|
||||
executionWorkspaceCwd: executionWorkspace.cwd,
|
||||
cleanupError: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
|
||||
},
|
||||
"Failed to cleanup realized execution workspace after persistence failure",
|
||||
);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await workspaceOperationRecorder.attachExecutionWorkspaceId(persistedExecutionWorkspace?.id ?? null);
|
||||
if (
|
||||
existingExecutionWorkspace &&
|
||||
persistedExecutionWorkspace &&
|
||||
existingExecutionWorkspace.id !== persistedExecutionWorkspace.id &&
|
||||
existingExecutionWorkspace.status === "active"
|
||||
) {
|
||||
await executionWorkspacesSvc.update(existingExecutionWorkspace.id, {
|
||||
status: "idle",
|
||||
cleanupReason: null,
|
||||
});
|
||||
}
|
||||
if (issueId && persistedExecutionWorkspace && issueRef?.executionWorkspaceId !== persistedExecutionWorkspace.id) {
|
||||
await issuesSvc.update(issueId, {
|
||||
executionWorkspaceId: persistedExecutionWorkspace.id,
|
||||
...(resolvedProjectWorkspaceId ? { projectWorkspaceId: resolvedProjectWorkspaceId } : {}),
|
||||
});
|
||||
}
|
||||
if (persistedExecutionWorkspace) {
|
||||
context.executionWorkspaceId = persistedExecutionWorkspace.id;
|
||||
await db
|
||||
.update(heartbeatRuns)
|
||||
.set({
|
||||
contextSnapshot: context,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(heartbeatRuns.id, run.id));
|
||||
}
|
||||
const runtimeSessionResolution = resolveRuntimeSessionParamsForWorkspace({
|
||||
agentId: agent.id,
|
||||
previousSessionParams,
|
||||
@@ -1776,6 +2079,7 @@ export function heartbeatService(db: Db) {
|
||||
},
|
||||
issue: issueRef,
|
||||
workspace: executionWorkspace,
|
||||
executionWorkspaceId: persistedExecutionWorkspace?.id ?? issueRef?.executionWorkspaceId ?? null,
|
||||
config: resolvedConfig,
|
||||
adapterEnv,
|
||||
onLog,
|
||||
|
||||
Reference in New Issue
Block a user