diff --git a/extensions/workboard/src/store-card-helpers.ts b/extensions/workboard/src/store-card-helpers.ts index e9745f7280c1..ce11ea2c0e9e 100644 --- a/extensions/workboard/src/store-card-helpers.ts +++ b/extensions/workboard/src/store-card-helpers.ts @@ -16,7 +16,6 @@ import { } from "@openclaw/workboard-contract"; import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; -import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { BLOCKED_TOO_LONG_MS, MAX_CARD_ATTEMPTS, @@ -26,6 +25,7 @@ import { } from "./store-constants.js"; import type { WorkboardMutationScope } from "./store-inputs.js"; import { + capText, metadataIsEmpty, normalizeEvents, normalizeTimestamp, @@ -522,13 +522,6 @@ export function computeCardDiagnostics(card: WorkboardCard, now: number): Workbo return diagnostics; } -export function capText(value: string | undefined, max: number): string | undefined { - if (!value) { - return undefined; - } - return value.length <= max ? value : `${truncateUtf16Safe(value, Math.max(0, max - 1))}…`; -} - export function cardBoardId(card: WorkboardCard): string { return card.metadata?.automation?.boardId ?? "default"; } diff --git a/extensions/workboard/src/store-enrichment.ts b/extensions/workboard/src/store-enrichment.ts index 136ada0ff2ef..15db426aaf52 100644 --- a/extensions/workboard/src/store-enrichment.ts +++ b/extensions/workboard/src/store-enrichment.ts @@ -8,7 +8,6 @@ import type { import type { PersistedWorkboardAttachment } from "./persistence-types.js"; import { assertCanMutateClaimedCard, - capText, cardRunId, cardSessionKey, closeRunningAttempts, @@ -30,6 +29,7 @@ import type { WorkboardWorkerLogInput, } from "./store-inputs.js"; import { + capText, clearDiagnostics, normalizeArtifact, normalizeAttachmentInput, diff --git a/extensions/workboard/src/store-normalizers.ts b/extensions/workboard/src/store-normalizers.ts index 35b1e5abf4ac..155a37e169fb 100644 --- a/extensions/workboard/src/store-normalizers.ts +++ b/extensions/workboard/src/store-normalizers.ts @@ -44,6 +44,7 @@ import { } from "@openclaw/workboard-contract"; import { resolveNonNegativeIntegerOption } from "openclaw/plugin-sdk/number-runtime"; import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { MAX_ATTACHMENT_BYTES, MAX_CARD_ARTIFACTS, @@ -278,6 +279,13 @@ export function normalizeBoundedString( return normalized; } +export function capText(value: string | undefined, max: number): string | undefined { + if (!value) { + return undefined; + } + return value.length <= max ? value : `${truncateUtf16Safe(value, Math.max(0, max - 1))}…`; +} + export function normalizeStatus(value: unknown, fallback: WorkboardStatus): WorkboardStatus { if (typeof value !== "string" || !value.trim()) { return fallback; @@ -941,7 +949,7 @@ function normalizeNotification(value: unknown): WorkboardNotification | null { 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"); + const message = capText(normalizeOptionalString(record.message), 240); if (!kind || !message) { return null; } diff --git a/extensions/workboard/src/store-workflow.ts b/extensions/workboard/src/store-workflow.ts index 3c646d2efc13..1a52661790a7 100644 --- a/extensions/workboard/src/store-workflow.ts +++ b/extensions/workboard/src/store-workflow.ts @@ -13,7 +13,6 @@ import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { assertCanMutateClaimedCard, - capText, cardBoardId, cardChildIds, cardParentIds, @@ -48,6 +47,7 @@ import type { } from "./store-inputs.js"; import { appendCompletionProof, + capText, clearDiagnostics, deriveChildIdempotencyKey, normalizeArtifact, diff --git a/extensions/workboard/src/store.test.ts b/extensions/workboard/src/store.test.ts index 1568fc5eaa9d..143cdf84c67b 100644 --- a/extensions/workboard/src/store.test.ts +++ b/extensions/workboard/src/store.test.ts @@ -2748,6 +2748,50 @@ describe("WorkboardStore", () => { expect(blocked.metadata?.notifications?.[0]?.message.length).toBeLessThanOrEqual(240); }); + it("heals oversized persisted notifications and keeps dispatching sibling cards", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-workboard-notification-")); + const dbPath = path.join(dir, "workboard.sqlite"); + const stores = createWorkboardSqliteStores({ dbPath }); + try { + const store = new WorkboardStore(stores.cards); + const poisoned = await store.create({ title: "Oversized notification", status: "ready" }); + const sibling = await store.create({ title: "Unaffected sibling", status: "ready" }); + const oversized = `${"x".repeat(238)}🦞${" tail".repeat(60)}`; + const rawDb = new DatabaseSync(dbPath); + try { + rawDb + .prepare( + "INSERT INTO workboard_card_notifications (id, card_id, ordinal, kind, message, created_at) VALUES (?, ?, ?, ?, ?, ?)", + ) + .run("oversized", poisoned.id, 0, "failed", oversized, Date.now()); + } finally { + rawDb.close(); + } + + expect((await store.get(poisoned.id))?.metadata?.notifications?.[0]?.message).toBe(oversized); + await expect(store.dispatch()).resolves.toBeDefined(); + + const repaired = await store.get(poisoned.id); + expect(repaired?.metadata?.notifications?.[0]?.message).toBe(`${"x".repeat(238)}…`); + expect(repaired?.metadata?.automation?.dispatchCount).toBe(1); + expect((await store.get(sibling.id))?.metadata?.automation?.dispatchCount).toBe(1); + + const verifyDb = new DatabaseSync(dbPath, { readOnly: true }); + try { + expect( + verifyDb + .prepare("SELECT message FROM workboard_card_notifications WHERE id = ?") + .get("oversized"), + ).toEqual({ message: `${"x".repeat(238)}…` }); + } finally { + verifyDb.close(); + } + } finally { + stores.close(); + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + it("dispatches ready cards and blocks expired or timed-out work", async () => { vi.useFakeTimers(); try { diff --git a/extensions/workboard/src/store.ts b/extensions/workboard/src/store.ts index 01cb519cfdc1..540820e1f08c 100644 --- a/extensions/workboard/src/store.ts +++ b/extensions/workboard/src/store.ts @@ -21,7 +21,6 @@ import { createWorkboardSqliteStores } from "./sqlite-store.js"; import { buildWorkerContext, assertCanMutateClaimedCard, - capText, cardBoardId, cardRunId, cardSessionKey, @@ -49,7 +48,7 @@ import type { WorkboardDispatchResult, WorkboardMutationScope, } from "./store-inputs.js"; -import { normalizeBoardId, normalizeTimestamp } from "./store-normalizers.js"; +import { capText, normalizeBoardId, normalizeTimestamp } from "./store-normalizers.js"; import { WorkboardNotificationStore } from "./store-notifications.js"; export type { WorkboardDispatchResult } from "./store-inputs.js";