feat(FN-2922): merge fusion/fn-2922

This commit is contained in:
gsxdsm
2026-04-29 12:01:13 -07:00
parent 8796a32401
commit cecf14a4f1
11 changed files with 253 additions and 27 deletions

View File

@@ -131,7 +131,7 @@ describe("Database", () => {
});
it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
});
it("seeds lastModified", () => {
@@ -154,7 +154,7 @@ describe("Database", () => {
it("is idempotent - calling init() twice does not fail", () => {
expect(() => db.init()).not.toThrow();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
});
it("does not overwrite existing config on re-init", () => {
@@ -761,7 +761,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29 (includes v1→v2 through v26→v29)
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -786,11 +786,11 @@ describe("schema migrations", () => {
const db = new Database(fusionDir);
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
// Re-init should not fail
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
db.close();
});
@@ -825,7 +825,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
expect(cols.map((col) => col.name)).toContain("priority");
@@ -866,7 +866,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name);
@@ -935,7 +935,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name);
@@ -994,7 +994,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
const cols = db.prepare("PRAGMA table_info(chat_messages)").all() as Array<{ name: string }>;
expect(cols.map((col) => col.name)).toContain("attachments");
@@ -1005,6 +1005,58 @@ describe("schema migrations", () => {
db.close();
});
it("migration v53 adds task provenance columns", () => {
tmpDir = makeTmpDir();
const fusionDir = join(tmpDir, ".fusion");
const localDb = new Database(fusionDir);
localDb.init();
const columns = localDb.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const columnNames = columns.map((c) => c.name);
expect(columnNames).toContain("sourceType");
expect(columnNames).toContain("sourceAgentId");
expect(columnNames).toContain("sourceRunId");
expect(columnNames).toContain("sourceSessionId");
expect(columnNames).toContain("sourceMessageId");
expect(columnNames).toContain("sourceParentTaskId");
expect(columnNames).toContain("sourceMetadata");
localDb.close();
});
it("migration v53 backfills sourceType to unknown", () => {
tmpDir = makeTmpDir();
const fusionDir = join(tmpDir, ".fusion");
const legacyDb = new Database(fusionDir);
legacyDb.exec(`
CREATE TABLE IF NOT EXISTS __meta (key TEXT PRIMARY KEY, value TEXT);
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
description TEXT NOT NULL,
"column" TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS config (
id INTEGER PRIMARY KEY CHECK (id = 1),
nextId INTEGER DEFAULT 1,
nextWorkflowStepId INTEGER DEFAULT 1,
settings TEXT DEFAULT '{}',
workflowSteps TEXT DEFAULT '[]',
updatedAt TEXT
);
`);
legacyDb.exec("INSERT INTO __meta (key, value) VALUES ('schemaVersion', '52')");
legacyDb.exec("INSERT INTO __meta (key, value) VALUES ('lastModified', '1000')");
legacyDb.exec(`INSERT INTO tasks (id, description, "column", createdAt, updatedAt) VALUES ('FN-53', 'legacy', 'triage', '2026-01-01', '2026-01-01')`);
legacyDb.init();
const row = legacyDb.prepare("SELECT sourceType FROM tasks WHERE id = 'FN-53'").get() as { sourceType: string | null };
expect(row.sourceType).toBe("unknown");
legacyDb.close();
});
it("applies migration 14+15 by creating agentRatings and ai_sessions indexes", () => {
tmpDir = makeTmpDir();
const fusionDir = join(tmpDir, ".fusion");
@@ -1016,7 +1068,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "agentRatings" }]);
@@ -1040,7 +1092,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "mission_events" }]);
@@ -1144,7 +1196,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -1595,7 +1647,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(fusionDir);
db.init();
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
expect(db.getLastModified()).toBeGreaterThan(0);
db.close();

View File

@@ -779,7 +779,7 @@ describe("Migration: pre-33 DB upgrade", () => {
// Step 1: Create a fresh database at v33 (runs all migrations up to 33)
const db1 = createDatabase(legacyDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(52);
expect(db1.getSchemaVersion()).toBe(53);
db1.close();
// Step 2: Manually downgrade to version 32 and drop insight tables
@@ -814,7 +814,7 @@ describe("Migration: pre-33 DB upgrade", () => {
expect(tableNamesBefore).not.toContain("project_insight_runs");
// Now run init — this triggers the v32→v33 migration
db3.init();
expect(db3.getSchemaVersion()).toBe(52);
expect(db3.getSchemaVersion()).toBe(53);
// Step 4: Verify insight tables exist after migration
const tablesAfter = db3.prepare(
@@ -845,12 +845,12 @@ describe("Migration: pre-33 DB upgrade", () => {
try {
const db1 = createDatabase(testDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(52);
expect(db1.getSchemaVersion()).toBe(53);
db1.close();
const db2 = createDatabase(testDir);
expect(() => db2.init()).not.toThrow();
expect(db2.getSchemaVersion()).toBe(52);
expect(db2.getSchemaVersion()).toBe(53);
db2.close();
} finally {
rmSync(testDir, { recursive: true, force: true });

View File

@@ -2629,7 +2629,7 @@ describe("MissionStore", () => {
describe("Loop State & Validator Run Schema (v31)", () => {
it("schema version is 40 after migration", () => {
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
});
it("mission_features table has loop state columns", () => {

View File

@@ -742,7 +742,7 @@ describe("RoadmapStore", () => {
describe("schema version", () => {
it("schema version is 40 after init", () => {
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
});
});

View File

@@ -465,7 +465,7 @@ describe("Run Audit", () => {
});
it("schema version is bumped to 40", () => {
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
});
});
});

View File

@@ -7022,6 +7022,77 @@ Task with acceptance criteria
});
});
describe("task provenance", () => {
it("defaults sourceType to unknown when source is omitted", async () => {
const task = await store.createTask({ description: "Provenance default" });
const fetched = await store.getTask(task.id);
expect(fetched.sourceType).toBe("unknown");
});
it("persists simple source type from createTask", async () => {
const task = await store.createTask({
description: "Created from dashboard",
source: { sourceType: "dashboard_ui" },
});
const fetched = await store.getTask(task.id);
expect(fetched.sourceType).toBe("dashboard_ui");
});
it("roundtrips full provenance metadata", async () => {
const task = await store.createTask({
description: "Heartbeat-generated task",
source: {
sourceType: "agent_heartbeat",
sourceAgentId: "agent-123",
sourceRunId: "run-456",
sourceSessionId: "session-789",
sourceMessageId: "msg-001",
sourceMetadata: { reason: "scheduled" },
},
});
const fetched = await store.getTask(task.id);
expect(fetched.sourceType).toBe("agent_heartbeat");
expect(fetched.sourceAgentId).toBe("agent-123");
expect(fetched.sourceRunId).toBe("run-456");
expect(fetched.sourceSessionId).toBe("session-789");
expect(fetched.sourceMessageId).toBe("msg-001");
expect(fetched.sourceMetadata).toEqual({ reason: "scheduled" });
});
it("sets duplicate and refine provenance parent links", async () => {
const source = await store.createTask({ description: "Original" });
const duplicated = await store.duplicateTask(source.id);
expect(duplicated.sourceType).toBe("task_duplicate");
expect(duplicated.sourceParentTaskId).toBe(source.id);
await store.moveTask(source.id, "todo");
await store.moveTask(source.id, "in-progress");
await store.moveTask(source.id, "in-review");
await store.moveTask(source.id, "done");
const refined = await store.refineTask(source.id, "Needs polish");
expect(refined.sourceType).toBe("task_refine");
expect(refined.sourceParentTaskId).toBe(source.id);
});
it("preserves provenance on updateTask", async () => {
const task = await store.createTask({
description: "Will be updated",
source: {
sourceType: "automation",
sourceAgentId: "agent-auto",
sourceMetadata: { trigger: "nightly" },
},
});
await store.updateTask(task.id, { title: "Updated" });
const fetched = await store.getTask(task.id);
expect(fetched.sourceType).toBe("automation");
expect(fetched.sourceAgentId).toBe("agent-auto");
expect(fetched.sourceMetadata).toEqual({ trigger: "nightly" });
});
});
// ── Title Handling Tests ────────────────────────────────────────
describe("title handling", () => {

View File

@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
expect(tableNames.has("task_documents")).toBe(true);
expect(tableNames.has("task_document_revisions")).toBe(true);
expect(db.getSchemaVersion()).toBe(52);
expect(db.getSchemaVersion()).toBe(53);
const index = db
.prepare(

View File

@@ -86,7 +86,7 @@ export function probeFts5(db: DatabaseSync): boolean {
// ── Schema Definition ────────────────────────────────────────────────
const SCHEMA_VERSION = 52;
const SCHEMA_VERSION = 53;
function normalizeTaskComments(
steeringComments: SteeringComment[] | undefined,
@@ -211,7 +211,14 @@ CREATE TABLE IF NOT EXISTS tasks (
missionId TEXT,
sliceId TEXT,
assignedAgentId TEXT,
assigneeUserId TEXT
assigneeUserId TEXT,
sourceType TEXT,
sourceAgentId TEXT,
sourceRunId TEXT,
sourceSessionId TEXT,
sourceMessageId TEXT,
sourceParentTaskId TEXT,
sourceMetadata TEXT
);
-- Config table (single row with project settings)
@@ -1964,6 +1971,23 @@ export class Database {
});
}
// Task provenance/source tracking columns (FN-2917).
if (version < 53) {
this.applyMigration(53, () => {
this.addColumnIfMissing("tasks", "sourceType", "TEXT");
this.addColumnIfMissing("tasks", "sourceAgentId", "TEXT");
this.addColumnIfMissing("tasks", "sourceRunId", "TEXT");
this.addColumnIfMissing("tasks", "sourceSessionId", "TEXT");
this.addColumnIfMissing("tasks", "sourceMessageId", "TEXT");
this.addColumnIfMissing("tasks", "sourceParentTaskId", "TEXT");
this.addColumnIfMissing("tasks", "sourceMetadata", "TEXT");
this.db.prepare(
`UPDATE tasks SET sourceType = 'unknown' WHERE sourceType IS NULL`
).run();
});
}
}
/**

View File

@@ -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, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, validateMessageMetadata, normalizeMergeConflictStrategy } from "./types.js";
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, MergeConflictStrategy, CanonicalMergeConflictStrategy, Settings, GlobalSettings, ProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, TaskSource, SourceType, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, MergeConflictStrategy, CanonicalMergeConflictStrategy, Settings, GlobalSettings, ProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
export { AGENT_VALID_TRANSITIONS } from "./types.js";
export {
BUILTIN_AGENT_PROMPTS,

View File

@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
import { mkdir, readdir, readFile, writeFile, rename, unlink } from "node:fs/promises";
import { join } from "node:path";
import { existsSync, watch, type FSWatcher } from "node:fs";
import type { Task, TaskDetail, TaskCreateInput, TaskAttachment, AgentLogEntry, BoardConfig, Column, MergeResult, Settings, GlobalSettings, ProjectSettings, ActivityLogEntry, ActivityEventType, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, InboxTask, TaskLogEntry, RunMutationContext, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, ArchivedTaskEntry, ArchiveAgentLogMode, TaskPriority } from "./types.js";
import type { Task, TaskDetail, TaskCreateInput, TaskAttachment, AgentLogEntry, BoardConfig, Column, MergeResult, Settings, GlobalSettings, ProjectSettings, ActivityLogEntry, ActivityEventType, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, InboxTask, TaskLogEntry, RunMutationContext, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, ArchivedTaskEntry, ArchiveAgentLogMode, TaskPriority, SourceType } from "./types.js";
import { VALID_TRANSITIONS, DEFAULT_SETTINGS, isGlobalSettingsKey, WORKFLOW_STEP_TEMPLATES, validateDocumentKey } from "./types.js";
import { normalizeTaskPriority } from "./task-priority.js";
import { GlobalSettingsStore } from "./global-settings.js";
@@ -94,6 +94,13 @@ interface TaskRow {
nodeId: string | null;
effectiveNodeId: string | null;
effectiveNodeSource: string | null;
sourceType: string | null;
sourceAgentId: string | null;
sourceRunId: string | null;
sourceSessionId: string | null;
sourceMessageId: string | null;
sourceParentTaskId: string | null;
sourceMetadata: string | null;
checkedOutBy: string | null;
checkedOutAt: string | null;
}
@@ -621,6 +628,13 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
nodeId: row.nodeId || undefined,
effectiveNodeId: row.effectiveNodeId || undefined,
effectiveNodeSource: (row.effectiveNodeSource as Task["effectiveNodeSource"]) || undefined,
sourceType: (row.sourceType as SourceType) || undefined,
sourceAgentId: row.sourceAgentId || undefined,
sourceRunId: row.sourceRunId || undefined,
sourceSessionId: row.sourceSessionId || undefined,
sourceMessageId: row.sourceMessageId || undefined,
sourceParentTaskId: row.sourceParentTaskId || undefined,
sourceMetadata: fromJson<Record<string, unknown>>(row.sourceMetadata) ?? undefined,
checkedOutBy: row.checkedOutBy || undefined,
checkedOutAt: row.checkedOutAt || undefined,
};
@@ -844,6 +858,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
"attachments", "prInfo", "issueInfo", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
"breakIntoSubtasks", "enabledWorkflowSteps", "modifiedFiles",
"missionId", "sliceId", "assignedAgentId", "assigneeUserId", "nodeId", "effectiveNodeId", "effectiveNodeSource",
"sourceType", "sourceAgentId", "sourceRunId", "sourceSessionId", "sourceMessageId", "sourceParentTaskId", "sourceMetadata",
"checkedOutBy", "checkedOutAt",
// `log` is fetched in slim mode so the server can aggregate
// `timedExecutionMs` from `[timing] … in <N>ms` entries before
@@ -892,6 +907,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
"comments", "workflowStepResults", "prInfo", "issueInfo", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
"breakIntoSubtasks", "enabledWorkflowSteps", "modifiedFiles",
"missionId", "sliceId", "assignedAgentId", "assigneeUserId", "nodeId", "effectiveNodeId", "effectiveNodeSource",
"sourceType", "sourceAgentId", "sourceRunId", "sourceSessionId", "sourceMessageId", "sourceParentTaskId", "sourceMetadata",
"checkedOutBy", "checkedOutAt",
];
@@ -932,9 +948,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
dependencies, steps, log, attachments, steeringComments,
comments, workflowStepResults, prInfo, issueInfo,
sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, checkedOutBy, checkedOutAt
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
ON CONFLICT(id) DO UPDATE SET
title = excluded.title,
@@ -1005,6 +1021,13 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
nodeId = excluded.nodeId,
effectiveNodeId = excluded.effectiveNodeId,
effectiveNodeSource = excluded.effectiveNodeSource,
sourceType = excluded.sourceType,
sourceAgentId = excluded.sourceAgentId,
sourceRunId = excluded.sourceRunId,
sourceSessionId = excluded.sourceSessionId,
sourceMessageId = excluded.sourceMessageId,
sourceParentTaskId = excluded.sourceParentTaskId,
sourceMetadata = excluded.sourceMetadata,
checkedOutBy = excluded.checkedOutBy,
checkedOutAt = excluded.checkedOutAt
`).run(
@@ -1077,6 +1100,13 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
task.nodeId ?? null,
task.effectiveNodeId ?? null,
task.effectiveNodeSource ?? null,
task.sourceType ?? null,
task.sourceAgentId ?? null,
task.sourceRunId ?? null,
task.sourceSessionId ?? null,
task.sourceMessageId ?? null,
task.sourceParentTaskId ?? null,
toJsonNullable(task.sourceMetadata),
task.checkedOutBy ?? null,
task.checkedOutAt ?? null,
);
@@ -2071,6 +2101,13 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
priority: normalizeTaskPriority(input.priority),
tokenUsage: input.tokenUsage,
sourceIssue: input.sourceIssue,
sourceType: input.source?.sourceType ?? "unknown",
sourceAgentId: input.source?.sourceAgentId,
sourceRunId: input.source?.sourceRunId,
sourceSessionId: input.source?.sourceSessionId,
sourceMessageId: input.source?.sourceMessageId,
sourceParentTaskId: input.source?.sourceParentTaskId,
sourceMetadata: input.source?.sourceMetadata,
column: input.column || "triage",
dependencies: input.dependencies || [],
breakIntoSubtasks: input.breakIntoSubtasks === true ? true : undefined,
@@ -2137,6 +2174,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
priority: normalizeTaskPriority(sourceTask.priority),
column: "triage",
modelPresetId: sourceTask.modelPresetId,
sourceType: "task_duplicate",
sourceParentTaskId: id,
dependencies: [], // Fresh task should have no dependencies
steps: [], // Reset execution state
currentStep: 0,
@@ -2214,6 +2253,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
priority: normalizeTaskPriority(sourceTask.priority),
column: "triage",
dependencies: [id], // Refinement depends on the original being complete
sourceType: "task_refine",
sourceParentTaskId: id,
steps: [], // Reset execution state
currentStep: 0,
log: [{ timestamp: now, action: `Created as refinement of ${id}` }],

View File

@@ -785,6 +785,34 @@ export class CheckoutConflictError extends Error {
}
}
/** Origin types for task creation provenance tracking. */
export type SourceType =
| "dashboard_ui"
| "quick_chat"
| "chat_session"
| "agent_heartbeat"
| "automation"
| "cron"
| "workflow_step"
| "github_import"
| "task_refine"
| "task_duplicate"
| "cli"
| "api"
| "recovery"
| "unknown";
/** Provenance metadata for how a task was created. */
export interface TaskSource {
sourceType: SourceType;
sourceAgentId?: string;
sourceRunId?: string;
sourceSessionId?: string;
sourceMessageId?: string;
sourceParentTaskId?: string;
sourceMetadata?: Record<string, unknown>;
}
export interface Task {
id: string;
title?: string;
@@ -941,6 +969,14 @@ export interface Task {
effectiveNodeId?: string;
/** How the effectiveNodeId was determined. Set by the scheduler at dispatch time. */
effectiveNodeSource?: "task-override" | "project-default" | "local";
/** Provenance: how this task was created. */
sourceType?: SourceType;
sourceAgentId?: string;
sourceRunId?: string;
sourceSessionId?: string;
sourceMessageId?: string;
sourceParentTaskId?: string;
sourceMetadata?: Record<string, unknown>;
/** Explicitly assigned user ID for task-user linking. Used during review handoff to indicate
* which user should review the task. The sentinel value "requesting-user" indicates the
* user who created or steered the task. */
@@ -982,6 +1018,8 @@ export interface TaskCreateInput {
sourceIssue?: TaskSourceIssue;
/** Optional persisted aggregate token usage snapshot for task creation/import paths. */
tokenUsage?: TaskTokenUsage;
/** Provenance metadata for task creation. */
source?: TaskSource;
/**
* Optional task importance level. Omitted values default to `normal`.
*/