mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(workboard): heal oversized persisted notifications (#120999)
Preserve the existing UTF-16-safe notification cap at the canonical metadata normalization boundary so a legacy oversized row cannot abort dispatch or starve sibling cards. Move the existing capText helper into its normalization owner without production growth. Co-authored-by: licheer-zte <licheer-zte@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user