fix(ai): defer seed-registry side-effect imports to browser boot

SSR 500'd on every (app)/* route with:

    TypeError: Cannot read properties of undefined (reading 'table')
    at apps/mana/apps/web/src/lib/modules/meditate/collections.ts:13:39
    at async eval (.../meditate/stores/meditate.svelte.ts:...)

Root cause: ai/missions/setup.ts had static side-effect imports of
meditate/habits/goals seed modules. Each seed module transitively
imports its module's collections.ts, which does `db.table(...)` at
module-eval time. During SSR, Vite's module-runner evaluates imports
depth-first — the seed imports race database.ts's own eager
dependency eval, observe `db` as still-undefined (live-binding in
the middle of a circular chain), and crash.

Fix: replace the three `import '$lib/modules/<X>/seed'` side-effect
imports with a single async `ensureSeedsRegistered()` that dynamic-
imports them, guarded by the `browser` flag so SSR never touches
them. Called fire-and-forget from `startMissionTick` (which is
itself client-only via the onMount wrapping in +layout.svelte), so
template applicators still see the registry populated before they
need it.

Net effect:
- SSR chain for any (app)/* route no longer touches meditate/habits/
  goals collections.ts → no db-undefined race.
- Browser behavior unchanged: seeds register at the first mission
  tick, just like before, before any template applier runs.

Verified: after HMR/manual cache-bust, curl / returns 200 in place
of the previous 500. Type-check 0 errors across 7230 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-21 15:54:00 +02:00
parent 260dd312a9
commit be45dcff82

View file

@ -15,18 +15,33 @@
* see COMPANION_BRAIN_ARCHITECTURE.md §20.5.
*/
import { browser } from '$app/environment';
import { createManaLlmClient } from './llm-client';
import { runDueMissions, type MissionRunnerDeps } from './runner';
import { registerDefaultInputResolvers } from './default-resolvers';
import { runAgentsBootstrap } from '../agents/bootstrap';
// Side-effect imports to populate the seed-handler registry before any
// template is applied. Keep this list tight — each module pulls its
// own imports (encryptRecord, Dexie tables, etc.) so adding a seed
// handler to a rarely-used module slows down the hot path otherwise.
// See docs/plans/workbench-templates.md §T1.
import '$lib/modules/meditate/seed';
import '$lib/modules/habits/seed';
import '$lib/companion/goals/seed';
/**
* Populate the seed-handler registry. Each import pulls the module's
* Dexie table accessors (via collections.ts db.table()) at evaluation
* time which crashes SSR when the eager module graph races
* database.ts's own evaluation and observes `db` as still-undefined.
* We therefore defer the imports to the browser and run them before the
* first mission tick kicks off earliest any template applicator would
* need them.
*
* See docs/plans/workbench-templates.md §T1.
*/
let seedsRegistered = false;
async function ensureSeedsRegistered(): Promise<void> {
if (seedsRegistered || !browser) return;
seedsRegistered = true;
await Promise.all([
import('$lib/modules/meditate/seed'),
import('$lib/modules/habits/seed'),
import('$lib/companion/goals/seed'),
]);
}
/** Default interval between tick scans. One minute is fine for foreground use. */
const DEFAULT_TICK_INTERVAL_MS = 60_000;
@ -47,6 +62,12 @@ export function startMissionTick(intervalMs: number = DEFAULT_TICK_INTERVAL_MS):
if (tickHandle !== null) return stopMissionTick;
registerDefaultInputResolvers();
// Populate the seed-handler registry before any template is applied.
// Client-only — SSR never needs the handlers. Fire-and-forget because
// templates can't be applied for a handful of microtasks after boot,
// and the template applicator itself awaits the registry.
void ensureSeedsRegistered();
// Multi-Agent Workbench: ensure a default "Mana" agent exists and
// backfill agentId on legacy missions. Fire-and-forget — the runner
// itself tolerates missions without an agentId during the migration