import { withSqliteRetry } from "../util/sqlite-retry.js"; import { createConversation } from "./conversation-crud.js"; import { AUTO_TITLE_DETERMINISTIC, deriveDeterministicTitle, type TitleOrigin, } from "./conversation-title-service.js"; export interface BootstrapConversationOptions { conversationType?: "standard" | "background" | "scheduled"; source?: string; origin: TitleOrigin; systemHint: string; scheduleJobId?: string; groupId?: string; /** * When set, the new conversation is linked to its parent via the * `fork_parent_conversation_id` column. Used by background jobs that * spawn conversations off a source conversation (memory-retrospective) * so the parent → child relationship is queryable later (e.g. "find the * most recent retrospective for this source"). */ forkParentConversationId?: string; /** * When set, persisted as the new conversation's `parent_conversation_id` — * the conversation that spawned this one (subagent spawns). Telemetry * resolves it at flush time to attribute the child's LLM usage back to the * parent's in-flight user turn. Unlike `forkParentConversationId` it * implies no history inheritance. */ parentConversationId?: string; /** * Role the subagent that owns this conversation was spawned with. Persisted * on the conversation row (not only on the ephemeral `subagents` row) so * usage telemetry can attribute delegated spend per role long after the * subagent record is disposed. */ subagentRole?: string; /** * How the subagent that owns this conversation was spawned, one of the modes * described on `SubagentSpawnMode` in `subagent/types.ts`. Orthogonal to * {@link subagentRole}; persisted for the same reason. */ subagentSpawnMode?: string; } /** * Create a system-initiated conversation with a deterministic title derived * from `systemHint` — no LLM call. Background conversations (heartbeat runs, * scheduled jobs, subagents, retrospectives) are created in bulk and rarely * read by name, so an LLM-generated title is not worth the tokens. The title * is persisted with `AUTO_TITLE_DETERMINISTIC`, which keeps it replaceable: * if a user ever sends a message in the conversation, the title-generate * hook upgrades it to an LLM title (see * `plugins/defaults/title-generate/hooks/user-prompt-submit.ts`). */ export async function bootstrapConversation( opts: BootstrapConversationOptions, ) { return withSqliteRetry( () => createConversation({ title: deriveDeterministicTitle({ origin: opts.origin, systemHint: opts.systemHint, }), isAutoTitle: AUTO_TITLE_DETERMINISTIC, ...(opts.conversationType && { conversationType: opts.conversationType, }), ...(opts.source && { source: opts.source }), ...(opts.scheduleJobId && { scheduleJobId: opts.scheduleJobId }), ...(opts.groupId && { groupId: opts.groupId }), ...(opts.forkParentConversationId && { forkParentConversationId: opts.forkParentConversationId, }), ...(opts.parentConversationId && { parentConversationId: opts.parentConversationId, }), ...(opts.subagentRole && { subagentRole: opts.subagentRole }), ...(opts.subagentSpawnMode && { subagentSpawnMode: opts.subagentSpawnMode, }), }), { op: "bootstrapConversation" }, ); }