From 394931e3b34c1146432de6a7b3a34482747a1959 Mon Sep 17 00:00:00 2001 From: Till JS Date: Wed, 15 Apr 2026 13:44:05 +0200 Subject: [PATCH] fix(ai-missions): strip Svelte \$state Proxies before Dexie writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `createMission`, `updateMission`, and `{start,finish}Iteration` all received caller-supplied objects that can be Svelte 5 \$state Proxies (MissionInputPicker binds the inputs array with \$state). IndexedDB's structured-clone algorithm doesn't accept proxied arrays and throws `DataCloneError: [object Array] could not be cloned` — visible to users as "Mission anlegen" failing silently after clicking Create. Wrap each proxy-carrying payload in `structuredClone()` at the store boundary: - createMission: `inputs` + `cadence` - updateMission: whole `patch` (anything can be proxy) - startIteration: `plan` - finishIteration: `plan` (conditional) `structuredClone` is the native browser / Bun helper; strips Proxies while preserving Dates / Maps / Sets / nested plain data. Store stays robust to any future caller that forgets to snapshot before passing. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/src/lib/data/ai/missions/store.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/mana/apps/web/src/lib/data/ai/missions/store.ts b/apps/mana/apps/web/src/lib/data/ai/missions/store.ts index 74d72ca13..b2019b6b6 100644 --- a/apps/mana/apps/web/src/lib/data/ai/missions/store.ts +++ b/apps/mana/apps/web/src/lib/data/ai/missions/store.ts @@ -38,6 +38,12 @@ export interface CreateMissionInput { export async function createMission(input: CreateMissionInput): Promise { const now = new Date().toISOString(); + // `structuredClone` strips Svelte 5 $state Proxies before the record + // hits IndexedDB — without it, Dexie throws DataCloneError on the + // proxied `inputs` array / `cadence` object that callers pass in + // from `$state` bindings (e.g. the MissionInputPicker). + const inputsPlain = structuredClone(input.inputs ?? []); + const cadencePlain = structuredClone(input.cadence); const mission: Mission = { id: crypto.randomUUID(), createdAt: now, @@ -45,10 +51,10 @@ export async function createMission(input: CreateMissionInput): Promise title: input.title, conceptMarkdown: input.conceptMarkdown, objective: input.objective, - inputs: input.inputs ?? [], - cadence: input.cadence, + inputs: inputsPlain, + cadence: cadencePlain, state: 'active', - nextRunAt: nextRunForCadence(input.cadence, new Date()), + nextRunAt: nextRunForCadence(cadencePlain, new Date()), iterations: [], }; await table().add(mission); @@ -91,8 +97,9 @@ export interface MissionPatch { } export async function updateMission(id: string, patch: MissionPatch): Promise { + // Same Proxy-stripping reason as createMission. const mods: Partial = { - ...patch, + ...structuredClone(patch), updatedAt: new Date().toISOString(), }; if (patch.cadence) { @@ -149,7 +156,9 @@ export async function startIteration( const iteration: MissionIteration = { id: crypto.randomUUID(), startedAt: new Date().toISOString(), - plan: input.plan, + // Strip $state Proxies from the plan array so structured-clone + // doesn't fail when Dexie serialises the row. + plan: structuredClone(input.plan), overallStatus: 'running', }; await table().update(missionId, { @@ -181,7 +190,7 @@ export async function finishIteration( finishedAt: new Date().toISOString(), overallStatus: input.overallStatus, ...(input.summary !== undefined ? { summary: input.summary } : {}), - ...(input.plan !== undefined ? { plan: input.plan } : {}), + ...(input.plan !== undefined ? { plan: structuredClone(input.plan) } : {}), } : it );