Merge branch 'main' into timothyjlaurent/Anthropic-error

This commit is contained in:
gsxdsm
2026-05-09 10:42:33 -07:00
committed by GitHub
690 changed files with 59296 additions and 22734 deletions

View File

@@ -3,17 +3,17 @@ 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, SourceType, WorkflowStepTemplate } 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, WorkflowStepTemplate, Agent } from "./types.js";
import { createActivityLogSnapshot, createRunAuditSnapshot, createTaskMetadataSnapshot, toTaskMetadataRecord, validateSnapshotEnvelope, type ActivityLogSnapshot, type RunAuditSnapshot, type TaskMetadataSnapshot } from "./shared-mesh-state.js";
import { VALID_TRANSITIONS, DEFAULT_SETTINGS, isGlobalSettingsKey, WORKFLOW_STEP_TEMPLATES, validateDocumentKey } from "./types.js";
import { normalizeTaskPriority } from "./task-priority.js";
import { canAgentTakeImplementationTask } from "./agent-role-policy.js";
import { GlobalSettingsStore } from "./global-settings.js";
import { Database, toJson, toJsonNullable, fromJson } from "./db.js";
import { ArchiveDatabase } from "./archive-db.js";
import { detectLegacyData, migrateFromLegacy } from "./db-migrate.js";
import { MissionStore } from "./mission-store.js";
import { PluginStore } from "./plugin-store.js";
import { RoadmapStore } from "./roadmap-store.js";
import { InsightStore } from "./insight-store.js";
import { ResearchStore } from "./research-store.js";
import { TodoStore } from "./todo-store.js";
@@ -90,6 +90,8 @@ interface TaskRow {
attachments: string | null;
steeringComments: string | null;
comments: string | null;
review: string | null;
reviewState: string | null;
workflowStepResults: string | null;
prInfo: string | null;
issueInfo: string | null;
@@ -177,6 +179,40 @@ interface ActivityLogRow {
metadata: string | null;
}
function normalizeTaskReviewState(reviewState: Task["reviewState"] | undefined): Task["reviewState"] | undefined {
if (!reviewState) {
return undefined;
}
const itemsById = new Map(reviewState.items.map((item) => [item.id, item]));
const sourceMode = reviewState.source;
const normalizedAddressing = reviewState.addressing.map((record) => {
const item = itemsById.get(record.itemId);
const source = item?.source === "reviewer-agent" ? "reviewer-agent" : "pr-review";
const summary = item?.summary?.trim() || item?.body?.trim().slice(0, 160) || `Review item ${record.itemId}`;
const body = item?.body ?? summary;
return {
...record,
snapshot: record.snapshot ?? {
itemId: record.itemId,
sourceMode,
source,
summary,
body,
authorLogin: item?.author?.login,
filePath: item?.path,
threadId: item?.threadId,
url: item?.htmlUrl,
},
};
});
return {
...reviewState,
addressing: normalizedAddressing,
};
}
const TASK_ACTIVITY_LOG_ENTRY_LIMIT = 1_000;
const TASK_ACTIVITY_LOG_OUTCOME_LIMIT = 4_000;
const ARCHIVE_AGENT_LOG_SNAPSHOT_LIMIT = 25;
@@ -511,8 +547,6 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
private missionStore: MissionStore | null = null;
/** Cached PluginStore instance */
private pluginStore: PluginStore | null = null;
/** Cached RoadmapStore instance */
private roadmapStore: RoadmapStore | null = null;
/** Cached InsightStore instance */
private insightStore: InsightStore | null = null;
/** Cached ResearchStore instance */
@@ -548,6 +582,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
// Tests that need cross-instance persistence (open store A, close,
// open store B on the same dir, expect data) must leave this false.
private readonly inMemoryDb: boolean;
private readonly globalSettingsDir?: string;
constructor(
private rootDir: string,
@@ -563,6 +598,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
this.inMemoryDb = options?.inMemoryDb === true;
const resolvedGlobalSettingsDir = globalSettingsDir
?? (process.env.VITEST === "true" ? join(rootDir, ".fusion-global-settings") : undefined);
this.globalSettingsDir = resolvedGlobalSettingsDir;
this.globalSettingsStore = new GlobalSettingsStore(resolvedGlobalSettingsDir);
}
@@ -751,6 +787,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
});
return deduped.length > 0 ? deduped : undefined;
})(),
review: fromJson<import("./types.js").TaskReview>(row.review) ?? undefined,
reviewState: normalizeTaskReviewState(fromJson<import("./types.js").TaskReviewState>(row.reviewState) ?? undefined),
workflowStepResults: (() => { const w = fromJson<import("./types.js").WorkflowStepResult[]>(row.workflowStepResults); return w && w.length > 0 ? w : undefined; })(),
prInfo: fromJson<import("./types.js").PrInfo>(row.prInfo),
issueInfo: fromJson<import("./types.js").IssueInfo>(row.issueInfo),
@@ -813,6 +851,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
sourceIssue: slim ? undefined : entry.sourceIssue,
attachments: slim ? undefined : entry.attachments,
comments: entry.comments,
review: slim ? undefined : entry.review,
log: slim ? [] : entry.log ?? [],
timedExecutionMs: slim ? this.computeTimedExecutionMs(entry.log) : undefined,
createdAt: entry.createdAt,
@@ -936,6 +975,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
sourceIssue: task.sourceIssue,
attachments: task.attachments,
comments: task.comments,
review: task.review,
reviewState: task.reviewState,
prompt,
...agentLogFields,
log: [{ timestamp: archivedAt, action: "Task archived" }],
@@ -1014,7 +1055,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
"error", "summary", "thinkingLevel", "executionMode",
"tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt",
"createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt",
"dependencies", "steps", "comments", "workflowStepResults", "steeringComments",
"dependencies", "steps", "comments", "review", "reviewState", "workflowStepResults", "steeringComments",
"attachments", "prInfo", "issueInfo", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
"breakIntoSubtasks", "enabledWorkflowSteps", "modifiedFiles",
"missionId", "sliceId", "assignedAgentId", "pausedByAgentId", "assigneeUserId", "nodeId", "effectiveNodeId", "effectiveNodeSource",
@@ -1064,7 +1105,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
"tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt",
"createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt",
"dependencies", "steps", "attachments", "steeringComments",
"comments", "workflowStepResults", "prInfo", "issueInfo", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
"comments", "review", "reviewState", "workflowStepResults", "prInfo", "issueInfo", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
"breakIntoSubtasks", "enabledWorkflowSteps", "modifiedFiles",
"missionId", "sliceId", "assignedAgentId", "pausedByAgentId", "assigneeUserId", "nodeId", "effectiveNodeId", "effectiveNodeSource",
"sourceType", "sourceAgentId", "sourceRunId", "sourceSessionId", "sourceMessageId", "sourceParentTaskId", "sourceMetadata",
@@ -1107,11 +1148,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
executionStartedAt, executionCompletedAt,
dependencies, steps, log, attachments, steeringComments,
comments, workflowStepResults, prInfo, issueInfo,
comments, review, reviewState, workflowStepResults, prInfo, issueInfo,
sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
ON CONFLICT(id) DO UPDATE SET
title = excluded.title,
@@ -1166,6 +1207,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
attachments = excluded.attachments,
steeringComments = excluded.steeringComments,
comments = excluded.comments,
review = excluded.review,
reviewState = excluded.reviewState,
workflowStepResults = excluded.workflowStepResults,
prInfo = excluded.prInfo,
issueInfo = excluded.issueInfo,
@@ -1249,6 +1292,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
toJson(task.attachments || []),
toJson(task.steeringComments || []),
toJson(task.comments || []),
toJsonNullable(task.review),
toJsonNullable(task.reviewState),
toJson(task.workflowStepResults || []),
toJsonNullable(task.prInfo),
toJsonNullable(task.issueInfo),
@@ -2714,6 +2759,51 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
return sorted.slice(offset, offset + Math.max(0, limit));
}
/**
* List slim task rows with `updatedAt` strictly greater than the cursor.
*
* Uses strict `>` cursor semantics (rows where `updatedAt === since` are excluded),
* returns rows ordered by `updatedAt ASC`, defaults limit to 50, and caps at 200.
* Archived tasks are excluded by default unless `opts.includeArchived` is true.
*
* Callers should re-invoke this method with the last returned task's `updatedAt`
* as the next `since` cursor.
*/
async listTasksModifiedSince(
since: string,
limit?: number,
opts?: { includeArchived?: boolean },
): Promise<{ tasks: Task[]; hasMore: boolean }> {
if (Number.isNaN(Date.parse(since))) {
throw new TypeError("listTasksModifiedSince: invalid since cursor");
}
const defaultLimit = 50;
const resolvedLimit = typeof limit !== "number" || !Number.isFinite(limit)
? defaultLimit
: Math.max(1, Math.min(200, Math.floor(limit)));
const includeArchived = opts?.includeArchived ?? false;
const selectClause = this.getTaskSelectClause(true);
const rows = includeArchived
? (this.db.prepare(
`SELECT ${selectClause} FROM tasks WHERE updatedAt > ? ORDER BY updatedAt ASC LIMIT ?`,
).all(since, resolvedLimit + 1) as TaskRow[])
: (this.db.prepare(
`SELECT ${selectClause} FROM tasks WHERE updatedAt > ? AND "column" != 'archived' ORDER BY updatedAt ASC LIMIT ?`,
).all(since, resolvedLimit + 1) as TaskRow[]);
const hasMore = rows.length > resolvedLimit;
const tasks = rows.slice(0, resolvedLimit).map((row) => {
const task = this.rowToTask(row);
task.timedExecutionMs = this.computeTimedExecutionMs(task.log);
task.log = [];
return task;
});
return { tasks, hasMore };
}
/**
* Returns the ID of a task currently in an active merge status ("merging" or
* "merging-pr"), optionally excluding a specific task ID.
@@ -2861,7 +2951,10 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
return rows.map((row) => this.rowToTask(row));
}
async selectNextTaskForAgent(agentId: string): Promise<InboxTask | null> {
async selectNextTaskForAgent(
agentId: string,
agent?: Pick<Agent, "id" | "role">,
): Promise<InboxTask | null> {
const tasks = await this.listTasks({ slim: true });
if (tasks.length === 0) {
return null;
@@ -2887,7 +2980,16 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
};
}
const todoCandidates = assignedTasks.filter((task) => task.column === "todo" && task.paused !== true);
const roleCompatibleAssignedTasks = agent
? assignedTasks.filter((task) => {
if (task.column === "in-progress") {
return true;
}
return canAgentTakeImplementationTask(agent, task);
})
: assignedTasks;
const todoCandidates = roleCompatibleAssignedTasks.filter((task) => task.column === "todo" && task.paused !== true);
const readyTodo = todoCandidates
.filter((task) => {
@@ -3185,7 +3287,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
async updateTask(
id: string,
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; currentStep?: number; blockedBy?: string | null; assignedAgentId?: string | null; pausedByAgentId?: string | null; assigneeUserId?: string | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; executionStartBranch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; postReviewFixCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; verificationFailureCount?: number | null; mergeConflictBounceCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; executionStartedAt?: string | null; executionCompletedAt?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; currentStep?: number; blockedBy?: string | null; assignedAgentId?: string | null; pausedByAgentId?: string | null; assigneeUserId?: string | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; executionStartBranch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; postReviewFixCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; verificationFailureCount?: number | null; mergeConflictBounceCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; executionStartedAt?: string | null; executionCompletedAt?: string | null; review?: import("./types.js").TaskReview | null; reviewState?: import("./types.js").TaskReviewState | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
runContext?: RunMutationContext,
): Promise<Task> {
return this.withTaskLock(id, async () => {
@@ -3455,6 +3557,16 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
} else if (updates.executionCompletedAt !== undefined) {
task.executionCompletedAt = updates.executionCompletedAt;
}
if (updates.review === null) {
task.review = undefined;
} else if (updates.review !== undefined) {
task.review = updates.review;
}
if (updates.reviewState === null) {
task.reviewState = undefined;
} else if (updates.reviewState !== undefined) {
task.reviewState = normalizeTaskReviewState(updates.reviewState);
}
if (updates.workflowStepResults === null) {
task.workflowStepResults = undefined;
} else if (updates.workflowStepResults !== undefined) {
@@ -6103,6 +6215,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
size: entry.size,
reviewLevel: entry.reviewLevel,
prInfo: entry.prInfo,
review: entry.review,
issueInfo: entry.issueInfo,
sourceIssue: entry.sourceIssue,
attachments: entry.attachments,
@@ -6794,22 +6907,13 @@ ${notificationsSection}`;
*/
getPluginStore(): PluginStore {
if (!this.pluginStore) {
this.pluginStore = new PluginStore(this.rootDir);
// PluginStore persists install/state rows in central DB, so it must use
// the same resolved global settings directory as TaskStore.
this.pluginStore = new PluginStore(this.rootDir, { centralGlobalDir: this.globalSettingsDir });
}
return this.pluginStore;
}
/**
* Get the RoadmapStore instance for standalone roadmap operations.
* Lazily initializes the RoadmapStore on first access.
*/
getRoadmapStore(): RoadmapStore {
if (!this.roadmapStore) {
this.roadmapStore = new RoadmapStore(this.db);
}
return this.roadmapStore;
}
/**
* Get the InsightStore instance for project insights operations.
* Lazily initializes the InsightStore on first access.