@cleocode/core 2026.4.44 → 2026.4.46
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.
- package/dist/conduit/index.js +749 -0
- package/dist/conduit/index.js.map +7 -0
- package/dist/index.js +81 -62
- package/dist/index.js.map +3 -3
- package/dist/internal.js +110682 -0
- package/dist/internal.js.map +7 -0
- package/dist/store/db-helpers.d.ts +8 -3
- package/dist/store/db-helpers.d.ts.map +1 -1
- package/dist/store/migration-manager.d.ts.map +1 -1
- package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/store/__tests__/db-helpers.test.ts +46 -6
- package/src/store/db-helpers.ts +25 -4
- package/src/store/migration-manager.ts +41 -9
- package/src/store/sqlite-data-accessor.ts +2 -1
- package/src/tasks/__tests__/epic-auto-complete.test.ts +331 -0
- package/src/tasks/complete.ts +7 -4
|
@@ -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": "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",
|
|
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
|
|
11445
|
+
const orphanedEntries = dbEntries.filter((e) => !localHashes.has(e.hash));
|
|
11446
|
+
const hasOrphanedEntries = orphanedEntries.length > 0;
|
|
11446
11447
|
if (hasOrphanedEntries) {
|
|
11447
|
-
const
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
|
|
11452
|
-
|
|
11453
|
-
|
|
11454
|
-
|
|
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 log11 = getLogger(logSubsystem);
|
|
11452
|
+
log11.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 log11 = getLogger(logSubsystem);
|
|
11458
|
+
log11.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
|
}
|
|
@@ -11475,8 +11486,8 @@ function reconcileJournal(nativeDb, migrationsFolder, existenceTable, logSubsyst
|
|
|
11475
11486
|
return cols.some((c) => c.name === column);
|
|
11476
11487
|
});
|
|
11477
11488
|
if (allColumnsExist) {
|
|
11478
|
-
const
|
|
11479
|
-
|
|
11489
|
+
const log11 = getLogger(logSubsystem);
|
|
11490
|
+
log11.warn(
|
|
11480
11491
|
{ migration: migration.name, columns: alterMatches },
|
|
11481
11492
|
`Detected partially-applied migration ${migration.name} \u2014 columns exist but journal entry missing. Auto-reconciling.`
|
|
11482
11493
|
);
|
|
@@ -11494,8 +11505,8 @@ function reconcileJournal(nativeDb, migrationsFolder, existenceTable, logSubsyst
|
|
|
11494
11505
|
for (const entry of unnamedEntries) {
|
|
11495
11506
|
const migrationName = hashToName.get(entry.hash);
|
|
11496
11507
|
if (!migrationName) continue;
|
|
11497
|
-
const
|
|
11498
|
-
|
|
11508
|
+
const log11 = getLogger(logSubsystem);
|
|
11509
|
+
log11.warn(
|
|
11499
11510
|
{ id: entry.id, hash: entry.hash, name: migrationName },
|
|
11500
11511
|
`Backfilling missing name on journal entry id=${entry.id} \u2014 Drizzle v1 beta requires name for applied-migration detection.`
|
|
11501
11512
|
);
|
|
@@ -11539,8 +11550,8 @@ function ensureColumns(nativeDb, tableName, requiredColumns, logSubsystem) {
|
|
|
11539
11550
|
const existingCols = new Set(columns.map((c) => c.name));
|
|
11540
11551
|
for (const req of requiredColumns) {
|
|
11541
11552
|
if (!existingCols.has(req.name)) {
|
|
11542
|
-
const
|
|
11543
|
-
|
|
11553
|
+
const log11 = getLogger(logSubsystem);
|
|
11554
|
+
log11.warn(
|
|
11544
11555
|
{ column: req.name },
|
|
11545
11556
|
`Adding missing column ${tableName}.${req.name} via ALTER TABLE`
|
|
11546
11557
|
);
|
|
@@ -13500,7 +13511,7 @@ function getDbPath(cwd) {
|
|
|
13500
13511
|
return join9(getCleoDirAbsolute(cwd), DB_FILENAME2);
|
|
13501
13512
|
}
|
|
13502
13513
|
async function autoRecoverFromBackup(nativeDb, dbPath, cwd) {
|
|
13503
|
-
const
|
|
13514
|
+
const log11 = getLogger("sqlite");
|
|
13504
13515
|
try {
|
|
13505
13516
|
const countResult = nativeDb.prepare("SELECT COUNT(*) as cnt FROM tasks").get();
|
|
13506
13517
|
const taskCount = countResult?.cnt ?? 0;
|
|
@@ -13521,7 +13532,7 @@ async function autoRecoverFromBackup(nativeDb, dbPath, cwd) {
|
|
|
13521
13532
|
if (backupTaskCount < MIN_BACKUP_TASK_COUNT) {
|
|
13522
13533
|
return;
|
|
13523
13534
|
}
|
|
13524
|
-
|
|
13535
|
+
log11.warn(
|
|
13525
13536
|
{ dbPath, backupPath: newestBackup.path, backupTasks: backupTaskCount },
|
|
13526
13537
|
`Empty database detected with ${backupTaskCount}-task backup available. Auto-recovering from backup. This likely happened because git-tracked WAL/SHM files were overwritten during a branch switch (T5188).`
|
|
13527
13538
|
);
|
|
@@ -13539,7 +13550,7 @@ async function autoRecoverFromBackup(nativeDb, dbPath, cwd) {
|
|
|
13539
13550
|
const tempPath = dbPath + ".recovery-tmp";
|
|
13540
13551
|
copyFileSync4(newestBackup.path, tempPath);
|
|
13541
13552
|
renameSync(tempPath, dbPath);
|
|
13542
|
-
|
|
13553
|
+
log11.info(
|
|
13543
13554
|
{ dbPath, backupPath: newestBackup.path, restoredTasks: backupTaskCount },
|
|
13544
13555
|
"Database auto-recovered from backup successfully."
|
|
13545
13556
|
);
|
|
@@ -13549,7 +13560,7 @@ async function autoRecoverFromBackup(nativeDb, dbPath, cwd) {
|
|
|
13549
13560
|
runMigrations(restoredNativeDb, restoredDb);
|
|
13550
13561
|
_db = restoredDb;
|
|
13551
13562
|
} catch (err) {
|
|
13552
|
-
|
|
13563
|
+
log11.error({ err, dbPath }, "Auto-recovery from backup failed. Continuing with empty database.");
|
|
13553
13564
|
}
|
|
13554
13565
|
}
|
|
13555
13566
|
async function getDb(cwd) {
|
|
@@ -13583,7 +13594,7 @@ async function getDb(cwd) {
|
|
|
13583
13594
|
const { execFileSync: execFileSync12 } = await import("node:child_process");
|
|
13584
13595
|
const gitCwd = resolve3(dbPath, "..", "..");
|
|
13585
13596
|
const filesToCheck = [dbPath, dbPath + "-wal", dbPath + "-shm"];
|
|
13586
|
-
const
|
|
13597
|
+
const log11 = getLogger("sqlite");
|
|
13587
13598
|
for (const fileToCheck of filesToCheck) {
|
|
13588
13599
|
try {
|
|
13589
13600
|
execFileSync12("git", ["ls-files", "--error-unmatch", fileToCheck], {
|
|
@@ -13592,7 +13603,7 @@ async function getDb(cwd) {
|
|
|
13592
13603
|
});
|
|
13593
13604
|
const basename18 = fileToCheck.split(/[\\/]/).pop();
|
|
13594
13605
|
const relPath = fileToCheck.replace(gitCwd + sep, "");
|
|
13595
|
-
|
|
13606
|
+
log11.warn(
|
|
13596
13607
|
{ path: fileToCheck },
|
|
13597
13608
|
`${basename18} is tracked by project git \u2014 this risks data loss on branch switch. Resolution (ADR-013 \xA79): \`git rm --cached ${relPath}\` and rely on \`.cleo/backups/sqlite/\` snapshots + \`cleo backup add\` for recovery.`
|
|
13598
13609
|
);
|
|
@@ -14578,11 +14589,18 @@ var init_cross_db_cleanup = __esm({
|
|
|
14578
14589
|
|
|
14579
14590
|
// packages/core/src/store/db-helpers.ts
|
|
14580
14591
|
import { eq as eq4, inArray as inArray2 } from "drizzle-orm";
|
|
14581
|
-
async function upsertTask(db, row, archiveFields) {
|
|
14592
|
+
async function upsertTask(db, row, archiveFields, allowOrphanParent = false) {
|
|
14582
14593
|
if (row.parentId) {
|
|
14583
14594
|
const parent = await db.select({ id: tasks.id }).from(tasks).where(eq4(tasks.id, row.parentId)).limit(1).all();
|
|
14584
14595
|
if (parent.length === 0) {
|
|
14585
|
-
|
|
14596
|
+
if (allowOrphanParent) {
|
|
14597
|
+
row = { ...row, parentId: null };
|
|
14598
|
+
} else {
|
|
14599
|
+
log2.warn(
|
|
14600
|
+
{ taskId: row.id, parentId: row.parentId },
|
|
14601
|
+
"upsertTask: parentId references a non-existent task \u2014 parent relationship may be lost"
|
|
14602
|
+
);
|
|
14603
|
+
}
|
|
14586
14604
|
}
|
|
14587
14605
|
}
|
|
14588
14606
|
const values = archiveFields ? { ...row, ...archiveFields, status: "archived" } : row;
|
|
@@ -14726,10 +14744,13 @@ async function loadRelationsForTasks(db, tasks2) {
|
|
|
14726
14744
|
}
|
|
14727
14745
|
}
|
|
14728
14746
|
}
|
|
14747
|
+
var log2;
|
|
14729
14748
|
var init_db_helpers = __esm({
|
|
14730
14749
|
"packages/core/src/store/db-helpers.ts"() {
|
|
14731
14750
|
"use strict";
|
|
14751
|
+
init_logger();
|
|
14732
14752
|
init_tasks_schema();
|
|
14753
|
+
log2 = getLogger("db-helpers");
|
|
14733
14754
|
}
|
|
14734
14755
|
});
|
|
14735
14756
|
|
|
@@ -15063,7 +15084,7 @@ async function createSqliteDataAccessor(cwd) {
|
|
|
15063
15084
|
archiveReason: taskAny.archiveReason ?? "completed",
|
|
15064
15085
|
cycleTimeDays: taskAny.cycleTimeDays ?? null
|
|
15065
15086
|
};
|
|
15066
|
-
await upsertTask(db, row, archiveFields);
|
|
15087
|
+
await upsertTask(db, row, archiveFields, true);
|
|
15067
15088
|
depBatch.push({ taskId: task.id, deps: task.depends ?? [] });
|
|
15068
15089
|
}
|
|
15069
15090
|
await batchUpdateDependencies(db, depBatch, validDepIds);
|
|
@@ -16050,7 +16071,7 @@ async function ensureSequenceValid(cwd, options) {
|
|
|
16050
16071
|
if (!options?.validateSequence) return;
|
|
16051
16072
|
const check2 = await checkSequence(cwd);
|
|
16052
16073
|
if (!check2.valid) {
|
|
16053
|
-
|
|
16074
|
+
log3.warn({ counter: check2.counter, maxId: check2.maxIdInData }, "Sequence behind, repairing");
|
|
16054
16075
|
const repair = await repairSequence(cwd);
|
|
16055
16076
|
if (!repair.repaired && options.strict) {
|
|
16056
16077
|
throw new DataSafetyError(`Sequence repair failed: ${repair.message}`, "SEQUENCE_INVALID", {
|
|
@@ -16067,7 +16088,7 @@ async function checkpoint(context, cwd, options) {
|
|
|
16067
16088
|
stats.checkpoints++;
|
|
16068
16089
|
stats.lastCheckpoint = /* @__PURE__ */ new Date();
|
|
16069
16090
|
} catch (err) {
|
|
16070
|
-
|
|
16091
|
+
log3.warn({ err }, "Checkpoint failed (non-fatal)");
|
|
16071
16092
|
}
|
|
16072
16093
|
vacuumIntoBackup({ cwd }).catch(() => {
|
|
16073
16094
|
});
|
|
@@ -16129,7 +16150,7 @@ async function safeAppendLog(accessor, entry, cwd, options) {
|
|
|
16129
16150
|
stats.writes++;
|
|
16130
16151
|
await checkpoint("log entry", cwd, opts);
|
|
16131
16152
|
}
|
|
16132
|
-
var
|
|
16153
|
+
var log3, DataSafetyError, DEFAULT_SAFETY, stats;
|
|
16133
16154
|
var init_data_safety_central = __esm({
|
|
16134
16155
|
"packages/core/src/store/data-safety-central.ts"() {
|
|
16135
16156
|
"use strict";
|
|
@@ -16137,7 +16158,7 @@ var init_data_safety_central = __esm({
|
|
|
16137
16158
|
init_sequence();
|
|
16138
16159
|
init_git_checkpoint();
|
|
16139
16160
|
init_sqlite_backup();
|
|
16140
|
-
|
|
16161
|
+
log3 = getLogger("data-safety");
|
|
16141
16162
|
DataSafetyError = class extends Error {
|
|
16142
16163
|
constructor(message, code, context) {
|
|
16143
16164
|
super(message);
|
|
@@ -16177,7 +16198,7 @@ function isSafetyDisabled() {
|
|
|
16177
16198
|
}
|
|
16178
16199
|
function wrapWithSafety(accessor, cwd) {
|
|
16179
16200
|
if (isSafetyDisabled()) {
|
|
16180
|
-
|
|
16201
|
+
log4.warn(
|
|
16181
16202
|
"Safety disabled - emergency mode (CLEO_DISABLE_SAFETY=true). Data integrity checks bypassed."
|
|
16182
16203
|
);
|
|
16183
16204
|
return accessor;
|
|
@@ -16198,13 +16219,13 @@ function getSafetyStatus() {
|
|
|
16198
16219
|
enabled: true
|
|
16199
16220
|
};
|
|
16200
16221
|
}
|
|
16201
|
-
var
|
|
16222
|
+
var log4, SafetyDataAccessor;
|
|
16202
16223
|
var init_safety_data_accessor = __esm({
|
|
16203
16224
|
"packages/core/src/store/safety-data-accessor.ts"() {
|
|
16204
16225
|
"use strict";
|
|
16205
16226
|
init_logger();
|
|
16206
16227
|
init_data_safety_central();
|
|
16207
|
-
|
|
16228
|
+
log4 = getLogger("data-safety");
|
|
16208
16229
|
SafetyDataAccessor = class {
|
|
16209
16230
|
/** The underlying accessor being wrapped. */
|
|
16210
16231
|
inner;
|
|
@@ -16228,7 +16249,7 @@ var init_safety_data_accessor = __esm({
|
|
|
16228
16249
|
...config2
|
|
16229
16250
|
};
|
|
16230
16251
|
if (this.config.verbose) {
|
|
16231
|
-
|
|
16252
|
+
log4.debug({ engine: inner.engine }, "SafetyDataAccessor initialized");
|
|
16232
16253
|
}
|
|
16233
16254
|
}
|
|
16234
16255
|
/** The storage engine backing this accessor. */
|
|
@@ -16240,7 +16261,7 @@ var init_safety_data_accessor = __esm({
|
|
|
16240
16261
|
*/
|
|
16241
16262
|
logVerbose(message) {
|
|
16242
16263
|
if (this.config.verbose) {
|
|
16243
|
-
|
|
16264
|
+
log4.debug(message);
|
|
16244
16265
|
}
|
|
16245
16266
|
}
|
|
16246
16267
|
/**
|
|
@@ -20600,16 +20621,16 @@ async function queryAudit(options) {
|
|
|
20600
20621
|
error: row.errorMessage ?? void 0
|
|
20601
20622
|
}));
|
|
20602
20623
|
} catch (err) {
|
|
20603
|
-
|
|
20624
|
+
log5.warn({ err }, "Failed to query audit entries from SQLite");
|
|
20604
20625
|
return [];
|
|
20605
20626
|
}
|
|
20606
20627
|
}
|
|
20607
|
-
var
|
|
20628
|
+
var log5;
|
|
20608
20629
|
var init_audit = __esm({
|
|
20609
20630
|
"packages/core/src/audit.ts"() {
|
|
20610
20631
|
"use strict";
|
|
20611
20632
|
init_logger();
|
|
20612
|
-
|
|
20633
|
+
log5 = getLogger("audit");
|
|
20613
20634
|
}
|
|
20614
20635
|
});
|
|
20615
20636
|
|
|
@@ -52539,9 +52560,7 @@ async function completeTask(options, cwd, accessor) {
|
|
|
52539
52560
|
const parent = await acc.loadSingleTask(task.parentId);
|
|
52540
52561
|
if (parent && parent.type === "epic" && !parent.noAutoComplete) {
|
|
52541
52562
|
const siblings = await acc.getChildren(parent.id);
|
|
52542
|
-
const allDone = siblings.every(
|
|
52543
|
-
(c) => c.id === task.id || c.status === "done" || c.status === "cancelled"
|
|
52544
|
-
);
|
|
52563
|
+
const allDone = siblings.length > 0 && siblings.every((c) => c.id === task.id || c.status === "done" || c.status === "cancelled");
|
|
52545
52564
|
if (allDone) {
|
|
52546
52565
|
parent.status = "done";
|
|
52547
52566
|
parent.completedAt = now;
|
|
@@ -59560,7 +59579,7 @@ init_logger();
|
|
|
59560
59579
|
init_paths();
|
|
59561
59580
|
init_tasks_schema();
|
|
59562
59581
|
import { basename as basename6, relative as relative2 } from "node:path";
|
|
59563
|
-
var
|
|
59582
|
+
var log6 = getLogger("lifecycle:evidence");
|
|
59564
59583
|
async function recordEvidence(epicId, stage, uri, type, options) {
|
|
59565
59584
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
59566
59585
|
const stageId = `stage-${epicId}-${stage}`;
|
|
@@ -59587,7 +59606,7 @@ async function recordEvidence(epicId, stage, uri, type, options) {
|
|
|
59587
59606
|
description: options?.description ?? null
|
|
59588
59607
|
}).run();
|
|
59589
59608
|
} catch (err) {
|
|
59590
|
-
|
|
59609
|
+
log6.warn({ err }, "Failed to write evidence to SQLite");
|
|
59591
59610
|
}
|
|
59592
59611
|
return record2;
|
|
59593
59612
|
}
|
|
@@ -65886,7 +65905,7 @@ init_sqlite2();
|
|
|
65886
65905
|
init_tasks_schema();
|
|
65887
65906
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
65888
65907
|
import { and as and8, eq as eq11, sql as sql11 } from "drizzle-orm";
|
|
65889
|
-
var
|
|
65908
|
+
var log7 = getLogger("link-store");
|
|
65890
65909
|
async function getLinksByProvider(providerId, cwd) {
|
|
65891
65910
|
const db = await getDb(cwd);
|
|
65892
65911
|
const rows = await db.select().from(externalTaskLinks).where(eq11(externalTaskLinks.providerId, providerId));
|
|
@@ -65941,7 +65960,7 @@ async function ensureExternalTaskLinksTable(cwd) {
|
|
|
65941
65960
|
)
|
|
65942
65961
|
);
|
|
65943
65962
|
} catch (err) {
|
|
65944
|
-
|
|
65963
|
+
log7.warn({ err }, "Failed to ensure external_task_links table exists");
|
|
65945
65964
|
throw err;
|
|
65946
65965
|
}
|
|
65947
65966
|
}
|
|
@@ -66015,7 +66034,7 @@ init_brain_accessor();
|
|
|
66015
66034
|
init_brain_sqlite();
|
|
66016
66035
|
init_data_accessor();
|
|
66017
66036
|
init_registry3();
|
|
66018
|
-
var
|
|
66037
|
+
var log8 = getLogger("nexus:transfer");
|
|
66019
66038
|
async function previewTransfer(params) {
|
|
66020
66039
|
return executeTransferInternal({ ...params, dryRun: true });
|
|
66021
66040
|
}
|
|
@@ -66156,7 +66175,7 @@ async function executeTransferInternal(params) {
|
|
|
66156
66175
|
}
|
|
66157
66176
|
}
|
|
66158
66177
|
} catch (err) {
|
|
66159
|
-
|
|
66178
|
+
log8.warn(
|
|
66160
66179
|
{ err, linksCreated },
|
|
66161
66180
|
"Failed to create external_task_links during transfer \u2014 tasks were transferred successfully but provenance links could not be written"
|
|
66162
66181
|
);
|
|
@@ -66184,7 +66203,7 @@ async function executeTransferInternal(params) {
|
|
|
66184
66203
|
})
|
|
66185
66204
|
});
|
|
66186
66205
|
} catch (err) {
|
|
66187
|
-
|
|
66206
|
+
log8.warn({ err }, "nexus transfer audit write failed");
|
|
66188
66207
|
}
|
|
66189
66208
|
if (mode === "move") {
|
|
66190
66209
|
let archived = 0;
|
|
@@ -66197,7 +66216,7 @@ async function executeTransferInternal(params) {
|
|
|
66197
66216
|
});
|
|
66198
66217
|
archived++;
|
|
66199
66218
|
} catch (err) {
|
|
66200
|
-
|
|
66219
|
+
log8.warn({ err, taskId: entry.sourceId }, "failed to archive source task after transfer");
|
|
66201
66220
|
}
|
|
66202
66221
|
}
|
|
66203
66222
|
}
|
|
@@ -66235,7 +66254,7 @@ async function executeTransferInternal(params) {
|
|
|
66235
66254
|
}
|
|
66236
66255
|
}
|
|
66237
66256
|
} catch (err) {
|
|
66238
|
-
|
|
66257
|
+
log8.warn({ err }, "brain observation transfer failed");
|
|
66239
66258
|
}
|
|
66240
66259
|
result.brainObservationsTransferred = brainTransferred;
|
|
66241
66260
|
result.manifest.brainObservationsTransferred = brainTransferred;
|
|
@@ -66283,8 +66302,8 @@ async function loadProjectACL(projectPath) {
|
|
|
66283
66302
|
async function logAclFailure(projectPath) {
|
|
66284
66303
|
try {
|
|
66285
66304
|
const { getLogger: getLogger2 } = await Promise.resolve().then(() => (init_logger(), logger_exports));
|
|
66286
|
-
const
|
|
66287
|
-
|
|
66305
|
+
const log11 = getLogger2("nexus.acl");
|
|
66306
|
+
log11.warn({ projectPath }, "Failed to load ACL configuration, defaulting to deny-all");
|
|
66288
66307
|
} catch {
|
|
66289
66308
|
}
|
|
66290
66309
|
}
|
|
@@ -66439,9 +66458,9 @@ async function executeOperation(operation, taskId, projectPath, accessor, direct
|
|
|
66439
66458
|
async function logRouteAudit(directive, projectName, taskId, operation, success2, error48) {
|
|
66440
66459
|
try {
|
|
66441
66460
|
const { getLogger: getLogger2 } = await Promise.resolve().then(() => (init_logger(), logger_exports));
|
|
66442
|
-
const
|
|
66461
|
+
const log11 = getLogger2("nexus.route");
|
|
66443
66462
|
const level = success2 ? "info" : "warn";
|
|
66444
|
-
|
|
66463
|
+
log11[level](
|
|
66445
66464
|
{
|
|
66446
66465
|
directive: directive.verb,
|
|
66447
66466
|
agentId: directive.agentId,
|
|
@@ -75017,7 +75036,7 @@ init_data_accessor();
|
|
|
75017
75036
|
|
|
75018
75037
|
// packages/core/src/stats/workflow-telemetry.ts
|
|
75019
75038
|
init_logger();
|
|
75020
|
-
var
|
|
75039
|
+
var log9 = getLogger("workflow-telemetry");
|
|
75021
75040
|
async function queryTasks(cwd, since) {
|
|
75022
75041
|
try {
|
|
75023
75042
|
const { getDb: getDb3 } = await Promise.resolve().then(() => (init_sqlite2(), sqlite_exports));
|
|
@@ -75039,7 +75058,7 @@ async function queryTasks(cwd, since) {
|
|
|
75039
75058
|
}).from(tasks2).where(conditions.length > 0 ? and10(...conditions) : void 0).all();
|
|
75040
75059
|
return rows;
|
|
75041
75060
|
} catch (err) {
|
|
75042
|
-
|
|
75061
|
+
log9.warn({ err }, "Failed to query tasks for workflow telemetry");
|
|
75043
75062
|
return [];
|
|
75044
75063
|
}
|
|
75045
75064
|
}
|
|
@@ -75073,7 +75092,7 @@ async function queryCompletionAuditRows(cwd, since) {
|
|
|
75073
75092
|
return isComplete;
|
|
75074
75093
|
});
|
|
75075
75094
|
} catch (err) {
|
|
75076
|
-
|
|
75095
|
+
log9.warn({ err }, "Failed to query audit log for workflow telemetry");
|
|
75077
75096
|
return [];
|
|
75078
75097
|
}
|
|
75079
75098
|
}
|
|
@@ -76529,11 +76548,11 @@ import { join as join87 } from "node:path";
|
|
|
76529
76548
|
import { Readable } from "node:stream";
|
|
76530
76549
|
import { pipeline } from "node:stream/promises";
|
|
76531
76550
|
import { createGzip } from "node:zlib";
|
|
76532
|
-
var
|
|
76551
|
+
var log10 = getLogger("prune");
|
|
76533
76552
|
async function pruneAuditLog(cleoDir, config2) {
|
|
76534
76553
|
try {
|
|
76535
76554
|
if (!config2.auditRetentionDays || config2.auditRetentionDays <= 0) {
|
|
76536
|
-
|
|
76555
|
+
log10.debug("auditRetentionDays is 0 or unset; skipping audit prune");
|
|
76537
76556
|
return { rowsArchived: 0, rowsDeleted: 0 };
|
|
76538
76557
|
}
|
|
76539
76558
|
const cutoff = new Date(Date.now() - config2.auditRetentionDays * 864e5).toISOString();
|
|
@@ -76544,7 +76563,7 @@ async function pruneAuditLog(cleoDir, config2) {
|
|
|
76544
76563
|
const db = await getDb3(projectRoot);
|
|
76545
76564
|
const oldRows = await db.select().from(auditLog2).where(lt3(auditLog2.timestamp, cutoff));
|
|
76546
76565
|
if (oldRows.length === 0) {
|
|
76547
|
-
|
|
76566
|
+
log10.debug("No audit_log rows older than cutoff; nothing to prune");
|
|
76548
76567
|
return { rowsArchived: 0, rowsDeleted: 0 };
|
|
76549
76568
|
}
|
|
76550
76569
|
let archivePath;
|
|
@@ -76562,17 +76581,17 @@ async function pruneAuditLog(cleoDir, config2) {
|
|
|
76562
76581
|
const inStream = Readable.from([jsonlContent]);
|
|
76563
76582
|
await pipeline(inStream, gzip, outStream);
|
|
76564
76583
|
rowsArchived = oldRows.length;
|
|
76565
|
-
|
|
76584
|
+
log10.info(
|
|
76566
76585
|
{ archivePath, rowsArchived },
|
|
76567
76586
|
`Archived ${rowsArchived} audit rows to ${archivePath}`
|
|
76568
76587
|
);
|
|
76569
76588
|
} catch (archiveErr) {
|
|
76570
|
-
|
|
76589
|
+
log10.warn({ err: archiveErr }, "Failed to archive audit rows; continuing with deletion");
|
|
76571
76590
|
archivePath = void 0;
|
|
76572
76591
|
}
|
|
76573
76592
|
}
|
|
76574
76593
|
await db.delete(auditLog2).where(lt3(auditLog2.timestamp, cutoff)).run();
|
|
76575
|
-
|
|
76594
|
+
log10.info(
|
|
76576
76595
|
{ rowsDeleted: oldRows.length, cutoff },
|
|
76577
76596
|
`Pruned ${oldRows.length} audit_log rows older than ${cutoff}`
|
|
76578
76597
|
);
|
|
@@ -76582,7 +76601,7 @@ async function pruneAuditLog(cleoDir, config2) {
|
|
|
76582
76601
|
archivePath
|
|
76583
76602
|
};
|
|
76584
76603
|
} catch (err) {
|
|
76585
|
-
|
|
76604
|
+
log10.warn({ err }, "audit log pruning failed");
|
|
76586
76605
|
return { rowsArchived: 0, rowsDeleted: 0 };
|
|
76587
76606
|
}
|
|
76588
76607
|
}
|