@cleocode/core 2026.4.44 → 2026.4.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/conduit/conduit-client.ts", "../../src/conduit/http-transport.ts", "../../src/conduit/local-transport.ts", "../../src/store/conduit-sqlite.ts", "../../src/conduit/sse-transport.ts", "../../src/conduit/factory.ts"],
4
+ "sourcesContent": ["/**\n * ConduitClient \u2014 High-level agent messaging that wraps a Transport adapter.\n *\n * This is the WHAT layer: send messages, subscribe to events, manage presence.\n * The Transport adapter (HttpTransport, LocalTransport, etc.) handles the HOW.\n *\n * @see docs/specs/SIGNALDOCK-UNIFIED-AGENT-REGISTRY.md Section 4.3\n * @task T177\n */\n\nimport type {\n AgentCredential,\n Conduit,\n ConduitMessage,\n ConduitSendOptions,\n ConduitSendResult,\n ConduitState,\n ConduitUnsubscribe,\n Transport,\n} from '@cleocode/contracts';\n\n/** ConduitClient wraps a Transport, adding high-level messaging semantics. */\nexport class ConduitClient implements Conduit {\n private transport: Transport;\n private credential: AgentCredential;\n private state: ConduitState = 'disconnected';\n\n /** Create a ConduitClient backed by the given transport and credential. */\n constructor(transport: Transport, credential: AgentCredential) {\n this.transport = transport;\n this.credential = credential;\n }\n\n /** The agent ID from the bound credential. */\n get agentId(): string {\n return this.credential.agentId;\n }\n\n /** Current connection state (disconnected \u2192 connecting \u2192 connected | error). */\n getState(): ConduitState {\n return this.state;\n }\n\n /** Connect the underlying transport using the bound credential. */\n async connect(): Promise<void> {\n this.state = 'connecting';\n try {\n await this.transport.connect({\n agentId: this.credential.agentId,\n apiKey: this.credential.apiKey,\n apiBaseUrl: this.credential.apiBaseUrl,\n ...this.credential.transportConfig,\n });\n this.state = 'connected';\n } catch (err) {\n // H6 fix: transition to 'error' state instead of stuck at 'connecting'\n this.state = 'error';\n throw err;\n }\n }\n\n /** Send a message to another agent, optionally within a thread. */\n async send(\n to: string,\n content: string,\n options?: ConduitSendOptions,\n ): Promise<ConduitSendResult> {\n const result = await this.transport.push(to, content, {\n conversationId: options?.threadId,\n });\n return {\n messageId: result.messageId,\n deliveredAt: new Date().toISOString(),\n };\n }\n\n /** One-shot poll for new messages. Delegates to the transport's poll method. */\n async poll(options?: { limit?: number; since?: string }): Promise<ConduitMessage[]> {\n return this.transport.poll(options);\n }\n\n /** Subscribe to incoming messages. Uses real-time transport when available, else polls. */\n onMessage(handler: (message: ConduitMessage) => void): ConduitUnsubscribe {\n // Prefer real-time subscription if transport supports it\n if (this.transport.subscribe) {\n return this.transport.subscribe(handler);\n }\n // Fallback: polling loop\n const interval = setInterval(async () => {\n const messages = await this.transport.poll();\n for (const msg of messages) handler(msg);\n if (messages.length > 0) {\n await this.transport.ack(messages.map((m) => m.id));\n }\n }, this.credential.transportConfig.pollIntervalMs ?? 5000);\n return () => clearInterval(interval);\n }\n\n /** Send an empty heartbeat to maintain presence on the relay. */\n async heartbeat(): Promise<void> {\n // Send empty heartbeat via transport\n await this.transport.push(this.credential.agentId, '', {});\n }\n\n /** Check whether a remote agent is currently online via the cloud API. */\n async isOnline(agentId: string): Promise<boolean> {\n // Delegate to cloud API check \u2014 stub for now\n try {\n const response = await fetch(`${this.credential.apiBaseUrl}/agents/${agentId}`, {\n headers: {\n Authorization: `Bearer ${this.credential.apiKey}`,\n 'X-Agent-Id': this.credential.agentId,\n },\n });\n if (!response.ok) return false;\n const data = (await response.json()) as { data?: { agent?: { status?: string } } };\n return data.data?.agent?.status === 'online';\n } catch {\n return false;\n }\n }\n\n /** Disconnect the transport and reset state to disconnected. */\n async disconnect(): Promise<void> {\n await this.transport.disconnect();\n this.state = 'disconnected';\n }\n}\n", "/**\n * HttpTransport \u2014 HTTP polling transport with automatic failover.\n *\n * Tries the primary API URL (api.signaldock.io) first. If unreachable,\n * falls back to the legacy URL (api.clawmsgr.com). Failover is transparent\n * to callers \u2014 they see a single transport that always works if either\n * endpoint is up.\n *\n * @see docs/specs/SIGNALDOCK-UNIFIED-AGENT-REGISTRY.md Section 4.4\n * @task T177\n */\n\nimport type { ConduitMessage, Transport, TransportConnectConfig } from '@cleocode/contracts';\n\n/** Internal connection state. */\ninterface HttpTransportState {\n agentId: string;\n apiKey: string;\n primaryUrl: string;\n fallbackUrl: string | null;\n activeUrl: string;\n connected: boolean;\n}\n\n/** HTTP transport with automatic primary/fallback failover. */\nexport class HttpTransport implements Transport {\n readonly name = 'http';\n private state: HttpTransportState | null = null;\n\n /** Connect to the SignalDock API, probing primary/fallback health when both are configured. */\n async connect(config: TransportConnectConfig): Promise<void> {\n const primaryUrl = config.apiBaseUrl;\n const fallbackUrl = config.apiBaseUrlFallback ?? null;\n\n // Only probe health when there's a fallback to choose between\n let activeUrl = primaryUrl;\n if (fallbackUrl) {\n const [primaryResult, fallbackResult] = await Promise.allSettled([\n fetch(`${primaryUrl}/health`, { method: 'GET', signal: AbortSignal.timeout(5000) }),\n fetch(`${fallbackUrl}/health`, { method: 'GET', signal: AbortSignal.timeout(5000) }),\n ]);\n const primaryOk = primaryResult.status === 'fulfilled' && primaryResult.value.ok;\n const fallbackOk = fallbackResult.status === 'fulfilled' && fallbackResult.value.ok;\n if (!primaryOk && fallbackOk) {\n activeUrl = fallbackUrl;\n }\n }\n\n this.state = {\n agentId: config.agentId,\n apiKey: config.apiKey,\n primaryUrl,\n fallbackUrl,\n activeUrl,\n connected: true,\n };\n }\n\n /** Disconnect and clear connection state. */\n async disconnect(): Promise<void> {\n this.state = null;\n }\n\n /** Send a message to an agent (direct or within a conversation thread). */\n async push(\n to: string,\n content: string,\n options?: { conversationId?: string; replyTo?: string },\n ): Promise<{ messageId: string }> {\n this.ensureConnected();\n\n const body: Record<string, string> = { content };\n\n let path: string;\n if (options?.conversationId) {\n path = `/conversations/${options.conversationId}/messages`;\n if (options.replyTo) {\n body['replyTo'] = options.replyTo;\n }\n } else {\n path = '/messages';\n body['toAgentId'] = to;\n }\n\n const response = await this.fetchWithFallback(path, {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new Error(`HttpTransport push failed: ${response.status} ${text}`);\n }\n\n const data = (await response.json()) as {\n success?: boolean;\n data?: { message?: { id?: string }; id?: string };\n };\n const messageId = data.data?.message?.id ?? data.data?.id ?? 'unknown';\n return { messageId };\n }\n\n /** Poll for new messages for this agent. Returns empty array on HTTP error. */\n async poll(options?: { limit?: number; since?: string }): Promise<ConduitMessage[]> {\n this.ensureConnected();\n\n const params = new URLSearchParams();\n // Don't filter by mentioned \u2014 the API already scopes by X-Agent-Id header.\n // Using mentioned= misses messages sent TO this agent without @-mentions.\n if (options?.limit) params.set('limit', String(options.limit));\n if (options?.since) params.set('since', options.since);\n\n const response = await this.fetchWithFallback(`/messages/peek?${params}`, {\n method: 'GET',\n headers: this.headers(),\n });\n\n if (!response.ok) return [];\n\n const data = (await response.json()) as {\n data?: {\n messages?: Array<{\n id: string;\n fromAgentId?: string;\n content?: string;\n conversationId?: string;\n createdAt?: string;\n }>;\n };\n };\n\n return (data.data?.messages ?? []).map((m) => ({\n id: m.id,\n from: m.fromAgentId ?? 'unknown',\n content: m.content ?? '',\n threadId: m.conversationId,\n timestamp: m.createdAt ?? new Date().toISOString(),\n }));\n }\n\n /** Acknowledge messages by ID so they are not returned by future polls. */\n async ack(messageIds: string[]): Promise<void> {\n this.ensureConnected();\n\n await this.fetchWithFallback('/messages/ack', {\n method: 'POST',\n headers: this.headers(),\n body: JSON.stringify({ messageIds }),\n });\n }\n\n /**\n * Fetch with automatic failover. Tries activeUrl first.\n * If it fails and a fallback exists, retries on the other URL\n * and swaps activeUrl for subsequent calls.\n */\n private async fetchWithFallback(path: string, init: RequestInit): Promise<Response> {\n const timeout = AbortSignal.timeout(10000);\n const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout;\n const url = `${this.state!.activeUrl}${path}`;\n\n try {\n return await fetch(url, { ...init, signal });\n } catch (primaryErr) {\n const otherUrl =\n this.state!.activeUrl === this.state!.primaryUrl\n ? this.state!.fallbackUrl\n : this.state!.primaryUrl;\n\n if (!otherUrl) throw primaryErr;\n\n try {\n const fallbackSignal = init.signal\n ? AbortSignal.any([init.signal, AbortSignal.timeout(10000)])\n : AbortSignal.timeout(10000);\n const fallbackResponse = await fetch(`${otherUrl}${path}`, {\n ...init,\n signal: fallbackSignal,\n });\n this.state!.activeUrl = otherUrl;\n return fallbackResponse;\n } catch {\n throw primaryErr;\n }\n }\n }\n\n private headers(): Record<string, string> {\n return {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.state!.apiKey}`,\n 'X-Agent-Id': this.state!.agentId,\n };\n }\n\n private ensureConnected(): void {\n if (!this.state?.connected) {\n throw new Error('HttpTransport not connected. Call connect() first.');\n }\n }\n}\n", "/**\n * LocalTransport \u2014 In-process SQLite transport for fully offline agent messaging.\n *\n * Reads and writes messages directly to conduit.db via node:sqlite.\n * No network calls. Works fully offline. Messages are stored in the\n * project-local conduit.db (ADR-037), keeping agent messaging isolated\n * from the global-identity signaldock.db.\n *\n * Priority: LocalTransport is preferred over HttpTransport when\n * conduit.db is available (see factory.ts).\n *\n * @see docs/specs/SIGNALDOCK-UNIFIED-AGENT-REGISTRY.md Section 4.4\n * @task T213\n * @task T356\n * @epic T310\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport type { DatabaseSync } from 'node:sqlite';\nimport type { ConduitMessage, Transport, TransportConnectConfig } from '@cleocode/contracts';\nimport { getConduitDbPath } from '../store/conduit-sqlite.js';\n\nconst _require = createRequire(import.meta.url);\nconst { DatabaseSync: DatabaseSyncClass } = _require('node:sqlite') as {\n DatabaseSync: new (...args: ConstructorParameters<typeof DatabaseSync>) => DatabaseSync;\n};\n\n/** Internal state for an active local transport connection. */\ninterface LocalTransportState {\n agentId: string;\n db: DatabaseSync;\n dbPath: string;\n subscribers: Set<(message: ConduitMessage) => void>;\n pollTimer: ReturnType<typeof setInterval> | null;\n}\n\n/** In-process SQLite transport for fully offline agent messaging. */\nexport class LocalTransport implements Transport {\n readonly name = 'local';\n private state: LocalTransportState | null = null;\n\n /**\n * Connect to conduit.db for in-process messaging.\n *\n * Opens the database, sets WAL mode pragmas, and verifies\n * the messages table exists. Throws if conduit.db is missing\n * or uninitialized (run `cleo init` first).\n *\n * @task T356\n * @epic T310\n */\n async connect(config: TransportConnectConfig): Promise<void> {\n const dbPath = getConduitDbPath(process.cwd());\n\n if (!existsSync(dbPath)) {\n throw new Error(`LocalTransport: conduit.db not found at ${dbPath}. Run: cleo init`);\n }\n\n const db = new DatabaseSyncClass(dbPath);\n db.exec('PRAGMA journal_mode = WAL');\n db.exec('PRAGMA busy_timeout = 5000');\n db.exec('PRAGMA foreign_keys = ON');\n\n // Verify the messages table exists\n const hasMessages = db\n .prepare(\"SELECT name FROM sqlite_master WHERE type='table' AND name='messages'\")\n .get() as { name: string } | undefined;\n\n if (!hasMessages) {\n db.close();\n throw new Error(\n 'LocalTransport: conduit.db exists but messages table missing \u2014 run cleo init or allow auto-migration (T358)',\n );\n }\n\n this.state = {\n agentId: config.agentId,\n db,\n dbPath,\n subscribers: new Set(),\n pollTimer: null,\n };\n }\n\n /** Close the database connection and stop any subscriber polling. */\n async disconnect(): Promise<void> {\n if (!this.state) return;\n\n if (this.state.pollTimer) {\n clearInterval(this.state.pollTimer);\n }\n this.state.subscribers.clear();\n this.state.db.close();\n this.state = null;\n }\n\n /**\n * Store a message in conduit.db.\n *\n * Inserts into the messages table with status 'pending'.\n * For conversation messages, also links via conversation_participants\n * if not already present.\n */\n async push(\n to: string,\n content: string,\n options?: { conversationId?: string; replyTo?: string },\n ): Promise<{ messageId: string }> {\n this.ensureConnected();\n const { db, agentId } = this.state!;\n const messageId = randomUUID();\n const nowUnix = Math.floor(Date.now() / 1000);\n\n if (options?.conversationId) {\n db.prepare(\n `INSERT INTO messages (id, conversation_id, from_agent_id, to_agent_id, content, content_type, status, created_at)\n VALUES (?, ?, ?, ?, ?, 'text', 'pending', ?)`,\n ).run(messageId, options.conversationId, agentId, to, content, nowUnix);\n } else {\n // Direct message \u2014 create or reuse a DM conversation\n const convId = this.ensureDmConversation(agentId, to);\n db.prepare(\n `INSERT INTO messages (id, conversation_id, from_agent_id, to_agent_id, content, content_type, status, created_at)\n VALUES (?, ?, ?, ?, ?, 'text', 'pending', ?)`,\n ).run(messageId, convId, agentId, to, content, nowUnix);\n }\n\n // Notify local subscribers\n this.notifySubscribers({\n id: messageId,\n from: agentId,\n content,\n threadId: options?.conversationId,\n timestamp: new Date(nowUnix * 1000).toISOString(),\n });\n\n return { messageId };\n }\n\n /**\n * Poll for messages addressed to this agent.\n *\n * Returns messages with status 'pending' where to_agent_id matches\n * the connected agent. Messages are returned oldest-first.\n */\n async poll(options?: { limit?: number; since?: string }): Promise<ConduitMessage[]> {\n this.ensureConnected();\n const { db, agentId } = this.state!;\n const limit = options?.limit ?? 50;\n\n let query: string;\n let params: (string | number)[];\n\n if (options?.since) {\n query = `SELECT id, from_agent_id, content, conversation_id, created_at\n FROM messages\n WHERE to_agent_id = ? AND status = 'pending' AND created_at > ?\n ORDER BY created_at ASC\n LIMIT ?`;\n params = [agentId, options.since, limit];\n } else {\n query = `SELECT id, from_agent_id, content, conversation_id, created_at\n FROM messages\n WHERE to_agent_id = ? AND status = 'pending'\n ORDER BY created_at ASC\n LIMIT ?`;\n params = [agentId, limit];\n }\n\n const rows = db.prepare(query).all(...params) as Array<{\n id: string;\n from_agent_id: string;\n content: string;\n conversation_id: string | null;\n created_at: number;\n }>;\n\n return rows.map((r) => ({\n id: r.id,\n from: r.from_agent_id,\n content: r.content,\n threadId: r.conversation_id ?? undefined,\n timestamp: new Date(r.created_at * 1000).toISOString(),\n }));\n }\n\n /**\n * Acknowledge messages by marking them as 'delivered'.\n *\n * Updates the status and delivered_at timestamp for each message ID.\n */\n async ack(messageIds: string[]): Promise<void> {\n this.ensureConnected();\n if (messageIds.length === 0) return;\n\n const { db } = this.state!;\n const nowUnix = Math.floor(Date.now() / 1000);\n\n const placeholders = messageIds.map(() => '?').join(', ');\n db.prepare(\n `UPDATE messages SET status = 'delivered', delivered_at = ? WHERE id IN (${placeholders})`,\n ).run(nowUnix, ...messageIds);\n }\n\n /**\n * Subscribe to real-time local messages.\n *\n * Since this is in-process, subscribers are notified synchronously\n * when push() is called. Additionally, a polling interval checks\n * for messages inserted by other processes (e.g., Rust CLI).\n *\n * @returns Unsubscribe function.\n */\n subscribe(handler: (message: ConduitMessage) => void): () => void {\n this.ensureConnected();\n this.state!.subscribers.add(handler);\n\n // Start cross-process polling if not already running\n if (!this.state!.pollTimer && this.state!.subscribers.size === 1) {\n this.state!.pollTimer = setInterval(() => {\n void this.pollAndNotify();\n }, 1000);\n }\n\n return () => {\n this.state?.subscribers.delete(handler);\n if (this.state?.subscribers.size === 0 && this.state.pollTimer) {\n clearInterval(this.state.pollTimer);\n this.state.pollTimer = null;\n }\n };\n }\n\n /**\n * Check whether conduit.db is available for local transport.\n *\n * Used by factory.ts to decide whether to use LocalTransport.\n *\n * @task T356\n * @epic T310\n * @param cwd - Optional working directory override (defaults to process.cwd()).\n * @returns `true` if conduit.db exists at the expected path.\n */\n static isAvailable(cwd?: string): boolean {\n const dbPath = getConduitDbPath(cwd ?? process.cwd());\n return existsSync(dbPath);\n }\n\n /** Poll for new messages and notify subscribers (cross-process sync). */\n private async pollAndNotify(): Promise<void> {\n if (!this.state || this.state.subscribers.size === 0) return;\n\n const messages = await this.poll({ limit: 20 });\n for (const msg of messages) {\n this.notifySubscribers(msg);\n }\n if (messages.length > 0) {\n await this.ack(messages.map((m) => m.id));\n }\n }\n\n /** Notify all active subscribers of a new message. */\n private notifySubscribers(message: ConduitMessage): void {\n if (!this.state) return;\n for (const handler of this.state.subscribers) {\n try {\n handler(message);\n } catch {\n // Subscriber errors must not break the transport\n }\n }\n }\n\n /**\n * Ensure a DM conversation exists between two agents.\n *\n * Conversations store participants as a comma-separated TEXT field.\n * We search for existing private conversations containing both agents.\n *\n * @returns The conversation ID.\n */\n private ensureDmConversation(fromAgentId: string, toAgentId: string): string {\n const { db } = this.state!;\n\n // Participants are stored as comma-separated text, sorted alphabetically\n const sortedParticipants = [fromAgentId, toAgentId].sort().join(',');\n\n // Check for existing DM conversation with these exact participants\n const existing = db\n .prepare(\n `SELECT id FROM conversations\n WHERE visibility = 'private' AND participants = ?\n LIMIT 1`,\n )\n .get(sortedParticipants) as { id: string } | undefined;\n\n if (existing) return existing.id;\n\n // Create new DM conversation\n const convId = randomUUID();\n const nowUnix = Math.floor(Date.now() / 1000);\n\n db.prepare(\n `INSERT INTO conversations (id, participants, visibility, message_count, created_at, updated_at)\n VALUES (?, ?, 'private', 0, ?, ?)`,\n ).run(convId, sortedParticipants, nowUnix, nowUnix);\n\n return convId;\n }\n\n /** Throw if not connected. */\n private ensureConnected(): void {\n if (!this.state) {\n throw new Error('LocalTransport not connected. Call connect() first.');\n }\n }\n}\n", "/**\n * SQLite store for conduit.db \u2014 project-tier messaging and agent-ref database.\n *\n * Creates and manages .cleo/conduit.db using node:sqlite directly.\n * Applies the full conduit.db DDL (from spec \u00A72.1) to bootstrap all\n * project-local messaging tables and the project_agent_refs override table.\n *\n * Architecture (ADR-037):\n * conduit.db \u2014 project-scoped (this module) \u2014 messaging, delivery, attachments,\n * project_agent_refs\n * signaldock.db \u2014 global-scoped (T346) \u2014 agents, capabilities, cloud-sync tables\n *\n * CRUD accessors for project_agent_refs land in T353.\n * Cross-DB join accessor changes land in T355.\n * Migration executor from signaldock.db \u2192 conduit.db lands in T358.\n *\n * @task T344\n * @epic T310\n * @why ADR-037 splits single signaldock.db into project-tier conduit.db\n * (this module) and global-tier signaldock.db (T346). This module owns\n * the project-tier path helper, initializer, schema DDL, and health check.\n * @what Path helper, database initializer, schema applier, health check, and\n * native DB accessor for project-local tables. No CRUD, no migrations.\n */\n\nimport { existsSync, mkdirSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, join } from 'node:path';\n// underscore-import: node:sqlite type alias required for createRequire interop.\n// Vitest/Vite cannot resolve `node:sqlite` as an ESM import (strips `node:` prefix).\n// Use createRequire as the runtime loader; keep type-only import for annotations.\nimport type { DatabaseSync as _DatabaseSyncType } from 'node:sqlite';\nimport type { ProjectAgentRef } from '@cleocode/contracts';\n\nconst _require = createRequire(import.meta.url);\ntype DatabaseSync = _DatabaseSyncType;\nconst { DatabaseSync } = _require('node:sqlite') as {\n DatabaseSync: new (...args: ConstructorParameters<typeof _DatabaseSyncType>) => DatabaseSync;\n};\n\n/** Database file name within .cleo/ directory. */\nexport const CONDUIT_DB_FILENAME = 'conduit.db';\n\n/** Schema version for conduit.db \u2014 updated when DDL changes. */\nexport const CONDUIT_SCHEMA_VERSION = '2026.4.12';\n\n// ---------------------------------------------------------------------------\n// Singleton state\n// ---------------------------------------------------------------------------\n\nlet _conduitNativeDb: DatabaseSync | null = null;\nlet _conduitDbPath: string | null = null;\n\n// ---------------------------------------------------------------------------\n// DDL\n// ---------------------------------------------------------------------------\n\n/**\n * Full conduit.db schema SQL.\n *\n * All tables use CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS /\n * CREATE TRIGGER IF NOT EXISTS for idempotency. Carried over verbatim from\n * the project-local tables in signaldock-sqlite.ts (migration\n * `2026-03-28-000000_initial` + subsequent migrations), minus the global-\n * identity tables (agents, capabilities, skills, agent_capabilities,\n * agent_skills, agent_connections, users, organization, accounts, sessions,\n * verifications, claim_codes, org_agent_keys) which move to global-tier\n * signaldock.db (T346).\n *\n * Additional new table: project_agent_refs (ADR-037 \u00A73, Q6=A).\n *\n * NOTE: The `connections` table from the original migration is a cross-agent\n * social graph that references `agents(id)` \u2014 it is a global-identity\n * concern and stays with signaldock.db (T346). It is NOT included here.\n */\nconst CONDUIT_SCHEMA_SQL = `\n-- -------------------------------------------------------------------------\n-- Project-scoped conversations (LocalTransport DM threads).\n-- -------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n participants TEXT NOT NULL,\n visibility TEXT NOT NULL DEFAULT 'private',\n message_count INTEGER NOT NULL DEFAULT 0,\n last_message_at INTEGER,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\n-- -------------------------------------------------------------------------\n-- Project-scoped agent-to-agent messages (LocalTransport content).\n-- -------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL REFERENCES conversations(id),\n from_agent_id TEXT NOT NULL,\n to_agent_id TEXT NOT NULL,\n content TEXT NOT NULL,\n content_type TEXT NOT NULL DEFAULT 'text',\n status TEXT NOT NULL DEFAULT 'pending',\n attachments TEXT NOT NULL DEFAULT '[]',\n group_id TEXT,\n metadata TEXT DEFAULT '{}',\n reply_to TEXT,\n created_at INTEGER NOT NULL,\n delivered_at INTEGER,\n read_at INTEGER\n);\nCREATE INDEX IF NOT EXISTS messages_conversation_idx ON messages(conversation_id);\nCREATE INDEX IF NOT EXISTS messages_from_agent_idx ON messages(from_agent_id);\nCREATE INDEX IF NOT EXISTS messages_to_agent_idx ON messages(to_agent_id);\nCREATE INDEX IF NOT EXISTS messages_created_at_idx ON messages(created_at);\nCREATE INDEX IF NOT EXISTS idx_messages_group_id ON messages(group_id) WHERE group_id IS NOT NULL;\nCREATE INDEX IF NOT EXISTS idx_messages_reply_to ON messages(reply_to) WHERE reply_to IS NOT NULL;\n\n-- -------------------------------------------------------------------------\n-- FTS5 virtual table for full-text search on message content.\n-- NOTE: Must be migrated using VACUUM INTO, not DDL-only copy, to preserve\n-- triggers. The INSERT INTO messages_fts(messages_fts) VALUES('rebuild')\n-- is idempotent \u2014 safe to run on every open.\n-- -------------------------------------------------------------------------\nCREATE VIRTUAL TABLE IF NOT EXISTS messages_fts\n USING fts5(content, from_agent_id, content='messages', content_rowid='rowid');\nINSERT INTO messages_fts(messages_fts) VALUES('rebuild');\nCREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN\n INSERT INTO messages_fts(rowid, content, from_agent_id)\n VALUES (new.rowid, new.content, new.from_agent_id);\nEND;\nCREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN\n INSERT INTO messages_fts(messages_fts, rowid, content, from_agent_id)\n VALUES('delete', old.rowid, old.content, old.from_agent_id);\nEND;\nCREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN\n INSERT INTO messages_fts(messages_fts, rowid, content, from_agent_id)\n VALUES('delete', old.rowid, old.content, old.from_agent_id);\n INSERT INTO messages_fts(rowid, content, from_agent_id)\n VALUES (new.rowid, new.content, new.from_agent_id);\nEND;\n\n-- -------------------------------------------------------------------------\n-- Async delivery queue for deferred message dispatch.\n-- -------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS delivery_jobs (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n payload TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 6,\n next_attempt_at INTEGER NOT NULL,\n last_error TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_delivery_jobs_status ON delivery_jobs(status, next_attempt_at);\n\n-- -------------------------------------------------------------------------\n-- Dead-letter queue for messages that exceeded max delivery attempts.\n-- -------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS dead_letters (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n job_id TEXT NOT NULL,\n reason TEXT NOT NULL,\n attempts INTEGER NOT NULL,\n created_at INTEGER NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_dead_letters_message ON dead_letters(message_id);\n\n-- -------------------------------------------------------------------------\n-- Pinned messages within a conversation.\n-- -------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS message_pins (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n pinned_by TEXT NOT NULL,\n note TEXT,\n created_at INTEGER NOT NULL,\n UNIQUE(message_id, pinned_by)\n);\nCREATE INDEX IF NOT EXISTS idx_pins_conversation ON message_pins(conversation_id);\nCREATE INDEX IF NOT EXISTS idx_pins_agent ON message_pins(pinned_by);\n\n-- -------------------------------------------------------------------------\n-- File/blob attachments associated with messages.\n-- -------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS attachments (\n slug TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n from_agent_id TEXT NOT NULL,\n content BLOB NOT NULL,\n original_size INTEGER NOT NULL,\n compressed_size INTEGER NOT NULL,\n content_hash TEXT NOT NULL,\n format TEXT NOT NULL DEFAULT 'text',\n title TEXT,\n tokens INTEGER NOT NULL DEFAULT 0,\n expires_at INTEGER NOT NULL DEFAULT 0,\n storage_key TEXT,\n mode TEXT NOT NULL DEFAULT 'draft',\n version_count INTEGER NOT NULL DEFAULT 1,\n current_version INTEGER NOT NULL DEFAULT 1,\n created_at INTEGER NOT NULL\n);\nCREATE INDEX IF NOT EXISTS attachments_conversation_idx ON attachments(conversation_id);\nCREATE INDEX IF NOT EXISTS attachments_agent_idx ON attachments(from_agent_id);\n\n-- -------------------------------------------------------------------------\n-- Version history for attachments (collaborative editing).\n-- -------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS attachment_versions (\n id TEXT PRIMARY KEY,\n slug TEXT NOT NULL REFERENCES attachments(slug) ON DELETE CASCADE,\n version_number INTEGER NOT NULL,\n author_agent_id TEXT NOT NULL,\n change_type TEXT NOT NULL DEFAULT 'patch',\n patch_text TEXT,\n storage_key TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n original_size INTEGER NOT NULL,\n compressed_size INTEGER NOT NULL,\n tokens INTEGER NOT NULL,\n change_summary TEXT,\n sections_modified TEXT NOT NULL DEFAULT '[]',\n tokens_added INTEGER NOT NULL DEFAULT 0,\n tokens_removed INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL,\n UNIQUE(slug, version_number)\n);\nCREATE INDEX IF NOT EXISTS idx_attachment_versions_slug ON attachment_versions(slug);\nCREATE INDEX IF NOT EXISTS idx_attachment_versions_author ON attachment_versions(author_agent_id);\n\n-- -------------------------------------------------------------------------\n-- Approval records for attachment content review.\n-- -------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS attachment_approvals (\n id TEXT PRIMARY KEY,\n slug TEXT NOT NULL REFERENCES attachments(slug) ON DELETE CASCADE,\n reviewer_agent_id TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n comment TEXT,\n version_reviewed INTEGER NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n UNIQUE(slug, reviewer_agent_id)\n);\nCREATE INDEX IF NOT EXISTS idx_attachment_approvals_slug ON attachment_approvals(slug);\n\n-- -------------------------------------------------------------------------\n-- Contributor statistics per attachment (who edited, how much).\n-- -------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS attachment_contributors (\n slug TEXT NOT NULL REFERENCES attachments(slug) ON DELETE CASCADE,\n agent_id TEXT NOT NULL,\n version_count INTEGER NOT NULL DEFAULT 0,\n total_tokens_added INTEGER NOT NULL DEFAULT 0,\n total_tokens_removed INTEGER NOT NULL DEFAULT 0,\n first_contribution_at INTEGER NOT NULL,\n last_contribution_at INTEGER NOT NULL,\n PRIMARY KEY (slug, agent_id)\n);\n\n-- -------------------------------------------------------------------------\n-- NEW: Per-project agent reference overrides (ADR-037 \u00A73, Q6=A).\n-- agent_id is a SOFT FK to global signaldock.db:agents.agent_id.\n-- Cross-DB FK enforcement is not possible in SQLite; the accessor layer\n-- (T355) validates on every cross-DB join.\n-- -------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS project_agent_refs (\n agent_id TEXT PRIMARY KEY,\n attached_at TEXT NOT NULL,\n role TEXT,\n capabilities_override TEXT,\n last_used_at TEXT,\n enabled INTEGER NOT NULL DEFAULT 1\n);\n-- Partial index: covers the dominant query path (list enabled agents).\nCREATE INDEX IF NOT EXISTS idx_project_agent_refs_enabled\n ON project_agent_refs(enabled) WHERE enabled = 1;\n\n-- -------------------------------------------------------------------------\n-- Schema tracking tables (mirrors _signaldock_meta / _signaldock_migrations).\n-- -------------------------------------------------------------------------\nCREATE TABLE IF NOT EXISTS _conduit_meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n);\nCREATE TABLE IF NOT EXISTS _conduit_migrations (\n name TEXT PRIMARY KEY,\n applied_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n);\n`;\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the project-tier conduit.db path.\n *\n * Always resolves to `<projectRoot>/.cleo/conduit.db`. The caller is\n * responsible for supplying the absolute project root (e.g. via\n * `getProjectRoot()` from `../paths.js`).\n *\n * @task T344\n * @epic T310\n * @param projectRoot - Absolute path to the project root directory.\n * @returns Absolute path to `<projectRoot>/.cleo/conduit.db`.\n */\nexport function getConduitDbPath(projectRoot: string): string {\n return join(projectRoot, '.cleo', CONDUIT_DB_FILENAME);\n}\n\n/**\n * Applies the conduit.db schema idempotently using CREATE TABLE IF NOT EXISTS.\n *\n * Exposed for the migration executor (T358) which needs to apply the schema\n * to a newly created conduit.db during the signaldock.db \u2192 conduit.db\n * migration. Also called internally by `ensureConduitDb` on every open.\n *\n * @task T344\n * @epic T310\n * @param db - An open node:sqlite DatabaseSync instance.\n */\nexport function applyConduitSchema(db: DatabaseSync): void {\n db.exec(CONDUIT_SCHEMA_SQL);\n}\n\n/**\n * Opens or creates conduit.db for the given project root.\n *\n * On first call for a given projectRoot:\n * 1. Creates `<projectRoot>/.cleo/` directory if missing.\n * 2. Opens (or creates) the SQLite file.\n * 3. Sets WAL mode and enables foreign keys.\n * 4. Applies all DDL via `applyConduitSchema` (idempotent).\n * 5. Records `schema_version` in `_conduit_meta`.\n * 6. Records the initial migration in `_conduit_migrations`.\n * 7. Stores the open handle in the module singleton.\n *\n * On subsequent calls the existing singleton is returned immediately if the\n * resolved path matches; otherwise the previous handle is closed and a new\n * one is opened (test-isolation safety).\n *\n * Caller MUST call `closeConduitDb()` when done to release the handle.\n *\n * @task T344\n * @epic T310\n * @param projectRoot - Absolute path to the project root directory.\n * @returns Object with `action` (`'created'` | `'exists'`) and `path`.\n */\nexport function ensureConduitDb(projectRoot: string): {\n action: 'created' | 'exists';\n path: string;\n} {\n const dbPath = getConduitDbPath(projectRoot);\n\n // If singleton already open at the same path, skip re-initialization.\n if (_conduitNativeDb && _conduitDbPath === dbPath) {\n return { action: 'exists', path: dbPath };\n }\n\n // Close any stale singleton pointing at a different path (e.g. between tests).\n if (_conduitNativeDb) {\n closeConduitDb();\n }\n\n const alreadyExists = existsSync(dbPath);\n\n // Ensure parent .cleo/ directory exists.\n mkdirSync(dirname(dbPath), { recursive: true });\n\n const db = new DatabaseSync(dbPath);\n\n db.exec('PRAGMA journal_mode = WAL');\n db.exec('PRAGMA busy_timeout = 5000');\n db.exec('PRAGMA synchronous = NORMAL');\n db.exec('PRAGMA foreign_keys = ON');\n db.exec('PRAGMA cache_size = -64000'); // 64 MB\n\n // Check whether the schema sentinel table already exists before applying DDL.\n const hasSchema = (() => {\n try {\n const result = db\n .prepare(\"SELECT name FROM sqlite_master WHERE type='table' AND name='conversations'\")\n .get() as { name: string } | undefined;\n return !!result;\n } catch {\n return false;\n }\n })();\n\n // Apply schema (idempotent \u2014 all statements use IF NOT EXISTS).\n applyConduitSchema(db);\n\n // Record schema version and initial migration.\n db.exec(\n `INSERT OR REPLACE INTO _conduit_meta (key, value, updated_at)\n VALUES ('schema_version', '${CONDUIT_SCHEMA_VERSION}', strftime('%s', 'now'))`,\n );\n db.prepare(\n `INSERT OR IGNORE INTO _conduit_migrations (name, applied_at)\n VALUES (?, strftime('%s', 'now'))`,\n ).run('2026-04-12-000000_initial_conduit');\n\n _conduitNativeDb = db;\n _conduitDbPath = dbPath;\n\n return {\n action: alreadyExists && hasSchema ? 'exists' : 'created',\n path: dbPath,\n };\n}\n\n/**\n * Returns the live node:sqlite DatabaseSync handle for conduit.db.\n *\n * Returns `null` if `ensureConduitDb` has not been called yet for this\n * process, or if `closeConduitDb` has been called since the last open.\n *\n * @task T344\n * @epic T310\n * @returns The open DatabaseSync instance, or `null` if not initialized.\n */\nexport function getConduitNativeDb(): DatabaseSync | null {\n return _conduitNativeDb;\n}\n\n/**\n * Closes the conduit.db connection and resets the module singleton.\n *\n * Safe to call multiple times. No-op if the database is already closed.\n *\n * @task T344\n * @epic T310\n */\nexport function closeConduitDb(): void {\n if (_conduitNativeDb) {\n try {\n if (_conduitNativeDb.isOpen) {\n _conduitNativeDb.close();\n }\n } catch {\n // Ignore close errors \u2014 the handle is being discarded regardless.\n }\n _conduitNativeDb = null;\n }\n _conduitDbPath = null;\n}\n\n// ---------------------------------------------------------------------------\n// project_agent_refs CRUD accessors (T353)\n// ---------------------------------------------------------------------------\n\n/**\n * Attaches an agent to the current project. If a row exists with enabled=0,\n * re-enables it (update attached_at timestamp). If a row exists with enabled=1,\n * no-op. Inserts a new row otherwise.\n *\n * @param db - conduit.db handle (from ensureConduitDb).\n * @param agentId - Global signaldock.db:agents.id (soft FK, not validated here).\n * @param opts - Optional role and capabilities override.\n * @task T353\n * @epic T310\n */\nexport function attachAgentToProject(\n db: DatabaseSync,\n agentId: string,\n opts?: { role?: string | null; capabilitiesOverride?: string | null },\n): void {\n const now = new Date().toISOString();\n db.prepare(\n `INSERT INTO project_agent_refs (agent_id, attached_at, role, capabilities_override, last_used_at, enabled)\n VALUES (?, ?, ?, ?, NULL, 1)\n ON CONFLICT(agent_id) DO UPDATE SET\n enabled = 1,\n attached_at = CASE WHEN project_agent_refs.enabled = 0 THEN excluded.attached_at ELSE project_agent_refs.attached_at END,\n role = excluded.role,\n capabilities_override = excluded.capabilities_override`,\n ).run(agentId, now, opts?.role ?? null, opts?.capabilitiesOverride ?? null);\n}\n\n/**\n * Detaches an agent from the current project by setting enabled=0.\n * Does NOT delete the row (preserves attachment history for audit).\n *\n * @param db - conduit.db handle (from ensureConduitDb).\n * @param agentId - Agent ID to detach.\n * @task T353\n * @epic T310\n */\nexport function detachAgentFromProject(db: DatabaseSync, agentId: string): void {\n db.prepare(`UPDATE project_agent_refs SET enabled = 0 WHERE agent_id = ?`).run(agentId);\n}\n\n/**\n * Lists project_agent_refs rows. By default returns only enabled=1 rows.\n * Pass enabledOnly=false to return all rows regardless of enabled state.\n *\n * @param db - conduit.db handle (from ensureConduitDb).\n * @param opts - Filter options. Defaults to `{ enabledOnly: true }`.\n * @returns Array of ProjectAgentRef rows ordered by attached_at DESC.\n * @task T353\n * @epic T310\n */\nexport function listProjectAgentRefs(\n db: DatabaseSync,\n opts?: { enabledOnly?: boolean },\n): ProjectAgentRef[] {\n const enabledOnly = opts?.enabledOnly ?? true;\n const sql = enabledOnly\n ? `SELECT agent_id, attached_at, role, capabilities_override, last_used_at, enabled\n FROM project_agent_refs WHERE enabled = 1\n ORDER BY attached_at DESC`\n : `SELECT agent_id, attached_at, role, capabilities_override, last_used_at, enabled\n FROM project_agent_refs\n ORDER BY attached_at DESC`;\n const rows = db.prepare(sql).all() as Array<{\n agent_id: string;\n attached_at: string;\n role: string | null;\n capabilities_override: string | null;\n last_used_at: string | null;\n enabled: number;\n }>;\n return rows.map((r) => ({\n agentId: r.agent_id,\n attachedAt: r.attached_at,\n role: r.role,\n capabilitiesOverride: r.capabilities_override,\n lastUsedAt: r.last_used_at,\n enabled: r.enabled,\n }));\n}\n\n/**\n * Returns a single project_agent_refs row by agentId, or null if not found.\n *\n * @param db - conduit.db handle (from ensureConduitDb).\n * @param agentId - Agent ID to look up.\n * @returns The ProjectAgentRef row, or null if the agent is not attached.\n * @task T353\n * @epic T310\n */\nexport function getProjectAgentRef(db: DatabaseSync, agentId: string): ProjectAgentRef | null {\n const row = db\n .prepare(\n `SELECT agent_id, attached_at, role, capabilities_override, last_used_at, enabled\n FROM project_agent_refs WHERE agent_id = ?`,\n )\n .get(agentId) as\n | {\n agent_id: string;\n attached_at: string;\n role: string | null;\n capabilities_override: string | null;\n last_used_at: string | null;\n enabled: number;\n }\n | undefined;\n if (!row) return null;\n return {\n agentId: row.agent_id,\n attachedAt: row.attached_at,\n role: row.role,\n capabilitiesOverride: row.capabilities_override,\n lastUsedAt: row.last_used_at,\n enabled: row.enabled,\n };\n}\n\n/**\n * Updates the last_used_at timestamp for an agent to now.\n * No-op if the agent_id does not exist in project_agent_refs.\n *\n * @param db - conduit.db handle (from ensureConduitDb).\n * @param agentId - Agent ID to update.\n * @task T353\n * @epic T310\n */\nexport function updateProjectAgentLastUsed(db: DatabaseSync, agentId: string): void {\n db.prepare(`UPDATE project_agent_refs SET last_used_at = ? WHERE agent_id = ?`).run(\n new Date().toISOString(),\n agentId,\n );\n}\n\n/**\n * Checks conduit.db health \u2014 table count, WAL mode, schema version, and\n * foreign keys status.\n *\n * Used by `cleo doctor` to verify conduit.db integrity. Does NOT require\n * `ensureConduitDb` to have been called; opens and closes the DB internally.\n *\n * @task T344\n * @epic T310\n * @param projectRoot - Absolute path to the project root directory.\n * @returns Health report object. `exists: false` when conduit.db is absent.\n */\nexport function checkConduitDbHealth(projectRoot: string): {\n exists: boolean;\n path: string;\n tableCount: number;\n walMode: boolean;\n schemaVersion: string | null;\n foreignKeysEnabled: boolean;\n} {\n const dbPath = getConduitDbPath(projectRoot);\n\n if (!existsSync(dbPath)) {\n return {\n exists: false,\n path: dbPath,\n tableCount: 0,\n walMode: false,\n schemaVersion: null,\n foreignKeysEnabled: false,\n };\n }\n\n const db = new DatabaseSync(dbPath);\n try {\n const tables = db\n .prepare(\n \"SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'\",\n )\n .get() as { count: number };\n\n const journalMode = db.prepare('PRAGMA journal_mode').get() as { journal_mode: string };\n const fkEnabled = db.prepare('PRAGMA foreign_keys').get() as { foreign_keys: number };\n\n let schemaVersion: string | null = null;\n try {\n const meta = db\n .prepare(\"SELECT value FROM _conduit_meta WHERE key = 'schema_version'\")\n .get() as { value: string } | undefined;\n schemaVersion = meta?.value ?? null;\n } catch {\n // _conduit_meta may not exist on a partially-initialized DB.\n }\n\n return {\n exists: true,\n path: dbPath,\n tableCount: tables.count,\n walMode: journalMode.journal_mode === 'wal',\n schemaVersion,\n foreignKeysEnabled: fkEnabled.foreign_keys === 1,\n };\n } finally {\n db.close();\n }\n}\n", "/**\n * SseTransport \u2014 Server-Sent Events transport with HTTP polling fallback.\n *\n * Receives messages in real-time via SSE from the SignalDock v2 API.\n * Sends messages and acks via HTTP POST (SSE is receive-only).\n * Falls back to HTTP polling when SSE is unavailable or disconnects.\n *\n * @see docs/specs/SIGNALDOCK-UNIFIED-AGENT-REGISTRY.md Section 4.4\n * @task T216\n */\n\nimport type { ConduitMessage, Transport, TransportConnectConfig } from '@cleocode/contracts';\n\n/** Maximum reconnect attempts before permanent HTTP fallback. */\nconst MAX_RECONNECT_ATTEMPTS = 3;\n\n/** Maximum reconnect delay in milliseconds. */\nconst MAX_RECONNECT_DELAY_MS = 30_000;\n\n/** SSE transport mode. */\ntype SseMode = 'sse' | 'http-fallback';\n\n/** Internal connection state. */\ninterface SseTransportState {\n agentId: string;\n apiKey: string;\n apiBaseUrl: string;\n sseEndpoint: string;\n eventSource: EventSource | null;\n mode: SseMode;\n messageBuffer: ConduitMessage[];\n subscribers: Set<(message: ConduitMessage) => void>;\n reconnectAttempts: number;\n reconnectTimer: ReturnType<typeof setTimeout> | null;\n connected: boolean;\n}\n\n/** SseTransport \u2014 real-time SSE with HTTP polling fallback. */\nexport class SseTransport implements Transport {\n readonly name = 'sse';\n private state: SseTransportState | null = null;\n\n /**\n * Connect to the SSE endpoint for real-time message delivery.\n *\n * If SSE connection fails, falls back to HTTP polling mode.\n * Auth is conveyed via query parameter (SSE doesn't support custom headers).\n */\n async connect(config: TransportConnectConfig): Promise<void> {\n if (this.state?.connected) {\n throw new Error('SseTransport already connected. Disconnect first.');\n }\n\n const sseEndpoint = config.sseEndpoint;\n if (!sseEndpoint && !config.apiBaseUrl) {\n throw new Error('SseTransport requires sseEndpoint or apiBaseUrl in config.');\n }\n\n const endpoint = sseEndpoint ?? `${config.apiBaseUrl}/messages/stream`;\n\n this.state = {\n agentId: config.agentId,\n apiKey: config.apiKey,\n apiBaseUrl: config.apiBaseUrl,\n sseEndpoint: endpoint,\n eventSource: null,\n mode: 'sse',\n messageBuffer: [],\n subscribers: new Set(),\n reconnectAttempts: 0,\n reconnectTimer: null,\n connected: false,\n };\n\n try {\n await this.connectSse();\n } catch {\n // SSE failed \u2014 fall back to HTTP polling\n this.state.mode = 'http-fallback';\n this.state.connected = true;\n }\n }\n\n /** Disconnect the transport, closing SSE and clearing all state. */\n async disconnect(): Promise<void> {\n if (!this.state) return;\n\n if (this.state.eventSource) {\n this.state.eventSource.close();\n }\n if (this.state.reconnectTimer) {\n clearTimeout(this.state.reconnectTimer);\n }\n this.state.messageBuffer = [];\n this.state.connected = false;\n this.state = null;\n }\n\n /**\n * Send a message via HTTP POST.\n *\n * SSE is receive-only \u2014 all sends go through HTTP regardless of SSE state.\n */\n async push(\n to: string,\n content: string,\n options?: { conversationId?: string; replyTo?: string },\n ): Promise<{ messageId: string }> {\n this.ensureConnected();\n\n const body: Record<string, string> = { content, toAgentId: to };\n let path = '/messages';\n\n if (options?.conversationId) {\n path = `/conversations/${options.conversationId}/messages`;\n if (options.replyTo) {\n body['replyTo'] = options.replyTo;\n }\n }\n\n const response = await this.httpFetch(path, {\n method: 'POST',\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new Error(`SseTransport push failed: ${response.status} ${text}`);\n }\n\n const data = (await response.json()) as {\n data?: { message?: { id?: string }; id?: string };\n };\n return { messageId: data.data?.message?.id ?? data.data?.id ?? 'unknown' };\n }\n\n /**\n * Poll for messages.\n *\n * In SSE mode: drains the internal message buffer (no HTTP request).\n * In HTTP fallback mode: fetches via GET /messages/peek.\n */\n async poll(options?: { limit?: number; since?: string }): Promise<ConduitMessage[]> {\n this.ensureConnected();\n\n if (this.state!.mode === 'sse' && this.state!.eventSource) {\n // Drain buffer \u2014 messages arrived via SSE push\n let messages = this.state!.messageBuffer.splice(0);\n\n if (options?.since) {\n messages = messages.filter((m) => m.timestamp > options.since!);\n }\n if (options?.limit && messages.length > options.limit) {\n // Put excess back in buffer\n const excess = messages.splice(options.limit);\n this.state!.messageBuffer.unshift(...excess);\n }\n return messages;\n }\n\n // HTTP fallback mode\n return this.httpPoll(options);\n }\n\n /** Acknowledge messages via HTTP POST. */\n async ack(messageIds: string[]): Promise<void> {\n this.ensureConnected();\n if (messageIds.length === 0) return;\n\n await this.httpFetch('/messages/ack', {\n method: 'POST',\n body: JSON.stringify({ messageIds }),\n });\n }\n\n /**\n * Subscribe to real-time messages.\n *\n * In SSE mode, messages are pushed to the handler as they arrive.\n * In HTTP fallback mode, polls on an interval.\n */\n subscribe(handler: (message: ConduitMessage) => void): () => void {\n this.ensureConnected();\n\n this.state!.subscribers.add(handler);\n\n // Start HTTP polling interval only if in fallback mode\n const interval =\n this.state!.mode === 'http-fallback'\n ? setInterval(async () => {\n if (this.state?.mode === 'http-fallback') {\n const messages = await this.httpPoll({ limit: 20 });\n for (const msg of messages) handler(msg);\n if (messages.length > 0) {\n await this.ack(messages.map((m) => m.id));\n }\n }\n }, 5000)\n : null;\n\n return () => {\n if (interval) clearInterval(interval);\n if (this.state) {\n this.state.subscribers.delete(handler);\n }\n };\n }\n\n // --------------------------------------------------------------------------\n // SSE connection management\n // --------------------------------------------------------------------------\n\n /** Establish SSE connection. Resolves when open, rejects on error. */\n private connectSse(): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (!this.state) {\n reject(new Error('No state'));\n return;\n }\n\n // SSE doesn't support custom headers \u2014 auth via query param\n const url = `${this.state.sseEndpoint}?token=${encodeURIComponent(this.state.apiKey)}&agent_id=${encodeURIComponent(this.state.agentId)}`;\n\n const es = new EventSource(url);\n this.state.eventSource = es;\n\n const timeout = setTimeout(() => {\n es.close();\n reject(new Error('SSE connection timeout'));\n }, 10_000);\n\n es.addEventListener('open', () => {\n clearTimeout(timeout);\n this.state!.connected = true;\n this.state!.reconnectAttempts = 0;\n resolve();\n });\n\n es.addEventListener('message', (event: MessageEvent) => {\n this.handleSseMessage(event);\n });\n\n es.addEventListener('error', () => {\n clearTimeout(timeout);\n if (!this.state!.connected) {\n // Initial connection failed\n es.close();\n reject(new Error('SSE connection failed'));\n } else {\n // Connection dropped \u2014 attempt reconnect\n this.handleSseDisconnect();\n }\n });\n });\n }\n\n /** Handle an incoming SSE message event. */\n private handleSseMessage(event: MessageEvent): void {\n if (!this.state) return;\n\n try {\n const data = JSON.parse(event.data as string) as {\n id?: string;\n from_agent_id?: string;\n from?: string;\n content?: string;\n conversation_id?: string;\n threadId?: string;\n created_at?: string;\n timestamp?: string;\n type?: string;\n };\n\n // Skip heartbeat events\n if (data.type === 'heartbeat' || data.type === 'ping') return;\n\n // Skip self-sent messages\n const from = data.from_agent_id ?? data.from ?? 'unknown';\n if (from === this.state.agentId) return;\n\n const message: ConduitMessage = {\n id: data.id ?? `sse-${Date.now()}`,\n from,\n content: data.content ?? '',\n threadId: data.conversation_id ?? data.threadId,\n timestamp: data.created_at ?? data.timestamp ?? new Date().toISOString(),\n };\n\n this.state.messageBuffer.push(message);\n for (const h of this.state.subscribers) {\n try {\n h(message);\n } catch {\n /* subscriber error must not crash transport */\n }\n }\n } catch {\n // Malformed SSE data \u2014 skip silently\n }\n }\n\n /** Handle SSE connection drop \u2014 attempt reconnect with backoff. */\n private handleSseDisconnect(): void {\n if (!this.state) return;\n\n this.state.eventSource?.close();\n this.state.eventSource = null;\n this.state.reconnectAttempts++;\n\n if (this.state.reconnectAttempts > MAX_RECONNECT_ATTEMPTS) {\n // Switch to permanent HTTP fallback\n this.state.mode = 'http-fallback';\n return;\n }\n\n // Exponential backoff: 1s, 2s, 4s, ...\n const delay = Math.min(1000 * 2 ** (this.state.reconnectAttempts - 1), MAX_RECONNECT_DELAY_MS);\n\n this.state.reconnectTimer = setTimeout(() => {\n void this.connectSse().catch(() => {\n this.handleSseDisconnect();\n });\n }, delay);\n }\n\n // --------------------------------------------------------------------------\n // HTTP helpers\n // --------------------------------------------------------------------------\n\n /** HTTP poll for messages (used in fallback mode). */\n private async httpPoll(options?: { limit?: number; since?: string }): Promise<ConduitMessage[]> {\n const params = new URLSearchParams();\n params.set('mentioned', this.state!.agentId);\n if (options?.limit) params.set('limit', String(options.limit));\n if (options?.since) params.set('since', options.since);\n\n const response = await this.httpFetch(`/messages/peek?${params}`, { method: 'GET' });\n if (!response.ok) return [];\n\n const data = (await response.json()) as {\n data?: {\n messages?: Array<{\n id: string;\n fromAgentId?: string;\n content?: string;\n conversationId?: string;\n createdAt?: string;\n }>;\n };\n };\n\n return (data.data?.messages ?? []).map((m) => ({\n id: m.id,\n from: m.fromAgentId ?? 'unknown',\n content: m.content ?? '',\n threadId: m.conversationId,\n timestamp: m.createdAt ?? new Date().toISOString(),\n }));\n }\n\n /** Make an authenticated HTTP request to the API. */\n private async httpFetch(path: string, init: RequestInit): Promise<Response> {\n const url = `${this.state!.apiBaseUrl}${path}`;\n return fetch(url, {\n ...init,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.state!.apiKey}`,\n 'X-Agent-Id': this.state!.agentId,\n ...(init.headers as Record<string, string>),\n },\n signal: init.signal ?? AbortSignal.timeout(10_000),\n });\n }\n\n /** Throw if not connected. */\n private ensureConnected(): void {\n if (!this.state?.connected) {\n throw new Error('SseTransport not connected. Call connect() first.');\n }\n }\n}\n", "/**\n * Conduit factory \u2014 creates a Conduit instance from the agent registry.\n *\n * Auto-selects the appropriate Transport based on the agent's credential\n * configuration. Priority: Local (napi-rs) > WebSocket > SSE > HTTP polling.\n *\n * @see docs/specs/SIGNALDOCK-UNIFIED-AGENT-REGISTRY.md Section 4.5\n * @task T177\n */\n\nimport type { AgentCredential, AgentRegistryAPI, Conduit, Transport } from '@cleocode/contracts';\nimport { ConduitClient } from './conduit-client.js';\nimport { HttpTransport } from './http-transport.js';\nimport { LocalTransport } from './local-transport.js';\nimport { SseTransport } from './sse-transport.js';\n\n/**\n * Resolve the best available transport for a credential.\n *\n * Priority (highest to lowest):\n * 1. LocalTransport \u2014 when conduit.db exists in the project's .cleo/ dir.\n * Local delivery is always preferred for inter-agent messaging within\n * the same project, even when the agent also has cloud credentials.\n * 2. SseTransport \u2014 when the credential includes an SSE endpoint URL.\n * 3. HttpTransport \u2014 fallback for cloud-only or no conduit.db.\n *\n * Note: LocalTransport and cloud credentials are not mutually exclusive.\n * Agents registered with a remote apiBaseUrl still use LocalTransport when\n * conduit.db is available, since local delivery does not require a network.\n */\nexport function resolveTransport(credential: AgentCredential): Transport {\n // Prefer LocalTransport when conduit.db exists \u2014 works offline and for\n // same-project agent-to-agent messaging without any cloud round-trip.\n if (LocalTransport.isAvailable()) {\n return new LocalTransport();\n }\n\n const isCloudBacked =\n credential.apiBaseUrl &&\n credential.apiBaseUrl !== 'local' &&\n credential.apiBaseUrl.startsWith('http');\n\n // Cloud-backed agents without local conduit.db: prefer SSE over HTTP polling\n if (isCloudBacked && credential.transportConfig.sseEndpoint) {\n return new SseTransport();\n }\n\n // Fallback to HTTP (cloud polling or no other option)\n return new HttpTransport();\n}\n\n/** Create a Conduit instance from the agent registry. */\nexport async function createConduit(\n registry: AgentRegistryAPI,\n agentId?: string,\n): Promise<Conduit> {\n const credential = agentId ? await registry.get(agentId) : await registry.getActive();\n\n if (!credential) {\n throw new Error(\n 'No agent credential found. Run: cleo agent register --id <id> --api-key <key>',\n );\n }\n\n const transport = resolveTransport(credential);\n const conduit = new ConduitClient(transport, credential);\n await conduit.connect();\n return conduit;\n}\n"],
5
+ "mappings": ";AAsBO,IAAM,gBAAN,MAAuC;AAAA,EACpC;AAAA,EACA;AAAA,EACA,QAAsB;AAAA;AAAA,EAG9B,YAAY,WAAsB,YAA6B;AAC7D,SAAK,YAAY;AACjB,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA,EAGA,IAAI,UAAkB;AACpB,WAAO,KAAK,WAAW;AAAA,EACzB;AAAA;AAAA,EAGA,WAAyB;AACvB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,MAAM,UAAyB;AAC7B,SAAK,QAAQ;AACb,QAAI;AACF,YAAM,KAAK,UAAU,QAAQ;AAAA,QAC3B,SAAS,KAAK,WAAW;AAAA,QACzB,QAAQ,KAAK,WAAW;AAAA,QACxB,YAAY,KAAK,WAAW;AAAA,QAC5B,GAAG,KAAK,WAAW;AAAA,MACrB,CAAC;AACD,WAAK,QAAQ;AAAA,IACf,SAAS,KAAK;AAEZ,WAAK,QAAQ;AACb,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,KACJ,IACA,SACA,SAC4B;AAC5B,UAAM,SAAS,MAAM,KAAK,UAAU,KAAK,IAAI,SAAS;AAAA,MACpD,gBAAgB,SAAS;AAAA,IAC3B,CAAC;AACD,WAAO;AAAA,MACL,WAAW,OAAO;AAAA,MAClB,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACtC;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,KAAK,SAAyE;AAClF,WAAO,KAAK,UAAU,KAAK,OAAO;AAAA,EACpC;AAAA;AAAA,EAGA,UAAU,SAAgE;AAExE,QAAI,KAAK,UAAU,WAAW;AAC5B,aAAO,KAAK,UAAU,UAAU,OAAO;AAAA,IACzC;AAEA,UAAM,WAAW,YAAY,YAAY;AACvC,YAAM,WAAW,MAAM,KAAK,UAAU,KAAK;AAC3C,iBAAW,OAAO,SAAU,SAAQ,GAAG;AACvC,UAAI,SAAS,SAAS,GAAG;AACvB,cAAM,KAAK,UAAU,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAAA,MACpD;AAAA,IACF,GAAG,KAAK,WAAW,gBAAgB,kBAAkB,GAAI;AACzD,WAAO,MAAM,cAAc,QAAQ;AAAA,EACrC;AAAA;AAAA,EAGA,MAAM,YAA2B;AAE/B,UAAM,KAAK,UAAU,KAAK,KAAK,WAAW,SAAS,IAAI,CAAC,CAAC;AAAA,EAC3D;AAAA;AAAA,EAGA,MAAM,SAAS,SAAmC;AAEhD,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,GAAG,KAAK,WAAW,UAAU,WAAW,OAAO,IAAI;AAAA,QAC9E,SAAS;AAAA,UACP,eAAe,UAAU,KAAK,WAAW,MAAM;AAAA,UAC/C,cAAc,KAAK,WAAW;AAAA,QAChC;AAAA,MACF,CAAC;AACD,UAAI,CAAC,SAAS,GAAI,QAAO;AACzB,YAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,aAAO,KAAK,MAAM,OAAO,WAAW;AAAA,IACtC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,aAA4B;AAChC,UAAM,KAAK,UAAU,WAAW;AAChC,SAAK,QAAQ;AAAA,EACf;AACF;;;ACtGO,IAAM,gBAAN,MAAyC;AAAA,EACrC,OAAO;AAAA,EACR,QAAmC;AAAA;AAAA,EAG3C,MAAM,QAAQ,QAA+C;AAC3D,UAAM,aAAa,OAAO;AAC1B,UAAM,cAAc,OAAO,sBAAsB;AAGjD,QAAI,YAAY;AAChB,QAAI,aAAa;AACf,YAAM,CAAC,eAAe,cAAc,IAAI,MAAM,QAAQ,WAAW;AAAA,QAC/D,MAAM,GAAG,UAAU,WAAW,EAAE,QAAQ,OAAO,QAAQ,YAAY,QAAQ,GAAI,EAAE,CAAC;AAAA,QAClF,MAAM,GAAG,WAAW,WAAW,EAAE,QAAQ,OAAO,QAAQ,YAAY,QAAQ,GAAI,EAAE,CAAC;AAAA,MACrF,CAAC;AACD,YAAM,YAAY,cAAc,WAAW,eAAe,cAAc,MAAM;AAC9E,YAAM,aAAa,eAAe,WAAW,eAAe,eAAe,MAAM;AACjF,UAAI,CAAC,aAAa,YAAY;AAC5B,oBAAY;AAAA,MACd;AAAA,IACF;AAEA,SAAK,QAAQ;AAAA,MACX,SAAS,OAAO;AAAA,MAChB,QAAQ,OAAO;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW;AAAA,IACb;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,aAA4B;AAChC,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAGA,MAAM,KACJ,IACA,SACA,SACgC;AAChC,SAAK,gBAAgB;AAErB,UAAM,OAA+B,EAAE,QAAQ;AAE/C,QAAI;AACJ,QAAI,SAAS,gBAAgB;AAC3B,aAAO,kBAAkB,QAAQ,cAAc;AAC/C,UAAI,QAAQ,SAAS;AACnB,aAAK,SAAS,IAAI,QAAQ;AAAA,MAC5B;AAAA,IACF,OAAO;AACL,aAAO;AACP,WAAK,WAAW,IAAI;AAAA,IACtB;AAEA,UAAM,WAAW,MAAM,KAAK,kBAAkB,MAAM;AAAA,MAClD,QAAQ;AAAA,MACR,SAAS,KAAK,QAAQ;AAAA,MACtB,MAAM,KAAK,UAAU,IAAI;AAAA,IAC3B,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,OAAO,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,EAAE;AACjD,YAAM,IAAI,MAAM,8BAA8B,SAAS,MAAM,IAAI,IAAI,EAAE;AAAA,IACzE;AAEA,UAAM,OAAQ,MAAM,SAAS,KAAK;AAIlC,UAAM,YAAY,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,MAAM;AAC7D,WAAO,EAAE,UAAU;AAAA,EACrB;AAAA;AAAA,EAGA,MAAM,KAAK,SAAyE;AAClF,SAAK,gBAAgB;AAErB,UAAM,SAAS,IAAI,gBAAgB;AAGnC,QAAI,SAAS,MAAO,QAAO,IAAI,SAAS,OAAO,QAAQ,KAAK,CAAC;AAC7D,QAAI,SAAS,MAAO,QAAO,IAAI,SAAS,QAAQ,KAAK;AAErD,UAAM,WAAW,MAAM,KAAK,kBAAkB,kBAAkB,MAAM,IAAI;AAAA,MACxE,QAAQ;AAAA,MACR,SAAS,KAAK,QAAQ;AAAA,IACxB,CAAC;AAED,QAAI,CAAC,SAAS,GAAI,QAAO,CAAC;AAE1B,UAAM,OAAQ,MAAM,SAAS,KAAK;AAYlC,YAAQ,KAAK,MAAM,YAAY,CAAC,GAAG,IAAI,CAAC,OAAO;AAAA,MAC7C,IAAI,EAAE;AAAA,MACN,MAAM,EAAE,eAAe;AAAA,MACvB,SAAS,EAAE,WAAW;AAAA,MACtB,UAAU,EAAE;AAAA,MACZ,WAAW,EAAE,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACnD,EAAE;AAAA,EACJ;AAAA;AAAA,EAGA,MAAM,IAAI,YAAqC;AAC7C,SAAK,gBAAgB;AAErB,UAAM,KAAK,kBAAkB,iBAAiB;AAAA,MAC5C,QAAQ;AAAA,MACR,SAAS,KAAK,QAAQ;AAAA,MACtB,MAAM,KAAK,UAAU,EAAE,WAAW,CAAC;AAAA,IACrC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,kBAAkB,MAAc,MAAsC;AAClF,UAAM,UAAU,YAAY,QAAQ,GAAK;AACzC,UAAM,SAAS,KAAK,SAAS,YAAY,IAAI,CAAC,KAAK,QAAQ,OAAO,CAAC,IAAI;AACvE,UAAM,MAAM,GAAG,KAAK,MAAO,SAAS,GAAG,IAAI;AAE3C,QAAI;AACF,aAAO,MAAM,MAAM,KAAK,EAAE,GAAG,MAAM,OAAO,CAAC;AAAA,IAC7C,SAAS,YAAY;AACnB,YAAM,WACJ,KAAK,MAAO,cAAc,KAAK,MAAO,aAClC,KAAK,MAAO,cACZ,KAAK,MAAO;AAElB,UAAI,CAAC,SAAU,OAAM;AAErB,UAAI;AACF,cAAM,iBAAiB,KAAK,SACxB,YAAY,IAAI,CAAC,KAAK,QAAQ,YAAY,QAAQ,GAAK,CAAC,CAAC,IACzD,YAAY,QAAQ,GAAK;AAC7B,cAAM,mBAAmB,MAAM,MAAM,GAAG,QAAQ,GAAG,IAAI,IAAI;AAAA,UACzD,GAAG;AAAA,UACH,QAAQ;AAAA,QACV,CAAC;AACD,aAAK,MAAO,YAAY;AACxB,eAAO;AAAA,MACT,QAAQ;AACN,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,UAAkC;AACxC,WAAO;AAAA,MACL,gBAAgB;AAAA,MAChB,eAAe,UAAU,KAAK,MAAO,MAAM;AAAA,MAC3C,cAAc,KAAK,MAAO;AAAA,IAC5B;AAAA,EACF;AAAA,EAEQ,kBAAwB;AAC9B,QAAI,CAAC,KAAK,OAAO,WAAW;AAC1B,YAAM,IAAI,MAAM,oDAAoD;AAAA,IACtE;AAAA,EACF;AACF;;;ACxLA,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,iBAAAA,sBAAqB;;;ACO9B,SAAS,qBAAqB;AAC9B,SAAS,SAAS,YAAY;AAO9B,IAAM,WAAW,cAAc,YAAY,GAAG;AAE9C,IAAM,EAAE,aAAa,IAAI,SAAS,aAAa;AAKxC,IAAM,sBAAsB;AA8Q5B,SAAS,iBAAiB,aAA6B;AAC5D,SAAO,KAAK,aAAa,SAAS,mBAAmB;AACvD;;;ADjSA,IAAMC,YAAWC,eAAc,YAAY,GAAG;AAC9C,IAAM,EAAE,cAAc,kBAAkB,IAAID,UAAS,aAAa;AAc3D,IAAM,iBAAN,MAA0C;AAAA,EACtC,OAAO;AAAA,EACR,QAAoC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAY5C,MAAM,QAAQ,QAA+C;AAC3D,UAAM,SAAS,iBAAiB,QAAQ,IAAI,CAAC;AAE7C,QAAI,CAAC,WAAW,MAAM,GAAG;AACvB,YAAM,IAAI,MAAM,2CAA2C,MAAM,kBAAkB;AAAA,IACrF;AAEA,UAAM,KAAK,IAAI,kBAAkB,MAAM;AACvC,OAAG,KAAK,2BAA2B;AACnC,OAAG,KAAK,4BAA4B;AACpC,OAAG,KAAK,0BAA0B;AAGlC,UAAM,cAAc,GACjB,QAAQ,uEAAuE,EAC/E,IAAI;AAEP,QAAI,CAAC,aAAa;AAChB,SAAG,MAAM;AACT,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,SAAK,QAAQ;AAAA,MACX,SAAS,OAAO;AAAA,MAChB;AAAA,MACA;AAAA,MACA,aAAa,oBAAI,IAAI;AAAA,MACrB,WAAW;AAAA,IACb;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,aAA4B;AAChC,QAAI,CAAC,KAAK,MAAO;AAEjB,QAAI,KAAK,MAAM,WAAW;AACxB,oBAAc,KAAK,MAAM,SAAS;AAAA,IACpC;AACA,SAAK,MAAM,YAAY,MAAM;AAC7B,SAAK,MAAM,GAAG,MAAM;AACpB,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,KACJ,IACA,SACA,SACgC;AAChC,SAAK,gBAAgB;AACrB,UAAM,EAAE,IAAI,QAAQ,IAAI,KAAK;AAC7B,UAAM,YAAY,WAAW;AAC7B,UAAM,UAAU,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAE5C,QAAI,SAAS,gBAAgB;AAC3B,SAAG;AAAA,QACD;AAAA;AAAA,MAEF,EAAE,IAAI,WAAW,QAAQ,gBAAgB,SAAS,IAAI,SAAS,OAAO;AAAA,IACxE,OAAO;AAEL,YAAM,SAAS,KAAK,qBAAqB,SAAS,EAAE;AACpD,SAAG;AAAA,QACD;AAAA;AAAA,MAEF,EAAE,IAAI,WAAW,QAAQ,SAAS,IAAI,SAAS,OAAO;AAAA,IACxD;AAGA,SAAK,kBAAkB;AAAA,MACrB,IAAI;AAAA,MACJ,MAAM;AAAA,MACN;AAAA,MACA,UAAU,SAAS;AAAA,MACnB,WAAW,IAAI,KAAK,UAAU,GAAI,EAAE,YAAY;AAAA,IAClD,CAAC;AAED,WAAO,EAAE,UAAU;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,KAAK,SAAyE;AAClF,SAAK,gBAAgB;AACrB,UAAM,EAAE,IAAI,QAAQ,IAAI,KAAK;AAC7B,UAAM,QAAQ,SAAS,SAAS;AAEhC,QAAI;AACJ,QAAI;AAEJ,QAAI,SAAS,OAAO;AAClB,cAAQ;AAAA;AAAA;AAAA;AAAA;AAKR,eAAS,CAAC,SAAS,QAAQ,OAAO,KAAK;AAAA,IACzC,OAAO;AACL,cAAQ;AAAA;AAAA;AAAA;AAAA;AAKR,eAAS,CAAC,SAAS,KAAK;AAAA,IAC1B;AAEA,UAAM,OAAO,GAAG,QAAQ,KAAK,EAAE,IAAI,GAAG,MAAM;AAQ5C,WAAO,KAAK,IAAI,CAAC,OAAO;AAAA,MACtB,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,SAAS,EAAE;AAAA,MACX,UAAU,EAAE,mBAAmB;AAAA,MAC/B,WAAW,IAAI,KAAK,EAAE,aAAa,GAAI,EAAE,YAAY;AAAA,IACvD,EAAE;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,IAAI,YAAqC;AAC7C,SAAK,gBAAgB;AACrB,QAAI,WAAW,WAAW,EAAG;AAE7B,UAAM,EAAE,GAAG,IAAI,KAAK;AACpB,UAAM,UAAU,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAE5C,UAAM,eAAe,WAAW,IAAI,MAAM,GAAG,EAAE,KAAK,IAAI;AACxD,OAAG;AAAA,MACD,2EAA2E,YAAY;AAAA,IACzF,EAAE,IAAI,SAAS,GAAG,UAAU;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,UAAU,SAAwD;AAChE,SAAK,gBAAgB;AACrB,SAAK,MAAO,YAAY,IAAI,OAAO;AAGnC,QAAI,CAAC,KAAK,MAAO,aAAa,KAAK,MAAO,YAAY,SAAS,GAAG;AAChE,WAAK,MAAO,YAAY,YAAY,MAAM;AACxC,aAAK,KAAK,cAAc;AAAA,MAC1B,GAAG,GAAI;AAAA,IACT;AAEA,WAAO,MAAM;AACX,WAAK,OAAO,YAAY,OAAO,OAAO;AACtC,UAAI,KAAK,OAAO,YAAY,SAAS,KAAK,KAAK,MAAM,WAAW;AAC9D,sBAAc,KAAK,MAAM,SAAS;AAClC,aAAK,MAAM,YAAY;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,OAAO,YAAY,KAAuB;AACxC,UAAM,SAAS,iBAAiB,OAAO,QAAQ,IAAI,CAAC;AACpD,WAAO,WAAW,MAAM;AAAA,EAC1B;AAAA;AAAA,EAGA,MAAc,gBAA+B;AAC3C,QAAI,CAAC,KAAK,SAAS,KAAK,MAAM,YAAY,SAAS,EAAG;AAEtD,UAAM,WAAW,MAAM,KAAK,KAAK,EAAE,OAAO,GAAG,CAAC;AAC9C,eAAW,OAAO,UAAU;AAC1B,WAAK,kBAAkB,GAAG;AAAA,IAC5B;AACA,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,KAAK,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAAA,IAC1C;AAAA,EACF;AAAA;AAAA,EAGQ,kBAAkB,SAA+B;AACvD,QAAI,CAAC,KAAK,MAAO;AACjB,eAAW,WAAW,KAAK,MAAM,aAAa;AAC5C,UAAI;AACF,gBAAQ,OAAO;AAAA,MACjB,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,qBAAqB,aAAqB,WAA2B;AAC3E,UAAM,EAAE,GAAG,IAAI,KAAK;AAGpB,UAAM,qBAAqB,CAAC,aAAa,SAAS,EAAE,KAAK,EAAE,KAAK,GAAG;AAGnE,UAAM,WAAW,GACd;AAAA,MACC;AAAA;AAAA;AAAA,IAGF,EACC,IAAI,kBAAkB;AAEzB,QAAI,SAAU,QAAO,SAAS;AAG9B,UAAM,SAAS,WAAW;AAC1B,UAAM,UAAU,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAE5C,OAAG;AAAA,MACD;AAAA;AAAA,IAEF,EAAE,IAAI,QAAQ,oBAAoB,SAAS,OAAO;AAElD,WAAO;AAAA,EACT;AAAA;AAAA,EAGQ,kBAAwB;AAC9B,QAAI,CAAC,KAAK,OAAO;AACf,YAAM,IAAI,MAAM,qDAAqD;AAAA,IACvE;AAAA,EACF;AACF;;;AEhTA,IAAM,yBAAyB;AAG/B,IAAM,yBAAyB;AAqBxB,IAAM,eAAN,MAAwC;AAAA,EACpC,OAAO;AAAA,EACR,QAAkC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ1C,MAAM,QAAQ,QAA+C;AAC3D,QAAI,KAAK,OAAO,WAAW;AACzB,YAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AAEA,UAAM,cAAc,OAAO;AAC3B,QAAI,CAAC,eAAe,CAAC,OAAO,YAAY;AACtC,YAAM,IAAI,MAAM,4DAA4D;AAAA,IAC9E;AAEA,UAAM,WAAW,eAAe,GAAG,OAAO,UAAU;AAEpD,SAAK,QAAQ;AAAA,MACX,SAAS,OAAO;AAAA,MAChB,QAAQ,OAAO;AAAA,MACf,YAAY,OAAO;AAAA,MACnB,aAAa;AAAA,MACb,aAAa;AAAA,MACb,MAAM;AAAA,MACN,eAAe,CAAC;AAAA,MAChB,aAAa,oBAAI,IAAI;AAAA,MACrB,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,WAAW;AAAA,IACb;AAEA,QAAI;AACF,YAAM,KAAK,WAAW;AAAA,IACxB,QAAQ;AAEN,WAAK,MAAM,OAAO;AAClB,WAAK,MAAM,YAAY;AAAA,IACzB;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,aAA4B;AAChC,QAAI,CAAC,KAAK,MAAO;AAEjB,QAAI,KAAK,MAAM,aAAa;AAC1B,WAAK,MAAM,YAAY,MAAM;AAAA,IAC/B;AACA,QAAI,KAAK,MAAM,gBAAgB;AAC7B,mBAAa,KAAK,MAAM,cAAc;AAAA,IACxC;AACA,SAAK,MAAM,gBAAgB,CAAC;AAC5B,SAAK,MAAM,YAAY;AACvB,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,KACJ,IACA,SACA,SACgC;AAChC,SAAK,gBAAgB;AAErB,UAAM,OAA+B,EAAE,SAAS,WAAW,GAAG;AAC9D,QAAI,OAAO;AAEX,QAAI,SAAS,gBAAgB;AAC3B,aAAO,kBAAkB,QAAQ,cAAc;AAC/C,UAAI,QAAQ,SAAS;AACnB,aAAK,SAAS,IAAI,QAAQ;AAAA,MAC5B;AAAA,IACF;AAEA,UAAM,WAAW,MAAM,KAAK,UAAU,MAAM;AAAA,MAC1C,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,IAAI;AAAA,IAC3B,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,OAAO,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,EAAE;AACjD,YAAM,IAAI,MAAM,6BAA6B,SAAS,MAAM,IAAI,IAAI,EAAE;AAAA,IACxE;AAEA,UAAM,OAAQ,MAAM,SAAS,KAAK;AAGlC,WAAO,EAAE,WAAW,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,MAAM,UAAU;AAAA,EAC3E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,KAAK,SAAyE;AAClF,SAAK,gBAAgB;AAErB,QAAI,KAAK,MAAO,SAAS,SAAS,KAAK,MAAO,aAAa;AAEzD,UAAI,WAAW,KAAK,MAAO,cAAc,OAAO,CAAC;AAEjD,UAAI,SAAS,OAAO;AAClB,mBAAW,SAAS,OAAO,CAAC,MAAM,EAAE,YAAY,QAAQ,KAAM;AAAA,MAChE;AACA,UAAI,SAAS,SAAS,SAAS,SAAS,QAAQ,OAAO;AAErD,cAAM,SAAS,SAAS,OAAO,QAAQ,KAAK;AAC5C,aAAK,MAAO,cAAc,QAAQ,GAAG,MAAM;AAAA,MAC7C;AACA,aAAO;AAAA,IACT;AAGA,WAAO,KAAK,SAAS,OAAO;AAAA,EAC9B;AAAA;AAAA,EAGA,MAAM,IAAI,YAAqC;AAC7C,SAAK,gBAAgB;AACrB,QAAI,WAAW,WAAW,EAAG;AAE7B,UAAM,KAAK,UAAU,iBAAiB;AAAA,MACpC,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,EAAE,WAAW,CAAC;AAAA,IACrC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,UAAU,SAAwD;AAChE,SAAK,gBAAgB;AAErB,SAAK,MAAO,YAAY,IAAI,OAAO;AAGnC,UAAM,WACJ,KAAK,MAAO,SAAS,kBACjB,YAAY,YAAY;AACtB,UAAI,KAAK,OAAO,SAAS,iBAAiB;AACxC,cAAM,WAAW,MAAM,KAAK,SAAS,EAAE,OAAO,GAAG,CAAC;AAClD,mBAAW,OAAO,SAAU,SAAQ,GAAG;AACvC,YAAI,SAAS,SAAS,GAAG;AACvB,gBAAM,KAAK,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAAA,QAC1C;AAAA,MACF;AAAA,IACF,GAAG,GAAI,IACP;AAEN,WAAO,MAAM;AACX,UAAI,SAAU,eAAc,QAAQ;AACpC,UAAI,KAAK,OAAO;AACd,aAAK,MAAM,YAAY,OAAO,OAAO;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,aAA4B;AAClC,WAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,UAAI,CAAC,KAAK,OAAO;AACf,eAAO,IAAI,MAAM,UAAU,CAAC;AAC5B;AAAA,MACF;AAGA,YAAM,MAAM,GAAG,KAAK,MAAM,WAAW,UAAU,mBAAmB,KAAK,MAAM,MAAM,CAAC,aAAa,mBAAmB,KAAK,MAAM,OAAO,CAAC;AAEvI,YAAM,KAAK,IAAI,YAAY,GAAG;AAC9B,WAAK,MAAM,cAAc;AAEzB,YAAM,UAAU,WAAW,MAAM;AAC/B,WAAG,MAAM;AACT,eAAO,IAAI,MAAM,wBAAwB,CAAC;AAAA,MAC5C,GAAG,GAAM;AAET,SAAG,iBAAiB,QAAQ,MAAM;AAChC,qBAAa,OAAO;AACpB,aAAK,MAAO,YAAY;AACxB,aAAK,MAAO,oBAAoB;AAChC,gBAAQ;AAAA,MACV,CAAC;AAED,SAAG,iBAAiB,WAAW,CAAC,UAAwB;AACtD,aAAK,iBAAiB,KAAK;AAAA,MAC7B,CAAC;AAED,SAAG,iBAAiB,SAAS,MAAM;AACjC,qBAAa,OAAO;AACpB,YAAI,CAAC,KAAK,MAAO,WAAW;AAE1B,aAAG,MAAM;AACT,iBAAO,IAAI,MAAM,uBAAuB,CAAC;AAAA,QAC3C,OAAO;AAEL,eAAK,oBAAoB;AAAA,QAC3B;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA,EAGQ,iBAAiB,OAA2B;AAClD,QAAI,CAAC,KAAK,MAAO;AAEjB,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,MAAM,IAAc;AAa5C,UAAI,KAAK,SAAS,eAAe,KAAK,SAAS,OAAQ;AAGvD,YAAM,OAAO,KAAK,iBAAiB,KAAK,QAAQ;AAChD,UAAI,SAAS,KAAK,MAAM,QAAS;AAEjC,YAAM,UAA0B;AAAA,QAC9B,IAAI,KAAK,MAAM,OAAO,KAAK,IAAI,CAAC;AAAA,QAChC;AAAA,QACA,SAAS,KAAK,WAAW;AAAA,QACzB,UAAU,KAAK,mBAAmB,KAAK;AAAA,QACvC,WAAW,KAAK,cAAc,KAAK,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,MACzE;AAEA,WAAK,MAAM,cAAc,KAAK,OAAO;AACrC,iBAAW,KAAK,KAAK,MAAM,aAAa;AACtC,YAAI;AACF,YAAE,OAAO;AAAA,QACX,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAAA;AAAA,EAGQ,sBAA4B;AAClC,QAAI,CAAC,KAAK,MAAO;AAEjB,SAAK,MAAM,aAAa,MAAM;AAC9B,SAAK,MAAM,cAAc;AACzB,SAAK,MAAM;AAEX,QAAI,KAAK,MAAM,oBAAoB,wBAAwB;AAEzD,WAAK,MAAM,OAAO;AAClB;AAAA,IACF;AAGA,UAAM,QAAQ,KAAK,IAAI,MAAO,MAAM,KAAK,MAAM,oBAAoB,IAAI,sBAAsB;AAE7F,SAAK,MAAM,iBAAiB,WAAW,MAAM;AAC3C,WAAK,KAAK,WAAW,EAAE,MAAM,MAAM;AACjC,aAAK,oBAAoB;AAAA,MAC3B,CAAC;AAAA,IACH,GAAG,KAAK;AAAA,EACV;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,SAAS,SAAyE;AAC9F,UAAM,SAAS,IAAI,gBAAgB;AACnC,WAAO,IAAI,aAAa,KAAK,MAAO,OAAO;AAC3C,QAAI,SAAS,MAAO,QAAO,IAAI,SAAS,OAAO,QAAQ,KAAK,CAAC;AAC7D,QAAI,SAAS,MAAO,QAAO,IAAI,SAAS,QAAQ,KAAK;AAErD,UAAM,WAAW,MAAM,KAAK,UAAU,kBAAkB,MAAM,IAAI,EAAE,QAAQ,MAAM,CAAC;AACnF,QAAI,CAAC,SAAS,GAAI,QAAO,CAAC;AAE1B,UAAM,OAAQ,MAAM,SAAS,KAAK;AAYlC,YAAQ,KAAK,MAAM,YAAY,CAAC,GAAG,IAAI,CAAC,OAAO;AAAA,MAC7C,IAAI,EAAE;AAAA,MACN,MAAM,EAAE,eAAe;AAAA,MACvB,SAAS,EAAE,WAAW;AAAA,MACtB,UAAU,EAAE;AAAA,MACZ,WAAW,EAAE,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACnD,EAAE;AAAA,EACJ;AAAA;AAAA,EAGA,MAAc,UAAU,MAAc,MAAsC;AAC1E,UAAM,MAAM,GAAG,KAAK,MAAO,UAAU,GAAG,IAAI;AAC5C,WAAO,MAAM,KAAK;AAAA,MAChB,GAAG;AAAA,MACH,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,eAAe,UAAU,KAAK,MAAO,MAAM;AAAA,QAC3C,cAAc,KAAK,MAAO;AAAA,QAC1B,GAAI,KAAK;AAAA,MACX;AAAA,MACA,QAAQ,KAAK,UAAU,YAAY,QAAQ,GAAM;AAAA,IACnD,CAAC;AAAA,EACH;AAAA;AAAA,EAGQ,kBAAwB;AAC9B,QAAI,CAAC,KAAK,OAAO,WAAW;AAC1B,YAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AAAA,EACF;AACF;;;AC/VO,SAAS,iBAAiB,YAAwC;AAGvE,MAAI,eAAe,YAAY,GAAG;AAChC,WAAO,IAAI,eAAe;AAAA,EAC5B;AAEA,QAAM,gBACJ,WAAW,cACX,WAAW,eAAe,WAC1B,WAAW,WAAW,WAAW,MAAM;AAGzC,MAAI,iBAAiB,WAAW,gBAAgB,aAAa;AAC3D,WAAO,IAAI,aAAa;AAAA,EAC1B;AAGA,SAAO,IAAI,cAAc;AAC3B;AAGA,eAAsB,cACpB,UACA,SACkB;AAClB,QAAM,aAAa,UAAU,MAAM,SAAS,IAAI,OAAO,IAAI,MAAM,SAAS,UAAU;AAEpF,MAAI,CAAC,YAAY;AACf,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAY,iBAAiB,UAAU;AAC7C,QAAM,UAAU,IAAI,cAAc,WAAW,UAAU;AACvD,QAAM,QAAQ,QAAQ;AACtB,SAAO;AACT;",
6
+ "names": ["createRequire", "_require", "createRequire"]
7
+ }
package/dist/index.js CHANGED
@@ -11442,16 +11442,27 @@ function reconcileJournal(nativeDb, migrationsFolder, existenceTable, logSubsyst
11442
11442
  const localMigrations = readMigrationFiles({ migrationsFolder });
11443
11443
  const localHashes = new Set(localMigrations.map((m) => m.hash));
11444
11444
  const dbEntries = nativeDb.prepare('SELECT hash FROM "__drizzle_migrations"').all();
11445
- const hasOrphanedEntries = dbEntries.some((e) => !localHashes.has(e.hash));
11445
+ const orphanedEntries = dbEntries.filter((e) => !localHashes.has(e.hash));
11446
+ const hasOrphanedEntries = orphanedEntries.length > 0;
11446
11447
  if (hasOrphanedEntries) {
11447
- const log10 = getLogger(logSubsystem);
11448
- log10.warn(
11449
- { orphaned: dbEntries.filter((e) => !localHashes.has(e.hash)).length },
11450
- `Detected stale migration journal entries from a previous CLEO version. Reconciling.`
11451
- );
11452
- nativeDb.exec('DELETE FROM "__drizzle_migrations"');
11453
- for (const m of localMigrations) {
11454
- insertJournalEntry(nativeDb, m.hash, m.folderMillis, m.name ?? "");
11448
+ const dbHashes = new Set(dbEntries.map((e) => e.hash));
11449
+ const allLocalHashesPresentInDb = localMigrations.every((m) => dbHashes.has(m.hash));
11450
+ if (allLocalHashesPresentInDb) {
11451
+ const log10 = getLogger(logSubsystem);
11452
+ log10.debug(
11453
+ { extra: orphanedEntries.length },
11454
+ `Migration journal has ${orphanedEntries.length} entries for migrations not known to this install (DB is ahead). Skipping reconciliation.`
11455
+ );
11456
+ } else {
11457
+ const log10 = getLogger(logSubsystem);
11458
+ log10.warn(
11459
+ { orphaned: orphanedEntries.length },
11460
+ `Detected stale migration journal entries from a previous CLEO version. Reconciling.`
11461
+ );
11462
+ nativeDb.exec('DELETE FROM "__drizzle_migrations"');
11463
+ for (const m of localMigrations) {
11464
+ insertJournalEntry(nativeDb, m.hash, m.folderMillis, m.name ?? "");
11465
+ }
11455
11466
  }
11456
11467
  }
11457
11468
  }