feat(FN-2922): merge fusion/fn-2922
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ describe("Run Audit", () => {
|
||||
});
|
||||
|
||||
it("schema version is bumped to 40", () => {
|
||||
expect(db.getSchemaVersion()).toBe(52);
|
||||
expect(db.getSchemaVersion()).toBe(53);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}` }],
|
||||
|
||||
@@ -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`.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user