diff --git a/server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts b/server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts index 31bf37ff..771eaccc 100644 --- a/server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts +++ b/server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts @@ -818,6 +818,67 @@ describe("agent issue mutation checkout ownership", () => { 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 () => { const app = await createApp(boardActor()); diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index e600266e..48df2ac7 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -984,6 +984,42 @@ export function issueRoutes( return resolveActorSourceTrustForIssue({ db, issue, actor }); } + function hasExplicitIssueWorkspaceCreateSelection(input: Record) { + 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, + ): Promise { + 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 + : null; + if (!context || !readNonEmptyString(context.executionWorkspaceId)) return null; + const paperclipIssue = context.paperclipIssue && typeof context.paperclipIssue === "object" + ? context.paperclipIssue as Record + : null; + return readNonEmptyString(context.issueId) ?? readNonEmptyString(paperclipIssue?.id); + } + async function resolveAgentTrustForIssue( input: { agentId: string | null | undefined; @@ -4205,9 +4241,16 @@ export function issueRoutes( companyId, req.body.assigneeAgentId as string | null | undefined, ); + const actor = getActorInfo(req); + const runWorkspaceInheritanceSourceIssueId = hasExplicitIssueWorkspaceCreateSelection(req.body) + ? null + : await resolveRunIssueWorkspaceInheritanceSource(companyId, actor); const createBody = { ...req.body, ...(normalizedAssigneeAgentId !== undefined ? { assigneeAgentId: normalizedAssigneeAgentId } : {}), + ...(runWorkspaceInheritanceSourceIssueId + ? { inheritExecutionWorkspaceFromIssueId: runWorkspaceInheritanceSourceIssueId } + : {}), }; if (!(await assertCheapRecoveryIssueAssigneeProfileAllowed(req, res, { companyId }, createBody))) return; if (req.body.assigneeAgentId || req.body.assigneeUserId) { @@ -4224,7 +4267,6 @@ export function issueRoutes( } await assertIssueEnvironmentSelection(companyId, createBody.executionWorkspaceSettings?.environmentId); - const actor = getActorInfo(req); const executionPolicy = applyActorMonitorScheduledBy( normalizeIssueExecutionPolicy(createBody.executionPolicy), actor.actorType,