diff --git a/.changeset/fn-7187-in-review-chat-reengage.md b/.changeset/fn-7187-in-review-chat-reengage.md new file mode 100644 index 0000000000..300af91cc8 --- /dev/null +++ b/.changeset/fn-7187-in-review-chat-reengage.md @@ -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. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 039fb4493b..4dd060f21a 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -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: diff --git a/packages/dashboard/src/__tests__/routes-tasks-ops.test.ts b/packages/dashboard/src/__tests__/routes-tasks-ops.test.ts index 771840cc34..67d7991afe 100644 --- a/packages/dashboard/src/__tests__/routes-tasks-ops.test.ts +++ b/packages/dashboard/src/__tests__/routes-tasks-ops.test.ts @@ -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).mockResolvedValue(inReviewTask); + (scopedStore.moveTask as ReturnType).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); + 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).mockResolvedValue(inReviewTask); + (scopedStore.moveTask as ReturnType).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); + 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).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).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).mockResolvedValue(inReviewTask); + (scopedStore.moveTask as ReturnType).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).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"], diff --git a/packages/dashboard/src/routes/register-task-workflow-routes.ts b/packages/dashboard/src/routes/register-task-workflow-routes.ts index 20ce0a0886..bd6764951d 100644 --- a/packages/dashboard/src/routes/register-task-workflow-routes.ts +++ b/packages/dashboard/src/routes/register-task-workflow-routes.ts @@ -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 { + 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`);