Keep agent-created follow-ups in run workspace

Reviewed and merged for PAP-10871/PAP-10873.\n\nVerification:\n- pnpm vitest run server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts\n- git diff --check origin/master...HEAD\n- GitHub PR checks green before merge
This commit is contained in:
Dotta
2026-06-10 21:20:51 -05:00
committed by GitHub
parent b8fb81dee9
commit 11a64819f9
2 changed files with 104 additions and 1 deletions
@@ -818,6 +818,67 @@ describe("agent issue mutation checkout ownership", () => {
expect(mockIssueService.update).not.toHaveBeenCalled(); expect(mockIssueService.update).not.toHaveBeenCalled();
}); });
it("defaults agent-created root follow-up issues to inherit the current run workspace", async () => {
const app = await createApp(
ownerActor(),
createRunContextDb({
issueId,
executionWorkspaceId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
}),
);
const res = await request(app)
.post(`/api/companies/${companyId}/issues`)
.send({
title: "Follow-up in same worktree",
projectId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
});
expect(res.status, JSON.stringify(res.body)).toBe(201);
expect(mockIssueService.create).toHaveBeenCalledWith(
companyId,
expect.objectContaining({
title: "Follow-up in same worktree",
inheritExecutionWorkspaceFromIssueId: issueId,
}),
);
});
it("preserves explicit workspace choices on agent-created root issues", async () => {
const app = await createApp(
ownerActor(),
createRunContextDb({
issueId,
executionWorkspaceId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
}),
);
const explicitExecutionWorkspaceId = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
const res = await request(app)
.post(`/api/companies/${companyId}/issues`)
.send({
title: "Explicit different workspace",
executionWorkspaceId: explicitExecutionWorkspaceId,
executionWorkspacePreference: "reuse_existing",
});
expect(res.status, JSON.stringify(res.body)).toBe(201);
expect(mockIssueService.create).toHaveBeenCalledWith(
companyId,
expect.objectContaining({
title: "Explicit different workspace",
executionWorkspaceId: explicitExecutionWorkspaceId,
executionWorkspacePreference: "reuse_existing",
}),
);
expect(mockIssueService.create).toHaveBeenCalledWith(
companyId,
expect.not.objectContaining({
inheritExecutionWorkspaceFromIssueId: issueId,
}),
);
});
it("allows board users to set explicit cheap issue assignee profile overrides", async () => { it("allows board users to set explicit cheap issue assignee profile overrides", async () => {
const app = await createApp(boardActor()); const app = await createApp(boardActor());
+43 -1
View File
@@ -984,6 +984,42 @@ export function issueRoutes(
return resolveActorSourceTrustForIssue({ db, issue, actor }); return resolveActorSourceTrustForIssue({ db, issue, actor });
} }
function hasExplicitIssueWorkspaceCreateSelection(input: Record<string, unknown>) {
return input.parentId !== undefined ||
input.inheritExecutionWorkspaceFromIssueId !== undefined ||
input.projectWorkspaceId !== undefined ||
input.executionWorkspaceId !== undefined ||
input.executionWorkspacePreference !== undefined ||
input.executionWorkspaceSettings !== undefined;
}
async function resolveRunIssueWorkspaceInheritanceSource(
companyId: string,
actor: ReturnType<typeof getActorInfo>,
): Promise<string | null> {
if (actor.actorType !== "agent" || !actor.agentId || !actor.runId) return null;
const run = await db
.select({
agentId: heartbeatRuns.agentId,
contextSnapshot: heartbeatRuns.contextSnapshot,
})
.from(heartbeatRuns)
.where(and(
eq(heartbeatRuns.id, actor.runId),
eq(heartbeatRuns.companyId, companyId),
))
.then((rows) => rows[0] ?? null);
if (!run || run.agentId !== actor.agentId) return null;
const context = run.contextSnapshot && typeof run.contextSnapshot === "object"
? run.contextSnapshot as Record<string, unknown>
: null;
if (!context || !readNonEmptyString(context.executionWorkspaceId)) return null;
const paperclipIssue = context.paperclipIssue && typeof context.paperclipIssue === "object"
? context.paperclipIssue as Record<string, unknown>
: null;
return readNonEmptyString(context.issueId) ?? readNonEmptyString(paperclipIssue?.id);
}
async function resolveAgentTrustForIssue( async function resolveAgentTrustForIssue(
input: { input: {
agentId: string | null | undefined; agentId: string | null | undefined;
@@ -4205,9 +4241,16 @@ export function issueRoutes(
companyId, companyId,
req.body.assigneeAgentId as string | null | undefined, req.body.assigneeAgentId as string | null | undefined,
); );
const actor = getActorInfo(req);
const runWorkspaceInheritanceSourceIssueId = hasExplicitIssueWorkspaceCreateSelection(req.body)
? null
: await resolveRunIssueWorkspaceInheritanceSource(companyId, actor);
const createBody = { const createBody = {
...req.body, ...req.body,
...(normalizedAssigneeAgentId !== undefined ? { assigneeAgentId: normalizedAssigneeAgentId } : {}), ...(normalizedAssigneeAgentId !== undefined ? { assigneeAgentId: normalizedAssigneeAgentId } : {}),
...(runWorkspaceInheritanceSourceIssueId
? { inheritExecutionWorkspaceFromIssueId: runWorkspaceInheritanceSourceIssueId }
: {}),
}; };
if (!(await assertCheapRecoveryIssueAssigneeProfileAllowed(req, res, { companyId }, createBody))) return; if (!(await assertCheapRecoveryIssueAssigneeProfileAllowed(req, res, { companyId }, createBody))) return;
if (req.body.assigneeAgentId || req.body.assigneeUserId) { if (req.body.assigneeAgentId || req.body.assigneeUserId) {
@@ -4224,7 +4267,6 @@ export function issueRoutes(
} }
await assertIssueEnvironmentSelection(companyId, createBody.executionWorkspaceSettings?.environmentId); await assertIssueEnvironmentSelection(companyId, createBody.executionWorkspaceSettings?.environmentId);
const actor = getActorInfo(req);
const executionPolicy = applyActorMonitorScheduledBy( const executionPolicy = applyActorMonitorScheduledBy(
normalizeIssueExecutionPolicy(createBody.executionPolicy), normalizeIssueExecutionPolicy(createBody.executionPolicy),
actor.actorType, actor.actorType,