src/core/seed/seed-runtime.ts orchestrates the full seed pipeline in the sequence L0 → L1 → L2 → L3, using pipeline-factory for VectorStore/EmbeddingService init, L1 runner, L2 runner, L3 runner, and persister wiring.[1] executeSeed() is the core runtime called by src/cli/commands/seed.ts after all input validation and user confirmation are complete.[1]
The SeedRuntimeOptions interface exposes: outputDir (all seed output), openclawConfig, optional pluginConfig, optional inputFile (for manifest traceability), logger, and an optional onProgress callback invoked after each round.[1] In createSeedPipeline(), two separate LLMRunner instances are created from StandaloneLLMRunnerFactory: l1LlmRunner with enableTools: false and l2l3LlmRunner with enableTools: true; standalone runners are created only when cfg.llm.enabled && cfg.llm.apiKey.[1]
In executeSeed(), captureStartTimestamp is hardcoded to 0 so that the cold-start guard in captureAtomically() does NOT filter out historical messages — unlike live mode, which uses Date.now() to prevent the first agent_end from dumping full session history.[1] Round messages are mapped to objects with a timestamp field (not ts) before being passed to performAutoCapture(), because l0-recorder's extractUserAssistantMessages reads m.timestamp for incremental filtering.[1] everyNConversations (from openclawConfig) sets the batch size: executeSeed() feeds that many conversation rounds to the pipeline before pausing to wait for L1 to become idle.
After every everyNConversations rounds, executeSeed() must wait for L1 to finish before feeding more rounds; skipping this pause would cause L1 to run only once on the full batch, defeating the "every N" batching semantics.[1] The seed runtime only waits for L1 to become idle before calling pipeline.destroy(); L2 (scene extraction) and L3 (persona generation) may still be in-flight at that point, meaning seed output may not include the latest L2/L3 artifacts.[1]
waitForL1Idle() polls scheduler.getQueueSizes(), scheduler.getBufferedMessageCount(), and scheduler.getSessionState() until L1 is idle, with defaults: pollIntervalMs=1000, stableRounds=3, maxWaitMs=300_000 (5 minutes).[1] L1 is considered truly idle only when queues.l1Idle is true, totalBuffered === 0, and totalConversationCount === 0 — all held for stableRounds consecutive polls.[1]
executeSeed() handles Ctrl+C gracefully: the first SIGINT sets an interrupted flag and allows the current round to finish before shutting down; a second SIGINT calls process.exit(1) immediately.[1]
Sources