Files
openclaw/extensions/workboard/src/store.ts
T
2026-05-30 04:56:14 -04:00

3047 lines
100 KiB
TypeScript

import { randomUUID } from "node:crypto";
import {
WORKBOARD_DIAGNOSTIC_KINDS,
WORKBOARD_DIAGNOSTIC_SEVERITIES,
WORKBOARD_EXECUTION_ENGINES,
WORKBOARD_EXECUTION_MODES,
WORKBOARD_EXECUTION_STATUSES,
WORKBOARD_ATTEMPT_STATUSES,
WORKBOARD_EVENT_KINDS,
WORKBOARD_LINK_TYPES,
WORKBOARD_NOTIFICATION_KINDS,
WORKBOARD_PRIORITIES,
WORKBOARD_PROOF_STATUSES,
WORKBOARD_STATUSES,
WORKBOARD_TEMPLATE_IDS,
type WorkboardCard,
type WorkboardArtifact,
type WorkboardAttemptStatus,
type WorkboardAutomation,
type WorkboardClaim,
type WorkboardComment,
type WorkboardDiagnostic,
type WorkboardDiagnosticAction,
type WorkboardDiagnosticKind,
type WorkboardDiagnosticSeverity,
type WorkboardEvent,
type WorkboardEventKind,
type WorkboardExecution,
type WorkboardExecutionEngine,
type WorkboardExecutionMode,
type WorkboardExecutionStatus,
type WorkboardLink,
type WorkboardLinkType,
type WorkboardMetadata,
type WorkboardNotification,
type WorkboardNotificationKind,
type WorkboardPriority,
type WorkboardProof,
type WorkboardProofStatus,
type WorkboardRunAttempt,
type WorkboardStatus,
type WorkboardTemplateId,
type WorkboardWorkspace,
} from "./types.js";
const POSITION_STEP = 1000;
const MAX_CARDS = 2000;
const MAX_CARD_EVENTS = 50;
const MAX_CARD_ATTEMPTS = 30;
const MAX_CARD_COMMENTS = 50;
const MAX_CARD_LINKS = 50;
const MAX_CARD_PROOF = 40;
const MAX_CARD_ARTIFACTS = 40;
const MAX_CARD_DIAGNOSTICS = 12;
const MAX_CARD_NOTIFICATIONS = 20;
const MAX_CARD_METADATA_BYTES = 24 * 1024;
const DEFAULT_CLAIM_TTL_MS = 30 * 60 * 1000;
const READY_STRANDED_MS = 60 * 60 * 1000;
const RUNNING_HEARTBEAT_STALE_MS = 20 * 60 * 1000;
const BLOCKED_TOO_LONG_MS = 24 * 60 * 60 * 1000;
const CLAIM_RECLAIM_MS = 5 * 60 * 1000;
const MAX_SAFE_WORKBOARD_DURATION_MS = Number.MAX_SAFE_INTEGER;
function secondsToDurationMs(seconds: number): number {
const ms = Math.trunc(seconds) * 1000;
return Number.isFinite(ms)
? Math.min(MAX_SAFE_WORKBOARD_DURATION_MS, Math.max(1, ms))
: MAX_SAFE_WORKBOARD_DURATION_MS;
}
function addWorkboardDurationMs(now: number, durationMs: number): number {
const expiresAt = now + durationMs;
return Number.isFinite(expiresAt)
? Math.min(MAX_SAFE_WORKBOARD_DURATION_MS, expiresAt)
: MAX_SAFE_WORKBOARD_DURATION_MS;
}
export type PersistedWorkboardCard = {
version: 1;
card: WorkboardCard;
};
export type WorkboardKeyedStore = {
register(key: string, value: PersistedWorkboardCard): Promise<void>;
lookup(key: string): Promise<PersistedWorkboardCard | undefined>;
delete(key: string): Promise<boolean>;
entries(): Promise<Array<{ key: string; value: PersistedWorkboardCard }>>;
};
export type WorkboardCardInput = {
title?: unknown;
notes?: unknown;
status?: unknown;
priority?: unknown;
labels?: unknown;
agentId?: unknown;
sessionKey?: unknown;
runId?: unknown;
taskId?: unknown;
sourceUrl?: unknown;
execution?: unknown;
metadata?: unknown;
templateId?: unknown;
position?: unknown;
tenant?: unknown;
boardId?: unknown;
createdByCardId?: unknown;
idempotencyKey?: unknown;
skills?: unknown;
workspace?: unknown;
maxRuntimeSeconds?: unknown;
maxRetries?: unknown;
scheduledAt?: unknown;
startedAt?: unknown;
completedAt?: unknown;
parents?: unknown;
};
export type WorkboardCardPatch = Partial<WorkboardCardInput>;
export type WorkboardCommentInput = { body?: unknown };
export type WorkboardLinkInput = {
type?: unknown;
targetCardId?: unknown;
title?: unknown;
url?: unknown;
};
export type WorkboardLinkedCreateInput = WorkboardCardInput & {
parents?: unknown;
};
export type WorkboardProofInput = {
status?: unknown;
label?: unknown;
command?: unknown;
url?: unknown;
note?: unknown;
};
export type WorkboardArtifactInput = {
label?: unknown;
url?: unknown;
path?: unknown;
mimeType?: unknown;
};
export type WorkboardClaimInput = {
ownerId?: unknown;
token?: unknown;
ttlSeconds?: unknown;
};
export type WorkboardHeartbeatInput = {
token?: unknown;
ownerId?: unknown;
note?: unknown;
};
export type WorkboardBulkInput = {
ids?: unknown;
patch?: unknown;
archived?: unknown;
};
export type WorkboardCompleteInput = {
ownerId?: unknown;
token?: unknown;
summary?: unknown;
proof?: unknown;
artifacts?: unknown;
createdCardIds?: unknown;
};
export type WorkboardBlockInput = {
ownerId?: unknown;
token?: unknown;
reason?: unknown;
};
export type WorkboardDispatchResult = {
promoted: WorkboardCard[];
reclaimed: WorkboardCard[];
blocked: WorkboardCard[];
count: number;
};
export type WorkboardListOptions = {
boardId?: unknown;
};
export type WorkboardBoardSummary = {
id: string;
total: number;
active: number;
archived: number;
byStatus: Partial<Record<WorkboardStatus, number>>;
updatedAt?: number;
};
export type WorkboardStatsResult = WorkboardBoardSummary & {
byAgent: Record<string, number>;
oldestReadyAgeMs?: number;
};
export type WorkboardPromoteInput = {
force?: unknown;
reason?: unknown;
};
export type WorkboardReassignInput = {
agentId?: unknown;
status?: unknown;
resetFailures?: unknown;
reason?: unknown;
};
export type WorkboardReclaimInput = {
status?: unknown;
reason?: unknown;
};
export type WorkboardMutationScope = {
ownerId?: unknown;
token?: unknown;
};
export type WorkboardDiagnosticsResult = {
diagnostics: Array<{
card: WorkboardCard;
diagnostics: WorkboardDiagnostic[];
}>;
count: number;
};
function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function normalizeBoardId(value: unknown, fallback?: string): string | undefined {
const raw = normalizeBoundedString(value, fallback, 80, "board id");
if (!raw) {
return undefined;
}
const boardId = raw.toLowerCase();
if (!/^[a-z0-9][a-z0-9._-]{0,79}$/.test(boardId)) {
throw new Error(
"board id must start with a letter or number and use letters, numbers, dots, dashes, or underscores.",
);
}
return boardId;
}
function normalizeTitle(value: unknown): string {
const title = normalizeOptionalString(value);
if (!title) {
throw new Error("title is required.");
}
if (title.length > 180) {
throw new Error("title must be 180 characters or fewer.");
}
return title;
}
function normalizeNotes(value: unknown): string | undefined {
const notes = normalizeOptionalString(value);
if (!notes) {
return undefined;
}
if (notes.length > 4000) {
throw new Error("notes must be 4000 characters or fewer.");
}
return notes;
}
function normalizeBoundedString(
value: unknown,
fallback: string | undefined,
maxLength: number,
fieldName: string,
): string | undefined {
const normalized = normalizeOptionalString(value);
if (!normalized) {
return fallback;
}
if (normalized.length > maxLength) {
throw new Error(`${fieldName} must be ${maxLength} characters or fewer.`);
}
return normalized;
}
function normalizeStatus(value: unknown, fallback: WorkboardStatus): WorkboardStatus {
if (typeof value !== "string" || !value.trim()) {
return fallback;
}
if ((WORKBOARD_STATUSES as readonly string[]).includes(value)) {
return value as WorkboardStatus;
}
throw new Error(`status must be one of: ${WORKBOARD_STATUSES.join(", ")}.`);
}
function normalizePriority(value: unknown, fallback: WorkboardPriority): WorkboardPriority {
if (typeof value !== "string" || !value.trim()) {
return fallback;
}
if ((WORKBOARD_PRIORITIES as readonly string[]).includes(value)) {
return value as WorkboardPriority;
}
throw new Error(`priority must be one of: ${WORKBOARD_PRIORITIES.join(", ")}.`);
}
function normalizeLabels(value: unknown, fallback: string[] = []): string[] {
if (value == null) {
return fallback;
}
const entries =
typeof value === "string" ? value.split(",") : Array.isArray(value) ? value : undefined;
if (!entries) {
throw new Error("labels must be an array or comma-separated string.");
}
const labels: string[] = [];
for (const entry of entries) {
const label = normalizeOptionalString(entry);
if (!label || labels.includes(label)) {
continue;
}
if (label.length > 40) {
throw new Error("labels must be 40 characters or fewer.");
}
labels.push(label);
if (labels.length >= 12) {
break;
}
}
return labels;
}
function normalizeStringList(value: unknown, fieldName: string, maxLength = 80): string[] {
if (value == null) {
return [];
}
const entries =
typeof value === "string" ? value.split(",") : Array.isArray(value) ? value : undefined;
if (!entries) {
throw new Error(`${fieldName} must be an array or comma-separated string.`);
}
const values: string[] = [];
for (const entry of entries) {
if (Array.isArray(value) && typeof entry !== "string") {
throw new Error(`${fieldName} entries must be strings.`);
}
const normalized = normalizeBoundedString(entry, undefined, maxLength, fieldName);
if (normalized && !values.includes(normalized)) {
values.push(normalized);
}
if (values.length > 20) {
throw new Error(`${fieldName} supports at most 20 entries.`);
}
}
return values;
}
function normalizePosition(value: unknown, fallback: number): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
return fallback;
}
return Math.max(0, Math.trunc(value));
}
function normalizePositiveInteger(value: unknown, fieldName: string): number | undefined {
if (value == null || value === "") {
return undefined;
}
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new Error(`${fieldName} must be a number.`);
}
return Math.max(1, Math.trunc(value));
}
function isAbsoluteWorkspacePath(value: string): boolean {
return (
value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value) || /^\\\\[^\\]+\\[^\\]+/.test(value)
);
}
function normalizeWorkspace(
value: unknown,
fallback?: WorkboardWorkspace,
): WorkboardWorkspace | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return fallback;
}
const record = value as Record<string, unknown>;
const kind =
record.kind === "scratch" || record.kind === "dir" || record.kind === "worktree"
? record.kind
: fallback?.kind;
if (!kind) {
throw new Error("workspace kind must be scratch, dir, or worktree.");
}
const workspacePath = normalizeBoundedString(record.path, fallback?.path, 2000, "workspace path");
if (kind === "dir" && (!workspacePath || !isAbsoluteWorkspacePath(workspacePath))) {
throw new Error("dir workspace path must be absolute.");
}
const branch = normalizeBoundedString(record.branch, fallback?.branch, 160, "workspace branch");
return {
kind,
...(workspacePath ? { path: workspacePath } : {}),
...(branch ? { branch } : {}),
};
}
function normalizeAutomation(
value: unknown,
fallback: WorkboardAutomation = {},
): WorkboardAutomation | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return Object.keys(fallback).length ? fallback : undefined;
}
const record = value as Record<string, unknown>;
const tenant = normalizeBoundedString(record.tenant, fallback.tenant, 80, "tenant");
const boardId = Object.hasOwn(record, "boardId")
? normalizeBoardId(record.boardId, fallback.boardId)
: fallback.boardId;
const createdByCardId = normalizeBoundedString(
record.createdByCardId,
fallback.createdByCardId,
120,
"created by card id",
);
const idempotencyKey = normalizeBoundedString(
record.idempotencyKey,
fallback.idempotencyKey,
160,
"idempotency key",
);
const summary = normalizeBoundedString(record.summary, fallback.summary, 2000, "summary");
const skills = Object.hasOwn(record, "skills")
? normalizeStringList(record.skills, "skills")
: fallback.skills;
const createdCardIds = Object.hasOwn(record, "createdCardIds")
? normalizeStringList(record.createdCardIds, "created card ids", 120)
: fallback.createdCardIds;
const scheduledAt = Object.hasOwn(record, "scheduledAt")
? normalizeTimestamp(record.scheduledAt, 0) || undefined
: fallback.scheduledAt;
const maxRuntimeSeconds = Object.hasOwn(record, "maxRuntimeSeconds")
? normalizePositiveInteger(record.maxRuntimeSeconds, "max runtime seconds")
: fallback.maxRuntimeSeconds;
const maxRetries = Object.hasOwn(record, "maxRetries")
? normalizePositiveInteger(record.maxRetries, "max retries")
: fallback.maxRetries;
const dispatchCount = Object.hasOwn(record, "dispatchCount")
? normalizeTimestamp(record.dispatchCount, 0) || undefined
: fallback.dispatchCount;
const lastDispatchAt = Object.hasOwn(record, "lastDispatchAt")
? normalizeTimestamp(record.lastDispatchAt, 0) || undefined
: fallback.lastDispatchAt;
const workspace = Object.hasOwn(record, "workspace")
? normalizeWorkspace(record.workspace, fallback.workspace)
: fallback.workspace;
const next = removeUndefinedAutomationFields({
...(tenant ? { tenant } : {}),
...(boardId ? { boardId } : {}),
...(createdByCardId ? { createdByCardId } : {}),
...(idempotencyKey ? { idempotencyKey } : {}),
...(skills?.length ? { skills } : {}),
...(workspace ? { workspace } : {}),
...(maxRuntimeSeconds ? { maxRuntimeSeconds } : {}),
...(maxRetries ? { maxRetries } : {}),
...(scheduledAt ? { scheduledAt } : {}),
...(summary ? { summary } : {}),
...(createdCardIds?.length ? { createdCardIds } : {}),
...(dispatchCount ? { dispatchCount } : {}),
...(lastDispatchAt ? { lastDispatchAt } : {}),
});
return Object.keys(next).length ? next : undefined;
}
function normalizeExecutionEngine(
value: unknown,
fallback: WorkboardExecutionEngine,
): WorkboardExecutionEngine {
if (
typeof value === "string" &&
WORKBOARD_EXECUTION_ENGINES.includes(value as WorkboardExecutionEngine)
) {
return value as WorkboardExecutionEngine;
}
return fallback;
}
function normalizeExecutionMode(
value: unknown,
fallback: WorkboardExecutionMode,
): WorkboardExecutionMode {
if (
typeof value === "string" &&
WORKBOARD_EXECUTION_MODES.includes(value as WorkboardExecutionMode)
) {
return value as WorkboardExecutionMode;
}
return fallback;
}
function normalizeExecutionStatus(
value: unknown,
fallback: WorkboardExecutionStatus,
): WorkboardExecutionStatus {
if (
typeof value === "string" &&
WORKBOARD_EXECUTION_STATUSES.includes(value as WorkboardExecutionStatus)
) {
return value as WorkboardExecutionStatus;
}
return fallback;
}
function normalizeAttemptStatus(
value: unknown,
fallback: WorkboardAttemptStatus,
): WorkboardAttemptStatus {
if (
typeof value === "string" &&
WORKBOARD_ATTEMPT_STATUSES.includes(value as WorkboardAttemptStatus)
) {
return value as WorkboardAttemptStatus;
}
return fallback;
}
function normalizeLinkType(value: unknown, fallback: WorkboardLinkType): WorkboardLinkType {
if (typeof value === "string" && WORKBOARD_LINK_TYPES.includes(value as WorkboardLinkType)) {
return value as WorkboardLinkType;
}
return fallback;
}
function normalizeProofStatus(
value: unknown,
fallback: WorkboardProofStatus,
): WorkboardProofStatus {
if (
typeof value === "string" &&
WORKBOARD_PROOF_STATUSES.includes(value as WorkboardProofStatus)
) {
return value as WorkboardProofStatus;
}
return fallback;
}
function normalizeTemplateId(value: unknown): WorkboardTemplateId | undefined {
return typeof value === "string" && WORKBOARD_TEMPLATE_IDS.includes(value as WorkboardTemplateId)
? (value as WorkboardTemplateId)
: undefined;
}
function normalizeTimestamp(value: unknown, fallback: number): number {
return typeof value === "number" && Number.isFinite(value)
? Math.max(0, Math.trunc(value))
: fallback;
}
function normalizeEvent(value: unknown): WorkboardEvent | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const record = value as Record<string, unknown>;
const id = normalizeOptionalString(record.id);
const kind = WORKBOARD_EVENT_KINDS.includes(record.kind as WorkboardEventKind)
? (record.kind as WorkboardEventKind)
: null;
const at = normalizeTimestamp(record.at, 0);
if (!id || !kind || !at) {
return null;
}
const fromStatus =
typeof record.fromStatus === "string" &&
WORKBOARD_STATUSES.includes(record.fromStatus as WorkboardStatus)
? (record.fromStatus as WorkboardStatus)
: undefined;
const toStatus =
typeof record.toStatus === "string" &&
WORKBOARD_STATUSES.includes(record.toStatus as WorkboardStatus)
? (record.toStatus as WorkboardStatus)
: undefined;
const sessionKey = normalizeOptionalString(record.sessionKey);
const runId = normalizeOptionalString(record.runId);
return {
id,
kind,
at,
...(fromStatus ? { fromStatus } : {}),
...(toStatus ? { toStatus } : {}),
...(sessionKey ? { sessionKey } : {}),
...(runId ? { runId } : {}),
};
}
function normalizeEvents(value: unknown): WorkboardEvent[] {
if (!Array.isArray(value)) {
return [];
}
return value
.map(normalizeEvent)
.filter((event): event is WorkboardEvent => event !== null)
.slice(-MAX_CARD_EVENTS);
}
function normalizeAttempt(value: unknown): WorkboardRunAttempt | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const record = value as Record<string, unknown>;
const id = normalizeOptionalString(record.id);
const startedAt = normalizeTimestamp(record.startedAt, 0);
if (!id || !startedAt) {
return null;
}
const endedAt = normalizeTimestamp(record.endedAt, 0);
const sessionKey = normalizeOptionalString(record.sessionKey);
const runId = normalizeOptionalString(record.runId);
const error = normalizeBoundedString(record.error, undefined, 800, "attempt error");
const model = normalizeBoundedString(record.model, undefined, 160, "attempt model");
return {
id,
status: normalizeAttemptStatus(record.status, "running"),
startedAt,
...(endedAt ? { endedAt } : {}),
...(typeof record.engine === "string" &&
WORKBOARD_EXECUTION_ENGINES.includes(record.engine as WorkboardExecutionEngine)
? { engine: record.engine as WorkboardExecutionEngine }
: {}),
...(typeof record.mode === "string" &&
WORKBOARD_EXECUTION_MODES.includes(record.mode as WorkboardExecutionMode)
? { mode: record.mode as WorkboardExecutionMode }
: {}),
...(model ? { model } : {}),
...(sessionKey ? { sessionKey } : {}),
...(runId ? { runId } : {}),
...(error ? { error } : {}),
};
}
function normalizeComment(value: unknown): WorkboardComment | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const record = value as Record<string, unknown>;
const id = normalizeOptionalString(record.id);
const body = normalizeBoundedString(record.body, undefined, 2000, "comment body");
const createdAt = normalizeTimestamp(record.createdAt, 0);
if (!id || !body || !createdAt) {
return null;
}
const updatedAt = normalizeTimestamp(record.updatedAt, 0);
return { id, body, createdAt, ...(updatedAt ? { updatedAt } : {}) };
}
function normalizeLink(value: unknown): WorkboardLink | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const record = value as Record<string, unknown>;
const id = normalizeOptionalString(record.id);
const createdAt = normalizeTimestamp(record.createdAt, 0);
if (!id || !createdAt) {
return null;
}
const targetCardId = normalizeBoundedString(record.targetCardId, undefined, 120, "link target");
const title = normalizeBoundedString(record.title, undefined, 180, "link title");
const url = normalizeBoundedString(record.url, undefined, 2000, "link URL");
if (!targetCardId && !url) {
return null;
}
return {
id,
type: normalizeLinkType(record.type, "relates_to"),
createdAt,
...(targetCardId ? { targetCardId } : {}),
...(title ? { title } : {}),
...(url ? { url } : {}),
};
}
function isDependencyLink(link: WorkboardLink): boolean {
return link.type === "parent" || link.type === "child";
}
function normalizeProof(value: unknown): WorkboardProof | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const record = value as Record<string, unknown>;
const id = normalizeOptionalString(record.id);
const createdAt = normalizeTimestamp(record.createdAt, 0);
if (!id || !createdAt) {
return null;
}
const label = normalizeBoundedString(record.label, undefined, 160, "proof label");
const command = normalizeBoundedString(record.command, undefined, 1000, "proof command");
const url = normalizeBoundedString(record.url, undefined, 2000, "proof URL");
const note = normalizeBoundedString(record.note, undefined, 2000, "proof note");
return {
id,
status: normalizeProofStatus(record.status, "unknown"),
createdAt,
...(label ? { label } : {}),
...(command ? { command } : {}),
...(url ? { url } : {}),
...(note ? { note } : {}),
};
}
function normalizeArtifact(value: unknown): WorkboardArtifact | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const record = value as Record<string, unknown>;
const id = normalizeOptionalString(record.id) ?? randomUUID();
const createdAt = normalizeTimestamp(record.createdAt, Date.now());
const label = normalizeBoundedString(record.label, undefined, 160, "artifact label");
const url = normalizeBoundedString(record.url, undefined, 2000, "artifact URL");
const artifactPath = normalizeBoundedString(record.path, undefined, 2000, "artifact path");
const mimeType = normalizeBoundedString(record.mimeType, undefined, 160, "artifact MIME type");
if (!url && !artifactPath) {
return null;
}
return {
id,
createdAt,
...(label ? { label } : {}),
...(url ? { url } : {}),
...(artifactPath ? { path: artifactPath } : {}),
...(mimeType ? { mimeType } : {}),
};
}
function normalizeClaim(value: unknown, fallback?: WorkboardClaim): WorkboardClaim | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return fallback;
}
const record = value as Record<string, unknown>;
const ownerId = normalizeBoundedString(record.ownerId, fallback?.ownerId, 120, "claim owner");
const token = normalizeBoundedString(record.token, fallback?.token, 160, "claim token");
const claimedAt = normalizeTimestamp(record.claimedAt, fallback?.claimedAt ?? Date.now());
const lastHeartbeatAt = normalizeTimestamp(
record.lastHeartbeatAt,
fallback?.lastHeartbeatAt ?? claimedAt,
);
const expiresAt = normalizeTimestamp(record.expiresAt, fallback?.expiresAt ?? 0);
if (!ownerId || !token || !claimedAt || !lastHeartbeatAt) {
return undefined;
}
return {
ownerId,
token,
claimedAt,
lastHeartbeatAt,
...(expiresAt ? { expiresAt } : {}),
};
}
function normalizeDiagnosticAction(value: unknown): WorkboardDiagnosticAction | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const record = value as Record<string, unknown>;
const kind =
record.kind === "claim" ||
record.kind === "unblock" ||
record.kind === "reassign" ||
record.kind === "add_proof" ||
record.kind === "open_session"
? record.kind
: undefined;
const label = normalizeBoundedString(record.label, undefined, 120, "diagnostic action label");
return kind && label ? { kind, label } : null;
}
function normalizeDiagnostic(value: unknown): WorkboardDiagnostic | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const record = value as Record<string, unknown>;
const kind = WORKBOARD_DIAGNOSTIC_KINDS.includes(record.kind as WorkboardDiagnosticKind)
? (record.kind as WorkboardDiagnosticKind)
: undefined;
const severity = WORKBOARD_DIAGNOSTIC_SEVERITIES.includes(
record.severity as WorkboardDiagnosticSeverity,
)
? (record.severity as WorkboardDiagnosticSeverity)
: "warning";
const title = normalizeBoundedString(record.title, undefined, 160, "diagnostic title");
const detail = normalizeBoundedString(record.detail, undefined, 800, "diagnostic detail");
const firstSeenAt = normalizeTimestamp(record.firstSeenAt, Date.now());
const lastSeenAt = normalizeTimestamp(record.lastSeenAt, firstSeenAt);
if (!kind || !title || !detail) {
return null;
}
return {
kind,
severity,
title,
detail,
firstSeenAt,
lastSeenAt,
count:
typeof record.count === "number" && Number.isFinite(record.count)
? Math.max(1, Math.trunc(record.count))
: 1,
actions: Array.isArray(record.actions)
? record.actions
.map(normalizeDiagnosticAction)
.filter((action): action is WorkboardDiagnosticAction => action !== null)
.slice(0, 4)
: [],
};
}
function normalizeNotification(value: unknown): WorkboardNotification | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const record = value as Record<string, unknown>;
const id = normalizeOptionalString(record.id) ?? randomUUID();
const kind = WORKBOARD_NOTIFICATION_KINDS.includes(record.kind as WorkboardNotificationKind)
? (record.kind as WorkboardNotificationKind)
: undefined;
const createdAt = normalizeTimestamp(record.createdAt, Date.now());
const message = normalizeBoundedString(record.message, undefined, 240, "notification message");
if (!kind || !message) {
return null;
}
const sessionKey = normalizeBoundedString(record.sessionKey, undefined, 240, "session key");
const runId = normalizeBoundedString(record.runId, undefined, 120, "run id");
return {
id,
kind,
createdAt,
message,
...(sessionKey ? { sessionKey } : {}),
...(runId ? { runId } : {}),
};
}
function normalizeProofInput(input: WorkboardProofInput, now: number): WorkboardProof {
const label = normalizeBoundedString(input.label, undefined, 160, "proof label");
const command = normalizeBoundedString(input.command, undefined, 1000, "proof command");
const url = normalizeBoundedString(input.url, undefined, 2000, "proof URL");
const note = normalizeBoundedString(input.note, undefined, 2000, "proof note");
return {
id: randomUUID(),
status: normalizeProofStatus(input.status, "unknown"),
createdAt: now,
...(label ? { label } : {}),
...(command ? { command } : {}),
...(url ? { url } : {}),
...(note ? { note } : {}),
};
}
function normalizeMetadata(
value: unknown,
fallback: WorkboardMetadata = {},
options: { allowDependencyLinks?: boolean } = {},
): WorkboardMetadata {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return trimMetadataToBudget(fallback);
}
const record = value as Record<string, unknown>;
const stale =
record.stale && typeof record.stale === "object" && !Array.isArray(record.stale)
? (record.stale as Record<string, unknown>)
: null;
const hasArchivedAt = Object.hasOwn(record, "archivedAt");
const hasStale = Object.hasOwn(record, "stale");
const links = Array.isArray(record.links)
? record.links.map(normalizeLink).filter((link): link is WorkboardLink => link !== null)
: undefined;
const normalizedLinks =
links === undefined
? fallback.links
: options.allowDependencyLinks === false
? (() => {
const dependencyLinks = (fallback.links ?? []).filter(isDependencyLink);
const ordinaryCapacity = Math.max(0, MAX_CARD_LINKS - dependencyLinks.length);
return [
...dependencyLinks.slice(-MAX_CARD_LINKS),
...(ordinaryCapacity > 0
? links.filter((link) => !isDependencyLink(link)).slice(-ordinaryCapacity)
: []),
];
})()
: links.slice(-MAX_CARD_LINKS);
return trimMetadataToBudget({
attempts: Array.isArray(record.attempts)
? record.attempts
.map(normalizeAttempt)
.filter((attempt): attempt is WorkboardRunAttempt => attempt !== null)
.slice(-MAX_CARD_ATTEMPTS)
: fallback.attempts,
comments: Array.isArray(record.comments)
? record.comments
.map(normalizeComment)
.filter((comment): comment is WorkboardComment => comment !== null)
.slice(-MAX_CARD_COMMENTS)
: fallback.comments,
links: normalizedLinks,
proof: Array.isArray(record.proof)
? record.proof
.map(normalizeProof)
.filter((proof): proof is WorkboardProof => proof !== null)
.slice(-MAX_CARD_PROOF)
: fallback.proof,
artifacts: Array.isArray(record.artifacts)
? record.artifacts
.map(normalizeArtifact)
.filter((artifact): artifact is WorkboardArtifact => artifact !== null)
.slice(-MAX_CARD_ARTIFACTS)
: fallback.artifacts,
automation: Object.hasOwn(record, "automation")
? normalizeAutomation(record.automation, fallback.automation)
: fallback.automation,
claim: Object.hasOwn(record, "claim")
? record.claim
? normalizeClaim(record.claim, fallback.claim)
: undefined
: fallback.claim,
diagnostics: Array.isArray(record.diagnostics)
? record.diagnostics
.map(normalizeDiagnostic)
.filter((diagnostic): diagnostic is WorkboardDiagnostic => diagnostic !== null)
.slice(-MAX_CARD_DIAGNOSTICS)
: fallback.diagnostics,
notifications: Array.isArray(record.notifications)
? record.notifications
.map(normalizeNotification)
.filter((notification): notification is WorkboardNotification => notification !== null)
.slice(-MAX_CARD_NOTIFICATIONS)
: fallback.notifications,
templateId: normalizeTemplateId(record.templateId) ?? fallback.templateId,
archivedAt: hasArchivedAt
? normalizeTimestamp(record.archivedAt, 0) || undefined
: fallback.archivedAt,
stale: hasStale
? stale
? {
detectedAt: normalizeTimestamp(stale.detectedAt, Date.now()),
lastSessionUpdatedAt: normalizeTimestamp(stale.lastSessionUpdatedAt, 0) || undefined,
reason:
normalizeBoundedString(stale.reason, fallback.stale?.reason, 240, "stale reason") ??
"Session has not reported recent activity.",
}
: undefined
: fallback.stale,
failureCount:
typeof record.failureCount === "number" && Number.isFinite(record.failureCount)
? Math.max(0, Math.trunc(record.failureCount))
: fallback.failureCount,
});
}
function normalizeExecution(value: unknown): WorkboardExecution | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
const record = value as Record<string, unknown>;
const now = Date.now();
const model = normalizeOptionalString(record.model);
const id = normalizeOptionalString(record.id) ?? randomUUID();
if (!model) {
return undefined;
}
const startedAt = normalizeTimestamp(record.startedAt, now);
const updatedAt = normalizeTimestamp(record.updatedAt, startedAt);
const sessionKey = normalizeOptionalString(record.sessionKey);
const runId = normalizeOptionalString(record.runId);
return {
id,
kind: "agent-session",
engine: normalizeExecutionEngine(record.engine, "codex"),
mode: normalizeExecutionMode(record.mode, "autonomous"),
status: normalizeExecutionStatus(record.status, "idle"),
model,
startedAt,
updatedAt,
...(sessionKey ? { sessionKey } : {}),
...(runId ? { runId } : {}),
};
}
function syncExecutionSessionKey(
execution: WorkboardExecution | undefined,
sessionKey: string | undefined,
): WorkboardExecution | undefined {
if (!execution) {
return undefined;
}
return removeUndefinedExecutionFields({
...execution,
sessionKey,
updatedAt: Date.now(),
});
}
function removeUndefinedExecutionFields(execution: WorkboardExecution): WorkboardExecution {
const next = { ...execution };
if (next.sessionKey === undefined) {
delete next.sessionKey;
}
if (next.runId === undefined) {
delete next.runId;
}
return next;
}
function removeUndefinedAutomationFields(automation: WorkboardAutomation): WorkboardAutomation {
const next = { ...automation };
for (const key of [
"tenant",
"boardId",
"createdByCardId",
"idempotencyKey",
"skills",
"workspace",
"maxRuntimeSeconds",
"maxRetries",
"scheduledAt",
"summary",
"createdCardIds",
"dispatchCount",
"lastDispatchAt",
] as const) {
const value = next[key];
if (
value === undefined ||
(Array.isArray(value) && value.length === 0) ||
(typeof value === "object" && value !== null && Object.keys(value).length === 0)
) {
delete next[key];
}
}
return next;
}
function removeUndefinedMetadataFields(metadata: WorkboardMetadata): WorkboardMetadata {
const next = { ...metadata };
for (const key of [
"attempts",
"comments",
"links",
"proof",
"artifacts",
"automation",
"claim",
"diagnostics",
"notifications",
"templateId",
"archivedAt",
"stale",
"failureCount",
] as const) {
const value = next[key];
if (
value === undefined ||
(Array.isArray(value) && value.length === 0) ||
(typeof value === "number" && value === 0 && key === "failureCount")
) {
delete next[key];
}
}
return next;
}
function clearDiagnostics(
metadata: WorkboardMetadata | undefined,
kinds: readonly WorkboardDiagnosticKind[],
): WorkboardMetadata {
if (!metadata?.diagnostics) {
return metadata ?? {};
}
return {
...metadata,
diagnostics: metadata.diagnostics.filter((entry) => !kinds.includes(entry.kind)),
};
}
function metadataIsEmpty(metadata: WorkboardMetadata | undefined): boolean {
return !metadata || Object.keys(metadata).length === 0;
}
function metadataByteSize(metadata: WorkboardMetadata): number {
return Buffer.byteLength(JSON.stringify(metadata), "utf8");
}
function dropFirst<T>(items: readonly T[] | undefined): T[] | undefined {
if (!items?.length) {
return undefined;
}
const next = items.slice(1);
return next.length ? next : undefined;
}
function dropFirstNonDependencyLink(
items: readonly WorkboardLink[] | undefined,
): WorkboardLink[] | undefined {
if (!items?.length) {
return undefined;
}
const index = items.findIndex((link) => !isDependencyLink(link));
if (index < 0) {
return items.slice();
}
const next = items.filter((_, itemIndex) => itemIndex !== index);
return next.length ? next : undefined;
}
function appendLinkPreservingDependencies(
links: readonly WorkboardLink[],
link: WorkboardLink,
): WorkboardLink[] {
const next = [...links, link];
if (next.length <= MAX_CARD_LINKS) {
return next;
}
const dropIndex = next.findIndex((entry) => !isDependencyLink(entry));
if (dropIndex < 0 || dropIndex === next.length - 1) {
throw new Error("card link limit reached.");
}
return next.filter((_, index) => index !== dropIndex);
}
function trimMetadataToBudget(metadata: WorkboardMetadata): WorkboardMetadata {
let next = removeUndefinedMetadataFields(metadata);
while (metadataByteSize(next) > MAX_CARD_METADATA_BYTES) {
const currentSize = metadataByteSize(next);
if (next.attempts?.length) {
next = removeUndefinedMetadataFields({ ...next, attempts: dropFirst(next.attempts) });
} else if (next.diagnostics?.length) {
next = removeUndefinedMetadataFields({ ...next, diagnostics: dropFirst(next.diagnostics) });
} else if (next.notifications?.length) {
next = removeUndefinedMetadataFields({
...next,
notifications: dropFirst(next.notifications),
});
} else if (next.proof?.length) {
next = removeUndefinedMetadataFields({ ...next, proof: dropFirst(next.proof) });
} else if (next.artifacts?.length) {
next = removeUndefinedMetadataFields({ ...next, artifacts: dropFirst(next.artifacts) });
} else if (next.links?.length) {
const links = dropFirstNonDependencyLink(next.links);
if (links?.length === next.links.length) {
next = removeUndefinedMetadataFields({ ...next, comments: dropFirst(next.comments) });
} else {
next = removeUndefinedMetadataFields({ ...next, links });
}
} else if (next.comments?.length) {
next = removeUndefinedMetadataFields({ ...next, comments: dropFirst(next.comments) });
}
if (metadataByteSize(next) >= currentSize) {
break;
}
}
return next;
}
function compareCards(left: WorkboardCard, right: WorkboardCard): number {
if (left.status !== right.status) {
return WORKBOARD_STATUSES.indexOf(left.status) - WORKBOARD_STATUSES.indexOf(right.status);
}
if (left.position !== right.position) {
return left.position - right.position;
}
return left.createdAt - right.createdAt;
}
function cardSessionKey(card: WorkboardCard): string | undefined {
return card.sessionKey ?? card.execution?.sessionKey;
}
function cardRunId(card: WorkboardCard): string | undefined {
return card.runId ?? card.execution?.runId;
}
function executionAttemptStatus(execution: WorkboardExecution): WorkboardAttemptStatus {
if (execution.status === "running") {
return "running";
}
if (execution.status === "blocked") {
return "blocked";
}
if (execution.status === "done" || execution.status === "review") {
return "succeeded";
}
return "stopped";
}
function syncExecutionAttemptMetadata(
metadata: WorkboardMetadata,
execution: WorkboardExecution | undefined,
now: number,
): WorkboardMetadata {
if (!execution) {
return metadata;
}
const attemptStatus = executionAttemptStatus(execution);
const attempts = [...(metadata.attempts ?? [])];
const key = execution.runId ?? execution.sessionKey ?? execution.id;
const existingIndex = attempts.findIndex(
(attempt) =>
(execution.runId && attempt.runId === execution.runId) ||
(!execution.runId && attempt.id === key),
);
const existingAttempt = existingIndex >= 0 ? attempts[existingIndex] : undefined;
const nextAttempt: WorkboardRunAttempt = {
id: existingAttempt?.id ?? key,
status: attemptStatus,
startedAt: existingAttempt?.startedAt ?? execution.startedAt,
engine: execution.engine,
mode: execution.mode,
model: execution.model,
...(execution.sessionKey ? { sessionKey: execution.sessionKey } : {}),
...(execution.runId ? { runId: execution.runId } : {}),
...(attemptStatus !== "running" && { endedAt: execution.updatedAt || now }),
...(attemptStatus !== "succeeded" && existingAttempt?.error
? { error: existingAttempt.error }
: {}),
};
if (existingIndex >= 0) {
attempts[existingIndex] = nextAttempt;
} else {
attempts.push(nextAttempt);
}
const previousFailed =
existingAttempt?.status === "blocked" || existingAttempt?.status === "failed";
const attemptFailed = attemptStatus === "blocked" || attemptStatus === "failed";
const failureCount = attemptFailed
? previousFailed
? metadata.failureCount
: (metadata.failureCount ?? 0) + 1
: attemptStatus === "succeeded"
? 0
: metadata.failureCount;
return removeUndefinedMetadataFields({
...metadata,
attempts: attempts.slice(-MAX_CARD_ATTEMPTS),
failureCount,
});
}
function appendEvent(
card: WorkboardCard,
event: Omit<WorkboardEvent, "id" | "at">,
at = Date.now(),
): WorkboardEvent[] {
return [
...normalizeEvents(card.events),
{
id: randomUUID(),
at,
...event,
},
].slice(-MAX_CARD_EVENTS);
}
function latestMetadataIdChanged(
existing: readonly { id: string }[] | undefined,
next: readonly { id: string }[] | undefined,
): boolean {
const latestId = next?.at(-1)?.id;
return Boolean(latestId && latestId !== existing?.at(-1)?.id);
}
function updateEvent(
existing: WorkboardCard,
next: WorkboardCard,
): Omit<WorkboardEvent, "id" | "at"> {
if (existing.status !== next.status || existing.position !== next.position) {
return {
kind: "moved",
fromStatus: existing.status,
toStatus: next.status,
};
}
if (cardSessionKey(existing) !== cardSessionKey(next)) {
return {
kind: "linked",
...(cardSessionKey(next) ? { sessionKey: cardSessionKey(next) } : {}),
};
}
if (existing.metadata?.claim?.token !== next.metadata?.claim?.token) {
return { kind: "claimed" };
}
if (existing.metadata?.claim?.lastHeartbeatAt !== next.metadata?.claim?.lastHeartbeatAt) {
return { kind: "heartbeat" };
}
if (
existing.execution?.status !== next.execution?.status ||
existing.execution?.engine !== next.execution?.engine ||
cardRunId(existing) !== cardRunId(next)
) {
const existingAttempts = existing.metadata?.attempts ?? [];
const nextAttempts = next.metadata?.attempts ?? [];
const latestAttempt = nextAttempts.at(-1);
if (nextAttempts.length > existingAttempts.length) {
return {
kind: "attempt_started",
...(latestAttempt?.sessionKey ? { sessionKey: latestAttempt.sessionKey } : {}),
...(latestAttempt?.runId ? { runId: latestAttempt.runId } : {}),
};
}
const previousAttempt = latestAttempt
? existingAttempts.find((attempt) => attempt.id === latestAttempt.id)
: undefined;
if (latestAttempt && previousAttempt?.status !== latestAttempt.status) {
return {
kind: "attempt_updated",
...(latestAttempt.sessionKey ? { sessionKey: latestAttempt.sessionKey } : {}),
...(latestAttempt.runId ? { runId: latestAttempt.runId } : {}),
};
}
return {
kind: "execution_updated",
...(cardSessionKey(next) ? { sessionKey: cardSessionKey(next) } : {}),
...(cardRunId(next) ? { runId: cardRunId(next) } : {}),
};
}
if (
(existing.metadata?.comments?.length ?? 0) !== (next.metadata?.comments?.length ?? 0) ||
latestMetadataIdChanged(existing.metadata?.comments, next.metadata?.comments)
) {
return { kind: "comment_added" };
}
if (
(existing.metadata?.links?.length ?? 0) !== (next.metadata?.links?.length ?? 0) ||
latestMetadataIdChanged(existing.metadata?.links, next.metadata?.links)
) {
return { kind: "link_added" };
}
if (
(existing.metadata?.proof?.length ?? 0) !== (next.metadata?.proof?.length ?? 0) ||
latestMetadataIdChanged(existing.metadata?.proof, next.metadata?.proof)
) {
return { kind: "proof_added" };
}
if (
(existing.metadata?.artifacts?.length ?? 0) !== (next.metadata?.artifacts?.length ?? 0) ||
latestMetadataIdChanged(existing.metadata?.artifacts, next.metadata?.artifacts)
) {
return { kind: "artifact_added" };
}
if ((existing.metadata?.diagnostics?.length ?? 0) !== (next.metadata?.diagnostics?.length ?? 0)) {
return { kind: "diagnostic" };
}
if (
(existing.metadata?.notifications?.length ?? 0) !==
(next.metadata?.notifications?.length ?? 0) ||
latestMetadataIdChanged(existing.metadata?.notifications, next.metadata?.notifications)
) {
return { kind: "notification" };
}
if (
existing.metadata?.automation?.dispatchCount !== next.metadata?.automation?.dispatchCount ||
existing.metadata?.automation?.lastDispatchAt !== next.metadata?.automation?.lastDispatchAt
) {
return { kind: "dispatch" };
}
if (!existing.metadata?.archivedAt && next.metadata?.archivedAt) {
return { kind: "archived" };
}
if (existing.metadata?.archivedAt && !next.metadata?.archivedAt) {
return { kind: "unarchived" };
}
if (!existing.metadata?.stale && next.metadata?.stale) {
return { kind: "stale" };
}
return { kind: "edited" };
}
function removeUndefinedCardFields(card: WorkboardCard): WorkboardCard {
const next = { ...card };
for (const key of [
"notes",
"agentId",
"sessionKey",
"runId",
"taskId",
"sourceUrl",
"execution",
"startedAt",
"completedAt",
"metadata",
] as const) {
if (next[key] === undefined) {
delete next[key];
}
}
if (metadataIsEmpty(next.metadata)) {
delete next.metadata;
}
return next;
}
function assertCanMutateClaimedCard(
card: WorkboardCard,
scope: WorkboardMutationScope | undefined,
) {
if (!scope) {
return;
}
const claim = card.metadata?.claim;
if (!claim) {
return;
}
const ownerId = normalizeOptionalString(scope.ownerId);
const token = normalizeOptionalString(scope.token);
if (claim.ownerId === ownerId || (token && claim.token === token)) {
return;
}
throw new Error(`card is claimed by ${claim.ownerId}.`);
}
function retryBudgetExhausted(card: WorkboardCard): boolean {
const maxRetries = card.metadata?.automation?.maxRetries;
return Boolean(maxRetries && (card.metadata?.failureCount ?? 0) > maxRetries);
}
function diagnostic(
params: {
kind: WorkboardDiagnosticKind;
severity: WorkboardDiagnosticSeverity;
title: string;
detail: string;
actions: WorkboardDiagnosticAction[];
},
now: number,
): WorkboardDiagnostic {
return {
...params,
firstSeenAt: now,
lastSeenAt: now,
count: 1,
};
}
function mergeDiagnostics(
previous: readonly WorkboardDiagnostic[] | undefined,
next: WorkboardDiagnostic[],
): WorkboardDiagnostic[] {
const byKind = new Map(previous?.map((entry) => [entry.kind, entry]));
return next.map((entry) => {
const prior = byKind.get(entry.kind);
return prior
? {
...entry,
firstSeenAt: prior.firstSeenAt,
count: prior.count + 1,
}
: entry;
});
}
function computeCardDiagnostics(card: WorkboardCard, now: number): WorkboardDiagnostic[] {
const diagnostics: WorkboardDiagnostic[] = [];
const claim = card.metadata?.claim;
const lastHeartbeatAt = claim?.lastHeartbeatAt ?? card.execution?.updatedAt ?? card.updatedAt;
if (
(card.status === "todo" || card.status === "backlog" || card.status === "ready") &&
card.agentId &&
now - card.updatedAt > READY_STRANDED_MS
) {
diagnostics.push(
diagnostic(
{
kind: "stranded_ready",
severity: "warning",
title: "Assigned card is waiting",
detail: "The card has an assigned agent but has not been claimed recently.",
actions: [{ kind: "claim", label: "Claim card" }],
},
now,
),
);
}
if (card.status === "running" && now - lastHeartbeatAt > RUNNING_HEARTBEAT_STALE_MS) {
diagnostics.push(
diagnostic(
{
kind: "running_without_heartbeat",
severity: "error",
title: "Running card has no recent heartbeat",
detail: "The linked run or claim has not reported recent activity.",
actions: [
{ kind: "open_session", label: "Open session" },
{ kind: "reassign", label: "Reassign card" },
],
},
now,
),
);
}
if (card.status === "blocked" && now - card.updatedAt > BLOCKED_TOO_LONG_MS) {
diagnostics.push(
diagnostic(
{
kind: "blocked_too_long",
severity: "warning",
title: "Blocked card needs attention",
detail: "The card has been blocked for more than a day.",
actions: [{ kind: "unblock", label: "Move to todo" }],
},
now,
),
);
}
if ((card.metadata?.failureCount ?? 0) >= 2) {
diagnostics.push(
diagnostic(
{
kind: "repeated_failures",
severity: "error",
title: "Repeated run failures",
detail: "Multiple attempts failed or blocked on this card.",
actions: [{ kind: "reassign", label: "Reassign card" }],
},
now,
),
);
}
if (
card.status === "done" &&
!(card.metadata?.proof?.length || card.metadata?.artifacts?.length)
) {
diagnostics.push(
diagnostic(
{
kind: "missing_proof",
severity: "warning",
title: "Done card has no proof",
detail: "The card is marked done without proof or an attached artifact.",
actions: [{ kind: "add_proof", label: "Add proof" }],
},
now,
),
);
}
if (card.sessionKey && !card.execution && card.status === "running") {
diagnostics.push(
diagnostic(
{
kind: "orphaned_session",
severity: "warning",
title: "Running card has only a loose session link",
detail: "The card is running but has no execution record for lifecycle handoff.",
actions: [{ kind: "open_session", label: "Open session" }],
},
now,
),
);
}
return diagnostics;
}
function capText(value: string | undefined, max: number): string | undefined {
if (!value) {
return undefined;
}
return value.length <= max ? value : `${value.slice(0, Math.max(0, max - 1))}…`;
}
function cardBoardId(card: WorkboardCard): string {
return card.metadata?.automation?.boardId ?? "default";
}
function cardResultSummary(card: WorkboardCard): string | undefined {
return (
card.metadata?.automation?.summary ??
card.metadata?.comments?.findLast((comment) => comment.body.trim())?.body ??
card.metadata?.proof?.findLast((proof) => proof.note?.trim())?.note
);
}
function buildWorkerContext(card: WorkboardCard, cards: readonly WorkboardCard[] = []): string {
const lines = [
`# Workboard card ${card.id}`,
`Title: ${card.title}`,
`Status: ${card.status}`,
`Priority: ${card.priority}`,
`Board: ${cardBoardId(card)}`,
`Agent: ${card.agentId ?? "(default)"}`,
];
if (card.notes) {
lines.push("", "## Notes", capText(card.notes, 4000) ?? "");
}
const attempts = card.metadata?.attempts?.slice(-8) ?? [];
if (attempts.length) {
lines.push("", "## Recent attempts");
for (const attempt of attempts) {
lines.push(
`- ${attempt.status} ${attempt.model ?? ""} ${attempt.error ? `error=${capText(attempt.error, 240)}` : ""}`.trim(),
);
}
}
const comments = card.metadata?.comments?.slice(-12) ?? [];
if (comments.length) {
lines.push("", "## Recent comments");
for (const comment of comments) {
lines.push(`- ${capText(comment.body, 400)}`);
}
}
const proof = card.metadata?.proof?.slice(-8) ?? [];
if (proof.length) {
lines.push("", "## Proof");
for (const entry of proof) {
lines.push(
`- ${entry.status}: ${capText(entry.label ?? entry.command ?? entry.url ?? entry.note, 400)}`,
);
}
}
const artifacts = card.metadata?.artifacts?.slice(-8) ?? [];
if (artifacts.length) {
lines.push("", "## Artifacts");
for (const artifact of artifacts) {
lines.push(`- ${capText(artifact.label ?? artifact.url ?? artifact.path, 400)}`);
}
}
const links = card.metadata?.links?.slice(-8) ?? [];
if (links.length) {
lines.push("", "## Links");
for (const link of links) {
lines.push(`- ${link.type}: ${link.title ?? link.url ?? link.targetCardId ?? ""}`);
}
}
const cardsById = new Map(cards.map((entry) => [entry.id, entry]));
const parentResults = cardParentIds(card)
.map((parentId) => cardsById.get(parentId))
.filter((parent): parent is WorkboardCard => parent !== undefined && parent.status === "done")
.slice(-6);
if (parentResults.length) {
lines.push("", "## Parent results");
for (const parent of parentResults) {
lines.push(
`- ${parent.id} ${parent.title}: ${capText(cardResultSummary(parent), 500) ?? "done"}`,
);
}
}
const recentAgentWork =
card.agentId && cards.length
? cards
.filter(
(entry) =>
entry.id !== card.id &&
cardBoardId(entry) === cardBoardId(card) &&
entry.agentId === card.agentId &&
entry.status === "done",
)
.toSorted((a, b) => b.updatedAt - a.updatedAt)
.slice(0, 5)
: [];
if (recentAgentWork.length) {
lines.push("", `## Recent done work by ${card.agentId}`);
for (const entry of recentAgentWork) {
lines.push(
`- ${entry.id} ${entry.title}: ${capText(cardResultSummary(entry), 300) ?? "done"}`,
);
}
}
const automation = card.metadata?.automation;
if (automation) {
lines.push("", "## Automation");
if (automation.tenant) {
lines.push(`Tenant: ${automation.tenant}`);
}
if (automation.boardId) {
lines.push(`Board: ${automation.boardId}`);
}
if (automation.skills?.length) {
lines.push(`Skills: ${automation.skills.join(", ")}`);
}
if (automation.workspace) {
lines.push(
`Workspace: ${automation.workspace.kind}${automation.workspace.path ? ` ${automation.workspace.path}` : ""}`,
);
}
if (automation.summary) {
lines.push(`Summary: ${capText(automation.summary, 400)}`);
}
}
const diagnostics = computeCardDiagnostics(card, Date.now());
if (diagnostics.length) {
lines.push("", "## Active diagnostics");
for (const entry of diagnostics) {
lines.push(`- ${entry.severity}: ${entry.title}`);
}
}
return lines.join("\n");
}
function cardParentIds(card: WorkboardCard): string[] {
return (card.metadata?.links ?? [])
.filter((link) => link.type === "parent" && link.targetCardId)
.map((link) => link.targetCardId!)
.filter((id, index, ids) => ids.indexOf(id) === index);
}
function cardChildIds(card: WorkboardCard): string[] {
return (card.metadata?.links ?? [])
.filter((link) => link.type === "child" && link.targetCardId)
.map((link) => link.targetCardId!)
.filter((id, index, ids) => ids.indexOf(id) === index);
}
function latestRunningAttempt(card: WorkboardCard): WorkboardRunAttempt | undefined {
return card.metadata?.attempts?.findLast((attempt) => attempt.status === "running");
}
function isDependencyPromotableStatus(status: WorkboardStatus): boolean {
return (
status === "backlog" ||
status === "triage" ||
status === "todo" ||
status === "scheduled" ||
status === "ready"
);
}
function isActiveDependencyTarget(
card: WorkboardCard,
options: { allowStatusOnly?: boolean } = {},
): boolean {
return (
Boolean(card.metadata?.claim) ||
card.execution?.status === "running" ||
Boolean(latestRunningAttempt(card)) ||
(!options.allowStatusOnly && (card.status === "running" || card.status === "review"))
);
}
function closeRunningAttempts(
attempts: WorkboardRunAttempt[] | undefined,
now: number,
status: WorkboardAttemptStatus,
reason?: string,
): WorkboardRunAttempt[] | undefined {
if (!attempts?.some((attempt) => attempt.status === "running")) {
return attempts;
}
return attempts.map((attempt) =>
attempt.status === "running"
? { ...attempt, status, endedAt: now, ...(reason ? { error: reason } : {}) }
: attempt,
);
}
export class WorkboardStore {
private mutationQueue: Promise<unknown> = Promise.resolve();
constructor(private readonly store: WorkboardKeyedStore) {}
private async enqueueMutation<T>(run: () => Promise<T>): Promise<T> {
const result = this.mutationQueue.then(run, run);
this.mutationQueue = result.then(
() => undefined,
() => undefined,
);
return await result;
}
private async updateMetadata(
id: string,
mutate: (existing: WorkboardCard) => WorkboardMetadata,
): Promise<WorkboardCard> {
return await this.enqueueMutation(async () => {
const existing = await this.get(id);
if (!existing) {
throw new Error(`card not found: ${id}`);
}
return await this.updateCard(id, { metadata: mutate(existing) });
});
}
async list(options: WorkboardListOptions = {}): Promise<WorkboardCard[]> {
const boardId = normalizeBoardId(options.boardId);
const entries = await this.store.entries();
return entries
.map((entry) => entry.value)
.filter(
(entry): entry is PersistedWorkboardCard => entry?.version === 1 && Boolean(entry.card?.id),
)
.map((entry) => entry.card)
.filter((card) => !boardId || cardBoardId(card) === boardId)
.toSorted(compareCards);
}
async listBoards(): Promise<{ boards: WorkboardBoardSummary[] }> {
const boards = new Map<string, WorkboardBoardSummary>();
for (const card of await this.list()) {
const boardId = cardBoardId(card);
const summary =
boards.get(boardId) ??
({
id: boardId,
total: 0,
active: 0,
archived: 0,
byStatus: {},
} satisfies WorkboardBoardSummary);
summary.total += 1;
if (card.metadata?.archivedAt) {
summary.archived += 1;
} else {
summary.active += 1;
}
summary.byStatus[card.status] = (summary.byStatus[card.status] ?? 0) + 1;
summary.updatedAt = Math.max(summary.updatedAt ?? 0, card.updatedAt);
boards.set(boardId, summary);
}
return { boards: [...boards.values()].toSorted((a, b) => a.id.localeCompare(b.id)) };
}
async stats(input: WorkboardListOptions = {}, now = Date.now()): Promise<WorkboardStatsResult> {
const cards = await this.list(input);
const boardId = normalizeBoardId(input.boardId) ?? "all";
const byStatus: Partial<Record<WorkboardStatus, number>> = {};
const byAgent = Object.create(null) as Record<string, number>;
let oldestReadyAt: number | undefined;
let updatedAt: number | undefined;
let archived = 0;
for (const card of cards) {
byStatus[card.status] = (byStatus[card.status] ?? 0) + 1;
byAgent[card.agentId ?? "(default)"] = (byAgent[card.agentId ?? "(default)"] ?? 0) + 1;
if (card.metadata?.archivedAt) {
archived += 1;
}
if (card.status === "ready") {
oldestReadyAt = Math.min(oldestReadyAt ?? card.updatedAt, card.updatedAt);
}
updatedAt = Math.max(updatedAt ?? 0, card.updatedAt);
}
return {
id: boardId,
total: cards.length,
active: cards.length - archived,
archived,
byStatus,
byAgent,
...(oldestReadyAt ? { oldestReadyAgeMs: Math.max(0, now - oldestReadyAt) } : {}),
...(updatedAt ? { updatedAt } : {}),
};
}
async get(id: string): Promise<WorkboardCard | undefined> {
const entry = await this.store.lookup(id.trim());
return entry?.version === 1 ? entry.card : undefined;
}
private async removeReferencesToCard(cardId: string): Promise<void> {
for (const card of await this.list()) {
const links = card.metadata?.links;
if (!links?.some((link) => link.targetCardId === cardId)) {
continue;
}
await this.updateCard(card.id, {
metadata: {
...card.metadata,
links: links.filter((link) => link.targetCardId !== cardId),
},
});
}
}
async create(
input: WorkboardLinkedCreateInput,
scope?: WorkboardMutationScope,
): Promise<WorkboardCard> {
return await this.enqueueMutation(async () => {
const now = Date.now();
const requestedStatus = normalizeStatus(input.status, "todo");
const cards = await this.list();
const parents = normalizeStringList(input.parents, "parents", 120);
const automation = normalizeAutomation({
tenant: input.tenant,
boardId: input.boardId,
createdByCardId: input.createdByCardId,
idempotencyKey: input.idempotencyKey,
skills: input.skills,
workspace: input.workspace,
maxRuntimeSeconds: input.maxRuntimeSeconds,
maxRetries: input.maxRetries,
scheduledAt: input.scheduledAt,
});
const heldBySchedule =
Boolean(automation?.scheduledAt && automation.scheduledAt > now) &&
requestedStatus !== "blocked";
let status: WorkboardStatus = heldBySchedule ? "scheduled" : requestedStatus;
let heldByDependencies = false;
if (parents.length > 0 && (status === "running" || status === "review")) {
status = "todo";
heldByDependencies = true;
}
if (automation?.idempotencyKey) {
const existing = cards.find(
(card) =>
card.metadata?.automation?.idempotencyKey === automation.idempotencyKey &&
card.metadata?.automation?.tenant === automation.tenant &&
cardBoardId(card) === (automation.boardId ?? "default"),
);
if (existing) {
return existing;
}
}
const cardsById = new Map(cards.map((card) => [card.id, card]));
const parentCards = parents.map((parentId) => {
const parent = cardsById.get(parentId);
if (!parent) {
throw new Error(`card not found: ${parentId}`);
}
return parent;
});
const childAutomation = normalizeAutomation(
{
...automation,
createdByCardId:
automation?.createdByCardId ?? (parents.length === 1 ? parents[0] : undefined),
},
automation,
);
const normalizedPosition = normalizePosition(input.position, Number.NaN);
const position = Number.isFinite(normalizedPosition)
? normalizedPosition
: Math.max(
0,
...cards.filter((card) => card.status === status).map((card) => card.position),
) + POSITION_STEP;
const notes = normalizeNotes(input.notes);
const agentId = normalizeOptionalString(input.agentId);
const sessionKey = normalizeOptionalString(input.sessionKey);
const runId = normalizeOptionalString(input.runId);
const taskId = normalizeOptionalString(input.taskId);
const sourceUrl = normalizeOptionalString(input.sourceUrl);
const normalizedExecution = normalizeExecution(input.execution);
const execution =
normalizedExecution?.status === "running" && (heldBySchedule || heldByDependencies)
? undefined
: normalizedExecution;
const startedAt =
input.startedAt === undefined
? status === "running"
? now
: undefined
: normalizeTimestamp(input.startedAt, 0) || undefined;
const completedAt =
input.completedAt === undefined
? status === "done"
? now
: undefined
: normalizeTimestamp(input.completedAt, 0) || undefined;
const metadata = normalizeMetadata(
input.metadata,
{
templateId: normalizeTemplateId(input.templateId),
...(childAutomation ? { automation: childAutomation } : {}),
},
{ allowDependencyLinks: false },
);
const syncedMetadata = trimMetadataToBudget(
syncExecutionAttemptMetadata(metadata, execution, now),
);
let card: WorkboardCard = {
id: randomUUID(),
title: normalizeTitle(input.title),
status,
priority: normalizePriority(input.priority, "normal"),
labels: normalizeLabels(input.labels),
position,
createdAt: now,
updatedAt: now,
events: [
{
id: randomUUID(),
kind: "created",
at: now,
toStatus: status,
...(sessionKey ? { sessionKey } : {}),
...(runId ? { runId } : {}),
},
],
...(notes ? { notes } : {}),
...(agentId ? { agentId } : {}),
...(sessionKey ? { sessionKey } : {}),
...(runId ? { runId } : {}),
...(taskId ? { taskId } : {}),
...(sourceUrl ? { sourceUrl } : {}),
...(execution ? { execution } : {}),
...(startedAt ? { startedAt } : {}),
...(completedAt ? { completedAt } : {}),
...(!metadataIsEmpty(syncedMetadata) ? { metadata: syncedMetadata } : {}),
};
await this.store.register(card.id, { version: 1, card });
try {
for (const parent of parentCards) {
card = await this.linkCardsDirect(parent.id, card.id, now, {
allowStatusOnlyActiveChild: true,
scope,
});
}
} catch (error) {
await this.store.delete(card.id);
await this.removeReferencesToCard(card.id);
throw error;
}
return card;
});
}
async update(id: string, patch: WorkboardCardPatch): Promise<WorkboardCard> {
return await this.enqueueMutation(
async () =>
await this.updateCard(id, patch, {
allowMetadataDependencyLinks: false,
enforceStatusHolds: true,
}),
);
}
private async updateCard(
id: string,
patch: WorkboardCardPatch,
options: { allowMetadataDependencyLinks?: boolean; enforceStatusHolds?: boolean } = {},
): Promise<WorkboardCard> {
const existing = await this.get(id);
if (!existing) {
throw new Error(`card not found: ${id}`);
}
const status = normalizeStatus(patch.status, existing.status);
const now = Date.now();
const startedAt =
patch.startedAt === undefined
? status === "running"
? (existing.startedAt ?? now)
: existing.startedAt
: normalizeTimestamp(patch.startedAt, 0) || undefined;
const completedAt =
patch.completedAt === undefined
? status === "done"
? (existing.completedAt ?? now)
: undefined
: normalizeTimestamp(patch.completedAt, 0) || undefined;
const sessionKey =
patch.sessionKey === undefined
? existing.sessionKey
: normalizeOptionalString(patch.sessionKey);
const execution =
patch.execution === undefined
? patch.sessionKey === undefined
? existing.execution
: syncExecutionSessionKey(existing.execution, sessionKey)
: normalizeExecution(patch.execution);
let metadata = normalizeMetadata(patch.metadata, existing.metadata, {
allowDependencyLinks: options.allowMetadataDependencyLinks !== false,
});
const automationPatch: Record<string, unknown> = {};
for (const key of [
"tenant",
"boardId",
"createdByCardId",
"idempotencyKey",
"skills",
"workspace",
"maxRuntimeSeconds",
"maxRetries",
"scheduledAt",
] as const) {
if (Object.hasOwn(patch, key) && patch[key] !== undefined) {
automationPatch[key] = patch[key];
}
}
if (Object.keys(automationPatch).length > 0) {
metadata = trimMetadataToBudget({
...metadata,
automation: normalizeAutomation(automationPatch, metadata.automation),
});
}
const next = removeUndefinedCardFields({
...existing,
title: patch.title === undefined ? existing.title : normalizeTitle(patch.title),
notes: patch.notes === undefined ? existing.notes : normalizeNotes(patch.notes),
status,
priority:
patch.priority === undefined
? existing.priority
: normalizePriority(patch.priority, existing.priority),
labels: patch.labels === undefined ? existing.labels : normalizeLabels(patch.labels),
agentId:
patch.agentId === undefined ? existing.agentId : normalizeOptionalString(patch.agentId),
sessionKey,
runId: patch.runId === undefined ? existing.runId : normalizeOptionalString(patch.runId),
taskId: patch.taskId === undefined ? existing.taskId : normalizeOptionalString(patch.taskId),
sourceUrl:
patch.sourceUrl === undefined
? existing.sourceUrl
: normalizeOptionalString(patch.sourceUrl),
execution,
metadata:
patch.templateId === undefined
? metadata
: { ...metadata, templateId: normalizeTemplateId(patch.templateId) },
position:
patch.position === undefined
? existing.position
: normalizePosition(patch.position, existing.position),
updatedAt: now,
...(startedAt ? { startedAt } : {}),
...(completedAt ? { completedAt } : {}),
});
next.metadata = trimMetadataToBudget(
syncExecutionAttemptMetadata(next.metadata ?? {}, execution, now),
);
next.events = appendEvent(next, updateEvent(existing, next), now);
if (options.enforceStatusHolds && patch.status !== undefined) {
await this.assertActiveStatusAllowed(existing, next, now);
}
if (status !== "done") {
delete next.completedAt;
}
if (patch.startedAt !== undefined && !startedAt) {
delete next.startedAt;
}
if (patch.completedAt !== undefined && !completedAt) {
delete next.completedAt;
}
if (metadataIsEmpty(next.metadata)) {
delete next.metadata;
}
await this.store.register(next.id, { version: 1, card: next });
return next;
}
private async assertActiveStatusAllowed(
existing: WorkboardCard,
next: WorkboardCard,
now: number,
): Promise<void> {
if (
next.status !== "ready" &&
next.status !== "running" &&
next.status !== "review" &&
next.status !== "done"
) {
return;
}
const parents = cardParentIds(next);
const cards =
parents.length > 0 ? new Map((await this.list()).map((card) => [card.id, card])) : undefined;
if (
parents.length > 0 &&
!parents.every((parentId) => cards?.get(parentId)?.status === "done")
) {
throw new Error("card dependencies are not done.");
}
if (next.status === "done") {
return;
}
const scheduledAt = next.metadata?.automation?.scheduledAt;
if ((scheduledAt && scheduledAt > now) || (existing.status === "scheduled" && !scheduledAt)) {
throw new Error("card is scheduled for later.");
}
}
async move(id: string, status: unknown, position: unknown): Promise<WorkboardCard> {
return await this.update(id, {
status,
position,
});
}
async delete(id: string): Promise<{ deleted: boolean }> {
return await this.enqueueMutation(async () => {
const cardId = id.trim();
const deleted = await this.store.delete(cardId);
if (!deleted) {
return { deleted: false };
}
await this.removeReferencesToCard(cardId);
return { deleted: true };
});
}
async addComment(
id: string,
input: WorkboardCommentInput,
scope?: WorkboardMutationScope,
): Promise<WorkboardCard> {
const now = Date.now();
const body = normalizeBoundedString(input.body, undefined, 2000, "comment body");
if (!body) {
throw new Error("comment body is required.");
}
const comment = { id: randomUUID(), body, createdAt: now };
return await this.updateMetadata(id, (existing) => {
assertCanMutateClaimedCard(existing, scope);
return {
...existing.metadata,
comments: [...(existing.metadata?.comments ?? []), comment].slice(-MAX_CARD_COMMENTS),
};
});
}
async addLink(id: string, input: WorkboardLinkInput): Promise<WorkboardCard> {
const now = Date.now();
const targetCardId = normalizeBoundedString(input.targetCardId, undefined, 120, "link target");
const url = normalizeBoundedString(input.url, undefined, 2000, "link URL");
const title = normalizeBoundedString(input.title, undefined, 180, "link title");
if (!targetCardId && !url) {
throw new Error("link targetCardId or url is required.");
}
const type = normalizeLinkType(input.type, "relates_to");
if (type === "parent" || type === "child") {
throw new Error("parent and child dependency links must use linkDependency.");
}
const link: WorkboardLink = {
id: randomUUID(),
type,
createdAt: now,
...(targetCardId ? { targetCardId } : {}),
...(title ? { title } : {}),
...(url ? { url } : {}),
};
return await this.updateMetadata(id, (existing) => ({
...existing.metadata,
links: appendLinkPreservingDependencies(existing.metadata?.links ?? [], link),
}));
}
async linkCards(
parentId: string,
childId: string,
scope?: WorkboardMutationScope,
): Promise<WorkboardCard> {
return await this.enqueueMutation(
async () => await this.linkCardsDirect(parentId, childId, Date.now(), { scope }),
);
}
private async linkCardsDirect(
parentId: string,
childId: string,
now = Date.now(),
options: { allowStatusOnlyActiveChild?: boolean; scope?: WorkboardMutationScope } = {},
): Promise<WorkboardCard> {
if (parentId.trim() === childId.trim()) {
throw new Error("parent and child cards must differ.");
}
const parent = await this.get(parentId);
const child = await this.get(childId);
if (!parent) {
throw new Error(`card not found: ${parentId}`);
}
if (!child) {
throw new Error(`card not found: ${childId}`);
}
assertCanMutateClaimedCard(parent, options.scope);
assertCanMutateClaimedCard(child, options.scope);
if (child.status === "done" || child.status === "blocked") {
const cardsById = new Map((await this.list()).map((card) => [card.id, card]));
const parentIds = [...cardParentIds(child), parent.id].filter(
(id, index, ids) => ids.indexOf(id) === index,
);
if (parentIds.some((id) => cardsById.get(id)?.status !== "done")) {
throw new Error("terminal child cards cannot gain incomplete parent dependencies.");
}
}
if (isActiveDependencyTarget(child, { allowStatusOnly: options.allowStatusOnlyActiveChild })) {
throw new Error("active child cards cannot gain parent dependencies.");
}
if (await this.dependsOn(parent.id, child.id)) {
throw new Error("dependency link would create a cycle.");
}
const parentLinks = parent.metadata?.links ?? [];
const childLinks = child.metadata?.links ?? [];
const nextParentLinks = parentLinks.some(
(link) => link.type === "child" && link.targetCardId === child.id,
)
? parentLinks
: appendLinkPreservingDependencies(parentLinks, {
id: randomUUID(),
type: "child" as const,
targetCardId: child.id,
createdAt: now,
});
const nextChildLinks = childLinks.some(
(link) => link.type === "parent" && link.targetCardId === parent.id,
)
? childLinks
: appendLinkPreservingDependencies(childLinks, {
id: randomUUID(),
type: "parent" as const,
targetCardId: parent.id,
createdAt: now,
});
await this.updateCard(parent.id, {
metadata: { ...parent.metadata, links: nextParentLinks },
});
const nextChild = await this.updateCard(child.id, {
metadata: { ...child.metadata, links: nextChildLinks },
});
return await this.promoteDependencyReady(nextChild.id);
}
async linkParents(childId: string, parentIds: readonly string[]): Promise<WorkboardCard> {
let child = await this.get(childId);
if (!child) {
throw new Error(`card not found: ${childId}`);
}
for (const parentId of parentIds) {
child = await this.linkCards(parentId, child.id);
}
return child;
}
private async dependencyTargetStatus(card: WorkboardCard, now: number): Promise<WorkboardStatus> {
const scheduledAt = card.metadata?.automation?.scheduledAt;
const parents = cardParentIds(card);
if (card.status === "scheduled" && !scheduledAt) {
return "scheduled";
}
if (parents.length === 0) {
if (scheduledAt && scheduledAt > now && isDependencyPromotableStatus(card.status)) {
return "scheduled";
}
return card.status === "scheduled" ? "ready" : card.status;
}
const cards = new Map((await this.list()).map((entry) => [entry.id, entry]));
const parentsDone = parents.every((parentId) => cards.get(parentId)?.status === "done");
if (
!parentsDone &&
scheduledAt &&
scheduledAt > now &&
isDependencyPromotableStatus(card.status)
) {
return "scheduled";
}
if (!parentsDone && isDependencyPromotableStatus(card.status)) {
return "todo";
}
if (
parentsDone &&
scheduledAt &&
scheduledAt > now &&
isDependencyPromotableStatus(card.status)
) {
return "scheduled";
}
return parentsDone && isDependencyPromotableStatus(card.status) ? "ready" : card.status;
}
private async dependsOn(cardId: string, targetParentId: string): Promise<boolean> {
const cards = new Map((await this.list()).map((entry) => [entry.id, entry]));
const seen = new Set<string>();
const visit = (id: string): boolean => {
if (id === targetParentId) {
return true;
}
if (seen.has(id)) {
return false;
}
seen.add(id);
const card = cards.get(id);
return Boolean(card && cardParentIds(card).some(visit));
};
return visit(cardId);
}
private async recordDispatch(card: WorkboardCard, now: number): Promise<WorkboardCard> {
const metadata = trimMetadataToBudget(
normalizeMetadata(
{
...card.metadata,
automation: normalizeAutomation(
{
...card.metadata?.automation,
dispatchCount: (card.metadata?.automation?.dispatchCount ?? 0) + 1,
lastDispatchAt: now,
},
card.metadata?.automation,
),
},
card.metadata,
),
);
const next = removeUndefinedCardFields({
...card,
...(!metadataIsEmpty(metadata) ? { metadata } : { metadata: undefined }),
events: appendEvent(card, { kind: "dispatch" }, now),
});
await this.store.register(card.id, { version: 1, card: next });
return next;
}
private async promoteDependencyReady(id: string, now = Date.now()): Promise<WorkboardCard> {
const card = await this.get(id);
if (!card) {
throw new Error(`card not found: ${id}`);
}
const target = await this.dependencyTargetStatus(card, now);
if (target === card.status) {
return card;
}
return await this.updateCard(card.id, { status: target });
}
async promoteReady(now = Date.now()): Promise<{ cards: WorkboardCard[]; count: number }> {
return await this.enqueueMutation(async () => {
const promoted: WorkboardCard[] = [];
for (const card of await this.list()) {
const next = await this.promoteDependencyReady(card.id, now);
if (next.status !== card.status) {
promoted.push(next);
}
}
return { cards: promoted, count: promoted.length };
});
}
async addProof(
id: string,
input: WorkboardProofInput,
scope?: WorkboardMutationScope,
): Promise<WorkboardCard> {
const now = Date.now();
const proof = normalizeProofInput(input, now);
return await this.updateMetadata(id, (existing) => {
assertCanMutateClaimedCard(existing, scope);
const metadata = clearDiagnostics(existing.metadata, ["missing_proof"]);
return {
...metadata,
proof: [...(metadata.proof ?? []), proof].slice(-MAX_CARD_PROOF),
};
});
}
async addProofWithArtifact(
id: string,
proofInput: WorkboardProofInput,
artifactInput: WorkboardArtifactInput,
scope?: WorkboardMutationScope,
): Promise<WorkboardCard> {
const now = Date.now();
const proof = normalizeProofInput(proofInput, now);
const artifact = normalizeArtifact({ ...artifactInput, createdAt: now });
if (!artifact) {
throw new Error("artifact url or path is required.");
}
return await this.updateMetadata(id, (existing) => {
assertCanMutateClaimedCard(existing, scope);
const metadata = clearDiagnostics(existing.metadata, ["missing_proof"]);
return {
...metadata,
proof: [...(metadata.proof ?? []), proof].slice(-MAX_CARD_PROOF),
artifacts: [...(metadata.artifacts ?? []), artifact].slice(-MAX_CARD_ARTIFACTS),
};
});
}
async addArtifact(
id: string,
input: WorkboardArtifactInput,
scope?: WorkboardMutationScope,
): Promise<WorkboardCard> {
const artifact = normalizeArtifact({ ...input, createdAt: Date.now() });
if (!artifact) {
throw new Error("artifact url or path is required.");
}
return await this.updateMetadata(id, (existing) => {
assertCanMutateClaimedCard(existing, scope);
const metadata = clearDiagnostics(existing.metadata, ["missing_proof"]);
return {
...metadata,
artifacts: [...(metadata.artifacts ?? []), artifact].slice(-MAX_CARD_ARTIFACTS),
};
});
}
async claim(
id: string,
input: WorkboardClaimInput,
): Promise<{ card: WorkboardCard; token: string }> {
const ownerId = normalizeBoundedString(input.ownerId, undefined, 120, "claim owner");
if (!ownerId) {
throw new Error("claim ownerId is required.");
}
const ttlSeconds =
typeof input.ttlSeconds === "number" && Number.isFinite(input.ttlSeconds)
? Math.max(1, Math.trunc(input.ttlSeconds))
: undefined;
const token =
normalizeBoundedString(input.token, undefined, 160, "claim token") ?? randomUUID();
return await this.enqueueMutation(async () => {
const now = Date.now();
const expiresAt = addWorkboardDurationMs(
now,
ttlSeconds ? secondsToDurationMs(ttlSeconds) : DEFAULT_CLAIM_TTL_MS,
);
const guarded = await this.promoteDependencyReady(id, now);
if (cardParentIds(guarded).length > 0 && guarded.status !== "ready") {
throw new Error("card dependencies are not done.");
}
if (guarded.status === "scheduled") {
throw new Error("card is scheduled for later.");
}
if (retryBudgetExhausted(guarded)) {
throw new Error("card exhausted its retry budget.");
}
const existingClaim = guarded.metadata?.claim;
if (existingClaim && existingClaim.expiresAt && existingClaim.expiresAt > now) {
throw new Error(`card already claimed by ${existingClaim.ownerId}.`);
}
const metadata = clearDiagnostics(guarded.metadata, ["stranded_ready"]);
const card = await this.updateCard(id, {
metadata: {
...metadata,
claim: { ownerId, token, claimedAt: now, lastHeartbeatAt: now, expiresAt },
},
});
const next = await this.updateCard(card.id, {
status:
card.status === "backlog" || card.status === "todo" || card.status === "ready"
? "running"
: card.status,
agentId: card.agentId ?? ownerId,
});
return { card: next, token };
});
}
async heartbeat(id: string, input: WorkboardHeartbeatInput): Promise<WorkboardCard> {
const note = normalizeBoundedString(input.note, undefined, 400, "heartbeat note");
const card = await this.updateMetadata(id, (existing) => {
const claim = existing.metadata?.claim;
if (!claim) {
throw new Error("card is not claimed.");
}
const now = Math.max(Date.now(), claim.lastHeartbeatAt + 1);
const token = normalizeOptionalString(input.token);
const ownerId = normalizeOptionalString(input.ownerId);
if (token && token !== claim.token) {
throw new Error("claim token does not match.");
}
if (!token && ownerId && ownerId !== claim.ownerId) {
throw new Error("claim owner does not match.");
}
const nextClaim = {
...claim,
lastHeartbeatAt: now,
expiresAt: claim.expiresAt
? now +
Math.max(
1,
claim.expiresAt > claim.claimedAt
? claim.expiresAt - claim.lastHeartbeatAt
: DEFAULT_CLAIM_TTL_MS,
)
: undefined,
};
const metadata = clearDiagnostics(existing.metadata, ["running_without_heartbeat"]);
return {
...metadata,
claim: removeUndefinedMetadataFields({ claim: nextClaim }).claim,
comments: note
? [...(metadata.comments ?? []), { id: randomUUID(), body: note, createdAt: now }].slice(
-MAX_CARD_COMMENTS,
)
: metadata.comments,
};
});
return card;
}
async releaseClaim(
id: string,
input: WorkboardHeartbeatInput & { status?: unknown } = {},
): Promise<WorkboardCard> {
return await this.enqueueMutation(async () => {
const existing = await this.get(id);
if (!existing) {
throw new Error(`card not found: ${id}`);
}
const status =
input.status === undefined
? existing.status
: normalizeStatus(input.status, existing.status);
const claim = existing.metadata?.claim;
if (claim) {
const token = normalizeOptionalString(input.token);
const ownerId = normalizeOptionalString(input.ownerId);
if (token && token !== claim.token) {
throw new Error("claim token does not match.");
}
if (!token && ownerId && ownerId !== claim.ownerId) {
throw new Error("claim owner does not match.");
}
}
return await this.updateCard(
id,
{
status,
metadata: { ...existing.metadata, claim: undefined },
},
{ enforceStatusHolds: input.status !== undefined },
);
});
}
async complete(
id: string,
input: WorkboardCompleteInput = {},
scope: WorkboardMutationScope | null | undefined = input,
): Promise<WorkboardCard> {
return await this.enqueueMutation(async () => {
const existing = await this.get(id);
if (!existing) {
throw new Error(`card not found: ${id}`);
}
assertCanMutateClaimedCard(existing, scope === null ? undefined : scope);
const now = Date.now();
const createdCardIds = normalizeStringList(input.createdCardIds, "created card ids", 120);
const childIds = cardChildIds(existing);
for (const createdCardId of createdCardIds) {
const createdCard = await this.get(createdCardId);
if (!createdCard) {
throw new Error(`created card not found: ${createdCardId}`);
}
const linkedFromParent =
childIds.includes(createdCardId) && cardParentIds(createdCard).includes(existing.id);
if (!linkedFromParent) {
throw new Error(`created card is not linked to this card: ${createdCardId}`);
}
}
const summary = normalizeBoundedString(input.summary, undefined, 2000, "summary");
const proofInput =
input.proof && typeof input.proof === "object" && !Array.isArray(input.proof)
? (input.proof as WorkboardProofInput)
: undefined;
const proof = proofInput ? normalizeProofInput(proofInput, now) : undefined;
const artifacts = Array.isArray(input.artifacts)
? input.artifacts
.map((artifact) => normalizeArtifact({ ...artifact, createdAt: now }))
.filter((artifact): artifact is WorkboardArtifact => artifact !== null)
.slice(-MAX_CARD_ARTIFACTS)
: [];
const metadata = clearDiagnostics(existing.metadata, ["missing_proof"]);
const notification: WorkboardNotification = {
id: randomUUID(),
kind: "completed",
createdAt: now,
message: capText(summary, 240) ?? "Workboard card completed.",
...(cardSessionKey(existing) ? { sessionKey: cardSessionKey(existing) } : {}),
...(cardRunId(existing) ? { runId: cardRunId(existing) } : {}),
};
const execution =
existing.execution?.status === "running"
? { ...existing.execution, status: "done" as const, updatedAt: now }
: existing.execution;
return await this.updateCard(
id,
{
status: "done",
...(execution ? { execution } : {}),
metadata: {
...metadata,
claim: undefined,
attempts: closeRunningAttempts(metadata.attempts, now, "succeeded"),
failureCount: 0,
automation: normalizeAutomation(
{
...metadata.automation,
summary,
createdCardIds,
},
metadata.automation,
),
comments: summary
? [
...(metadata.comments ?? []),
{ id: randomUUID(), body: summary, createdAt: now },
].slice(-MAX_CARD_COMMENTS)
: metadata.comments,
proof: proof
? [...(metadata.proof ?? []), proof].slice(-MAX_CARD_PROOF)
: metadata.proof,
artifacts: artifacts.length
? [...(metadata.artifacts ?? []), ...artifacts].slice(-MAX_CARD_ARTIFACTS)
: metadata.artifacts,
notifications: [...(metadata.notifications ?? []), notification].slice(
-MAX_CARD_NOTIFICATIONS,
),
},
},
{ enforceStatusHolds: true },
);
});
}
async block(
id: string,
input: WorkboardBlockInput = {},
scope: WorkboardMutationScope | null | undefined = input,
): Promise<WorkboardCard> {
return await this.enqueueMutation(async () => {
const existing = await this.get(id);
if (!existing) {
throw new Error(`card not found: ${id}`);
}
assertCanMutateClaimedCard(existing, scope === null ? undefined : scope);
const now = Date.now();
const reason =
normalizeBoundedString(input.reason, undefined, 2000, "block reason") ??
"Workboard card blocked.";
const metadata = existing.metadata ?? {};
const notification: WorkboardNotification = {
id: randomUUID(),
kind: "failed",
createdAt: now,
message: capText(reason, 240) ?? "Workboard card blocked.",
...(cardSessionKey(existing) ? { sessionKey: cardSessionKey(existing) } : {}),
...(cardRunId(existing) ? { runId: cardRunId(existing) } : {}),
};
const execution =
existing.execution?.status === "running"
? { ...existing.execution, status: "blocked" as const, updatedAt: now }
: existing.execution;
return await this.updateCard(id, {
status: "blocked",
...(execution ? { execution } : {}),
metadata: {
...metadata,
claim: undefined,
attempts: closeRunningAttempts(metadata.attempts, now, "blocked", reason),
failureCount: (metadata.failureCount ?? 0) + 1,
comments: [
...(metadata.comments ?? []),
{ id: randomUUID(), body: reason, createdAt: now },
].slice(-MAX_CARD_COMMENTS),
notifications: [...(metadata.notifications ?? []), notification].slice(
-MAX_CARD_NOTIFICATIONS,
),
},
});
});
}
async unblock(id: string, scope?: WorkboardMutationScope): Promise<WorkboardCard> {
return await this.enqueueMutation(async () => {
const existing = await this.get(id);
if (!existing) {
throw new Error(`card not found: ${id}`);
}
assertCanMutateClaimedCard(existing, scope);
const metadata = clearDiagnostics(existing.metadata, ["blocked_too_long"]);
return await this.updateCard(id, { status: "todo", metadata: { ...metadata, stale: null } });
});
}
async promote(
id: string,
input: WorkboardPromoteInput = {},
scope?: WorkboardMutationScope | null,
): Promise<WorkboardCard> {
return await this.enqueueMutation(async () => {
const existing = await this.get(id);
if (!existing) {
throw new Error(`card not found: ${id}`);
}
assertCanMutateClaimedCard(existing, scope === null ? undefined : scope);
const reason = normalizeBoundedString(input.reason, undefined, 1000, "promote reason");
const comments = reason
? [
...(existing.metadata?.comments ?? []),
{ id: randomUUID(), body: reason, createdAt: Date.now() },
].slice(-MAX_CARD_COMMENTS)
: existing.metadata?.comments;
return await this.updateCard(
id,
{
status: "ready",
metadata: {
...clearDiagnostics(existing.metadata, ["stranded_ready", "blocked_too_long"]),
comments,
stale: null,
},
},
{ enforceStatusHolds: input.force !== true },
);
});
}
async reassign(
id: string,
input: WorkboardReassignInput = {},
scope?: WorkboardMutationScope | null,
): Promise<WorkboardCard> {
return await this.enqueueMutation(async () => {
const existing = await this.get(id);
if (!existing) {
throw new Error(`card not found: ${id}`);
}
assertCanMutateClaimedCard(existing, scope === null ? undefined : scope);
const agentId =
input.agentId === undefined ? existing.agentId : normalizeOptionalString(input.agentId);
const status =
input.status === undefined
? existing.status
: normalizeStatus(input.status, existing.status);
const reason = normalizeBoundedString(input.reason, undefined, 1000, "reassign reason");
const shouldResetFailures = input.resetFailures !== false;
const baseMetadata = shouldResetFailures
? clearDiagnostics(existing.metadata, ["blocked_too_long", "repeated_failures"])
: existing.metadata;
const metadata = {
...baseMetadata,
...(shouldResetFailures ? { failureCount: 0 } : {}),
comments: reason
? [
...(baseMetadata?.comments ?? []),
{ id: randomUUID(), body: reason, createdAt: Date.now() },
].slice(-MAX_CARD_COMMENTS)
: baseMetadata?.comments,
};
return await this.updateCard(id, { agentId, status, metadata }, { enforceStatusHolds: true });
});
}
async reclaim(
id: string,
input: WorkboardReclaimInput = {},
scope?: WorkboardMutationScope | null,
): Promise<WorkboardCard> {
return await this.enqueueMutation(async () => {
const existing = await this.get(id);
if (!existing) {
throw new Error(`card not found: ${id}`);
}
assertCanMutateClaimedCard(existing, scope === null ? undefined : scope);
const now = Date.now();
const reason =
normalizeBoundedString(input.reason, undefined, 1000, "reclaim reason") ??
"Workboard claim reclaimed.";
const targetStatus =
input.status === undefined
? existing.status === "running"
? "ready"
: existing.status
: normalizeStatus(input.status, existing.status);
const reclaimed = await this.updateCard(
id,
{
status: targetStatus,
execution: existing.execution?.status === "running" ? null : existing.execution,
metadata: {
...existing.metadata,
claim: undefined,
attempts: closeRunningAttempts(existing.metadata?.attempts, now, "stopped", reason),
comments: [
...(existing.metadata?.comments ?? []),
{ id: randomUUID(), body: reason, createdAt: now },
].slice(-MAX_CARD_COMMENTS),
stale: null,
},
},
{ enforceStatusHolds: true },
);
return await this.promoteDependencyReady(reclaimed.id, now);
});
}
async dispatch(now = Date.now()): Promise<WorkboardDispatchResult> {
return await this.enqueueMutation(async () => {
const promoted: WorkboardCard[] = [];
const reclaimed: WorkboardCard[] = [];
const blocked: WorkboardCard[] = [];
for (const card of await this.list()) {
let latest = await this.promoteDependencyReady(card.id, now);
const wasPromoted = latest.status !== card.status;
const claim = latest.metadata?.claim;
const latestAttempt = latestRunningAttempt(latest);
const maxRuntimeSeconds = latest.metadata?.automation?.maxRuntimeSeconds;
const runtimeStartedAt = latestAttempt?.startedAt ?? claim?.claimedAt ?? latest.startedAt;
const timedOut =
Boolean(maxRuntimeSeconds && runtimeStartedAt) &&
now - runtimeStartedAt! > secondsToDurationMs(maxRuntimeSeconds!);
const claimExpired = Boolean(claim?.expiresAt && now - claim.expiresAt > CLAIM_RECLAIM_MS);
const retriesExhausted = retryBudgetExhausted(latest);
if (latest.status === "running" && (timedOut || claimExpired)) {
const reason = timedOut
? "Run exceeded the card max runtime."
: "Claim expired without a recent heartbeat.";
const execution =
latest.execution?.status === "running"
? { ...latest.execution, status: "blocked" as const, updatedAt: now }
: latest.execution;
latest = await this.updateCard(latest.id, {
status: "blocked",
...(execution ? { execution } : {}),
metadata: {
...latest.metadata,
claim: undefined,
attempts: closeRunningAttempts(latest.metadata?.attempts, now, "blocked", reason),
failureCount: (latest.metadata?.failureCount ?? 0) + 1,
notifications: [
...(latest.metadata?.notifications ?? []),
{
id: randomUUID(),
kind: "failed" as const,
createdAt: now,
message: reason,
},
].slice(-MAX_CARD_NOTIFICATIONS),
},
});
blocked.push(latest);
} else if (claimExpired) {
latest = await this.updateCard(latest.id, {
metadata: { ...latest.metadata, claim: undefined },
});
reclaimed.push(latest);
}
if (
!latest.metadata?.claim &&
retriesExhausted &&
isDependencyPromotableStatus(latest.status)
) {
latest = await this.updateCard(latest.id, {
status: "blocked",
metadata: {
...latest.metadata,
notifications: [
...(latest.metadata?.notifications ?? []),
{
id: randomUUID(),
kind: "failed" as const,
createdAt: now,
message: "Card exhausted its retry budget.",
},
].slice(-MAX_CARD_NOTIFICATIONS),
},
});
blocked.push(latest);
}
if (latest.status === "ready") {
latest = await this.recordDispatch(latest, now);
}
if (wasPromoted && latest.status !== "blocked") {
promoted.push(latest);
}
}
return {
promoted,
reclaimed,
blocked,
count: promoted.length + reclaimed.length + blocked.length,
};
});
}
async bulkUpdate(input: WorkboardBulkInput): Promise<{ cards: WorkboardCard[] }> {
const ids = Array.isArray(input.ids)
? input.ids.filter((id): id is string => typeof id === "string" && id.trim() !== "")
: [];
if (ids.length === 0) {
throw new Error("ids are required.");
}
const patch =
input.patch && typeof input.patch === "object" && !Array.isArray(input.patch)
? (input.patch as WorkboardCardPatch)
: {};
const cards: WorkboardCard[] = [];
for (const id of ids) {
const updated =
input.archived === undefined
? await this.update(id, patch)
: await this.archive(id, input.archived);
cards.push(updated);
}
return { cards };
}
async archive(id: string, archived: unknown): Promise<WorkboardCard> {
const shouldArchive = archived !== false;
return await this.updateMetadata(id, (existing) => ({
...existing.metadata,
archivedAt: shouldArchive ? Date.now() : 0,
}));
}
async exportCards(): Promise<{ cards: WorkboardCard[]; exportedAt: number }> {
return { cards: await this.list(), exportedAt: Date.now() };
}
async diagnostics(now = Date.now()): Promise<WorkboardDiagnosticsResult> {
const cards = await this.list();
const rows = cards.flatMap((card) => {
const diagnostics = computeCardDiagnostics(card, now);
return diagnostics.length ? [{ card, diagnostics }] : [];
});
return {
diagnostics: rows,
count: rows.reduce((total, row) => total + row.diagnostics.length, 0),
};
}
async refreshDiagnostics(now = Date.now()): Promise<WorkboardDiagnosticsResult> {
return await this.enqueueMutation(async () => {
const cards = await this.list();
const rows: WorkboardDiagnosticsResult["diagnostics"] = [];
for (const card of cards) {
const latest = await this.get(card.id);
if (!latest) {
continue;
}
const diagnostics = mergeDiagnostics(
latest.metadata?.diagnostics,
computeCardDiagnostics(latest, now),
);
if (diagnostics.length === 0 && !latest.metadata?.diagnostics?.length) {
continue;
}
const metadata = trimMetadataToBudget({ ...latest.metadata, diagnostics });
const next = removeUndefinedCardFields({
...latest,
metadata: metadataIsEmpty(metadata) ? undefined : metadata,
});
await this.store.register(next.id, { version: 1, card: next });
if (diagnostics.length > 0) {
rows.push({ card: next, diagnostics });
}
}
return {
diagnostics: rows,
count: rows.reduce((total, row) => total + row.diagnostics.length, 0),
};
});
}
async buildWorkerContext(id: string): Promise<string> {
const card = await this.get(id);
if (!card) {
throw new Error(`card not found: ${id}`);
}
return buildWorkerContext(card, await this.list());
}
static open(
openKeyedStore: (options: { namespace: string; maxEntries: number }) => WorkboardKeyedStore,
) {
return new WorkboardStore(
openKeyedStore({
namespace: "workboard.cards",
maxEntries: MAX_CARDS,
}),
);
}
}