diff --git a/extensions/workboard/src/dispatcher-ownership.test.ts b/extensions/workboard/src/dispatcher-ownership.test.ts index 8d5cc35b8cc0..5fc23072d698 100644 --- a/extensions/workboard/src/dispatcher-ownership.test.ts +++ b/extensions/workboard/src/dispatcher-ownership.test.ts @@ -24,6 +24,29 @@ function createMemoryStore(): WorkboardKeyedStore { } describe("Workboard dispatcher ownership", () => { + it("dispatches a card whose create input tried to inject archivedAt", async () => { + const store = new WorkboardStore(createMemoryStore()); + const now = 10; + const card = await store.create({ + title: "Injected archive", + status: "ready", + workspaceAccess: { unrestricted: true }, + metadata: { archivedAt: now }, + }); + const run = vi.fn().mockResolvedValue({ runId: "run-injected" }); + + await dispatchAndStartWorkboardCards({ + store, + subagent: { run }, + options: { now, maxStarts: 1 }, + }); + + expect(run).toHaveBeenCalledTimes(1); + await expect(store.get(card.id)).resolves.toMatchObject({ + execution: { runId: "run-injected" }, + }); + }); + it("falls back to one default owner for persisted blank and unassigned agents", async () => { const keyed = createMemoryStore(); const store = new WorkboardStore(keyed); diff --git a/extensions/workboard/src/store-core.ts b/extensions/workboard/src/store-core.ts index 96a9c92ee0fa..bbff7ea07299 100644 --- a/extensions/workboard/src/store-core.ts +++ b/extensions/workboard/src/store-core.ts @@ -389,7 +389,7 @@ export class WorkboardCoreStore { templateId: normalizeTemplateId(input.templateId), ...(childAutomation ? { automation: childAutomation } : {}), }, - { allowDependencyLinks: false }, + { allowDependencyLinks: false, allowArchivedAt: false }, ); const syncedMetadata = trimMetadataToBudget( syncExecutionAttemptMetadata(metadata, execution, now), diff --git a/extensions/workboard/src/store-normalizers.ts b/extensions/workboard/src/store-normalizers.ts index 1fcd4c5faca1..5627dbb68405 100644 --- a/extensions/workboard/src/store-normalizers.ts +++ b/extensions/workboard/src/store-normalizers.ts @@ -1024,7 +1024,11 @@ export function appendCompletionProof( export function normalizeMetadata( value: unknown, fallback: WorkboardMetadata = {}, - options: { allowDependencyLinks?: boolean; preserveProofId?: string } = {}, + options: { + allowDependencyLinks?: boolean; + allowArchivedAt?: boolean; + preserveProofId?: string; + } = {}, ): WorkboardMetadata { if (!value || typeof value !== "object" || Array.isArray(value)) { return trimMetadataToBudget(fallback, options); @@ -1034,7 +1038,11 @@ export function normalizeMetadata( record.stale && typeof record.stale === "object" && !Array.isArray(record.stale) ? (record.stale as Record) : null; - const hasArchivedAt = Object.hasOwn(record, "archivedAt"); + // Archival is a transition owned by archive(), which appends the matching + // `archived` event. Callers that cannot produce that event (create) must not + // be able to declare it, or the card is excluded from dispatch from the + // instant it exists with an event log recording only `created`. + const hasArchivedAt = Object.hasOwn(record, "archivedAt") && options.allowArchivedAt !== false; const hasStale = Object.hasOwn(record, "stale"); const hasLifecycleStatusSourceUpdatedAt = Object.hasOwn(record, "lifecycleStatusSourceUpdatedAt"); const links = Array.isArray(record.links) diff --git a/extensions/workboard/src/store.test.ts b/extensions/workboard/src/store.test.ts index b16b8694042d..8cf6c15c9cf0 100644 --- a/extensions/workboard/src/store.test.ts +++ b/extensions/workboard/src/store.test.ts @@ -1010,6 +1010,24 @@ describe("WorkboardStore", () => { expect(restored.events?.at(-1)).toMatchObject({ kind: "unarchived" }); }); + it("ignores caller-supplied archivedAt on create so no card is born archived", async () => { + const store = new WorkboardStore(createMemoryStore()); + const card = await store.create({ + title: "Injected archive", + metadata: { archivedAt: Date.now() }, + }); + + // Archival is a transition owned by archive(), which appends the matching + // event. Honouring it here would exclude the card from dispatch from birth + // with an event log recording only "created". + expect(card.metadata?.archivedAt).toBeUndefined(); + expect(card.events?.map((event) => event.kind)).toEqual(["created"]); + + const archived = await store.archive(card.id, true); + expect(archived.metadata?.archivedAt).toBeGreaterThan(0); + expect(archived.events?.at(-1)).toMatchObject({ kind: "archived" }); + }); + it("resolves matching unknown proof on completion without duplicating it", async () => { vi.useFakeTimers(); try {