@cleocode/core 2026.4.11 → 2026.4.13

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.
Files changed (184) hide show
  1. package/dist/codebase-map/analyzers/architecture.d.ts.map +1 -1
  2. package/dist/codebase-map/analyzers/architecture.js +0 -1
  3. package/dist/codebase-map/analyzers/architecture.js.map +1 -1
  4. package/dist/conduit/local-transport.d.ts +18 -8
  5. package/dist/conduit/local-transport.d.ts.map +1 -1
  6. package/dist/conduit/local-transport.js +23 -13
  7. package/dist/conduit/local-transport.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +0 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/errors.d.ts +19 -0
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/errors.js +6 -0
  14. package/dist/errors.js.map +1 -1
  15. package/dist/index.js +175 -68950
  16. package/dist/index.js.map +1 -7
  17. package/dist/init.d.ts +1 -2
  18. package/dist/init.d.ts.map +1 -1
  19. package/dist/init.js +1 -2
  20. package/dist/init.js.map +1 -1
  21. package/dist/internal.d.ts +8 -3
  22. package/dist/internal.d.ts.map +1 -1
  23. package/dist/internal.js +13 -6
  24. package/dist/internal.js.map +1 -1
  25. package/dist/memory/learnings.d.ts +2 -2
  26. package/dist/memory/patterns.d.ts +6 -6
  27. package/dist/output.d.ts +32 -11
  28. package/dist/output.d.ts.map +1 -1
  29. package/dist/output.js +67 -67
  30. package/dist/output.js.map +1 -1
  31. package/dist/paths.js +80 -14
  32. package/dist/paths.js.map +1 -1
  33. package/dist/skills/dynamic-skill-generator.d.ts +0 -2
  34. package/dist/skills/dynamic-skill-generator.d.ts.map +1 -1
  35. package/dist/skills/dynamic-skill-generator.js.map +1 -1
  36. package/dist/store/agent-registry-accessor.d.ts +203 -12
  37. package/dist/store/agent-registry-accessor.d.ts.map +1 -1
  38. package/dist/store/agent-registry-accessor.js +618 -100
  39. package/dist/store/agent-registry-accessor.js.map +1 -1
  40. package/dist/store/api-key-kdf.d.ts +73 -0
  41. package/dist/store/api-key-kdf.d.ts.map +1 -0
  42. package/dist/store/api-key-kdf.js +84 -0
  43. package/dist/store/api-key-kdf.js.map +1 -0
  44. package/dist/store/cleanup-legacy.js +171 -0
  45. package/dist/store/cleanup-legacy.js.map +1 -0
  46. package/dist/store/conduit-sqlite.d.ts +184 -0
  47. package/dist/store/conduit-sqlite.d.ts.map +1 -0
  48. package/dist/store/conduit-sqlite.js +570 -0
  49. package/dist/store/conduit-sqlite.js.map +1 -0
  50. package/dist/store/global-salt.d.ts +78 -0
  51. package/dist/store/global-salt.d.ts.map +1 -0
  52. package/dist/store/global-salt.js +147 -0
  53. package/dist/store/global-salt.js.map +1 -0
  54. package/dist/store/migrate-signaldock-to-conduit.d.ts +81 -0
  55. package/dist/store/migrate-signaldock-to-conduit.d.ts.map +1 -0
  56. package/dist/store/migrate-signaldock-to-conduit.js +555 -0
  57. package/dist/store/migrate-signaldock-to-conduit.js.map +1 -0
  58. package/dist/store/nexus-sqlite.js +28 -3
  59. package/dist/store/nexus-sqlite.js.map +1 -1
  60. package/dist/store/signaldock-sqlite.d.ts +122 -19
  61. package/dist/store/signaldock-sqlite.d.ts.map +1 -1
  62. package/dist/store/signaldock-sqlite.js +401 -251
  63. package/dist/store/signaldock-sqlite.js.map +1 -1
  64. package/dist/store/sqlite-backup.js +122 -4
  65. package/dist/store/sqlite-backup.js.map +1 -1
  66. package/dist/system/backup.d.ts +0 -26
  67. package/dist/system/backup.d.ts.map +1 -1
  68. package/dist/system/runtime.d.ts +0 -2
  69. package/dist/system/runtime.d.ts.map +1 -1
  70. package/dist/system/runtime.js +3 -3
  71. package/dist/system/runtime.js.map +1 -1
  72. package/dist/tasks/add.d.ts +1 -1
  73. package/dist/tasks/add.d.ts.map +1 -1
  74. package/dist/tasks/add.js +98 -23
  75. package/dist/tasks/add.js.map +1 -1
  76. package/dist/tasks/complete.d.ts.map +1 -1
  77. package/dist/tasks/complete.js +4 -1
  78. package/dist/tasks/complete.js.map +1 -1
  79. package/dist/tasks/find.d.ts.map +1 -1
  80. package/dist/tasks/find.js +4 -1
  81. package/dist/tasks/find.js.map +1 -1
  82. package/dist/tasks/labels.d.ts.map +1 -1
  83. package/dist/tasks/labels.js +4 -1
  84. package/dist/tasks/labels.js.map +1 -1
  85. package/dist/tasks/relates.d.ts.map +1 -1
  86. package/dist/tasks/relates.js +16 -4
  87. package/dist/tasks/relates.js.map +1 -1
  88. package/dist/tasks/show.d.ts.map +1 -1
  89. package/dist/tasks/show.js +4 -1
  90. package/dist/tasks/show.js.map +1 -1
  91. package/dist/tasks/update.d.ts.map +1 -1
  92. package/dist/tasks/update.js +32 -6
  93. package/dist/tasks/update.js.map +1 -1
  94. package/dist/validation/engine.d.ts.map +1 -1
  95. package/dist/validation/engine.js +16 -4
  96. package/dist/validation/engine.js.map +1 -1
  97. package/dist/validation/param-utils.d.ts +5 -3
  98. package/dist/validation/param-utils.d.ts.map +1 -1
  99. package/dist/validation/param-utils.js +8 -6
  100. package/dist/validation/param-utils.js.map +1 -1
  101. package/dist/validation/protocols/_shared.d.ts.map +1 -1
  102. package/dist/validation/protocols/_shared.js +13 -6
  103. package/dist/validation/protocols/_shared.js.map +1 -1
  104. package/package.json +9 -7
  105. package/src/adapters/__tests__/manager.test.ts +0 -1
  106. package/src/codebase-map/analyzers/architecture.ts +0 -1
  107. package/src/conduit/__tests__/local-credential-flow.test.ts +20 -18
  108. package/src/conduit/__tests__/local-transport.test.ts +14 -12
  109. package/src/conduit/local-transport.ts +23 -13
  110. package/src/config.ts +0 -1
  111. package/src/errors.ts +24 -0
  112. package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +2 -5
  113. package/src/init.ts +1 -2
  114. package/src/internal.ts +96 -2
  115. package/src/lifecycle/cant/lifecycle-rcasd.cant +133 -0
  116. package/src/memory/__tests__/engine-compat.test.ts +2 -2
  117. package/src/memory/__tests__/pipeline-manifest-sqlite.test.ts +4 -4
  118. package/src/observability/__tests__/index.test.ts +4 -4
  119. package/src/observability/__tests__/log-filter.test.ts +4 -4
  120. package/src/output.ts +73 -75
  121. package/src/sessions/__tests__/session-grade.integration.test.ts +1 -1
  122. package/src/sessions/__tests__/session-grade.test.ts +2 -2
  123. package/src/skills/__tests__/dynamic-skill-generator.test.ts +0 -2
  124. package/src/skills/dynamic-skill-generator.ts +0 -2
  125. package/src/store/__tests__/agent-registry-accessor.test.ts +807 -0
  126. package/src/store/__tests__/api-key-kdf.test.ts +113 -0
  127. package/src/store/__tests__/backup-crypto.test.ts +101 -0
  128. package/src/store/__tests__/backup-pack.test.ts +491 -0
  129. package/src/store/__tests__/backup-unpack.test.ts +298 -0
  130. package/src/store/__tests__/conduit-sqlite.test.ts +413 -0
  131. package/src/store/__tests__/global-salt.test.ts +195 -0
  132. package/src/store/__tests__/migrate-signaldock-to-conduit.test.ts +715 -0
  133. package/src/store/__tests__/regenerators.test.ts +234 -0
  134. package/src/store/__tests__/restore-conflict-report.test.ts +274 -0
  135. package/src/store/__tests__/restore-json-merge.test.ts +521 -0
  136. package/src/store/__tests__/signaldock-sqlite.test.ts +652 -0
  137. package/src/store/__tests__/sqlite-backup-global.test.ts +307 -3
  138. package/src/store/__tests__/sqlite-backup.test.ts +5 -1
  139. package/src/store/__tests__/t310-integration.test.ts +1150 -0
  140. package/src/store/__tests__/t310-readiness.test.ts +111 -0
  141. package/src/store/__tests__/t311-integration.test.ts +661 -0
  142. package/src/store/agent-registry-accessor.ts +847 -140
  143. package/src/store/api-key-kdf.ts +104 -0
  144. package/src/store/backup-crypto.ts +209 -0
  145. package/src/store/backup-pack.ts +739 -0
  146. package/src/store/backup-unpack.ts +583 -0
  147. package/src/store/conduit-sqlite.ts +655 -0
  148. package/src/store/global-salt.ts +175 -0
  149. package/src/store/migrate-signaldock-to-conduit.ts +669 -0
  150. package/src/store/regenerators.ts +243 -0
  151. package/src/store/restore-conflict-report.ts +317 -0
  152. package/src/store/restore-json-merge.ts +653 -0
  153. package/src/store/signaldock-sqlite.ts +431 -254
  154. package/src/store/sqlite-backup.ts +185 -10
  155. package/src/store/t310-readiness.ts +119 -0
  156. package/src/system/backup.ts +2 -62
  157. package/src/system/runtime.ts +4 -6
  158. package/src/tasks/__tests__/error-hints.test.ts +256 -0
  159. package/src/tasks/add.ts +99 -9
  160. package/src/tasks/complete.ts +4 -1
  161. package/src/tasks/find.ts +4 -1
  162. package/src/tasks/labels.ts +4 -1
  163. package/src/tasks/relates.ts +16 -4
  164. package/src/tasks/show.ts +4 -1
  165. package/src/tasks/update.ts +32 -3
  166. package/src/validation/__tests__/error-hints.test.ts +97 -0
  167. package/src/validation/engine.ts +16 -1
  168. package/src/validation/param-utils.ts +10 -7
  169. package/src/validation/protocols/_shared.ts +14 -6
  170. package/src/validation/protocols/cant/architecture-decision.cant +80 -0
  171. package/src/validation/protocols/cant/artifact-publish.cant +95 -0
  172. package/src/validation/protocols/cant/consensus.cant +74 -0
  173. package/src/validation/protocols/cant/contribution.cant +82 -0
  174. package/src/validation/protocols/cant/decomposition.cant +92 -0
  175. package/src/validation/protocols/cant/implementation.cant +67 -0
  176. package/src/validation/protocols/cant/provenance.cant +88 -0
  177. package/src/validation/protocols/cant/release.cant +96 -0
  178. package/src/validation/protocols/cant/research.cant +66 -0
  179. package/src/validation/protocols/cant/specification.cant +67 -0
  180. package/src/validation/protocols/cant/testing.cant +88 -0
  181. package/src/validation/protocols/cant/validation.cant +65 -0
  182. package/src/validation/protocols/protocols-markdown/decomposition.md +0 -4
  183. package/templates/config.template.json +0 -1
  184. package/templates/global-config.template.json +0 -1
@@ -1,310 +1,426 @@
1
1
  /**
2
- * SQLite store for signaldock.db — local agent messaging database.
2
+ * SQLite store for global-tier signaldock.db — canonical agent identity database.
3
3
  *
4
- * Creates and manages .cleo/signaldock.db using node:sqlite directly.
5
- * Runs the consolidated Diesel migration SQL (from signaldock-storage crate)
6
- * to bootstrap all 22 tables for local agent infrastructure.
4
+ * Post-T310 (ADR-037), signaldock.db lives at `$XDG_DATA_HOME/cleo/signaldock.db`
5
+ * (resolved via getCleoHome()). It holds cross-project agent identity, capabilities
6
+ * catalog, and cloud-sync tables. Project-local messaging state has moved to
7
+ * conduit.db (managed by conduit-sqlite.ts, T344).
7
8
  *
8
- * This is the Node.js bootstrap path. In production cloud, the Rust
9
- * signaldock-storage crate manages this DB via Diesel ORM directly.
10
- * Locally, we create the DB here so that cleo init scaffolds the full
11
- * .cleo/ directory with all databases ready.
9
+ * GLOBAL-TIER ONLY. This module MUST NOT resolve paths under any project's .cleo/
10
+ * directory. The path guard in getGlobalSignaldockDbPath() enforces this invariant.
12
11
  *
13
- * @task T223
12
+ * @task T346
13
+ * @epic T310
14
+ * @related ADR-037
14
15
  */
15
16
 
16
17
  import { existsSync, mkdirSync } from 'node:fs';
17
18
  import { createRequire } from 'node:module';
18
- import { dirname, join } from 'node:path';
19
+ import { join } from 'node:path';
19
20
  import type { DatabaseSync } from 'node:sqlite';
20
- import { getCleoDirAbsolute } from '../paths.js';
21
+ import { getCleoHome } from '../paths.js';
21
22
 
22
23
  const _require = createRequire(import.meta.url);
23
24
  const { DatabaseSync: DatabaseSyncClass } = _require('node:sqlite') as {
24
25
  DatabaseSync: new (...args: ConstructorParameters<typeof DatabaseSync>) => DatabaseSync;
25
26
  };
26
27
 
27
- /** Database file name within .cleo/ directory. */
28
- const DB_FILENAME = 'signaldock.db';
28
+ /**
29
+ * Database file name within the global cleo home directory.
30
+ *
31
+ * @task T346
32
+ * @epic T310
33
+ */
34
+ export const GLOBAL_SIGNALDOCK_DB_FILENAME = 'signaldock.db';
35
+
36
+ /**
37
+ * Schema version for global signaldock databases.
38
+ *
39
+ * @task T346
40
+ * @epic T310
41
+ */
42
+ export const GLOBAL_SIGNALDOCK_SCHEMA_VERSION = '2026.4.12';
43
+
44
+ /**
45
+ * @deprecated Use GLOBAL_SIGNALDOCK_SCHEMA_VERSION. Retained during T310
46
+ * migration window. Will be removed after all callers migrate (T355).
47
+ */
48
+ export const SIGNALDOCK_SCHEMA_VERSION = GLOBAL_SIGNALDOCK_SCHEMA_VERSION;
29
49
 
30
- /** Schema version for signaldock databases. */
31
- export const SIGNALDOCK_SCHEMA_VERSION = '2026.3.76';
50
+ // ---------------------------------------------------------------------------
51
+ // Path helpers
52
+ // ---------------------------------------------------------------------------
32
53
 
33
54
  /**
34
- * Get the path to the signaldock.db SQLite database file.
55
+ * Returns the GLOBAL-tier signaldock.db path. Post-T310, signaldock.db
56
+ * holds canonical agent identity + cloud-sync tables. Project-local
57
+ * messaging state lives in conduit.db (T344).
58
+ *
59
+ * Resolves to `getCleoHome() + '/signaldock.db'`.
60
+ * Guard: asserts the resolved path starts with getCleoHome() (defense in depth,
61
+ * mirrors the ADR-036 pattern used by getNexusDbPath in nexus-sqlite.ts).
62
+ *
63
+ * @task T346
64
+ * @epic T310
65
+ * @why ADR-037 split single signaldock.db into project conduit + global signaldock
66
+ * @throws {Error} If resolved path is not under getCleoHome() — indicates a code
67
+ * path that bypasses canonical path resolution. Fix the caller, do not suppress.
68
+ */
69
+ export function getGlobalSignaldockDbPath(): string {
70
+ const cleoHome = getCleoHome();
71
+ const dbPath = join(cleoHome, GLOBAL_SIGNALDOCK_DB_FILENAME);
72
+ if (!dbPath.startsWith(cleoHome)) {
73
+ throw new Error(
74
+ `BUG: getGlobalSignaldockDbPath() resolved to "${dbPath}" which is NOT under ` +
75
+ `getCleoHome() ("${cleoHome}"). signaldock.db is global-only per ADR-037. ` +
76
+ `This indicates a code path that bypasses path resolution — ` +
77
+ `fix the caller, do not suppress this error.`,
78
+ );
79
+ }
80
+ return dbPath;
81
+ }
82
+
83
+ /**
84
+ * @deprecated Use getGlobalSignaldockDbPath() directly. Retained during T310
85
+ * migration window so the TypeScript build does not break until all callers
86
+ * are updated (tracked in T355 accessor refactor).
87
+ *
88
+ * When called WITHOUT arguments: returns the global-tier path (forwards to
89
+ * getGlobalSignaldockDbPath()).
90
+ *
91
+ * When called WITH a non-undefined `cwd` argument: throws a migration error
92
+ * immediately. The project-tier path is now owned by conduit-sqlite.ts (T344).
93
+ *
94
+ * @param cwd - Must be undefined. Any other value throws a migration error.
95
+ * @task T346
96
+ * @epic T310
35
97
  */
36
98
  export function getSignaldockDbPath(cwd?: string): string {
37
- const cleoDir = cwd ? join(cwd, '.cleo') : getCleoDirAbsolute();
38
- return join(cleoDir, DB_FILENAME);
99
+ if (cwd !== undefined) {
100
+ throw new Error(
101
+ 'getSignaldockDbPath(cwd) is removed as of T310 (v2026.4.12). ' +
102
+ 'signaldock.db is now global-only at $XDG_DATA_HOME/cleo/signaldock.db. ' +
103
+ 'Use getGlobalSignaldockDbPath(), or for project-local messaging use ' +
104
+ 'getConduitDbPath() from conduit-sqlite.ts (T344).',
105
+ );
106
+ }
107
+ return getGlobalSignaldockDbPath();
39
108
  }
40
109
 
41
110
  // ---------------------------------------------------------------------------
42
- // Embedded migration SQL — bundled so signaldock.db works in ANY project,
43
- // not just the monorepo where crates/signaldock-storage/ exists.
44
- // Source: crates/signaldock-storage/migrations/
111
+ // Embedded migration SQL — consolidated global-tier schema.
112
+ // Source: spec §2.2, ADR-037.
113
+ // All incremental ALTER TABLE migrations from the pre-T310 schema are
114
+ // collapsed into the single initial migration below. This avoids the
115
+ // multi-step ALTER pattern and ensures fresh installs get the full schema
116
+ // in one idempotent pass.
45
117
  // ---------------------------------------------------------------------------
46
118
 
47
119
  /**
48
- * Ordered migration entries. Each has a name (for tracking) and SQL.
120
+ * Ordered migration entries for the global signaldock.db.
49
121
  * Add new migrations to the END of this array.
122
+ *
123
+ * @task T346
124
+ * @epic T310
50
125
  */
51
- const EMBEDDED_MIGRATIONS: Array<{ name: string; sql: string }> = [
126
+ const GLOBAL_EMBEDDED_MIGRATIONS: Array<{ name: string; sql: string }> = [
52
127
  {
53
- name: '2026-03-28-000000_initial',
54
- sql: `-- Consolidated initial migration for SignalDock storage (19 sqlx migrations merged).
128
+ name: '2026-04-12-000000_initial_global_signaldock',
129
+ sql: `-- Global-tier signaldock.db initial migration (T310, ADR-037 §2.2).
130
+ -- Consolidated from pre-T310 incremental migrations. Global identity and
131
+ -- cloud-sync tables only. Project-local messaging state is in conduit.db.
132
+
133
+ -- Cloud-sync: user accounts (zero rows in pure-local mode).
55
134
  CREATE TABLE IF NOT EXISTS users (
56
- id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL,
57
- name TEXT, slug TEXT, default_agent_id TEXT, username TEXT, display_username TEXT,
58
- email_verified INTEGER NOT NULL DEFAULT 0, image TEXT, role TEXT NOT NULL DEFAULT 'user',
59
- banned INTEGER NOT NULL DEFAULT 0, ban_reason TEXT, ban_expires TEXT,
60
- two_factor_enabled INTEGER NOT NULL DEFAULT 0, metadata TEXT,
61
- created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
135
+ id TEXT PRIMARY KEY,
136
+ email TEXT NOT NULL UNIQUE,
137
+ password_hash TEXT NOT NULL,
138
+ name TEXT,
139
+ slug TEXT,
140
+ default_agent_id TEXT,
141
+ username TEXT,
142
+ display_username TEXT,
143
+ email_verified INTEGER NOT NULL DEFAULT 0,
144
+ image TEXT,
145
+ role TEXT NOT NULL DEFAULT 'user',
146
+ banned INTEGER NOT NULL DEFAULT 0,
147
+ ban_reason TEXT,
148
+ ban_expires TEXT,
149
+ two_factor_enabled INTEGER NOT NULL DEFAULT 0,
150
+ metadata TEXT,
151
+ created_at INTEGER NOT NULL,
152
+ updated_at INTEGER NOT NULL
62
153
  );
63
154
  CREATE UNIQUE INDEX IF NOT EXISTS idx_users_slug ON users(slug);
64
155
 
156
+ -- Cloud-sync: organization/team records.
65
157
  CREATE TABLE IF NOT EXISTS organization (
66
- id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL, slug TEXT, logo TEXT, metadata TEXT,
67
- owner_id TEXT, created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
158
+ id TEXT PRIMARY KEY NOT NULL,
159
+ name TEXT NOT NULL,
160
+ slug TEXT,
161
+ logo TEXT,
162
+ metadata TEXT,
163
+ owner_id TEXT,
164
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
68
165
  updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
69
166
  );
70
167
  CREATE UNIQUE INDEX IF NOT EXISTS idx_organization_slug ON organization(slug);
71
168
 
169
+ -- Global identity: canonical agent registry (cross-project).
170
+ -- api_key_encrypted uses KDF: HMAC-SHA256(machine-key || global-salt, agentId) — ADR-037 §5.
171
+ -- requires_reauth=1 is set during T310 migration for all pre-existing agents.
72
172
  CREATE TABLE IF NOT EXISTS agents (
73
- id TEXT PRIMARY KEY, agent_id TEXT NOT NULL UNIQUE, name TEXT NOT NULL,
74
- description TEXT, class TEXT NOT NULL DEFAULT 'custom',
75
- privacy_tier TEXT NOT NULL DEFAULT 'public', owner_id TEXT REFERENCES users(id),
76
- endpoint TEXT, webhook_secret TEXT, capabilities TEXT NOT NULL DEFAULT '[]',
77
- skills TEXT NOT NULL DEFAULT '[]', avatar TEXT, messages_sent INTEGER NOT NULL DEFAULT 0,
78
- messages_received INTEGER NOT NULL DEFAULT 0, conversation_count INTEGER NOT NULL DEFAULT 0,
79
- friend_count INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'online',
80
- last_seen INTEGER, payment_config TEXT, api_key_hash TEXT,
173
+ id TEXT PRIMARY KEY,
174
+ agent_id TEXT NOT NULL UNIQUE,
175
+ name TEXT NOT NULL,
176
+ description TEXT,
177
+ class TEXT NOT NULL DEFAULT 'custom',
178
+ privacy_tier TEXT NOT NULL DEFAULT 'public',
179
+ owner_id TEXT REFERENCES users(id),
180
+ endpoint TEXT,
181
+ webhook_secret TEXT,
182
+ capabilities TEXT NOT NULL DEFAULT '[]',
183
+ skills TEXT NOT NULL DEFAULT '[]',
184
+ avatar TEXT,
185
+ messages_sent INTEGER NOT NULL DEFAULT 0,
186
+ messages_received INTEGER NOT NULL DEFAULT 0,
187
+ conversation_count INTEGER NOT NULL DEFAULT 0,
188
+ friend_count INTEGER NOT NULL DEFAULT 0,
189
+ status TEXT NOT NULL DEFAULT 'online',
190
+ last_seen INTEGER,
191
+ payment_config TEXT,
192
+ api_key_hash TEXT,
81
193
  organization_id TEXT REFERENCES organization(id) ON DELETE SET NULL,
82
- created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
194
+ created_at INTEGER NOT NULL,
195
+ updated_at INTEGER NOT NULL,
196
+ transport_type TEXT NOT NULL DEFAULT 'http',
197
+ api_key_encrypted TEXT,
198
+ api_base_url TEXT NOT NULL DEFAULT 'https://api.signaldock.io',
199
+ classification TEXT,
200
+ transport_config TEXT NOT NULL DEFAULT '{}',
201
+ is_active INTEGER NOT NULL DEFAULT 1,
202
+ last_used_at INTEGER,
203
+ requires_reauth INTEGER NOT NULL DEFAULT 0
83
204
  );
84
205
  CREATE UNIQUE INDEX IF NOT EXISTS agents_agent_id_idx ON agents(agent_id);
85
206
  CREATE INDEX IF NOT EXISTS agents_owner_idx ON agents(owner_id);
86
207
  CREATE INDEX IF NOT EXISTS agents_class_idx ON agents(class);
87
208
  CREATE INDEX IF NOT EXISTS agents_privacy_idx ON agents(privacy_tier);
88
209
  CREATE INDEX IF NOT EXISTS agents_org_idx ON agents(organization_id);
210
+ CREATE INDEX IF NOT EXISTS idx_agents_transport_type ON agents(transport_type);
211
+ CREATE INDEX IF NOT EXISTS idx_agents_is_active ON agents(is_active);
212
+ CREATE INDEX IF NOT EXISTS idx_agents_last_used ON agents(last_used_at);
213
+ CREATE INDEX IF NOT EXISTS idx_agents_reauth ON agents(requires_reauth) WHERE requires_reauth = 1;
89
214
 
90
- CREATE TABLE IF NOT EXISTS conversations (
91
- id TEXT PRIMARY KEY, participants TEXT NOT NULL,
92
- visibility TEXT NOT NULL DEFAULT 'private', message_count INTEGER NOT NULL DEFAULT 0,
93
- last_message_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
94
- );
95
-
96
- CREATE TABLE IF NOT EXISTS messages (
97
- id TEXT PRIMARY KEY, conversation_id TEXT NOT NULL REFERENCES conversations(id),
98
- from_agent_id TEXT NOT NULL, to_agent_id TEXT NOT NULL, content TEXT NOT NULL,
99
- content_type TEXT NOT NULL DEFAULT 'text', status TEXT NOT NULL DEFAULT 'pending',
100
- attachments TEXT NOT NULL DEFAULT '[]', group_id TEXT, metadata TEXT DEFAULT '{}',
101
- reply_to TEXT, created_at INTEGER NOT NULL, delivered_at INTEGER, read_at INTEGER
102
- );
103
- CREATE INDEX IF NOT EXISTS messages_conversation_idx ON messages(conversation_id);
104
- CREATE INDEX IF NOT EXISTS messages_from_agent_idx ON messages(from_agent_id);
105
- CREATE INDEX IF NOT EXISTS messages_to_agent_idx ON messages(to_agent_id);
106
- CREATE INDEX IF NOT EXISTS messages_created_at_idx ON messages(created_at);
107
- CREATE INDEX IF NOT EXISTS idx_messages_group_id ON messages(group_id) WHERE group_id IS NOT NULL;
108
- CREATE INDEX IF NOT EXISTS idx_messages_reply_to ON messages(reply_to) WHERE reply_to IS NOT NULL;
109
-
215
+ -- Cloud-sync: one-time agent claim tokens (api.signaldock.io provisioning).
110
216
  CREATE TABLE IF NOT EXISTS claim_codes (
111
- id TEXT PRIMARY KEY, agent_id TEXT NOT NULL REFERENCES agents(id),
112
- code TEXT NOT NULL UNIQUE, expires_at INTEGER NOT NULL, used_at INTEGER,
113
- used_by TEXT REFERENCES users(id), created_at INTEGER NOT NULL
217
+ id TEXT PRIMARY KEY,
218
+ agent_id TEXT NOT NULL REFERENCES agents(id),
219
+ code TEXT NOT NULL UNIQUE,
220
+ expires_at INTEGER NOT NULL,
221
+ used_at INTEGER,
222
+ used_by TEXT REFERENCES users(id),
223
+ created_at INTEGER NOT NULL
114
224
  );
115
225
  CREATE UNIQUE INDEX IF NOT EXISTS claim_codes_code_idx ON claim_codes(code);
116
226
  CREATE INDEX IF NOT EXISTS claim_codes_agent_idx ON claim_codes(agent_id);
117
227
 
118
- CREATE TABLE IF NOT EXISTS connections (
119
- id TEXT PRIMARY KEY, agent_a TEXT NOT NULL REFERENCES agents(id),
120
- agent_b TEXT NOT NULL REFERENCES agents(id), status TEXT NOT NULL DEFAULT 'pending',
121
- initiated_by TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
122
- );
123
- CREATE INDEX IF NOT EXISTS connections_agent_a_idx ON connections(agent_a);
124
- CREATE INDEX IF NOT EXISTS connections_agent_b_idx ON connections(agent_b);
125
-
126
- CREATE TABLE IF NOT EXISTS delivery_jobs (
127
- id TEXT PRIMARY KEY, message_id TEXT NOT NULL, payload TEXT NOT NULL,
128
- status TEXT NOT NULL DEFAULT 'pending', attempts INTEGER NOT NULL DEFAULT 0,
129
- max_attempts INTEGER NOT NULL DEFAULT 6, next_attempt_at INTEGER NOT NULL,
130
- last_error TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
131
- );
132
- CREATE INDEX IF NOT EXISTS idx_delivery_jobs_status ON delivery_jobs(status, next_attempt_at);
133
-
134
- CREATE TABLE IF NOT EXISTS dead_letters (
135
- id TEXT PRIMARY KEY, message_id TEXT NOT NULL, job_id TEXT NOT NULL,
136
- reason TEXT NOT NULL, attempts INTEGER NOT NULL, created_at INTEGER NOT NULL
137
- );
138
- CREATE INDEX IF NOT EXISTS idx_dead_letters_message ON dead_letters(message_id);
139
-
140
- CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(content, from_agent_id, content='messages', content_rowid='rowid');
141
- INSERT INTO messages_fts(messages_fts) VALUES('rebuild');
142
- CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
143
- INSERT INTO messages_fts(rowid, content, from_agent_id) VALUES (new.rowid, new.content, new.from_agent_id);
144
- END;
145
- CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
146
- INSERT INTO messages_fts(messages_fts, rowid, content, from_agent_id) VALUES('delete', old.rowid, old.content, old.from_agent_id);
147
- END;
148
- CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
149
- INSERT INTO messages_fts(messages_fts, rowid, content, from_agent_id) VALUES('delete', old.rowid, old.content, old.from_agent_id);
150
- INSERT INTO messages_fts(rowid, content, from_agent_id) VALUES (new.rowid, new.content, new.from_agent_id);
151
- END;
152
-
153
- CREATE TABLE IF NOT EXISTS message_pins (
154
- id TEXT PRIMARY KEY, message_id TEXT NOT NULL, conversation_id TEXT NOT NULL,
155
- pinned_by TEXT NOT NULL, note TEXT, created_at INTEGER NOT NULL, UNIQUE(message_id, pinned_by)
156
- );
157
- CREATE INDEX IF NOT EXISTS idx_pins_conversation ON message_pins(conversation_id);
158
- CREATE INDEX IF NOT EXISTS idx_pins_agent ON message_pins(pinned_by);
159
-
160
- CREATE TABLE IF NOT EXISTS attachments (
161
- slug TEXT PRIMARY KEY, conversation_id TEXT NOT NULL, from_agent_id TEXT NOT NULL,
162
- content BLOB NOT NULL, original_size INTEGER NOT NULL, compressed_size INTEGER NOT NULL,
163
- content_hash TEXT NOT NULL, format TEXT NOT NULL DEFAULT 'text', title TEXT,
164
- tokens INTEGER NOT NULL DEFAULT 0, expires_at INTEGER NOT NULL DEFAULT 0,
165
- storage_key TEXT, mode TEXT NOT NULL DEFAULT 'draft',
166
- version_count INTEGER NOT NULL DEFAULT 1, current_version INTEGER NOT NULL DEFAULT 1,
228
+ -- Identity catalog: pre-seeded capability slugs (19 entries).
229
+ CREATE TABLE IF NOT EXISTS capabilities (
230
+ id TEXT PRIMARY KEY,
231
+ slug TEXT NOT NULL UNIQUE,
232
+ name TEXT NOT NULL,
233
+ description TEXT NOT NULL,
234
+ category TEXT NOT NULL,
167
235
  created_at INTEGER NOT NULL
168
236
  );
169
- CREATE INDEX IF NOT EXISTS attachments_conversation_idx ON attachments(conversation_id);
170
- CREATE INDEX IF NOT EXISTS attachments_agent_idx ON attachments(from_agent_id);
171
237
 
172
- CREATE TABLE IF NOT EXISTS capabilities (
173
- id TEXT PRIMARY KEY, slug TEXT NOT NULL UNIQUE, name TEXT NOT NULL,
174
- description TEXT NOT NULL, category TEXT NOT NULL, created_at INTEGER NOT NULL
175
- );
238
+ -- Identity catalog: pre-seeded skill slugs (36 entries).
176
239
  CREATE TABLE IF NOT EXISTS skills (
177
- id TEXT PRIMARY KEY, slug TEXT NOT NULL UNIQUE, name TEXT NOT NULL,
178
- description TEXT NOT NULL, category TEXT NOT NULL, created_at INTEGER NOT NULL
240
+ id TEXT PRIMARY KEY,
241
+ slug TEXT NOT NULL UNIQUE,
242
+ name TEXT NOT NULL,
243
+ description TEXT NOT NULL,
244
+ category TEXT NOT NULL,
245
+ created_at INTEGER NOT NULL
179
246
  );
247
+
248
+ -- Junction: agent <-> capability catalog bindings.
180
249
  CREATE TABLE IF NOT EXISTS agent_capabilities (
181
- agent_id TEXT NOT NULL REFERENCES agents(id), capability_id TEXT NOT NULL REFERENCES capabilities(id),
250
+ agent_id TEXT NOT NULL REFERENCES agents(id),
251
+ capability_id TEXT NOT NULL REFERENCES capabilities(id),
182
252
  PRIMARY KEY (agent_id, capability_id)
183
253
  );
254
+
255
+ -- Junction: agent <-> skill catalog bindings.
184
256
  CREATE TABLE IF NOT EXISTS agent_skills (
185
- agent_id TEXT NOT NULL REFERENCES agents(id), skill_id TEXT NOT NULL REFERENCES skills(id),
257
+ agent_id TEXT NOT NULL REFERENCES agents(id),
258
+ skill_id TEXT NOT NULL REFERENCES skills(id),
186
259
  PRIMARY KEY (agent_id, skill_id)
187
260
  );
188
261
 
262
+ -- Live transport connection tracking (heartbeat state).
263
+ CREATE TABLE IF NOT EXISTS agent_connections (
264
+ id TEXT PRIMARY KEY NOT NULL,
265
+ agent_id TEXT NOT NULL,
266
+ transport_type TEXT NOT NULL DEFAULT 'http',
267
+ connection_id TEXT,
268
+ connected_at BIGINT NOT NULL,
269
+ last_heartbeat BIGINT NOT NULL,
270
+ connection_metadata TEXT,
271
+ created_at BIGINT NOT NULL,
272
+ FOREIGN KEY (agent_id) REFERENCES agents(agent_id) ON DELETE CASCADE,
273
+ UNIQUE(agent_id, connection_id)
274
+ );
275
+ CREATE INDEX IF NOT EXISTS idx_agent_connections_agent ON agent_connections(agent_id);
276
+ CREATE INDEX IF NOT EXISTS idx_agent_connections_transport ON agent_connections(transport_type);
277
+ CREATE INDEX IF NOT EXISTS idx_agent_connections_heartbeat ON agent_connections(last_heartbeat);
278
+
279
+ -- Cloud-sync: OAuth/provider accounts.
189
280
  CREATE TABLE IF NOT EXISTS accounts (
190
- id TEXT PRIMARY KEY NOT NULL, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
191
- account_id TEXT NOT NULL, provider_id TEXT NOT NULL, access_token TEXT, refresh_token TEXT,
192
- id_token TEXT, access_token_expires_at TEXT, refresh_token_expires_at TEXT, scope TEXT,
193
- password TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
281
+ id TEXT PRIMARY KEY NOT NULL,
282
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
283
+ account_id TEXT NOT NULL,
284
+ provider_id TEXT NOT NULL,
285
+ access_token TEXT,
286
+ refresh_token TEXT,
287
+ id_token TEXT,
288
+ access_token_expires_at TEXT,
289
+ refresh_token_expires_at TEXT,
290
+ scope TEXT,
291
+ password TEXT,
292
+ created_at TEXT NOT NULL,
293
+ updated_at TEXT NOT NULL
194
294
  );
195
295
  CREATE INDEX IF NOT EXISTS idx_accounts_user_id ON accounts(user_id);
196
296
  CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_provider ON accounts(provider_id, account_id);
197
297
 
298
+ -- Cloud-sync: authenticated sessions.
198
299
  CREATE TABLE IF NOT EXISTS sessions (
199
- id TEXT PRIMARY KEY NOT NULL, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
200
- token TEXT NOT NULL UNIQUE, ip_address TEXT, user_agent TEXT, expires_at TEXT NOT NULL,
201
- active_organization_id TEXT, impersonated_by TEXT, active INTEGER NOT NULL DEFAULT 1,
202
- created_at TEXT NOT NULL, updated_at TEXT NOT NULL
300
+ id TEXT PRIMARY KEY NOT NULL,
301
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
302
+ token TEXT NOT NULL UNIQUE,
303
+ ip_address TEXT,
304
+ user_agent TEXT,
305
+ expires_at TEXT NOT NULL,
306
+ active_organization_id TEXT,
307
+ impersonated_by TEXT,
308
+ active INTEGER NOT NULL DEFAULT 1,
309
+ created_at TEXT NOT NULL,
310
+ updated_at TEXT NOT NULL
203
311
  );
204
312
  CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);
205
313
  CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
206
314
 
315
+ -- Cloud-sync: email/2FA verification tokens.
207
316
  CREATE TABLE IF NOT EXISTS verifications (
208
- id TEXT PRIMARY KEY NOT NULL, identifier TEXT NOT NULL, value TEXT NOT NULL,
209
- expires_at TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
317
+ id TEXT PRIMARY KEY NOT NULL,
318
+ identifier TEXT NOT NULL,
319
+ value TEXT NOT NULL,
320
+ expires_at TEXT NOT NULL,
321
+ created_at TEXT NOT NULL,
322
+ updated_at TEXT NOT NULL
210
323
  );
211
324
  CREATE INDEX IF NOT EXISTS idx_verifications_identifier ON verifications(identifier);
212
325
 
326
+ -- Org-scoped agent API keys (cloud use; zero rows locally).
213
327
  CREATE TABLE IF NOT EXISTS org_agent_keys (
214
- id TEXT PRIMARY KEY NOT NULL, organization_id TEXT NOT NULL REFERENCES organization(id) ON DELETE CASCADE,
328
+ id TEXT PRIMARY KEY NOT NULL,
329
+ organization_id TEXT NOT NULL REFERENCES organization(id) ON DELETE CASCADE,
215
330
  agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
216
- created_by TEXT NOT NULL, created_at INTEGER NOT NULL
331
+ created_by TEXT NOT NULL,
332
+ created_at INTEGER NOT NULL
217
333
  );
218
334
  CREATE INDEX IF NOT EXISTS org_agent_keys_org_idx ON org_agent_keys(organization_id);
219
- CREATE INDEX IF NOT EXISTS org_agent_keys_agent_idx ON org_agent_keys(agent_id);
220
-
221
- CREATE TABLE IF NOT EXISTS attachment_versions (
222
- id TEXT PRIMARY KEY, slug TEXT NOT NULL REFERENCES attachments(slug) ON DELETE CASCADE,
223
- version_number INTEGER NOT NULL, author_agent_id TEXT NOT NULL,
224
- change_type TEXT NOT NULL DEFAULT 'patch', patch_text TEXT, storage_key TEXT NOT NULL,
225
- content_hash TEXT NOT NULL, original_size INTEGER NOT NULL, compressed_size INTEGER NOT NULL,
226
- tokens INTEGER NOT NULL, change_summary TEXT, sections_modified TEXT NOT NULL DEFAULT '[]',
227
- tokens_added INTEGER NOT NULL DEFAULT 0, tokens_removed INTEGER NOT NULL DEFAULT 0,
228
- created_at INTEGER NOT NULL, UNIQUE(slug, version_number)
229
- );
230
- CREATE INDEX IF NOT EXISTS idx_attachment_versions_slug ON attachment_versions(slug);
231
- CREATE INDEX IF NOT EXISTS idx_attachment_versions_author ON attachment_versions(author_agent_id);
232
-
233
- CREATE TABLE IF NOT EXISTS attachment_approvals (
234
- id TEXT PRIMARY KEY, slug TEXT NOT NULL REFERENCES attachments(slug) ON DELETE CASCADE,
235
- reviewer_agent_id TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', comment TEXT,
236
- version_reviewed INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL,
237
- UNIQUE(slug, reviewer_agent_id)
238
- );
239
- CREATE INDEX IF NOT EXISTS idx_attachment_approvals_slug ON attachment_approvals(slug);
240
-
241
- CREATE TABLE IF NOT EXISTS attachment_contributors (
242
- slug TEXT NOT NULL REFERENCES attachments(slug) ON DELETE CASCADE,
243
- agent_id TEXT NOT NULL, version_count INTEGER NOT NULL DEFAULT 0,
244
- total_tokens_added INTEGER NOT NULL DEFAULT 0, total_tokens_removed INTEGER NOT NULL DEFAULT 0,
245
- first_contribution_at INTEGER NOT NULL, last_contribution_at INTEGER NOT NULL,
246
- PRIMARY KEY (slug, agent_id)
247
- );`,
248
- },
249
- {
250
- name: '2026-03-30-000001_agent_connections',
251
- sql: `-- Add transport_type to agents table for connection mode classification.
252
- ALTER TABLE agents ADD COLUMN transport_type TEXT NOT NULL DEFAULT 'http';
253
- CREATE INDEX idx_agents_transport_type ON agents(transport_type);
254
-
255
- CREATE TABLE agent_connections (
256
- id TEXT PRIMARY KEY NOT NULL, agent_id TEXT NOT NULL,
257
- transport_type TEXT NOT NULL DEFAULT 'http', connection_id TEXT,
258
- connected_at BIGINT NOT NULL, last_heartbeat BIGINT NOT NULL,
259
- connection_metadata TEXT, created_at BIGINT NOT NULL,
260
- FOREIGN KEY (agent_id) REFERENCES agents(agent_id) ON DELETE CASCADE,
261
- UNIQUE(agent_id, connection_id)
262
- );
263
- CREATE INDEX idx_agent_connections_agent ON agent_connections(agent_id);
264
- CREATE INDEX idx_agent_connections_transport ON agent_connections(transport_type);
265
- CREATE INDEX idx_agent_connections_heartbeat ON agent_connections(last_heartbeat);`,
266
- },
267
- {
268
- name: '2026-03-31-000001_agent_credentials',
269
- sql: `-- Move agent credentials into signaldock.db agents table (T234 clean-cut).
270
- ALTER TABLE agents ADD COLUMN api_key_encrypted TEXT;
271
- ALTER TABLE agents ADD COLUMN api_base_url TEXT NOT NULL DEFAULT 'https://api.signaldock.io';
272
- ALTER TABLE agents ADD COLUMN classification TEXT;
273
- ALTER TABLE agents ADD COLUMN transport_config TEXT NOT NULL DEFAULT '{}';
274
- ALTER TABLE agents ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1;
275
- ALTER TABLE agents ADD COLUMN last_used_at INTEGER;
276
- CREATE INDEX IF NOT EXISTS idx_agents_is_active ON agents(is_active);
277
- CREATE INDEX IF NOT EXISTS idx_agents_last_used ON agents(last_used_at);`,
335
+ CREATE INDEX IF NOT EXISTS org_agent_keys_agent_idx ON org_agent_keys(agent_id);`,
278
336
  },
279
337
  ];
280
338
 
339
+ // ---------------------------------------------------------------------------
340
+ // Database lifecycle
341
+ // ---------------------------------------------------------------------------
342
+
343
+ /** Singleton native DatabaseSync handle for the current process. */
344
+ let _globalSignaldockNativeDb: DatabaseSync | null = null;
345
+
281
346
  /**
282
- * Ensure signaldock.db exists and has the full schema applied.
347
+ * Apply the global signaldock schema to an already-open database.
348
+ * Idempotent — uses `CREATE TABLE IF NOT EXISTS` and migration tracking.
283
349
  *
284
- * Idempotent safe to call multiple times. Uses `CREATE TABLE IF NOT EXISTS`
285
- * and `CREATE INDEX IF NOT EXISTS` throughout.
350
+ * @param db - An open DatabaseSync instance at the global path
351
+ * @task T346
352
+ * @epic T310
353
+ */
354
+ function applyGlobalSignaldockSchema(db: DatabaseSync): void {
355
+ // Ensure migration tracking tables exist
356
+ db.exec(`
357
+ CREATE TABLE IF NOT EXISTS _signaldock_meta (
358
+ key TEXT PRIMARY KEY,
359
+ value TEXT NOT NULL,
360
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
361
+ )
362
+ `);
363
+ db.exec(`
364
+ CREATE TABLE IF NOT EXISTS _signaldock_migrations (
365
+ name TEXT PRIMARY KEY,
366
+ applied_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
367
+ )
368
+ `);
369
+
370
+ // Apply embedded migrations (skips already-applied ones)
371
+ for (const migration of GLOBAL_EMBEDDED_MIGRATIONS) {
372
+ const applied = db
373
+ .prepare('SELECT name FROM _signaldock_migrations WHERE name = ?')
374
+ .get(migration.name) as { name: string } | undefined;
375
+ if (applied) continue;
376
+
377
+ db.exec('BEGIN TRANSACTION');
378
+ try {
379
+ db.exec(migration.sql);
380
+ db.prepare('INSERT INTO _signaldock_migrations (name) VALUES (?)').run(migration.name);
381
+ db.exec('COMMIT');
382
+ } catch (err) {
383
+ db.exec('ROLLBACK');
384
+ throw err;
385
+ }
386
+ }
387
+
388
+ // Record schema version
389
+ db.exec(`
390
+ INSERT OR REPLACE INTO _signaldock_meta (key, value, updated_at)
391
+ VALUES ('schema_version', '${GLOBAL_SIGNALDOCK_SCHEMA_VERSION}', strftime('%s', 'now'))
392
+ `);
393
+ }
394
+
395
+ /**
396
+ * Ensure global signaldock.db exists with the full global schema applied.
397
+ * Creates the global cleo home directory if it doesn't exist.
398
+ * Idempotent — safe to call multiple times.
286
399
  *
287
- * @returns Object with action ('created' | 'exists') and the database path.
400
+ * @returns Object with action ('created' | 'exists') and the database path
401
+ * @task T346
402
+ * @epic T310
288
403
  */
289
- export async function ensureSignaldockDb(
290
- cwd?: string,
291
- ): Promise<{ action: 'created' | 'exists'; path: string }> {
292
- const dbPath = getSignaldockDbPath(cwd);
404
+ export async function ensureGlobalSignaldockDb(): Promise<{
405
+ action: 'created' | 'exists';
406
+ path: string;
407
+ }> {
408
+ const dbPath = getGlobalSignaldockDbPath();
293
409
  const alreadyExists = existsSync(dbPath);
294
410
 
295
- // Ensure parent directory exists
296
- mkdirSync(dirname(dbPath), { recursive: true });
411
+ // Ensure global cleo home directory exists
412
+ const cleoHome = getCleoHome();
413
+ if (!existsSync(cleoHome)) {
414
+ mkdirSync(cleoHome, { recursive: true });
415
+ }
297
416
 
298
- // Open or create the database
299
417
  const db = new DatabaseSyncClass(dbPath);
300
-
301
418
  try {
302
- // Set pragmas for optimal performance
303
419
  db.exec('PRAGMA journal_mode = WAL');
304
420
  db.exec('PRAGMA busy_timeout = 5000');
305
421
  db.exec('PRAGMA synchronous = NORMAL');
306
422
  db.exec('PRAGMA foreign_keys = ON');
307
- db.exec('PRAGMA cache_size = -64000'); // 64MB
423
+ db.exec('PRAGMA cache_size = -64000'); // 64 MB
308
424
 
309
425
  // Check if schema already applied (agents table as sentinel)
310
426
  const hasSchema = (() => {
@@ -318,63 +434,59 @@ export async function ensureSignaldockDb(
318
434
  }
319
435
  })();
320
436
 
321
- // Ensure migration tracking tables exist
322
- db.exec(`
323
- CREATE TABLE IF NOT EXISTS _signaldock_meta (
324
- key TEXT PRIMARY KEY,
325
- value TEXT NOT NULL,
326
- updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
327
- )
328
- `);
329
- db.exec(`
330
- CREATE TABLE IF NOT EXISTS _signaldock_migrations (
331
- name TEXT PRIMARY KEY,
332
- applied_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
333
- )
334
- `);
437
+ applyGlobalSignaldockSchema(db);
335
438
 
336
- // Apply embedded migrations (works in ANY project, not just monorepo)
337
- for (const migration of EMBEDDED_MIGRATIONS) {
338
- // Skip already-applied migrations
339
- const applied = db
340
- .prepare('SELECT name FROM _signaldock_migrations WHERE name = ?')
341
- .get(migration.name) as { name: string } | undefined;
342
- if (applied) continue;
343
-
344
- db.exec('BEGIN TRANSACTION');
345
- try {
346
- db.exec(migration.sql);
347
- db.prepare('INSERT INTO _signaldock_migrations (name) VALUES (?)').run(migration.name);
348
- db.exec('COMMIT');
349
- } catch (err) {
350
- db.exec('ROLLBACK');
351
- throw err;
352
- }
353
- }
354
-
355
- // Record schema version
356
- db.exec(`
357
- INSERT OR REPLACE INTO _signaldock_meta (key, value, updated_at)
358
- VALUES ('schema_version', '${SIGNALDOCK_SCHEMA_VERSION}', strftime('%s', 'now'))
359
- `);
439
+ // Store native handle for backup integration (getGlobalSignaldockNativeDb)
440
+ _globalSignaldockNativeDb = db;
360
441
 
361
442
  return {
362
443
  action: alreadyExists && hasSchema ? 'exists' : 'created',
363
444
  path: dbPath,
364
445
  };
365
- } finally {
446
+ } catch (err) {
366
447
  db.close();
448
+ _globalSignaldockNativeDb = null;
449
+ throw err;
367
450
  }
451
+ // NOTE: We intentionally do NOT close `db` here — the native handle is
452
+ // retained as _globalSignaldockNativeDb for backup integration. Callers
453
+ // that need a short-lived open/close pattern should open the DB themselves.
368
454
  }
369
455
 
370
456
  /**
371
- * Check signaldock.db health table count, WAL mode, schema version.
457
+ * @deprecated Use ensureGlobalSignaldockDb(). Retained during T310 migration
458
+ * window for callers in init.ts and agent-registry-accessor.ts.
372
459
  *
373
- * Used by `cleo doctor` to verify signaldock.db integrity.
460
+ * When called WITHOUT arguments: forwards to ensureGlobalSignaldockDb().
461
+ * When called WITH a non-undefined `cwd` argument: throws a migration error.
374
462
  *
375
- * @returns Health report object or null if DB doesn't exist.
463
+ * @param cwd - Must be undefined. Any other value throws a migration error.
464
+ * @task T346
465
+ * @epic T310
376
466
  */
377
- export async function checkSignaldockDbHealth(cwd?: string): Promise<{
467
+ export async function ensureSignaldockDb(
468
+ cwd?: string,
469
+ ): Promise<{ action: 'created' | 'exists'; path: string }> {
470
+ if (cwd !== undefined) {
471
+ throw new Error(
472
+ 'ensureSignaldockDb(cwd) is removed as of T310 (v2026.4.12). ' +
473
+ 'signaldock.db is now global-only. ' +
474
+ 'Use ensureGlobalSignaldockDb() for global identity, or ' +
475
+ 'ensureConduitDb(cwd) from conduit-sqlite.ts for project messaging (T344).',
476
+ );
477
+ }
478
+ return ensureGlobalSignaldockDb();
479
+ }
480
+
481
+ /**
482
+ * Check global signaldock.db health: table count, WAL mode, schema version.
483
+ * Used by `cleo doctor` to verify global signaldock.db integrity.
484
+ *
485
+ * @returns Health report object, or object with exists=false if the DB does not exist.
486
+ * @task T346
487
+ * @epic T310
488
+ */
489
+ export async function checkGlobalSignaldockDbHealth(): Promise<{
378
490
  exists: boolean;
379
491
  path: string;
380
492
  tableCount: number;
@@ -382,7 +494,7 @@ export async function checkSignaldockDbHealth(cwd?: string): Promise<{
382
494
  schemaVersion: string | null;
383
495
  foreignKeysEnabled: boolean;
384
496
  } | null> {
385
- const dbPath = getSignaldockDbPath(cwd);
497
+ const dbPath = getGlobalSignaldockDbPath();
386
498
  if (!existsSync(dbPath)) {
387
499
  return {
388
500
  exists: false,
@@ -412,7 +524,7 @@ export async function checkSignaldockDbHealth(cwd?: string): Promise<{
412
524
  .get() as { value: string } | undefined;
413
525
  schemaVersion = meta?.value ?? null;
414
526
  } catch {
415
- // Meta table may not exist
527
+ // Meta table may not exist on very old or partially-initialized DBs
416
528
  }
417
529
 
418
530
  return {
@@ -427,3 +539,68 @@ export async function checkSignaldockDbHealth(cwd?: string): Promise<{
427
539
  db.close();
428
540
  }
429
541
  }
542
+
543
+ /**
544
+ * @deprecated Use checkGlobalSignaldockDbHealth(). Retained during T310 migration
545
+ * window for callers in `cleo doctor` and other diagnostics.
546
+ *
547
+ * When called WITHOUT arguments: forwards to checkGlobalSignaldockDbHealth().
548
+ * When called WITH a non-undefined `cwd` argument: throws a migration error.
549
+ *
550
+ * @param cwd - Must be undefined. Any other value throws a migration error.
551
+ * @task T346
552
+ * @epic T310
553
+ */
554
+ export async function checkSignaldockDbHealth(cwd?: string): Promise<{
555
+ exists: boolean;
556
+ path: string;
557
+ tableCount: number;
558
+ walMode: boolean;
559
+ schemaVersion: string | null;
560
+ foreignKeysEnabled: boolean;
561
+ } | null> {
562
+ if (cwd !== undefined) {
563
+ throw new Error(
564
+ 'checkSignaldockDbHealth(cwd) is removed as of T310 (v2026.4.12). ' +
565
+ 'signaldock.db is now global-only. ' +
566
+ 'Use checkGlobalSignaldockDbHealth() for global signaldock health, or ' +
567
+ 'checkConduitDbHealth(cwd) from conduit-sqlite.ts for project conduit health (T344).',
568
+ );
569
+ }
570
+ return checkGlobalSignaldockDbHealth();
571
+ }
572
+
573
+ /**
574
+ * Get the underlying node:sqlite DatabaseSync instance for global signaldock.db.
575
+ * Returns the handle stored by the most recent ensureGlobalSignaldockDb() call,
576
+ * or null if the database has not yet been initialized in this process.
577
+ *
578
+ * Used by sqlite-backup.ts to activate the signaldock GLOBAL_SNAPSHOT_TARGET
579
+ * (spec §6.2, T310).
580
+ *
581
+ * @task T346
582
+ * @epic T310
583
+ */
584
+ export function getGlobalSignaldockNativeDb(): DatabaseSync | null {
585
+ return _globalSignaldockNativeDb;
586
+ }
587
+
588
+ /**
589
+ * Reset the in-process global signaldock.db singleton.
590
+ * ONLY for use in test isolation — never call in production code.
591
+ *
592
+ * @task T346
593
+ * @epic T310
594
+ */
595
+ export function _resetGlobalSignaldockDb_TESTING_ONLY(): void {
596
+ if (_globalSignaldockNativeDb) {
597
+ try {
598
+ if (_globalSignaldockNativeDb.isOpen) {
599
+ _globalSignaldockNativeDb.close();
600
+ }
601
+ } catch {
602
+ // Ignore close errors
603
+ }
604
+ _globalSignaldockNativeDb = null;
605
+ }
606
+ }