refactor(workboard): consolidate card normalization helpers (#129236)

This commit is contained in:
Peter Steinberger
2026-08-25 03:51:09 -07:00
committed by GitHub
parent f830b8c159
commit b1e27db51c
3 changed files with 194 additions and 284 deletions
+1 -1
View File
@@ -1437,7 +1437,7 @@ extensions/workboard/src/sqlite-store.ts 44
extensions/workboard/src/store-automation.ts 1
extensions/workboard/src/store-card-helpers.ts 1
extensions/workboard/src/store-core.ts 5
extensions/workboard/src/store-normalizers.ts 33
extensions/workboard/src/store-normalizers.ts 11
extensions/workboard/src/store-workflow.ts 2
extensions/workboard/src/store.ts 5
extensions/workboard/src/tools.ts 11
+109 -124
View File
@@ -131,12 +131,25 @@ export function appendEvent(
].slice(-MAX_CARD_EVENTS);
}
function latestMetadataIdChanged(
existing: readonly { id: string }[] | undefined,
next: readonly { id: string }[] | undefined,
function metadataEntriesChanged(
existing: WorkboardCard,
next: WorkboardCard,
key:
| "comments"
| "links"
| "proof"
| "artifacts"
| "attachments"
| "workerLogs"
| "notifications",
): boolean {
const latestId = next?.at(-1)?.id;
return Boolean(latestId && latestId !== existing?.at(-1)?.id);
const previous = existing.metadata?.[key];
const current = next.metadata?.[key];
const latestId = current?.at(-1)?.id;
return (
(previous?.length ?? 0) !== (current?.length ?? 0) ||
Boolean(latestId && latestId !== previous?.at(-1)?.id)
);
}
export function lifecycleStatusSourceUpdatedAtFromPatch(metadata: unknown): number | undefined {
@@ -258,34 +271,17 @@ export function updateEvent(
...(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" };
for (const [key, kind] of [
["comments", "comment_added"],
["links", "link_added"],
["proof", "proof_added"],
["artifacts", "artifact_added"],
] as const) {
if (metadataEntriesChanged(existing, next, key)) {
return { kind };
}
}
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?.attachments?.length ?? 0) !== (next.metadata?.attachments?.length ?? 0) ||
latestMetadataIdChanged(existing.metadata?.attachments, next.metadata?.attachments)
) {
if (metadataEntriesChanged(existing, next, "attachments")) {
return (next.metadata?.attachments?.length ?? 0) > (existing.metadata?.attachments?.length ?? 0)
? { kind: "attachment_added" }
: { kind: "edited" };
@@ -293,20 +289,13 @@ export function updateEvent(
if (existing.metadata?.workerProtocol?.state !== next.metadata?.workerProtocol?.state) {
return { kind: "orchestration" };
}
if (
(existing.metadata?.workerLogs?.length ?? 0) !== (next.metadata?.workerLogs?.length ?? 0) ||
latestMetadataIdChanged(existing.metadata?.workerLogs, next.metadata?.workerLogs)
) {
if (metadataEntriesChanged(existing, next, "workerLogs")) {
return { kind: "orchestration" };
}
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)
) {
if (metadataEntriesChanged(existing, next, "notifications")) {
return { kind: "notification" };
}
if (
@@ -552,6 +541,19 @@ function cardResultSummary(card: WorkboardCard): string | undefined {
);
}
function appendWorkerContextSection<T>(
lines: string[],
heading: string,
entries: readonly T[] | undefined,
format: (entry: T) => string,
maxEntries = 8,
): void {
const recent = entries?.slice(-maxEntries) ?? [];
if (recent.length) {
lines.push("", `## ${heading}`, ...recent.map(format));
}
}
export function buildWorkerContext(
card: WorkboardCard,
cards: readonly WorkboardCard[] = [],
@@ -567,85 +569,69 @@ export function buildWorkerContext(
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 attachments = card.metadata?.attachments?.slice(-8) ?? [];
if (attachments.length) {
lines.push("", "## Attachments");
for (const attachment of attachments) {
const detail = [
attachment.fileName,
`${attachment.byteSize} bytes`,
attachment.mimeType,
attachment.note,
]
.filter(Boolean)
.join(" · ");
lines.push(`- ${capText(detail, 500)}`);
}
}
appendWorkerContextSection(lines, "Recent attempts", card.metadata?.attempts, (attempt) =>
`- ${attempt.status} ${attempt.model ?? ""} ${attempt.error ? `error=${capText(attempt.error, 240)}` : ""}`.trim(),
);
appendWorkerContextSection(
lines,
"Recent comments",
card.metadata?.comments,
(comment) => `- ${capText(comment.body, 400)}`,
12,
);
appendWorkerContextSection(
lines,
"Proof",
card.metadata?.proof,
(entry) =>
`- ${entry.status}: ${capText(entry.label ?? entry.command ?? entry.url ?? entry.note, 400)}`,
);
appendWorkerContextSection(
lines,
"Artifacts",
card.metadata?.artifacts,
(artifact) => `- ${capText(artifact.label ?? artifact.url ?? artifact.path, 400)}`,
);
appendWorkerContextSection(lines, "Attachments", card.metadata?.attachments, (attachment) => {
const detail = [
attachment.fileName,
`${attachment.byteSize} bytes`,
attachment.mimeType,
attachment.note,
]
.filter(Boolean)
.join(" · ");
return `- ${capText(detail, 500)}`;
});
if (card.metadata?.workerProtocol) {
const protocol = card.metadata.workerProtocol;
lines.push("", "## Worker protocol");
lines.push(`${protocol.state}: ${capText(protocol.detail, 500) ?? "no detail"}`);
}
const workerLogs = card.metadata?.workerLogs?.slice(-8) ?? [];
if (workerLogs.length) {
lines.push("", "## Worker logs");
for (const log of workerLogs) {
lines.push(`- ${log.level}: ${capText(log.message, 500)}`);
}
}
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 ?? ""}`);
}
}
appendWorkerContextSection(
lines,
"Worker logs",
card.metadata?.workerLogs,
(log) => `- ${log.level}: ${capText(log.message, 500)}`,
);
appendWorkerContextSection(
lines,
"Links",
card.metadata?.links,
(link) => `- ${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"}`,
);
}
}
appendWorkerContextSection(
lines,
"Parent results",
parentResults,
(parent) =>
`- ${parent.id} ${parent.title}: ${capText(cardResultSummary(parent), 500) ?? "done"}`,
);
const recentAgentWork =
card.agentId && cards.length
? cards
@@ -659,14 +645,12 @@ export function buildWorkerContext(
.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"}`,
);
}
}
appendWorkerContextSection(
lines,
`Recent done work by ${card.agentId}`,
recentAgentWork,
(entry) => `- ${entry.id} ${entry.title}: ${capText(cardResultSummary(entry), 300) ?? "done"}`,
);
const automation = card.metadata?.automation;
if (automation) {
lines.push("", "## Automation");
@@ -689,12 +673,13 @@ export function buildWorkerContext(
}
}
const diagnostics = computeCardDiagnostics(card, Date.now());
if (diagnostics.length) {
lines.push("", "## Active diagnostics");
for (const entry of diagnostics) {
lines.push(`- ${entry.severity}: ${entry.title}`);
}
}
appendWorkerContextSection(
lines,
"Active diagnostics",
diagnostics,
(entry) => `- ${entry.severity}: ${entry.title}`,
diagnostics.length,
);
return lines.join("\n");
}
+84 -159
View File
@@ -15,7 +15,6 @@ import {
WORKBOARD_TEMPLATE_IDS,
type WorkboardArtifact,
type WorkboardAttachment,
type WorkboardAttemptStatus,
type WorkboardAutomation,
type WorkboardBoardMetadata,
type WorkboardClaim,
@@ -23,12 +22,9 @@ import {
type WorkboardDiagnostic,
type WorkboardDiagnosticAction,
type WorkboardDiagnosticKind,
type WorkboardDiagnosticSeverity,
type WorkboardEvent,
type WorkboardEventKind,
type WorkboardExecution,
type WorkboardExecutionMode,
type WorkboardExecutionStatus,
type WorkboardLink,
type WorkboardLinkType,
type WorkboardLaunchState,
@@ -39,7 +35,6 @@ import {
type WorkboardOrchestrationSettings,
type WorkboardPriority,
type WorkboardProof,
type WorkboardProofStatus,
type WorkboardRunAttempt,
type WorkboardStatus,
type WorkboardTemplateId,
@@ -550,69 +545,20 @@ export function deriveChildIdempotencyKey(
return key.length <= 160 ? key : undefined;
}
function normalizeExecutionMode(
function normalizeEnumValue<T extends string, TFallback extends T | undefined>(
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;
allowed: readonly T[],
fallback: TFallback,
): T | TFallback {
return typeof value === "string" && allowed.includes(value as T) ? (value as T) : fallback;
}
export 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;
return normalizeEnumValue(value, WORKBOARD_LINK_TYPES, fallback);
}
export function normalizeTemplateId(value: unknown): WorkboardTemplateId | undefined {
return typeof value === "string" && WORKBOARD_TEMPLATE_IDS.includes(value as WorkboardTemplateId)
? (value as WorkboardTemplateId)
: undefined;
return normalizeEnumValue(value, WORKBOARD_TEMPLATE_IDS, undefined);
}
export function normalizeTimestamp(value: unknown, fallback: number): number {
@@ -627,23 +573,13 @@ function normalizeEvent(value: unknown): WorkboardEvent | null {
}
const record = value;
const id = normalizeOptionalString(record.id);
const kind = WORKBOARD_EVENT_KINDS.includes(record.kind as WorkboardEventKind)
? (record.kind as WorkboardEventKind)
: null;
const kind = normalizeEnumValue(record.kind, WORKBOARD_EVENT_KINDS, undefined);
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 fromStatus = normalizeEnumValue(record.fromStatus, WORKBOARD_STATUSES, undefined);
const toStatus = normalizeEnumValue(record.toStatus, WORKBOARD_STATUSES, undefined);
const sessionKey = normalizeOptionalString(record.sessionKey);
const runId = normalizeOptionalString(record.runId);
return {
@@ -685,7 +621,7 @@ function normalizeAttempt(value: unknown): WorkboardRunAttempt | null {
const model = normalizeBoundedString(record.model, undefined, 160, "attempt model");
return {
id,
status: normalizeAttemptStatus(record.status, "running"),
status: normalizeEnumValue(record.status, WORKBOARD_ATTEMPT_STATUSES, "running"),
startedAt,
...(endedAt ? { endedAt } : {}),
...(engine ? { engine } : {}),
@@ -761,7 +697,7 @@ function normalizeProof(value: unknown): WorkboardProof | null {
const note = normalizeBoundedString(record.note, undefined, 2000, "proof note");
return {
id,
status: normalizeProofStatus(record.status, "unknown"),
status: normalizeEnumValue(record.status, WORKBOARD_PROOF_STATUSES, "unknown"),
createdAt,
...(label ? { label } : {}),
...(command ? { command } : {}),
@@ -858,14 +794,11 @@ function normalizeWorkerProtocol(
return fallback;
}
const record = value;
const state =
record.state === "idle" ||
record.state === "running" ||
record.state === "completed" ||
record.state === "blocked" ||
record.state === "violated"
? record.state
: fallback?.state;
const state = normalizeEnumValue(
record.state,
["idle", "running", "completed", "blocked", "violated"],
fallback?.state,
);
if (!state) {
return undefined;
}
@@ -970,14 +903,8 @@ function normalizeDiagnostic(value: unknown): WorkboardDiagnostic | null {
return null;
}
const record = value;
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 kind = normalizeEnumValue(record.kind, WORKBOARD_DIAGNOSTIC_KINDS, undefined);
const severity = normalizeEnumValue(record.severity, WORKBOARD_DIAGNOSTIC_SEVERITIES, "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());
@@ -1011,9 +938,7 @@ function normalizeNotification(value: unknown): WorkboardNotification | null {
}
const record = value;
const id = normalizeOptionalString(record.id) ?? randomUUID();
const kind = WORKBOARD_NOTIFICATION_KINDS.includes(record.kind as WorkboardNotificationKind)
? (record.kind as WorkboardNotificationKind)
: undefined;
const kind = normalizeEnumValue(record.kind, WORKBOARD_NOTIFICATION_KINDS, undefined);
const createdAt = normalizeTimestamp(record.createdAt, Date.now());
const sequence = normalizeTimestamp(record.sequence, 0) || undefined;
const message = normalizeBoundedString(record.message, undefined, 240, "notification message");
@@ -1040,7 +965,7 @@ export function normalizeProofInput(input: WorkboardProofInput, now: number): Wo
const note = normalizeBoundedString(input.note, undefined, 2000, "proof note");
return {
id: randomUUID(),
status: normalizeProofStatus(input.status, "unknown"),
status: normalizeEnumValue(input.status, WORKBOARD_PROOF_STATUSES, "unknown"),
createdAt: now,
...(label ? { label } : {}),
...(command ? { command } : {}),
@@ -1087,6 +1012,20 @@ export function appendCompletionProof(
return entries.slice(-MAX_CARD_PROOF);
}
function normalizeList<T>(
value: unknown,
normalize: (entry: unknown) => T | null,
limit: number,
fallback?: T[],
): T[] | undefined {
return Array.isArray(value)
? value
.map(normalize)
.filter((entry): entry is T => entry !== null)
.slice(-limit)
: fallback;
}
export function normalizeMetadata(
value: unknown,
fallback: WorkboardMetadata = {},
@@ -1131,43 +1070,38 @@ export function normalizeMetadata(
})()
: links.slice(-MAX_CARD_LINKS);
const normalized = {
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,
attempts: normalizeList(
record.attempts,
normalizeAttempt,
MAX_CARD_ATTEMPTS,
fallback.attempts,
),
comments: normalizeList(
record.comments,
normalizeComment,
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,
attachments: Array.isArray(record.attachments)
? record.attachments
.map(normalizeAttachment)
.filter((attachment): attachment is WorkboardAttachment => attachment !== null)
.slice(-MAX_CARD_ATTACHMENTS)
: fallback.attachments,
workerLogs: Array.isArray(record.workerLogs)
? record.workerLogs
.map(normalizeWorkerLog)
.filter((log): log is WorkboardWorkerLog => log !== null)
.slice(-MAX_CARD_WORKER_LOGS)
: fallback.workerLogs,
proof: normalizeList(record.proof, normalizeProof, MAX_CARD_PROOF, fallback.proof),
artifacts: normalizeList(
record.artifacts,
normalizeArtifact,
MAX_CARD_ARTIFACTS,
fallback.artifacts,
),
attachments: normalizeList(
record.attachments,
normalizeAttachment,
MAX_CARD_ATTACHMENTS,
fallback.attachments,
),
workerLogs: normalizeList(
record.workerLogs,
normalizeWorkerLog,
MAX_CARD_WORKER_LOGS,
fallback.workerLogs,
),
workerProtocol: Object.hasOwn(record, "workerProtocol")
? normalizeWorkerProtocol(record.workerProtocol, fallback.workerProtocol)
: fallback.workerProtocol,
@@ -1181,20 +1115,18 @@ export function normalizeMetadata(
? normalizeClaim(record.claim, fallback.claim)
: undefined
: fallback.claim,
diagnostics: Array.isArray(record.diagnostics)
? record.diagnostics
.map(normalizeDiagnostic)
.filter(
(diagnosticLocal): diagnosticLocal is WorkboardDiagnostic => diagnosticLocal !== 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,
diagnostics: normalizeList(
record.diagnostics,
normalizeDiagnostic,
MAX_CARD_DIAGNOSTICS,
fallback.diagnostics,
),
notifications: normalizeList(
record.notifications,
normalizeNotification,
MAX_CARD_NOTIFICATIONS,
fallback.notifications,
),
templateId: normalizeTemplateId(record.templateId) ?? fallback.templateId,
archivedAt: hasArchivedAt
? normalizeTimestamp(record.archivedAt, 0) || undefined
@@ -1242,8 +1174,8 @@ export function normalizeExecution(value: unknown): WorkboardExecution | undefin
return {
id,
kind: "agent-session",
mode: normalizeExecutionMode(record.mode, "autonomous"),
status: normalizeExecutionStatus(record.status, "idle"),
mode: normalizeEnumValue(record.mode, WORKBOARD_EXECUTION_MODES, "autonomous"),
status: normalizeEnumValue(record.status, WORKBOARD_EXECUTION_STATUSES, "idle"),
startedAt,
updatedAt,
...(engine ? { engine } : {}),
@@ -1269,17 +1201,10 @@ export function syncExecutionSessionKey(
function removeUndefinedExecutionFields(execution: WorkboardExecution): WorkboardExecution {
const next = { ...execution };
if (next.engine === undefined) {
delete next.engine;
}
if (next.model === undefined) {
delete next.model;
}
if (next.sessionKey === undefined) {
delete next.sessionKey;
}
if (next.runId === undefined) {
delete next.runId;
for (const key of ["engine", "model", "sessionKey", "runId"] as const) {
if (next[key] === undefined) {
delete next[key];
}
}
return next;
}