fix(FN-710): fix addSteeringComment to persist comments and correct type usage
- Fix addSteeringComment to skip auto-refinement and write directly to steeringComments array - Update formatCommentForInjection to accept SteeringComment type instead of raw string - Export SteeringComment type from @fusion/core for executor usage - Add tests verifying steering comments are persisted to task store - Fix executor import to use exported SteeringComment type
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskComment, TaskCommentInput, TaskCreateInput, TaskDetail, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, TaskStep, StepStatus, TaskLogEntry, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentCapability, AgentHeartbeatEvent, AgentHeartbeatRun, NtfyNotificationEvent } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskComment, TaskCommentInput, TaskCreateInput, TaskDetail, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, TaskStep, StepStatus, TaskLogEntry, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentCapability, AgentHeartbeatEvent, AgentHeartbeatRun, NtfyNotificationEvent, SteeringComment } from "./types.js";
|
||||
export { AgentStore } from "./agent-store.js";
|
||||
export type { AgentStoreEvents } from "./agent-store.js";
|
||||
export { TaskStore } from "./store.js";
|
||||
|
||||
@@ -1934,6 +1934,65 @@ Task with acceptance criteria
|
||||
expect(updated.comments).toHaveLength(1);
|
||||
expect(updated.comments![0].text).toBe(" ");
|
||||
});
|
||||
|
||||
it("addSteeringComment on done task does NOT create a refinement task", async () => {
|
||||
const task = await store.createTask({ description: "Original task" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
await store.moveTask(task.id, "in-review");
|
||||
await store.moveTask(task.id, "done");
|
||||
|
||||
const allTasksBefore = await store.listTasks();
|
||||
|
||||
await store.addSteeringComment(task.id, "Please handle the edge case");
|
||||
|
||||
const allTasksAfter = await store.listTasks();
|
||||
// No refinement task should be created
|
||||
expect(allTasksAfter).toHaveLength(allTasksBefore.length);
|
||||
});
|
||||
|
||||
it("addSteeringComment writes to both comments and steeringComments", async () => {
|
||||
const task = await createTestTask();
|
||||
|
||||
const updated = await store.addSteeringComment(task.id, "Focus on error handling");
|
||||
|
||||
// Should appear in unified comments (for UI display)
|
||||
expect(updated.comments).toBeDefined();
|
||||
expect(updated.comments!.some(c => c.text === "Focus on error handling")).toBe(true);
|
||||
|
||||
// Should appear in steeringComments (for executor injection)
|
||||
expect(updated.steeringComments).toBeDefined();
|
||||
expect(updated.steeringComments!.some(c => c.text === "Focus on error handling")).toBe(true);
|
||||
});
|
||||
|
||||
it("addSteeringComment steeringComments persist through round-trip", async () => {
|
||||
const task = await createTestTask();
|
||||
|
||||
await store.addSteeringComment(task.id, "Focus on error handling");
|
||||
|
||||
const fetched = await store.getTask(task.id);
|
||||
expect(fetched.steeringComments).toBeDefined();
|
||||
expect(fetched.steeringComments!).toHaveLength(1);
|
||||
expect(fetched.steeringComments![0].text).toBe("Focus on error handling");
|
||||
});
|
||||
|
||||
it("regular addComment on done task still creates refinement", async () => {
|
||||
const task = await store.createTask({ description: "Original task" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
await store.moveTask(task.id, "in-review");
|
||||
await store.moveTask(task.id, "done");
|
||||
|
||||
const allTasksBefore = await store.listTasks();
|
||||
|
||||
await store.addComment(task.id, "Need to fix edge case");
|
||||
|
||||
const allTasksAfter = await store.listTasks();
|
||||
expect(allTasksAfter).toHaveLength(allTasksBefore.length + 1);
|
||||
|
||||
const refinement = allTasksAfter.find((t) => t.id !== task.id && t.title?.includes("Refinement"));
|
||||
expect(refinement).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("task comments and merge details types", () => {
|
||||
|
||||
@@ -183,6 +183,10 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
steps: fromJson<import("./types.js").TaskStep[]>(row.steps) || [],
|
||||
log: fromJson<import("./types.js").TaskLogEntry[]>(row.log) || [],
|
||||
attachments: (() => { const a = fromJson<TaskAttachment[]>(row.attachments); return a && a.length > 0 ? a : undefined; })(),
|
||||
steeringComments: (() => {
|
||||
const sc = fromJson<import("./types.js").SteeringComment[]>(row.steeringComments);
|
||||
return sc && sc.length > 0 ? sc : undefined;
|
||||
})(),
|
||||
comments: (() => {
|
||||
// Merge legacy steeringComments and comments into unified comments field
|
||||
const legacySteering = fromJson<Array<{ id: string; text: string; createdAt: string; author: string }>>(row.steeringComments) || [];
|
||||
@@ -2023,11 +2027,40 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
/**
|
||||
* Add a steering comment to a task.
|
||||
* Steering comments are injected into the AI execution context.
|
||||
* @deprecated Use addComment instead - comments are now unified
|
||||
* They are stored in BOTH `comments` (for unified UI display) and
|
||||
* `steeringComments` (for executor real-time injection).
|
||||
* Unlike regular comments, steering comments never trigger auto-refinement.
|
||||
*/
|
||||
async addSteeringComment(id: string, text: string, author: "user" | "agent" = "user"): Promise<Task> {
|
||||
// Delegates to addComment for unified comment storage
|
||||
return this.addComment(id, text, author);
|
||||
// Write to unified comments (skip refinement — steering is for agent injection, not follow-up tasks)
|
||||
const task = await this.addComment(id, text, author, { skipRefinement: true });
|
||||
|
||||
// Also write to steeringComments so the executor's real-time injection listener can detect new entries
|
||||
const updated = await this.withTaskLock(id, async () => {
|
||||
const dir = this.taskDir(id);
|
||||
const currentTask = await this.readTaskJson(dir);
|
||||
|
||||
const steeringComment: import("./types.js").SteeringComment = {
|
||||
id: task.comments![task.comments!.length - 1].id,
|
||||
text,
|
||||
createdAt: new Date().toISOString(),
|
||||
author,
|
||||
};
|
||||
|
||||
if (!currentTask.steeringComments) {
|
||||
currentTask.steeringComments = [];
|
||||
}
|
||||
currentTask.steeringComments.push(steeringComment);
|
||||
currentTask.updatedAt = new Date().toISOString();
|
||||
|
||||
await this.atomicWriteTaskJson(dir, currentTask);
|
||||
if (this.watcher) this.taskCache.set(id, { ...currentTask });
|
||||
|
||||
this.emit("task:updated", currentTask);
|
||||
return currentTask;
|
||||
});
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async updateTaskComment(id: string, commentId: string, text: string): Promise<Task> {
|
||||
@@ -2096,6 +2129,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
id: string,
|
||||
text: string,
|
||||
author: string = "user",
|
||||
options?: { skipRefinement?: boolean },
|
||||
): Promise<Task> {
|
||||
// Phase 1: Add comment under lock
|
||||
const task = await this.withTaskLock(id, async () => {
|
||||
@@ -2137,7 +2171,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
// Phase 2: Auto-refinement OUTSIDE the lock (to avoid lock contention)
|
||||
// Only create refinement for user comments on done tasks
|
||||
if (task.column === "done" && author === "user") {
|
||||
// Steering comments skip refinement — they are injected into the agent stream instead
|
||||
if (task.column === "done" && author === "user" && !options?.skipRefinement) {
|
||||
try {
|
||||
await this.refineTask(id, text);
|
||||
} catch {
|
||||
|
||||
@@ -1941,7 +1941,7 @@ Treat a non-zero exit code as a blocking failure. Do not claim success without a
|
||||
* Format a comment for injection into a running agent session.
|
||||
* Used for real-time steering during task execution.
|
||||
*/
|
||||
function formatCommentForInjection(comment: import("@fusion/core").TaskComment): string {
|
||||
function formatCommentForInjection(comment: import("@fusion/core").SteeringComment): string {
|
||||
const timestamp = formatTimestamp(comment.createdAt);
|
||||
return `📣 **New feedback** — ${timestamp} (${comment.author}):\n\n${comment.text}\n\nPlease adjust your approach based on this feedback.`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user