mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Keep manual routine runs visible in the runner inbox (#4615)
## Thinking Path > - Paperclip coordinates recurring agent work through scheduled and manual routines. > - Manual routine runs are board-initiated work and should stay visible to the human who kicked them off. > - Routine execution issues are agent-assigned, so they can be filtered away from a board user's inbox unless the user is recorded as touching the work. > - Coalesced or skipped active routine runs have the same visibility problem because they reuse an existing live issue. > - This pull request carries the manual runner actor into routine dispatch and touches the linked issue for that user's inbox. > - The benefit is that manually triggered routine work stays discoverable by the operator who started it. ## What Changed - Passed the board or agent actor from the routine run route into the routine service. - Recorded manual board runners as `createdByUserId` on fresh routine execution issues. - Touched coalesced or skipped active routine issues for the manual runner by updating read state and clearing that user's inbox archive. - Added route and service regressions for manual routine run actor propagation and inbox visibility. ## Verification - `pnpm exec vitest run server/src/__tests__/routines-routes.test.ts server/src/__tests__/routines-service.test.ts` ## Risks - Low risk: the change is scoped to manual routine runs and only updates issue attribution/read-state metadata for the initiating actor. - No migrations. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex coding agent based on GPT-5, tool-enabled local repository and shell access, Paperclip heartbeat context. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
68c37660f0
commit
f88f538e6d
4 changed files with 282 additions and 2 deletions
|
|
@ -281,6 +281,29 @@ describe("routine routes", () => {
|
||||||
expect(mockRoutineService.runRoutine).not.toHaveBeenCalled();
|
expect(mockRoutineService.runRoutine).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes the board actor through when manually running a routine", async () => {
|
||||||
|
mockAccessService.canUser.mockResolvedValue(true);
|
||||||
|
const app = await createApp({
|
||||||
|
type: "board",
|
||||||
|
userId: "board-user",
|
||||||
|
source: "session",
|
||||||
|
isInstanceAdmin: false,
|
||||||
|
companyIds: [companyId],
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post(`/api/routines/${routineId}/run`)
|
||||||
|
.send({});
|
||||||
|
|
||||||
|
expect(res.status).toBe(202);
|
||||||
|
expect(mockRoutineService.runRoutine).toHaveBeenCalledWith(routineId, {
|
||||||
|
source: "manual",
|
||||||
|
}, {
|
||||||
|
agentId: null,
|
||||||
|
userId: "board-user",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("allows routine creation when the board user has tasks:assign", async () => {
|
it("allows routine creation when the board user has tasks:assign", async () => {
|
||||||
mockAccessService.canUser.mockResolvedValue(true);
|
mockAccessService.canUser.mockResolvedValue(true);
|
||||||
const app = await createApp({
|
const app = await createApp({
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import {
|
||||||
executionWorkspaces,
|
executionWorkspaces,
|
||||||
heartbeatRuns,
|
heartbeatRuns,
|
||||||
instanceSettings,
|
instanceSettings,
|
||||||
|
issueInboxArchives,
|
||||||
|
issueReadStates,
|
||||||
issues,
|
issues,
|
||||||
projectWorkspaces,
|
projectWorkspaces,
|
||||||
projects,
|
projects,
|
||||||
|
|
@ -46,6 +48,8 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await db.delete(activityLog);
|
await db.delete(activityLog);
|
||||||
|
await db.delete(issueInboxArchives);
|
||||||
|
await db.delete(issueReadStates);
|
||||||
await db.delete(routineRuns);
|
await db.delete(routineRuns);
|
||||||
await db.delete(routineTriggers);
|
await db.delete(routineTriggers);
|
||||||
await db.delete(routines);
|
await db.delete(routines);
|
||||||
|
|
@ -269,6 +273,36 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("records the manual board runner on fresh routine issues so they appear in that user's inbox", async () => {
|
||||||
|
const { companyId, agentId, issueSvc, routine, svc } = await seedFixture();
|
||||||
|
const userId = randomUUID();
|
||||||
|
|
||||||
|
const run = await svc.runRoutine(routine.id, { source: "manual" }, { userId });
|
||||||
|
|
||||||
|
expect(run.status).toBe("issue_created");
|
||||||
|
expect(run.linkedIssueId).toBeTruthy();
|
||||||
|
const [createdIssue] = await db
|
||||||
|
.select({
|
||||||
|
id: issues.id,
|
||||||
|
assigneeAgentId: issues.assigneeAgentId,
|
||||||
|
createdByUserId: issues.createdByUserId,
|
||||||
|
})
|
||||||
|
.from(issues)
|
||||||
|
.where(eq(issues.id, run.linkedIssueId!));
|
||||||
|
expect(createdIssue).toMatchObject({
|
||||||
|
id: run.linkedIssueId,
|
||||||
|
assigneeAgentId: agentId,
|
||||||
|
createdByUserId: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const inboxIssues = await issueSvc.list(companyId, {
|
||||||
|
touchedByUserId: userId,
|
||||||
|
inboxArchivedByUserId: userId,
|
||||||
|
includeRoutineExecutions: true,
|
||||||
|
});
|
||||||
|
expect(inboxIssues.map((issue) => issue.id)).toContain(run.linkedIssueId);
|
||||||
|
});
|
||||||
|
|
||||||
it("waits for the assignee wakeup to be queued before returning the routine run", async () => {
|
it("waits for the assignee wakeup to be queued before returning the routine run", async () => {
|
||||||
let wakeupResolved = false;
|
let wakeupResolved = false;
|
||||||
const { routine, svc } = await seedFixture({
|
const { routine, svc } = await seedFixture({
|
||||||
|
|
@ -349,6 +383,166 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => {
|
||||||
expect(routineIssues[0]?.id).toBe(previousIssue.id);
|
expect(routineIssues[0]?.id).toBe(previousIssue.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("touches a coalesced routine issue for the manual runner's inbox", async () => {
|
||||||
|
const { agentId, companyId, issueSvc, routine, svc } = await seedFixture();
|
||||||
|
const userId = randomUUID();
|
||||||
|
const previousRunId = randomUUID();
|
||||||
|
const liveHeartbeatRunId = randomUUID();
|
||||||
|
const previousIssue = await issueSvc.create(companyId, {
|
||||||
|
projectId: routine.projectId,
|
||||||
|
title: routine.title,
|
||||||
|
description: routine.description,
|
||||||
|
status: "in_progress",
|
||||||
|
priority: routine.priority,
|
||||||
|
assigneeAgentId: routine.assigneeAgentId,
|
||||||
|
originKind: "routine_execution",
|
||||||
|
originId: routine.id,
|
||||||
|
originRunId: previousRunId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(routineRuns).values({
|
||||||
|
id: previousRunId,
|
||||||
|
companyId,
|
||||||
|
routineId: routine.id,
|
||||||
|
triggerId: null,
|
||||||
|
source: "manual",
|
||||||
|
status: "issue_created",
|
||||||
|
triggeredAt: new Date("2026-03-20T12:00:00.000Z"),
|
||||||
|
linkedIssueId: previousIssue.id,
|
||||||
|
});
|
||||||
|
await db.insert(heartbeatRuns).values({
|
||||||
|
id: liveHeartbeatRunId,
|
||||||
|
companyId,
|
||||||
|
agentId,
|
||||||
|
invocationSource: "assignment",
|
||||||
|
triggerDetail: "system",
|
||||||
|
status: "running",
|
||||||
|
contextSnapshot: { issueId: previousIssue.id },
|
||||||
|
startedAt: new Date("2026-03-20T12:01:00.000Z"),
|
||||||
|
});
|
||||||
|
await db
|
||||||
|
.update(issues)
|
||||||
|
.set({
|
||||||
|
checkoutRunId: liveHeartbeatRunId,
|
||||||
|
executionRunId: liveHeartbeatRunId,
|
||||||
|
executionLockedAt: new Date("2026-03-20T12:01:00.000Z"),
|
||||||
|
})
|
||||||
|
.where(eq(issues.id, previousIssue.id));
|
||||||
|
await db.insert(issueInboxArchives).values({
|
||||||
|
companyId,
|
||||||
|
issueId: previousIssue.id,
|
||||||
|
userId,
|
||||||
|
archivedAt: new Date("2026-03-20T12:02:00.000Z"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = await svc.runRoutine(routine.id, { source: "manual" }, { userId });
|
||||||
|
|
||||||
|
expect(run.status).toBe("coalesced");
|
||||||
|
expect(run.linkedIssueId).toBe(previousIssue.id);
|
||||||
|
await expect(
|
||||||
|
db.select().from(issueInboxArchives).where(eq(issueInboxArchives.issueId, previousIssue.id)),
|
||||||
|
).resolves.toHaveLength(0);
|
||||||
|
await expect(
|
||||||
|
db.select().from(issueReadStates).where(eq(issueReadStates.issueId, previousIssue.id)),
|
||||||
|
).resolves.toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
companyId,
|
||||||
|
issueId: previousIssue.id,
|
||||||
|
userId,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const inboxIssues = await issueSvc.list(companyId, {
|
||||||
|
touchedByUserId: userId,
|
||||||
|
inboxArchivedByUserId: userId,
|
||||||
|
includeRoutineExecutions: true,
|
||||||
|
});
|
||||||
|
expect(inboxIssues.map((issue) => issue.id)).toContain(previousIssue.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("touches a skipped active routine issue for the manual runner's inbox", async () => {
|
||||||
|
const { agentId, companyId, issueSvc, routine, svc } = await seedFixture();
|
||||||
|
const userId = randomUUID();
|
||||||
|
const previousRunId = randomUUID();
|
||||||
|
const liveHeartbeatRunId = randomUUID();
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(routines)
|
||||||
|
.set({ concurrencyPolicy: "skip_if_active" })
|
||||||
|
.where(eq(routines.id, routine.id));
|
||||||
|
|
||||||
|
const previousIssue = await issueSvc.create(companyId, {
|
||||||
|
projectId: routine.projectId,
|
||||||
|
title: routine.title,
|
||||||
|
description: routine.description,
|
||||||
|
status: "in_progress",
|
||||||
|
priority: routine.priority,
|
||||||
|
assigneeAgentId: routine.assigneeAgentId,
|
||||||
|
originKind: "routine_execution",
|
||||||
|
originId: routine.id,
|
||||||
|
originRunId: previousRunId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(routineRuns).values({
|
||||||
|
id: previousRunId,
|
||||||
|
companyId,
|
||||||
|
routineId: routine.id,
|
||||||
|
triggerId: null,
|
||||||
|
source: "manual",
|
||||||
|
status: "issue_created",
|
||||||
|
triggeredAt: new Date("2026-03-20T12:00:00.000Z"),
|
||||||
|
linkedIssueId: previousIssue.id,
|
||||||
|
});
|
||||||
|
await db.insert(heartbeatRuns).values({
|
||||||
|
id: liveHeartbeatRunId,
|
||||||
|
companyId,
|
||||||
|
agentId,
|
||||||
|
invocationSource: "assignment",
|
||||||
|
triggerDetail: "system",
|
||||||
|
status: "running",
|
||||||
|
contextSnapshot: { issueId: previousIssue.id },
|
||||||
|
startedAt: new Date("2026-03-20T12:01:00.000Z"),
|
||||||
|
});
|
||||||
|
await db
|
||||||
|
.update(issues)
|
||||||
|
.set({
|
||||||
|
checkoutRunId: liveHeartbeatRunId,
|
||||||
|
executionRunId: liveHeartbeatRunId,
|
||||||
|
executionLockedAt: new Date("2026-03-20T12:01:00.000Z"),
|
||||||
|
})
|
||||||
|
.where(eq(issues.id, previousIssue.id));
|
||||||
|
await db.insert(issueInboxArchives).values({
|
||||||
|
companyId,
|
||||||
|
issueId: previousIssue.id,
|
||||||
|
userId,
|
||||||
|
archivedAt: new Date("2026-03-20T12:02:00.000Z"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = await svc.runRoutine(routine.id, { source: "manual" }, { userId });
|
||||||
|
|
||||||
|
expect(run.status).toBe("skipped");
|
||||||
|
expect(run.linkedIssueId).toBe(previousIssue.id);
|
||||||
|
await expect(
|
||||||
|
db.select().from(issueInboxArchives).where(eq(issueInboxArchives.issueId, previousIssue.id)),
|
||||||
|
).resolves.toHaveLength(0);
|
||||||
|
await expect(
|
||||||
|
db.select().from(issueReadStates).where(eq(issueReadStates.issueId, previousIssue.id)),
|
||||||
|
).resolves.toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
companyId,
|
||||||
|
issueId: previousIssue.id,
|
||||||
|
userId,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const inboxIssues = await issueSvc.list(companyId, {
|
||||||
|
touchedByUserId: userId,
|
||||||
|
inboxArchivedByUserId: userId,
|
||||||
|
includeRoutineExecutions: true,
|
||||||
|
});
|
||||||
|
expect(inboxIssues.map((issue) => issue.id)).toContain(previousIssue.id);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not coalesce live routine runs with different resolved variables", async () => {
|
it("does not coalesce live routine runs with different resolved variables", async () => {
|
||||||
const { companyId, agentId, projectId, svc } = await seedFixture();
|
const { companyId, agentId, projectId, svc } = await seedFixture();
|
||||||
const variableRoutine = await svc.create(
|
const variableRoutine = await svc.create(
|
||||||
|
|
|
||||||
|
|
@ -283,7 +283,10 @@ export function routineRoutes(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await assertBoardCanAssignTasks(req, routine.companyId);
|
await assertBoardCanAssignTasks(req, routine.companyId);
|
||||||
const run = await svc.runRoutine(routine.id, req.body);
|
const run = await svc.runRoutine(routine.id, req.body, {
|
||||||
|
agentId: req.actor.type === "agent" ? req.actor.agentId : null,
|
||||||
|
userId: req.actor.type === "board" ? req.actor.userId ?? null : null,
|
||||||
|
});
|
||||||
const actor = getActorInfo(req);
|
const actor = getActorInfo(req);
|
||||||
await logActivity(db, {
|
await logActivity(db, {
|
||||||
companyId: routine.companyId,
|
companyId: routine.companyId,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import {
|
||||||
executionWorkspaces,
|
executionWorkspaces,
|
||||||
goals,
|
goals,
|
||||||
heartbeatRuns,
|
heartbeatRuns,
|
||||||
|
issueInboxArchives,
|
||||||
|
issueReadStates,
|
||||||
issues,
|
issues,
|
||||||
projects,
|
projects,
|
||||||
routineRuns,
|
routineRuns,
|
||||||
|
|
@ -758,6 +760,43 @@ export function routineService(
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function touchIssueForUserInbox(
|
||||||
|
executor: Db,
|
||||||
|
input: {
|
||||||
|
companyId: string;
|
||||||
|
issueId: string;
|
||||||
|
userId: string;
|
||||||
|
touchedAt: Date;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
await executor
|
||||||
|
.insert(issueReadStates)
|
||||||
|
.values({
|
||||||
|
companyId: input.companyId,
|
||||||
|
issueId: input.issueId,
|
||||||
|
userId: input.userId,
|
||||||
|
lastReadAt: input.touchedAt,
|
||||||
|
updatedAt: input.touchedAt,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: [issueReadStates.companyId, issueReadStates.issueId, issueReadStates.userId],
|
||||||
|
set: {
|
||||||
|
lastReadAt: input.touchedAt,
|
||||||
|
updatedAt: input.touchedAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await executor
|
||||||
|
.delete(issueInboxArchives)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(issueInboxArchives.companyId, input.companyId),
|
||||||
|
eq(issueInboxArchives.issueId, input.issueId),
|
||||||
|
eq(issueInboxArchives.userId, input.userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function dispatchRoutineRun(input: {
|
async function dispatchRoutineRun(input: {
|
||||||
routine: typeof routines.$inferSelect;
|
routine: typeof routines.$inferSelect;
|
||||||
trigger: typeof routineTriggers.$inferSelect | null;
|
trigger: typeof routineTriggers.$inferSelect | null;
|
||||||
|
|
@ -770,6 +809,7 @@ export function routineService(
|
||||||
executionWorkspaceId?: string | null;
|
executionWorkspaceId?: string | null;
|
||||||
executionWorkspacePreference?: string | null;
|
executionWorkspacePreference?: string | null;
|
||||||
executionWorkspaceSettings?: Record<string, unknown> | null;
|
executionWorkspaceSettings?: Record<string, unknown> | null;
|
||||||
|
actor?: Actor;
|
||||||
}) {
|
}) {
|
||||||
const projectId = input.projectId ?? input.routine.projectId ?? null;
|
const projectId = input.projectId ?? input.routine.projectId ?? null;
|
||||||
const assigneeAgentId = input.assigneeAgentId ?? input.routine.assigneeAgentId ?? null;
|
const assigneeAgentId = input.assigneeAgentId ?? input.routine.assigneeAgentId ?? null;
|
||||||
|
|
@ -840,6 +880,7 @@ export function routineService(
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggeredAt = new Date();
|
const triggeredAt = new Date();
|
||||||
|
const manualRunnerUserId = input.source === "manual" ? input.actor?.userId ?? null : null;
|
||||||
const [createdRun] = await txDb
|
const [createdRun] = await txDb
|
||||||
.insert(routineRuns)
|
.insert(routineRuns)
|
||||||
.values({
|
.values({
|
||||||
|
|
@ -864,6 +905,14 @@ export function routineService(
|
||||||
const activeIssue = await findLiveExecutionIssue(input.routine, txDb, dispatchFingerprint);
|
const activeIssue = await findLiveExecutionIssue(input.routine, txDb, dispatchFingerprint);
|
||||||
if (activeIssue && input.routine.concurrencyPolicy !== "always_enqueue") {
|
if (activeIssue && input.routine.concurrencyPolicy !== "always_enqueue") {
|
||||||
const status = input.routine.concurrencyPolicy === "skip_if_active" ? "skipped" : "coalesced";
|
const status = input.routine.concurrencyPolicy === "skip_if_active" ? "skipped" : "coalesced";
|
||||||
|
if (manualRunnerUserId) {
|
||||||
|
await touchIssueForUserInbox(txDb, {
|
||||||
|
companyId: input.routine.companyId,
|
||||||
|
issueId: activeIssue.id,
|
||||||
|
userId: manualRunnerUserId,
|
||||||
|
touchedAt: triggeredAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
const updated = await finalizeRun(createdRun.id, {
|
const updated = await finalizeRun(createdRun.id, {
|
||||||
status,
|
status,
|
||||||
linkedIssueId: activeIssue.id,
|
linkedIssueId: activeIssue.id,
|
||||||
|
|
@ -891,6 +940,8 @@ export function routineService(
|
||||||
status: "todo",
|
status: "todo",
|
||||||
priority: input.routine.priority,
|
priority: input.routine.priority,
|
||||||
assigneeAgentId,
|
assigneeAgentId,
|
||||||
|
createdByAgentId: input.source === "manual" ? input.actor?.agentId ?? null : null,
|
||||||
|
createdByUserId: manualRunnerUserId,
|
||||||
originKind: "routine_execution",
|
originKind: "routine_execution",
|
||||||
originId: input.routine.id,
|
originId: input.routine.id,
|
||||||
originRunId: createdRun.id,
|
originRunId: createdRun.id,
|
||||||
|
|
@ -914,6 +965,14 @@ export function routineService(
|
||||||
const existingIssue = await findLiveExecutionIssue(input.routine, txDb, dispatchFingerprint);
|
const existingIssue = await findLiveExecutionIssue(input.routine, txDb, dispatchFingerprint);
|
||||||
if (!existingIssue) throw error;
|
if (!existingIssue) throw error;
|
||||||
const status = input.routine.concurrencyPolicy === "skip_if_active" ? "skipped" : "coalesced";
|
const status = input.routine.concurrencyPolicy === "skip_if_active" ? "skipped" : "coalesced";
|
||||||
|
if (manualRunnerUserId) {
|
||||||
|
await touchIssueForUserInbox(txDb, {
|
||||||
|
companyId: input.routine.companyId,
|
||||||
|
issueId: existingIssue.id,
|
||||||
|
userId: manualRunnerUserId,
|
||||||
|
touchedAt: triggeredAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
const updated = await finalizeRun(createdRun.id, {
|
const updated = await finalizeRun(createdRun.id, {
|
||||||
status,
|
status,
|
||||||
linkedIssueId: existingIssue.id,
|
linkedIssueId: existingIssue.id,
|
||||||
|
|
@ -1383,7 +1442,7 @@ export function routineService(
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
runRoutine: async (id: string, input: RunRoutine) => {
|
runRoutine: async (id: string, input: RunRoutine, actor?: Actor) => {
|
||||||
const routine = await getRoutineById(id);
|
const routine = await getRoutineById(id);
|
||||||
if (!routine) throw notFound("Routine not found");
|
if (!routine) throw notFound("Routine not found");
|
||||||
if (routine.status === "archived") throw conflict("Routine is archived");
|
if (routine.status === "archived") throw conflict("Routine is archived");
|
||||||
|
|
@ -1405,6 +1464,7 @@ export function routineService(
|
||||||
executionWorkspacePreference: input.executionWorkspacePreference ?? null,
|
executionWorkspacePreference: input.executionWorkspacePreference ?? null,
|
||||||
executionWorkspaceSettings:
|
executionWorkspaceSettings:
|
||||||
(input.executionWorkspaceSettings as Record<string, unknown> | null | undefined) ?? null,
|
(input.executionWorkspaceSettings as Record<string, unknown> | null | undefined) ?? null,
|
||||||
|
actor,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue