FN-7187: re-engage executors from in-review chat
Route user comments on in-review tasks through the executor re-engagement path. - Reuse review-address re-engagement for Chat steering and Comments-tab user comments. - Suppress re-engagement for open PR blockers, live sessions, and non-user task comments. - Cover the in-review comment wake behavior with dashboard route tests and operator docs. Files changed: .changeset/fn-7187-in-review-chat-reengage.md | 7 + docs/dashboard-guide.md | 2 +- .../src/__tests__/routes-tasks-ops.test.ts | 236 +++++++++++++++++++++ .../src/routes/register-task-workflow-routes.ts | 138 +++++++++--- 4 files changed, 351 insertions(+), 32 deletions(-) Fusion-Task-Id: FN-7187 Fusion-Task-Lineage: f0984ede-4038-439a-a8b4-5d6ed6756c40 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7187-in-review-chat-reengage.md
Normal file
7
.changeset/fn-7187-in-review-chat-reengage.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Re-engage an executor when users chat on in-review tasks.
|
||||
category: feature
|
||||
dev: Shares the review-address re-engagement helper for Chat steering and Comments-tab task comments while preserving PR-await guards.
|
||||
@@ -1057,7 +1057,7 @@ Recommended workflow: ordinary chains stay as `Blocks N` so noise stays low, hig
|
||||
|
||||
### Logs → Agent Log view
|
||||
|
||||
The **Chat** tab sits between Definition and Logs and presents a live, chat-styled transcript of task agent output. Consecutive entries are grouped by role and labeled as Planner, Executor, Reviewer, or Merger; legacy log rows without an agent role use the neutral Agent fallback. Agent group headers and user message headers show a small muted relative timestamp (for example, “just now”, “1m ago”, or “2h ago”) based on the transcript timestamp, while agent group metadata still includes the entry count. Consecutive text/message chunks inside a role group render as one continuous markdown bubble, while consecutive tool/tool-result/tool-error rows collapse into one expandable, compact tool-call summary that stays collapsed by default; the summary counts tool invocations, lists deduped tool names with overflow, and shows an error count when failures are present, while the expanded body pairs each call with its result or error in dense entry cards. Thinking entries render in a collapsible block that starts expanded. The transcript opens at the latest output whenever the tab loads or becomes active, then follows new live output when you are already near the bottom while preserving your scroll position when you review older messages. When older task-agent history exists, scrolling to the top or selecting **Load previous messages** prepends earlier transcript entries without moving the message you were reading. When you scroll away from the bottom of a populated transcript, a sticky **Latest** button appears inside the transcript so you can jump back to the newest message and resume live follow. For non-`done` tasks, the composer sends guidance through the same steering path used by comments, including active planning/triage, `in-progress`, and `in-review` sessions, plus live CLI-agent sessions reported by the session bridge; messages are still saved as queued guidance when no session is currently live. The composer no longer renders guidance text above the entry box; active, idle, and completed task actions are communicated through the textarea placeholder while the input and Send button remain usable. On a `done` task, sending a Chat message starts a refinement task using the typed text as feedback and shows a success toast with the new task ID; the current task detail modal remains on the completed task. The task-detail Chat tab keeps the composer pinned and visible on mobile and desktop while the transcript scrolls internally; its textarea placeholder reads “Steer the currently executing agent” for steering mode and switches to refinement copy for completed tasks, with the same inline, icon-only send affordance to the right of the input at every breakpoint. In the composer, plain **Enter** sends, **Shift+Enter** inserts a newline, and **Cmd/Ctrl+Enter** remains a supported send shortcut.
|
||||
The **Chat** tab sits between Definition and Logs and presents a live, chat-styled transcript of task agent output. Consecutive entries are grouped by role and labeled as Planner, Executor, Reviewer, or Merger; legacy log rows without an agent role use the neutral Agent fallback. Agent group headers and user message headers show a small muted relative timestamp (for example, “just now”, “1m ago”, or “2h ago”) based on the transcript timestamp, while agent group metadata still includes the entry count. Consecutive text/message chunks inside a role group render as one continuous markdown bubble, while consecutive tool/tool-result/tool-error rows collapse into one expandable, compact tool-call summary that stays collapsed by default; the summary counts tool invocations, lists deduped tool names with overflow, and shows an error count when failures are present, while the expanded body pairs each call with its result or error in dense entry cards. Thinking entries render in a collapsible block that starts expanded. The transcript opens at the latest output whenever the tab loads or becomes active, then follows new live output when you are already near the bottom while preserving your scroll position when you review older messages. When older task-agent history exists, scrolling to the top or selecting **Load previous messages** prepends earlier transcript entries without moving the message you were reading. When you scroll away from the bottom of a populated transcript, a sticky **Latest** button appears inside the transcript so you can jump back to the newest message and resume live follow. For non-`done` tasks, the composer sends guidance through the same steering path used by comments, including active planning/triage, `in-progress`, and `in-review` sessions, plus live CLI-agent sessions reported by the session bridge; an `in-review` Chat message or Comments-tab task comment re-engages an executor unless an open PR blocks moving the task back, and other messages are still saved as queued guidance when no session is currently live. The composer no longer renders guidance text above the entry box; active, idle, and completed task actions are communicated through the textarea placeholder while the input and Send button remain usable. On a `done` task, sending a Chat message starts a refinement task using the typed text as feedback and shows a success toast with the new task ID; the current task detail modal remains on the completed task. The task-detail Chat tab keeps the composer pinned and visible on mobile and desktop while the transcript scrolls internally; its textarea placeholder reads “Steer the currently executing agent” for steering mode and switches to refinement copy for completed tasks, with the same inline, icon-only send affordance to the right of the input at every breakpoint. In the composer, plain **Enter** sends, **Shift+Enter** inserts a newline, and **Cmd/Ctrl+Enter** remains a supported send shortcut.
|
||||
|
||||
The **Logs** tab includes an **Agent Log** subview designed for debugging long-running and tool-heavy sessions:
|
||||
|
||||
|
||||
@@ -330,6 +330,46 @@ describe("POST /tasks/:id/steer", () => {
|
||||
return app;
|
||||
}
|
||||
|
||||
function buildTaskWorkflowApp(scopedStore: TaskStore, triggerCommentWakeForAssignedAgent = vi.fn().mockResolvedValue(undefined)) {
|
||||
const router = express.Router();
|
||||
registerTaskWorkflowRoutes({
|
||||
router,
|
||||
store: scopedStore,
|
||||
runtimeLogger: { error: vi.fn(), warn: vi.fn() },
|
||||
planningLogger: { error: vi.fn(), warn: vi.fn(), log: vi.fn() } as any,
|
||||
chatLogger: { error: vi.fn(), warn: vi.fn(), log: vi.fn() } as any,
|
||||
getProjectIdFromRequest: () => undefined,
|
||||
getScopedStore: async () => scopedStore,
|
||||
getProjectContext: async () => ({ store: scopedStore, projectId: undefined }),
|
||||
prioritizeProjectsForCurrentDirectory: (projects) => projects,
|
||||
emitRemoteRouteDiagnostic: vi.fn(),
|
||||
emitAuthSyncAuditLog: vi.fn(),
|
||||
parseScopeParam: () => undefined,
|
||||
resolveAutomationStore: vi.fn() as any,
|
||||
resolveRoutineStore: vi.fn() as any,
|
||||
resolveRoutineRunner: vi.fn() as any,
|
||||
registerDispose: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
rethrowAsApiError: (error) => { throw error; },
|
||||
}, {
|
||||
runtimeLogger: { error: vi.fn(), warn: vi.fn() },
|
||||
upload: { single: vi.fn(() => (_req: unknown, _res: unknown, next: () => void) => next()) },
|
||||
taskDetailActivityLogLimit: 100,
|
||||
validateOptionalModelField: () => undefined,
|
||||
normalizeModelSelectionPair: (provider, modelId) => ({ provider, modelId }),
|
||||
runGitCommand: vi.fn(),
|
||||
isGitRepo: vi.fn(),
|
||||
resolveIntegrationBranch: vi.fn(),
|
||||
trimTaskDetailActivityLog: (task) => task,
|
||||
triggerCommentWakeForAssignedAgent,
|
||||
resolveSelfHealingManager: () => undefined,
|
||||
});
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", router);
|
||||
return { app, triggerCommentWakeForAssignedAgent };
|
||||
}
|
||||
|
||||
it("passes the new steering comment id to the task-workflow wake dependency", async () => {
|
||||
const scopedStore = createMockStore();
|
||||
const updatedTask = {
|
||||
@@ -516,6 +556,202 @@ describe("POST /tasks/:id/steer", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("re-engages an ephemeral in-review task when chat steering is posted", async () => {
|
||||
const scopedStore = createMockStore();
|
||||
const inReviewTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-001",
|
||||
column: "in-review" as const,
|
||||
status: "waiting for review",
|
||||
error: "previous transient error",
|
||||
sessionFile: null,
|
||||
steps: [
|
||||
{ title: "Implement", status: "done" as const },
|
||||
{ title: "Review follow-up", status: "in-progress" as const },
|
||||
],
|
||||
steeringComments: [{ id: "steer-in-review", text: "Please rename the helper", author: "user" as const, createdAt: "2026-06-28T00:00:00.000Z" }],
|
||||
};
|
||||
const movedTask = {
|
||||
...inReviewTask,
|
||||
column: "in-progress" as const,
|
||||
status: null,
|
||||
error: null,
|
||||
steps: [
|
||||
{ title: "Implement", status: "done" as const },
|
||||
{ title: "Review follow-up", status: "pending" as const },
|
||||
],
|
||||
};
|
||||
const triggerCommentWakeForAssignedAgent = vi.fn().mockResolvedValue(undefined);
|
||||
(scopedStore.addSteeringComment as ReturnType<typeof vi.fn>).mockResolvedValue(inReviewTask);
|
||||
(scopedStore.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
|
||||
const { app } = buildTaskWorkflowApp(scopedStore, triggerCommentWakeForAssignedAgent);
|
||||
|
||||
const res = await REQUEST(app, "POST", "/api/tasks/FN-001/steer", JSON.stringify({ text: "Please rename the helper" }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(scopedStore.addSteeringComment).toHaveBeenCalledWith("FN-001", "Please rename the helper", "user");
|
||||
expect(scopedStore.updateTask).toHaveBeenCalledWith("FN-001", { status: null, error: null, sessionFile: null });
|
||||
expect(scopedStore.updateStep).toHaveBeenCalledWith("FN-001", 1, "pending");
|
||||
expect(scopedStore.moveTask).toHaveBeenCalledWith("FN-001", "in-progress", { preserveProgress: true });
|
||||
expect(triggerCommentWakeForAssignedAgent).toHaveBeenCalledWith(scopedStore, movedTask, {
|
||||
triggeringCommentType: "steering",
|
||||
triggeringCommentIds: ["steer-in-review"],
|
||||
triggerDetail: "steering-comment",
|
||||
});
|
||||
expect(res.body.column).toBe("in-progress");
|
||||
expect(res.body.steeringComments).toEqual(inReviewTask.steeringComments);
|
||||
});
|
||||
|
||||
it("re-engages in-review chat requests even when autoMerge is disabled", async () => {
|
||||
const scopedStore = createMockStore({
|
||||
getSettings: vi.fn().mockResolvedValue({ autoMerge: false }),
|
||||
getSettingsFast: vi.fn().mockResolvedValue({ autoMerge: false }),
|
||||
} as Partial<TaskStore>);
|
||||
const inReviewTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-001",
|
||||
column: "in-review" as const,
|
||||
sessionFile: null,
|
||||
steps: [],
|
||||
steeringComments: [{ id: "steer-automerge-off", text: "Please revise", author: "user" as const, createdAt: "2026-06-28T00:00:00.000Z" }],
|
||||
};
|
||||
const movedTask = { ...inReviewTask, column: "in-progress" as const };
|
||||
(scopedStore.addSteeringComment as ReturnType<typeof vi.fn>).mockResolvedValue(inReviewTask);
|
||||
(scopedStore.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
|
||||
const { app } = buildTaskWorkflowApp(scopedStore);
|
||||
|
||||
const res = await REQUEST(app, "POST", "/api/tasks/FN-001/steer", JSON.stringify({ text: "Please revise" }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(scopedStore.moveTask).toHaveBeenCalledWith("FN-001", "in-progress", { preserveProgress: true });
|
||||
expect(res.body.column).toBe("in-progress");
|
||||
});
|
||||
|
||||
it("keeps chat send successful but suppresses in-review re-engagement when an open PR blocks the move", async () => {
|
||||
const scopedStore = createMockStore({
|
||||
getActivePrEntityBySource: vi.fn().mockReturnValue({ state: "open" }),
|
||||
} as Partial<TaskStore>);
|
||||
const inReviewTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-001",
|
||||
column: "in-review" as const,
|
||||
sessionFile: null,
|
||||
steps: [{ title: "Implement", status: "done" as const }],
|
||||
steeringComments: [{ id: "steer-pr-blocked", text: "Please revise", author: "user" as const, createdAt: "2026-06-28T00:00:00.000Z" }],
|
||||
};
|
||||
const triggerCommentWakeForAssignedAgent = vi.fn().mockResolvedValue(undefined);
|
||||
(scopedStore.addSteeringComment as ReturnType<typeof vi.fn>).mockResolvedValue(inReviewTask);
|
||||
const { app } = buildTaskWorkflowApp(scopedStore, triggerCommentWakeForAssignedAgent);
|
||||
|
||||
const res = await REQUEST(app, "POST", "/api/tasks/FN-001/steer", JSON.stringify({ text: "Please revise" }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.column).toBe("in-review");
|
||||
expect(res.body.steeringComments).toEqual(inReviewTask.steeringComments);
|
||||
expect(scopedStore.updateTask).not.toHaveBeenCalled();
|
||||
expect(scopedStore.updateStep).not.toHaveBeenCalled();
|
||||
expect(scopedStore.moveTask).not.toHaveBeenCalled();
|
||||
expect(triggerCommentWakeForAssignedAgent).not.toHaveBeenCalled();
|
||||
expect(scopedStore.logEntry).toHaveBeenCalledWith(
|
||||
"FN-001",
|
||||
"In-review user comment re-engagement suppressed",
|
||||
"This task has an open PR. Merge or close the PR before moving it back.",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not use in-review re-engagement for a live in-progress chat session", async () => {
|
||||
const scopedStore = createMockStore();
|
||||
const liveTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-001",
|
||||
column: "in-progress" as const,
|
||||
assignedAgentId: "agent-1",
|
||||
sessionFile: "sessions/FN-001.json",
|
||||
steeringComments: [{ id: "steer-live", text: "Please revise", author: "user" as const, createdAt: "2026-06-28T00:00:00.000Z" }],
|
||||
};
|
||||
const triggerCommentWakeForAssignedAgent = vi.fn().mockResolvedValue(undefined);
|
||||
(scopedStore.addSteeringComment as ReturnType<typeof vi.fn>).mockResolvedValue(liveTask);
|
||||
const { app } = buildTaskWorkflowApp(scopedStore, triggerCommentWakeForAssignedAgent);
|
||||
|
||||
const res = await REQUEST(app, "POST", "/api/tasks/FN-001/steer", JSON.stringify({ text: "Please revise" }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(scopedStore.updateTask).not.toHaveBeenCalled();
|
||||
expect(scopedStore.updateStep).not.toHaveBeenCalled();
|
||||
expect(scopedStore.moveTask).not.toHaveBeenCalled();
|
||||
expect(triggerCommentWakeForAssignedAgent).toHaveBeenCalledWith(scopedStore, liveTask, {
|
||||
triggeringCommentType: "steering",
|
||||
triggeringCommentIds: ["steer-live"],
|
||||
triggerDetail: "steering-comment",
|
||||
});
|
||||
});
|
||||
|
||||
it("re-engages in-review tasks when user-authored task comments are posted", async () => {
|
||||
const scopedStore = createMockStore();
|
||||
const inReviewTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-001",
|
||||
column: "in-review" as const,
|
||||
sessionFile: null,
|
||||
steps: [],
|
||||
comments: [{ id: "task-comment-1", text: "Please also update docs", author: "user" as const, createdAt: "2026-06-28T00:00:00.000Z" }],
|
||||
};
|
||||
const movedTask = { ...inReviewTask, column: "in-progress" as const };
|
||||
const triggerCommentWakeForAssignedAgent = vi.fn().mockResolvedValue(undefined);
|
||||
(scopedStore.addTaskComment as ReturnType<typeof vi.fn>).mockResolvedValue(inReviewTask);
|
||||
(scopedStore.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
|
||||
const { app } = buildTaskWorkflowApp(scopedStore, triggerCommentWakeForAssignedAgent);
|
||||
|
||||
const res = await REQUEST(app, "POST", "/api/tasks/FN-001/comments", JSON.stringify({ text: "Please also update docs" }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(scopedStore.addTaskComment).toHaveBeenCalledWith("FN-001", "Please also update docs", "user");
|
||||
expect(scopedStore.moveTask).toHaveBeenCalledWith("FN-001", "in-progress", { preserveProgress: true });
|
||||
expect(triggerCommentWakeForAssignedAgent).toHaveBeenCalledWith(scopedStore, movedTask, {
|
||||
triggeringCommentType: "task",
|
||||
triggeringCommentIds: ["task-comment-1"],
|
||||
triggerDetail: "task-comment",
|
||||
});
|
||||
expect(res.body.column).toBe("in-progress");
|
||||
});
|
||||
|
||||
it("does not re-engage in-review tasks for non-user-authored task comments", async () => {
|
||||
const scopedStore = createMockStore();
|
||||
const inReviewTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-001",
|
||||
column: "in-review" as const,
|
||||
sessionFile: null,
|
||||
steps: [{ title: "Implement", status: "done" as const }],
|
||||
comments: [{ id: "task-comment-agent", text: "Internal note", author: "agent" as const, createdAt: "2026-06-28T00:00:00.000Z" }],
|
||||
};
|
||||
const triggerCommentWakeForAssignedAgent = vi.fn().mockResolvedValue(undefined);
|
||||
(scopedStore.addTaskComment as ReturnType<typeof vi.fn>).mockResolvedValue(inReviewTask);
|
||||
const { app } = buildTaskWorkflowApp(scopedStore, triggerCommentWakeForAssignedAgent);
|
||||
|
||||
const res = await REQUEST(app, "POST", "/api/tasks/FN-001/comments", JSON.stringify({ text: "Internal note", author: "agent" }), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(scopedStore.addTaskComment).toHaveBeenCalledWith("FN-001", "Internal note", "agent");
|
||||
expect(scopedStore.updateTask).not.toHaveBeenCalled();
|
||||
expect(scopedStore.updateStep).not.toHaveBeenCalled();
|
||||
expect(scopedStore.moveTask).not.toHaveBeenCalled();
|
||||
expect(triggerCommentWakeForAssignedAgent).not.toHaveBeenCalled();
|
||||
expect(res.body.column).toBe("in-review");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["", "text is required and must be a string"],
|
||||
["x".repeat(2001), "text must be between 1 and 2000 characters"],
|
||||
|
||||
@@ -595,6 +595,73 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
} = deps;
|
||||
const TASK_DETAIL_ACTIVITY_LOG_LIMIT = taskDetailActivityLogLimit;
|
||||
|
||||
type InReviewUserCommentReengagementInput = {
|
||||
triggeringCommentType: "steering" | "task";
|
||||
triggeringCommentIds?: string[];
|
||||
triggerDetail: string;
|
||||
};
|
||||
|
||||
type InReviewUserCommentReengagementResult = {
|
||||
task: Task;
|
||||
reengaged: boolean;
|
||||
suppressedReason?: "blocking-pr" | "live-session" | "not-in-review";
|
||||
};
|
||||
|
||||
/**
|
||||
* FNXC:InReviewChat 2026-06-28-00:00:
|
||||
* User-authored Chat steering comments and Comments-tab task comments on an in-review task are explicit instructions to re-engage an executor, even when autoMerge is false. Reuse the review-address lifecycle path so the agent can respond/work and later hand the task back to review; suppress only when an open PR entity blocks the backward in-review → in-progress move.
|
||||
* Non-user task-comment authors are persisted without re-engagement so agent/system/API notes cannot bounce review work back to execution.
|
||||
*/
|
||||
async function reengageInReviewTaskForUserComment(
|
||||
scopedStore: TaskStore,
|
||||
task: Task,
|
||||
wake: InReviewUserCommentReengagementInput,
|
||||
): Promise<InReviewUserCommentReengagementResult> {
|
||||
if (task.column !== "in-review") {
|
||||
return { task, reengaged: false, suppressedReason: "not-in-review" };
|
||||
}
|
||||
if (task.sessionFile) {
|
||||
return { task, reengaged: false, suppressedReason: "live-session" };
|
||||
}
|
||||
|
||||
const activePrEntity =
|
||||
scopedStore.getActivePrEntityBySource?.("task", task.id) ??
|
||||
(task.branchContext?.groupId
|
||||
? scopedStore.getActivePrEntityBySource?.("branch-group", task.branchContext.groupId)
|
||||
: null);
|
||||
if (
|
||||
isBackwardMoveBlockedByOpenPr({
|
||||
fromIndex: COLUMNS.indexOf(task.column as Column),
|
||||
toIndex: COLUMNS.indexOf("in-progress"),
|
||||
activePrEntity,
|
||||
})
|
||||
) {
|
||||
await scopedStore.logEntry(
|
||||
task.id,
|
||||
"In-review user comment re-engagement suppressed",
|
||||
PR_OPEN_BLOCKS_MOVE_BACK_MESSAGE,
|
||||
);
|
||||
return { task, reengaged: false, suppressedReason: "blocking-pr" };
|
||||
}
|
||||
|
||||
await scopedStore.updateTask(task.id, {
|
||||
status: null,
|
||||
error: null,
|
||||
sessionFile: null,
|
||||
});
|
||||
const lastDoneStep = [...(task.steps ?? [])]
|
||||
.map((step, index) => ({ step, index }))
|
||||
.reverse()
|
||||
.find(({ step }) => step.status === "done" || step.status === "in-progress");
|
||||
if (lastDoneStep) {
|
||||
await scopedStore.updateStep(task.id, lastDoneStep.index, "pending");
|
||||
}
|
||||
|
||||
const reengagedTask = await scopedStore.moveTask(task.id, "in-progress", { preserveProgress: true });
|
||||
await triggerCommentWakeForAssignedAgent(scopedStore, reengagedTask, wake);
|
||||
return { task: reengagedTask, reengaged: true };
|
||||
}
|
||||
|
||||
// Get recent integration-branch advance events for post-merge notice
|
||||
router.get("/tasks/merge-advance-events", async (req, res) => {
|
||||
try {
|
||||
@@ -2527,18 +2594,28 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
if (author !== undefined && typeof author !== "string") {
|
||||
throw badRequest("author must be a string");
|
||||
}
|
||||
const task = await scopedStore.addTaskComment(req.params.id, text, author?.trim() || "user");
|
||||
const normalizedAuthor = author?.trim() || "user";
|
||||
const task = await scopedStore.addTaskComment(req.params.id, text, normalizedAuthor);
|
||||
|
||||
const newCommentId = task.comments?.at(-1)?.id;
|
||||
void triggerCommentWakeForAssignedAgent(scopedStore, task, {
|
||||
triggeringCommentType: "task",
|
||||
const wake = {
|
||||
triggeringCommentType: "task" as const,
|
||||
triggeringCommentIds: newCommentId ? [newCommentId] : undefined,
|
||||
triggerDetail: "task-comment",
|
||||
}).catch((error) => {
|
||||
runtimeLogger.warn(
|
||||
`failed to trigger task-comment heartbeat for ${task.id}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
});
|
||||
};
|
||||
if (normalizedAuthor === "user") {
|
||||
if (task.column === "in-review" && !task.sessionFile) {
|
||||
const { task: reengagedTask } = await reengageInReviewTaskForUserComment(scopedStore, task, wake);
|
||||
res.json(reengagedTask);
|
||||
return;
|
||||
}
|
||||
|
||||
void triggerCommentWakeForAssignedAgent(scopedStore, task, wake).catch((error) => {
|
||||
runtimeLogger.warn(
|
||||
`failed to trigger task-comment heartbeat for ${task.id}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
res.json(task);
|
||||
} catch (err: unknown) {
|
||||
@@ -2876,11 +2953,18 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
const task = await scopedStore.addSteeringComment(req.params.id, text, "user");
|
||||
|
||||
const newSteeringCommentId = task.steeringComments?.at(-1)?.id;
|
||||
void triggerCommentWakeForAssignedAgent(scopedStore, task, {
|
||||
triggeringCommentType: "steering",
|
||||
const wake = {
|
||||
triggeringCommentType: "steering" as const,
|
||||
triggeringCommentIds: newSteeringCommentId ? [newSteeringCommentId] : undefined,
|
||||
triggerDetail: "steering-comment",
|
||||
}).catch((error) => {
|
||||
};
|
||||
if (task.column === "in-review" && !task.sessionFile) {
|
||||
const { task: reengagedTask } = await reengageInReviewTaskForUserComment(scopedStore, task, wake);
|
||||
res.json(reengagedTask);
|
||||
return;
|
||||
}
|
||||
|
||||
void triggerCommentWakeForAssignedAgent(scopedStore, task, wake).catch((error) => {
|
||||
runtimeLogger.warn(
|
||||
`failed to trigger steering-comment heartbeat for ${task.id}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
@@ -3716,28 +3800,20 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
let updatedTask: Task = await scopedStore.getTask(task.id);
|
||||
|
||||
if (task.column === "in-review") {
|
||||
await scopedStore.updateTask(task.id, {
|
||||
status: null,
|
||||
error: null,
|
||||
sessionFile: null,
|
||||
});
|
||||
const lastDoneStep = [...task.steps]
|
||||
.map((step, index) => ({ step, index }))
|
||||
.reverse()
|
||||
.find(({ step }) => step.status === "done" || step.status === "in-progress");
|
||||
if (lastDoneStep) {
|
||||
await scopedStore.updateStep(task.id, lastDoneStep.index, "pending");
|
||||
}
|
||||
updatedTask = await scopedStore.moveTask(task.id, "in-progress", { preserveProgress: true });
|
||||
}
|
||||
|
||||
const hasActiveSession = Boolean(updatedTask.sessionFile);
|
||||
if (steeringCommentId && updatedTask.column === "in-progress" && updatedTask.assignedAgentId && !hasActiveSession) {
|
||||
await triggerCommentWakeForAssignedAgent(scopedStore, updatedTask, {
|
||||
updatedTask = (await reengageInReviewTaskForUserComment(scopedStore, updatedTask, {
|
||||
triggeringCommentType: "steering",
|
||||
triggeringCommentIds: [steeringCommentId],
|
||||
triggeringCommentIds: steeringCommentId ? [steeringCommentId] : undefined,
|
||||
triggerDetail: "review-address",
|
||||
});
|
||||
})).task;
|
||||
} else {
|
||||
const hasActiveSession = Boolean(updatedTask.sessionFile);
|
||||
if (steeringCommentId && updatedTask.column === "in-progress" && updatedTask.assignedAgentId && !hasActiveSession) {
|
||||
await triggerCommentWakeForAssignedAgent(scopedStore, updatedTask, {
|
||||
triggeringCommentType: "steering",
|
||||
triggeringCommentIds: [steeringCommentId],
|
||||
triggerDetail: "review-address",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await scopedStore.logEntry(task.id, "Same-task review revision requested", `${selectedItems.length} item(s) submitted from review tab`);
|
||||
|
||||
Reference in New Issue
Block a user