sensorium-mcp 2.17.27 → 3.0.0

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 (224) hide show
  1. package/Install-Sensorium.ps1 +327 -0
  2. package/README.md +14 -0
  3. package/dist/config.d.ts +16 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +39 -2
  6. package/dist/config.js.map +1 -1
  7. package/dist/daily-session.d.ts +2 -1
  8. package/dist/daily-session.d.ts.map +1 -1
  9. package/dist/daily-session.js +23 -26
  10. package/dist/daily-session.js.map +1 -1
  11. package/dist/dashboard/routes/settings.d.ts +4 -0
  12. package/dist/dashboard/routes/settings.d.ts.map +1 -1
  13. package/dist/dashboard/routes/settings.js +57 -1
  14. package/dist/dashboard/routes/settings.js.map +1 -1
  15. package/dist/dashboard/routes/threads.d.ts +1 -0
  16. package/dist/dashboard/routes/threads.d.ts.map +1 -1
  17. package/dist/dashboard/routes/threads.js +23 -27
  18. package/dist/dashboard/routes/threads.js.map +1 -1
  19. package/dist/dashboard/routes.d.ts.map +1 -1
  20. package/dist/dashboard/routes.js +7 -2
  21. package/dist/dashboard/routes.js.map +1 -1
  22. package/dist/dashboard/spa.html +11 -11
  23. package/dist/data/interfaces.d.ts +36 -0
  24. package/dist/data/interfaces.d.ts.map +1 -0
  25. package/dist/data/interfaces.js +2 -0
  26. package/dist/data/interfaces.js.map +1 -0
  27. package/dist/data/memory/bootstrap.d.ts +36 -16
  28. package/dist/data/memory/bootstrap.d.ts.map +1 -1
  29. package/dist/data/memory/bootstrap.js +71 -217
  30. package/dist/data/memory/bootstrap.js.map +1 -1
  31. package/dist/data/memory/consolidation.d.ts +35 -34
  32. package/dist/data/memory/consolidation.d.ts.map +1 -1
  33. package/dist/data/memory/consolidation.js +43 -555
  34. package/dist/data/memory/consolidation.js.map +1 -1
  35. package/dist/data/memory/index.d.ts +0 -1
  36. package/dist/data/memory/index.d.ts.map +1 -1
  37. package/dist/data/memory/index.js +0 -1
  38. package/dist/data/memory/index.js.map +1 -1
  39. package/dist/data/memory/migration-runner.d.ts +5 -0
  40. package/dist/data/memory/migration-runner.d.ts.map +1 -0
  41. package/dist/data/memory/migration-runner.js +403 -0
  42. package/dist/data/memory/migration-runner.js.map +1 -0
  43. package/dist/data/memory/reflection.js +1 -1
  44. package/dist/data/memory/schema-ddl.d.ts +4 -0
  45. package/dist/data/memory/schema-ddl.d.ts.map +1 -0
  46. package/dist/data/memory/schema-ddl.js +194 -0
  47. package/dist/data/memory/schema-ddl.js.map +1 -0
  48. package/dist/data/memory/schema-guard.d.ts +3 -0
  49. package/dist/data/memory/schema-guard.d.ts.map +1 -0
  50. package/dist/data/memory/schema-guard.js +184 -0
  51. package/dist/data/memory/schema-guard.js.map +1 -0
  52. package/dist/data/memory/schema.d.ts +2 -5
  53. package/dist/data/memory/schema.d.ts.map +1 -1
  54. package/dist/data/memory/schema.js +6 -834
  55. package/dist/data/memory/schema.js.map +1 -1
  56. package/dist/data/memory/semantic.d.ts +0 -1
  57. package/dist/data/memory/semantic.d.ts.map +1 -1
  58. package/dist/data/memory/semantic.js +2 -8
  59. package/dist/data/memory/semantic.js.map +1 -1
  60. package/dist/data/memory/synthesis.js +2 -2
  61. package/dist/data/memory/synthesis.js.map +1 -1
  62. package/dist/data/memory/thread-registry.d.ts +18 -4
  63. package/dist/data/memory/thread-registry.d.ts.map +1 -1
  64. package/dist/data/memory/thread-registry.js +25 -0
  65. package/dist/data/memory/thread-registry.js.map +1 -1
  66. package/dist/data/sent-message.repository.d.ts +12 -0
  67. package/dist/data/sent-message.repository.d.ts.map +1 -0
  68. package/dist/data/sent-message.repository.js +31 -0
  69. package/dist/data/sent-message.repository.js.map +1 -0
  70. package/dist/http-server.d.ts.map +1 -1
  71. package/dist/http-server.js +23 -2
  72. package/dist/http-server.js.map +1 -1
  73. package/dist/index.js +27 -48
  74. package/dist/index.js.map +1 -1
  75. package/dist/logger.d.ts +7 -2
  76. package/dist/logger.d.ts.map +1 -1
  77. package/dist/logger.js +89 -12
  78. package/dist/logger.js.map +1 -1
  79. package/dist/scheduler.d.ts +8 -0
  80. package/dist/scheduler.d.ts.map +1 -1
  81. package/dist/scheduler.js +15 -0
  82. package/dist/scheduler.js.map +1 -1
  83. package/dist/server/factory.d.ts +2 -1
  84. package/dist/server/factory.d.ts.map +1 -1
  85. package/dist/server/factory.js +11 -4
  86. package/dist/server/factory.js.map +1 -1
  87. package/dist/services/agent-spawn.service.d.ts +39 -0
  88. package/dist/services/agent-spawn.service.d.ts.map +1 -0
  89. package/dist/services/agent-spawn.service.js +348 -0
  90. package/dist/services/agent-spawn.service.js.map +1 -0
  91. package/dist/services/background-runner.d.ts +26 -0
  92. package/dist/services/background-runner.d.ts.map +1 -0
  93. package/dist/services/background-runner.js +71 -0
  94. package/dist/services/background-runner.js.map +1 -0
  95. package/dist/services/consolidation.service.d.ts +16 -0
  96. package/dist/services/consolidation.service.d.ts.map +1 -0
  97. package/dist/services/consolidation.service.js +508 -0
  98. package/dist/services/consolidation.service.js.map +1 -0
  99. package/dist/services/dispatcher/broker.d.ts +2 -0
  100. package/dist/services/dispatcher/broker.d.ts.map +1 -1
  101. package/dist/services/dispatcher/broker.js +5 -10
  102. package/dist/services/dispatcher/broker.js.map +1 -1
  103. package/dist/services/dispatcher/index.d.ts +1 -1
  104. package/dist/services/dispatcher/index.d.ts.map +1 -1
  105. package/dist/services/dispatcher/index.js +1 -1
  106. package/dist/services/dispatcher/index.js.map +1 -1
  107. package/dist/services/dispatcher/lock.d.ts.map +1 -1
  108. package/dist/services/dispatcher/lock.js +7 -11
  109. package/dist/services/dispatcher/lock.js.map +1 -1
  110. package/dist/services/maintenance-signal.d.ts +18 -0
  111. package/dist/services/maintenance-signal.d.ts.map +1 -0
  112. package/dist/services/maintenance-signal.js +48 -0
  113. package/dist/services/maintenance-signal.js.map +1 -0
  114. package/dist/services/memory-briefing.service.d.ts +4 -0
  115. package/dist/services/memory-briefing.service.d.ts.map +1 -0
  116. package/dist/services/memory-briefing.service.js +143 -0
  117. package/dist/services/memory-briefing.service.js.map +1 -0
  118. package/dist/services/process.service.d.ts +31 -0
  119. package/dist/services/process.service.d.ts.map +1 -0
  120. package/dist/services/process.service.js +100 -0
  121. package/dist/services/process.service.js.map +1 -0
  122. package/dist/services/thread-health.service.d.ts +18 -0
  123. package/dist/services/thread-health.service.d.ts.map +1 -0
  124. package/dist/services/thread-health.service.js +118 -0
  125. package/dist/services/thread-health.service.js.map +1 -0
  126. package/dist/services/thread-lifecycle.service.d.ts +52 -0
  127. package/dist/services/thread-lifecycle.service.d.ts.map +1 -0
  128. package/dist/services/thread-lifecycle.service.js +174 -0
  129. package/dist/services/thread-lifecycle.service.js.map +1 -0
  130. package/dist/services/topic.service.d.ts +25 -0
  131. package/dist/services/topic.service.d.ts.map +1 -0
  132. package/dist/services/topic.service.js +65 -0
  133. package/dist/services/topic.service.js.map +1 -0
  134. package/dist/services/worker-cleanup.service.d.ts +8 -0
  135. package/dist/services/worker-cleanup.service.d.ts.map +1 -0
  136. package/dist/services/worker-cleanup.service.js +82 -0
  137. package/dist/services/worker-cleanup.service.js.map +1 -0
  138. package/dist/sessions.d.ts +14 -0
  139. package/dist/sessions.d.ts.map +1 -1
  140. package/dist/sessions.js +55 -0
  141. package/dist/sessions.js.map +1 -1
  142. package/dist/telegram.d.ts +13 -6
  143. package/dist/telegram.d.ts.map +1 -1
  144. package/dist/telegram.js +43 -14
  145. package/dist/telegram.js.map +1 -1
  146. package/dist/tools/defs/memory-defs.d.ts.map +1 -1
  147. package/dist/tools/defs/memory-defs.js +0 -19
  148. package/dist/tools/defs/memory-defs.js.map +1 -1
  149. package/dist/tools/delegate-tool.d.ts +4 -0
  150. package/dist/tools/delegate-tool.d.ts.map +1 -1
  151. package/dist/tools/delegate-tool.js +48 -109
  152. package/dist/tools/delegate-tool.js.map +1 -1
  153. package/dist/tools/memory-tools.d.ts.map +1 -1
  154. package/dist/tools/memory-tools.js +1 -16
  155. package/dist/tools/memory-tools.js.map +1 -1
  156. package/dist/tools/shared-agent-utils.d.ts +9 -1
  157. package/dist/tools/shared-agent-utils.d.ts.map +1 -1
  158. package/dist/tools/shared-agent-utils.js +24 -42
  159. package/dist/tools/shared-agent-utils.js.map +1 -1
  160. package/dist/tools/start-session-tool.d.ts +2 -0
  161. package/dist/tools/start-session-tool.d.ts.map +1 -1
  162. package/dist/tools/start-session-tool.js +66 -106
  163. package/dist/tools/start-session-tool.js.map +1 -1
  164. package/dist/tools/thread-lifecycle.d.ts +5 -127
  165. package/dist/tools/thread-lifecycle.d.ts.map +1 -1
  166. package/dist/tools/thread-lifecycle.js +5 -1163
  167. package/dist/tools/thread-lifecycle.js.map +1 -1
  168. package/dist/tools/utility-tools.js +5 -2
  169. package/dist/tools/utility-tools.js.map +1 -1
  170. package/dist/tools/wait/drive-handler.d.ts +0 -1
  171. package/dist/tools/wait/drive-handler.d.ts.map +1 -1
  172. package/dist/tools/wait/drive-handler.js +5 -22
  173. package/dist/tools/wait/drive-handler.js.map +1 -1
  174. package/dist/tools/wait/message-delivery.js +1 -1
  175. package/dist/tools/wait/message-delivery.js.map +1 -1
  176. package/dist/tools/wait/message-processing.d.ts.map +1 -1
  177. package/dist/tools/wait/message-processing.js +9 -8
  178. package/dist/tools/wait/message-processing.js.map +1 -1
  179. package/dist/tools/wait/poll-loop.d.ts +2 -0
  180. package/dist/tools/wait/poll-loop.d.ts.map +1 -1
  181. package/dist/tools/wait/poll-loop.js +27 -29
  182. package/dist/tools/wait/poll-loop.js.map +1 -1
  183. package/dist/tools/wait/task-handler.d.ts +0 -3
  184. package/dist/tools/wait/task-handler.d.ts.map +1 -1
  185. package/dist/tools/wait/task-handler.js +3 -2
  186. package/dist/tools/wait/task-handler.js.map +1 -1
  187. package/dist/types.d.ts +0 -1
  188. package/dist/types.d.ts.map +1 -1
  189. package/package.json +4 -8
  190. package/supervisor/config.go +182 -69
  191. package/supervisor/config_test.go +78 -0
  192. package/supervisor/go.mod +12 -0
  193. package/supervisor/go.sum +20 -0
  194. package/supervisor/health.go +60 -11
  195. package/supervisor/health_test.go +29 -0
  196. package/supervisor/keeper.go +15 -10
  197. package/supervisor/log.go +109 -28
  198. package/supervisor/log_test.go +86 -6
  199. package/supervisor/main.go +150 -19
  200. package/supervisor/main_test.go +130 -0
  201. package/supervisor/process.go +47 -4
  202. package/supervisor/process_test.go +14 -0
  203. package/supervisor/secrets.go +95 -0
  204. package/supervisor/secrets_securevault_test.go +98 -0
  205. package/supervisor/secrets_test.go +119 -0
  206. package/supervisor/self_update.go +282 -0
  207. package/supervisor/self_update_test.go +177 -0
  208. package/supervisor/service_restart_stub.go +9 -0
  209. package/supervisor/service_restart_windows.go +63 -0
  210. package/supervisor/service_stub.go +15 -0
  211. package/supervisor/service_windows.go +216 -0
  212. package/supervisor/update_state.go +264 -0
  213. package/supervisor/update_state_test.go +306 -0
  214. package/supervisor/updater.go +311 -10
  215. package/supervisor/updater_test.go +64 -0
  216. package/dist/data/memory/quality-scoring.d.ts +0 -32
  217. package/dist/data/memory/quality-scoring.d.ts.map +0 -1
  218. package/dist/data/memory/quality-scoring.js +0 -182
  219. package/dist/data/memory/quality-scoring.js.map +0 -1
  220. package/scripts/install-supervisor.ps1 +0 -67
  221. package/scripts/install-supervisor.sh +0 -43
  222. package/scripts/start-supervisor.ps1 +0 -46
  223. package/scripts/start-supervisor.sh +0 -20
  224. package/templates/coding-task.default.md +0 -12
@@ -6,815 +6,15 @@
6
6
  * `initMemoryDb()` to obtain the singleton handle.
7
7
  */
8
8
  import BetterSqlite3 from "better-sqlite3";
9
- import { mkdirSync, readFileSync } from "node:fs";
9
+ import { mkdirSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { log } from "../../logger.js";
13
+ import { SCHEMA_SQL, cleanupOldSentMessages } from "./schema-ddl.js";
14
+ import { runMigrations } from "./migration-runner.js";
15
+ import { ensureSchemaIntegrity } from "./schema-guard.js";
13
16
  import { nowISO } from "./utils.js";
14
- import { errorMessage } from "../../utils.js";
15
- // ─── Database Initialization ─────────────────────────────────────────────────
16
- const SCHEMA_VERSION = 20;
17
- // ─── Migrations ──────────────────────────────────────────────────────────────
18
- /**
19
- * Migration functions keyed by target schema version.
20
- * Each migration upgrades from version (key - 1) to version (key).
21
- * Add new migrations here when SCHEMA_VERSION is bumped.
22
- */
23
- const MIGRATIONS = {
24
- 2: (db) => {
25
- db.exec(`
26
- CREATE TABLE IF NOT EXISTS note_embeddings (
27
- note_id TEXT PRIMARY KEY,
28
- embedding BLOB NOT NULL,
29
- model TEXT NOT NULL DEFAULT 'text-embedding-3-small',
30
- created_at TEXT NOT NULL
31
- );
32
- CREATE INDEX IF NOT EXISTS idx_emb_note ON note_embeddings(note_id);
33
- `);
34
- },
35
- 3: (db) => {
36
- // Add priority column: 0=normal, 1=notable, 2=high importance
37
- // Use try/catch because new databases already have the column in SCHEMA_SQL
38
- try {
39
- db.exec(`ALTER TABLE semantic_notes ADD COLUMN priority INTEGER NOT NULL DEFAULT 0`);
40
- }
41
- catch {
42
- // Column already exists — safe to ignore
43
- }
44
- db.exec(`CREATE INDEX IF NOT EXISTS idx_sem_priority ON semantic_notes(priority DESC) WHERE valid_to IS NULL`);
45
- },
46
- 4: (db) => {
47
- // Add thread_id column: NULL = global, number = thread-scoped
48
- try {
49
- db.exec(`ALTER TABLE semantic_notes ADD COLUMN thread_id INTEGER`);
50
- }
51
- catch {
52
- // Column already exists — safe to ignore
53
- }
54
- db.exec(`CREATE INDEX IF NOT EXISTS idx_sem_thread ON semantic_notes(thread_id) WHERE valid_to IS NULL`);
55
- // Backfill thread_id from source episodes
56
- const notes = db.prepare(`SELECT note_id, source_episodes FROM semantic_notes WHERE thread_id IS NULL`).all();
57
- const update = db.prepare(`UPDATE semantic_notes SET thread_id = ? WHERE note_id = ?`);
58
- let backfilled = 0;
59
- for (const note of notes) {
60
- let episodeIds = [];
61
- try {
62
- episodeIds = JSON.parse(note.source_episodes ?? "[]");
63
- }
64
- catch { /* ignore */ }
65
- if (episodeIds.length === 0)
66
- continue;
67
- const placeholders = episodeIds.map(() => "?").join(",");
68
- const rows = db.prepare(`SELECT thread_id, COUNT(*) as cnt FROM episodes WHERE episode_id IN (${placeholders}) GROUP BY thread_id ORDER BY cnt DESC LIMIT 1`).all(...episodeIds);
69
- if (rows.length > 0 && rows[0].thread_id != null) {
70
- update.run(rows[0].thread_id, note.note_id);
71
- backfilled++;
72
- }
73
- }
74
- if (backfilled > 0) {
75
- log.info(`[migration-4] Backfilled thread_id on ${backfilled}/${notes.length} existing notes.`);
76
- }
77
- },
78
- 5: (db) => {
79
- // Widen CHECK constraints on episodes table to include 'operator_reaction'
80
- // type and 'reaction' modality. SQLite does not support ALTER COLUMN, so we
81
- // must recreate the table.
82
- db.exec(`
83
- CREATE TABLE IF NOT EXISTS episodes_new (
84
- episode_id TEXT PRIMARY KEY,
85
- session_id TEXT NOT NULL,
86
- thread_id INTEGER NOT NULL,
87
- timestamp TEXT NOT NULL,
88
- type TEXT NOT NULL CHECK(type IN ('operator_message','agent_action','system_event','operator_reaction')),
89
- modality TEXT NOT NULL CHECK(modality IN ('text','voice','photo','video_note','document','mixed','reaction')),
90
- content TEXT NOT NULL,
91
- topic_tags TEXT,
92
- importance REAL NOT NULL DEFAULT 0.5,
93
- consolidated INTEGER DEFAULT 0,
94
- accessed_count INTEGER DEFAULT 0,
95
- last_accessed TEXT,
96
- created_at TEXT NOT NULL
97
- );
98
- INSERT INTO episodes_new SELECT * FROM episodes;
99
- DROP TABLE episodes;
100
- ALTER TABLE episodes_new RENAME TO episodes;
101
- CREATE INDEX IF NOT EXISTS idx_ep_thread_time ON episodes(thread_id, timestamp DESC);
102
- CREATE INDEX IF NOT EXISTS idx_ep_importance ON episodes(importance DESC);
103
- CREATE INDEX IF NOT EXISTS idx_ep_uncons ON episodes(consolidated) WHERE consolidated = 0;
104
- `);
105
- },
106
- 6: (db) => {
107
- // Per-thread reaction routing: track which message_id belongs to which thread_id
108
- db.exec(`
109
- CREATE TABLE IF NOT EXISTS sent_messages (
110
- message_id INTEGER PRIMARY KEY,
111
- thread_id INTEGER NOT NULL,
112
- created_at TEXT DEFAULT (datetime('now'))
113
- );
114
- CREATE INDEX IF NOT EXISTS idx_sent_messages_thread ON sent_messages(thread_id);
115
- `);
116
- },
117
- 7: (db) => {
118
- // Topic registry table
119
- db.exec(`
120
- CREATE TABLE IF NOT EXISTS topic_registry (
121
- chat_id TEXT NOT NULL,
122
- name TEXT NOT NULL COLLATE NOCASE,
123
- thread_id INTEGER NOT NULL,
124
- registered_at TEXT NOT NULL DEFAULT (datetime('now')),
125
- PRIMARY KEY (chat_id, name)
126
- );
127
- `);
128
- },
129
- 8: (db) => {
130
- // Add is_guardrail flag to semantic_notes for explicit guardrail tagging
131
- try {
132
- db.exec(`ALTER TABLE semantic_notes ADD COLUMN is_guardrail INTEGER NOT NULL DEFAULT 0`);
133
- }
134
- catch {
135
- // Column already exists — safe to ignore
136
- }
137
- db.exec(`CREATE INDEX IF NOT EXISTS idx_sem_guardrail ON semantic_notes(is_guardrail) WHERE is_guardrail = 1 AND valid_to IS NULL`);
138
- },
139
- 9: (db) => {
140
- // Backfill orphan NULL thread_id notes: assign to the earliest (most-used) thread.
141
- // These notes were created before thread_id tracking and have no source_episodes to infer from.
142
- const oldest = db.prepare(`SELECT thread_id FROM semantic_notes WHERE thread_id IS NOT NULL GROUP BY thread_id ORDER BY MIN(created_at) ASC LIMIT 1`).get();
143
- if (!oldest)
144
- return;
145
- const result = db.prepare(`UPDATE semantic_notes SET thread_id = ? WHERE thread_id IS NULL`).run(oldest.thread_id);
146
- if (result.changes > 0) {
147
- log.info(`[migration-9] Assigned ${result.changes} orphan notes to thread ${oldest.thread_id}`);
148
- }
149
- },
150
- 10: (db) => {
151
- // Add "pinned" column: pinned notes always appear in bootstrap briefing.
152
- try {
153
- db.exec("ALTER TABLE semantic_notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0");
154
- }
155
- catch { /* already exists in fresh DBs */ }
156
- db.exec("CREATE INDEX IF NOT EXISTS idx_sem_pinned ON semantic_notes(pinned) WHERE pinned = 1 AND valid_to IS NULL");
157
- log.info("[migration-10] Added pinned column to semantic_notes");
158
- },
159
- 11: (db) => {
160
- // Temporal narratives: pre-generated summaries at day/week/month resolution
161
- db.exec(`
162
- CREATE TABLE IF NOT EXISTS temporal_narratives (
163
- id INTEGER PRIMARY KEY AUTOINCREMENT,
164
- thread_id INTEGER NOT NULL,
165
- resolution TEXT NOT NULL CHECK(resolution IN ('day', 'week', 'month', 'quarter', 'half_year')),
166
- period_start TEXT NOT NULL,
167
- period_end TEXT NOT NULL,
168
- narrative TEXT NOT NULL,
169
- source_episode_count INTEGER NOT NULL DEFAULT 0,
170
- source_note_count INTEGER NOT NULL DEFAULT 0,
171
- model TEXT,
172
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
173
- UNIQUE(thread_id, resolution, period_start)
174
- );
175
- CREATE INDEX IF NOT EXISTS idx_narrative_thread_res ON temporal_narratives(thread_id, resolution, created_at DESC);
176
- `);
177
- log.info("[migration-11] Created temporal_narratives table");
178
- },
179
- 12: (db) => {
180
- db.exec(`
181
- CREATE TABLE IF NOT EXISTS thread_registry (
182
- id INTEGER PRIMARY KEY AUTOINCREMENT,
183
- thread_id INTEGER NOT NULL UNIQUE,
184
- name TEXT NOT NULL,
185
- type TEXT NOT NULL CHECK(type IN ('root','daily','branch','worker')),
186
- root_thread_id INTEGER,
187
- badge TEXT NOT NULL DEFAULT 'root',
188
- client TEXT DEFAULT 'claude',
189
- max_retries INTEGER DEFAULT 5,
190
- cooldown_ms INTEGER DEFAULT 300000,
191
- keep_alive INTEGER DEFAULT 0,
192
- created_at TEXT NOT NULL,
193
- last_active_at TEXT,
194
- status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived','expired'))
195
- );
196
- CREATE INDEX IF NOT EXISTS idx_thread_reg_type ON thread_registry(type);
197
- CREATE INDEX IF NOT EXISTS idx_thread_reg_root ON thread_registry(root_thread_id);
198
- CREATE INDEX IF NOT EXISTS idx_thread_reg_status ON thread_registry(status);
199
- `);
200
- log.info("[migration-12] Created thread_registry table");
201
- },
202
- 13: (db) => {
203
- // session_reset_at may already exist if the DB was created with the latest SCHEMA_SQL
204
- const cols = db.prepare("PRAGMA table_info(thread_registry)").all();
205
- const hasCol = cols.some(c => c.name === "session_reset_at");
206
- if (!hasCol) {
207
- db.exec("ALTER TABLE thread_registry ADD COLUMN session_reset_at TEXT");
208
- }
209
- log.info("[migration-13] Added session_reset_at column to thread_registry");
210
- },
211
- 14: (db) => {
212
- // Widen temporal_narratives resolution CHECK to include 'quarter' and 'half_year'.
213
- // SQLite doesn't support ALTER CONSTRAINT, so recreate the table.
214
- db.exec(`
215
- CREATE TABLE IF NOT EXISTS temporal_narratives_new (
216
- id INTEGER PRIMARY KEY AUTOINCREMENT,
217
- thread_id INTEGER NOT NULL,
218
- resolution TEXT NOT NULL CHECK(resolution IN ('day', 'week', 'month', 'quarter', 'half_year')),
219
- period_start TEXT NOT NULL,
220
- period_end TEXT NOT NULL,
221
- narrative TEXT NOT NULL,
222
- source_episode_count INTEGER NOT NULL DEFAULT 0,
223
- source_note_count INTEGER NOT NULL DEFAULT 0,
224
- model TEXT,
225
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
226
- UNIQUE(thread_id, resolution, period_start)
227
- );
228
- INSERT OR IGNORE INTO temporal_narratives_new SELECT * FROM temporal_narratives;
229
- DROP TABLE temporal_narratives;
230
- ALTER TABLE temporal_narratives_new RENAME TO temporal_narratives;
231
- CREATE INDEX IF NOT EXISTS idx_narrative_thread_res ON temporal_narratives(thread_id, resolution, created_at DESC);
232
- `);
233
- log.info("[migration-14] Widened temporal_narratives resolution CHECK to include quarter and half_year");
234
- },
235
- 15: (db) => {
236
- // Widen thread_registry status CHECK to include 'exited'.
237
- // SQLite doesn't support ALTER CONSTRAINT, so recreate the table.
238
- const existingColumns = db
239
- .prepare("PRAGMA table_info(thread_registry)")
240
- .all()
241
- .map((r) => r.name);
242
- rebuildThreadRegistryWithExitedStatus(db, existingColumns);
243
- log.info("[migration-15] Widened thread_registry status CHECK to include 'exited'");
244
- },
245
- 16: (db) => {
246
- // Add daily_rotation column to thread_registry (default OFF).
247
- // Decouples daily session rotation from keepAlive.
248
- try {
249
- db.exec(`ALTER TABLE thread_registry ADD COLUMN daily_rotation INTEGER NOT NULL DEFAULT 0`);
250
- }
251
- catch {
252
- // Column may already exist from schema self-heal
253
- }
254
- log.info("[migration-16] Added daily_rotation column to thread_registry (default OFF)");
255
- },
256
- 17: (db) => {
257
- // Add autonomous_mode column to thread_registry (default OFF).
258
- // Per-thread toggle for drive system autonomous mode.
259
- try {
260
- db.exec(`ALTER TABLE thread_registry ADD COLUMN autonomous_mode INTEGER NOT NULL DEFAULT 0`);
261
- }
262
- catch {
263
- // Column may already exist from schema self-heal
264
- }
265
- log.info("[migration-17] Added autonomous_mode column to thread_registry (default OFF)");
266
- },
267
- 18: (db) => {
268
- // Backfill thread_registry from settings.json threadAgentTypes + keepAlive config.
269
- // This ensures threads created before the registry existed are properly represented.
270
- try {
271
- const settingsPath = join(homedir(), ".remote-copilot-mcp", "settings.json");
272
- let settings = {};
273
- try {
274
- settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
275
- }
276
- catch { /* no settings */ }
277
- const agentTypes = (settings.threadAgentTypes ?? {});
278
- const keepAliveThreadId = settings.keepAliveThreadId;
279
- const keepAliveClient = (settings.keepAliveClient ?? "claude");
280
- const keepAliveEnabled = !!settings.keepAliveEnabled;
281
- const upsert = db.prepare(`INSERT INTO thread_registry (thread_id, name, type, client, keep_alive, created_at, last_active_at, status)
282
- VALUES (?, ?, 'root', ?, ?, ?, ?, 'active')
283
- ON CONFLICT(thread_id) DO UPDATE SET
284
- client = CASE WHEN excluded.client != 'claude' THEN excluded.client ELSE thread_registry.client END,
285
- keep_alive = CASE WHEN excluded.keep_alive = 1 THEN 1 ELSE thread_registry.keep_alive END,
286
- status = CASE WHEN thread_registry.status = 'archived' AND excluded.keep_alive = 1 THEN 'active' ELSE thread_registry.status END`);
287
- let backfilled = 0;
288
- const now = nowISO();
289
- for (const [threadIdStr, client] of Object.entries(agentTypes)) {
290
- const tid = Number(threadIdStr);
291
- if (!Number.isFinite(tid))
292
- continue;
293
- const isKeepAlive = keepAliveEnabled && keepAliveThreadId === tid ? 1 : 0;
294
- const effectiveClient = isKeepAlive ? keepAliveClient : client;
295
- upsert.run(tid, `Thread ${tid}`, effectiveClient, isKeepAlive, now, now);
296
- backfilled++;
297
- }
298
- // Specifically fix thread 1327 if it exists and keepAlive is set for it
299
- if (keepAliveEnabled && keepAliveThreadId) {
300
- db.prepare(`UPDATE thread_registry SET keep_alive = 1, status = 'active', client = ? WHERE thread_id = ?`).run(keepAliveClient, keepAliveThreadId);
301
- }
302
- log.info(`[migration-18] Backfilled ${backfilled} threads from settings.json threadAgentTypes`);
303
- }
304
- catch (err) {
305
- log.warn(`[migration-18] Backfill from settings.json failed: ${errorMessage(err)}`);
306
- }
307
- },
308
- 19: (db) => {
309
- // Add telegram_topic_id column — allows logical thread_id to survive when
310
- // a Telegram forum topic is deleted and recreated.
311
- try {
312
- db.exec(`ALTER TABLE thread_registry ADD COLUMN telegram_topic_id INTEGER`);
313
- }
314
- catch {
315
- // Column may already exist from schema self-heal
316
- }
317
- log.info("[migration-19] Added telegram_topic_id column to thread_registry");
318
- },
319
- 20: (db) => {
320
- // Add identity_prompt column — per-thread first-person identity anchor
321
- // injected at the TOP of the memory briefing to maintain personality continuity.
322
- try {
323
- db.exec(`ALTER TABLE thread_registry ADD COLUMN identity_prompt TEXT`);
324
- }
325
- catch {
326
- // Column may already exist
327
- }
328
- log.info("[migration-20] Added identity_prompt column to thread_registry");
329
- },
330
- 21: (db) => {
331
- // Add working_directory column — persistent CWD for thread spawning.
332
- // Ensures threads restart in the correct directory on resume/keep-alive.
333
- try {
334
- db.exec(`ALTER TABLE thread_registry ADD COLUMN working_directory TEXT`);
335
- }
336
- catch {
337
- // Column may already exist
338
- }
339
- log.info("[migration-21] Added working_directory column to thread_registry");
340
- },
341
- };
342
- /**
343
- * Read the current schema version from the database.
344
- * Returns 1 if no version is recorded (initial schema).
345
- */
346
- function getCurrentSchemaVersion(db) {
347
- try {
348
- const row = db
349
- .prepare("SELECT MAX(version) as v FROM schema_version")
350
- .get();
351
- return row?.v ?? 1;
352
- }
353
- catch {
354
- // Table may not exist yet on first run
355
- return 0;
356
- }
357
- }
358
- function rebuildThreadRegistryWithExitedStatus(db, existingColumns) {
359
- const selectSessionResetAt = existingColumns.includes("session_reset_at")
360
- ? "session_reset_at"
361
- : "NULL";
362
- const selectDailyRotation = existingColumns.includes("daily_rotation")
363
- ? "daily_rotation"
364
- : "0";
365
- const selectAutonomousMode = existingColumns.includes("autonomous_mode")
366
- ? "autonomous_mode"
367
- : "0";
368
- const selectTelegramTopicId = existingColumns.includes("telegram_topic_id")
369
- ? "telegram_topic_id"
370
- : "NULL";
371
- const selectIdentityPrompt = existingColumns.includes("identity_prompt")
372
- ? "identity_prompt"
373
- : "NULL";
374
- const selectWorkingDirectory = existingColumns.includes("working_directory")
375
- ? "working_directory"
376
- : "NULL";
377
- db.exec(`
378
- CREATE TABLE IF NOT EXISTS thread_registry_new (
379
- id INTEGER PRIMARY KEY AUTOINCREMENT,
380
- thread_id INTEGER NOT NULL UNIQUE,
381
- name TEXT NOT NULL,
382
- type TEXT NOT NULL CHECK(type IN ('root','daily','branch','worker')),
383
- root_thread_id INTEGER,
384
- badge TEXT NOT NULL DEFAULT 'root',
385
- client TEXT DEFAULT 'claude',
386
- max_retries INTEGER DEFAULT 5,
387
- cooldown_ms INTEGER DEFAULT 300000,
388
- keep_alive INTEGER DEFAULT 0,
389
- created_at TEXT NOT NULL,
390
- last_active_at TEXT,
391
- session_reset_at TEXT,
392
- status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived','expired','exited')),
393
- daily_rotation INTEGER NOT NULL DEFAULT 0,
394
- autonomous_mode INTEGER NOT NULL DEFAULT 0,
395
- telegram_topic_id INTEGER,
396
- identity_prompt TEXT,
397
- working_directory TEXT
398
- );
399
- INSERT OR IGNORE INTO thread_registry_new (
400
- id, thread_id, name, type, root_thread_id, badge, client, max_retries,
401
- cooldown_ms, keep_alive, created_at, last_active_at, session_reset_at,
402
- status, daily_rotation, autonomous_mode, telegram_topic_id, identity_prompt, working_directory
403
- )
404
- SELECT
405
- id, thread_id, name, type, root_thread_id, badge, client, max_retries,
406
- cooldown_ms, keep_alive, created_at, last_active_at, ${selectSessionResetAt},
407
- status, ${selectDailyRotation}, ${selectAutonomousMode}, ${selectTelegramTopicId}, ${selectIdentityPrompt}, ${selectWorkingDirectory}
408
- FROM thread_registry;
409
- DROP TABLE thread_registry;
410
- ALTER TABLE thread_registry_new RENAME TO thread_registry;
411
- CREATE INDEX IF NOT EXISTS idx_thread_reg_type ON thread_registry(type);
412
- CREATE INDEX IF NOT EXISTS idx_thread_reg_root ON thread_registry(root_thread_id);
413
- CREATE INDEX IF NOT EXISTS idx_thread_reg_status ON thread_registry(status);
414
- `);
415
- }
416
- /**
417
- * Run any pending migrations sequentially from the current stored version
418
- * up to SCHEMA_VERSION. Each migration runs inside a transaction.
419
- */
420
- function runMigrations(db) {
421
- const currentVersion = getCurrentSchemaVersion(db);
422
- log.info(`[memory] Current schema version: ${currentVersion}, target: ${SCHEMA_VERSION}`);
423
- for (let v = currentVersion + 1; v <= SCHEMA_VERSION; v++) {
424
- const migration = MIGRATIONS[v];
425
- if (migration) {
426
- try {
427
- // Run DDL migrations outside transactions — SQLite DDL + transactions
428
- // can have subtle issues in WAL mode with better-sqlite3.
429
- migration(db);
430
- db.prepare("INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (?, ?)").run(v, nowISO());
431
- log.info(`[memory] Migrated schema to version ${v}`);
432
- }
433
- catch (err) {
434
- log.error(`[memory] Migration ${v} failed: ${errorMessage(err)}. Will attempt self-heal.`);
435
- // Don't throw — allow ensureSchemaIntegrity to fix what it can
436
- }
437
- }
438
- }
439
- }
440
- /**
441
- * Self-healing: verify that critical columns exist after migrations.
442
- * If any are missing (e.g. a migration was skipped or failed), add them
443
- * idempotently so downstream queries never crash on a missing column.
444
- *
445
- * After each successful self-heal, we record the corresponding migration
446
- * version so the migration loop won't retry it on next startup.
447
- */
448
- function ensureSchemaIntegrity(db) {
449
- const stampVersion = (v) => {
450
- try {
451
- db.prepare("INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (?, ?)").run(v, nowISO());
452
- }
453
- catch { /* non-critical */ }
454
- };
455
- const getTableSql = (tableName) => {
456
- const row = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName);
457
- return row?.sql ?? "";
458
- };
459
- const semanticNoteCols = db
460
- .prepare("PRAGMA table_info(semantic_notes)")
461
- .all()
462
- .map((r) => r.name);
463
- if (!semanticNoteCols.includes("is_guardrail")) {
464
- log.info("[memory] Self-heal: adding missing is_guardrail column");
465
- db.exec("ALTER TABLE semantic_notes ADD COLUMN is_guardrail INTEGER NOT NULL DEFAULT 0");
466
- db.exec("CREATE INDEX IF NOT EXISTS idx_sem_guardrail ON semantic_notes(is_guardrail) WHERE is_guardrail = 1 AND valid_to IS NULL");
467
- stampVersion(8); // migration 8 added is_guardrail
468
- }
469
- if (!semanticNoteCols.includes("pinned")) {
470
- log.info("[memory] Self-heal: adding missing pinned column");
471
- db.exec("ALTER TABLE semantic_notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0");
472
- db.exec("CREATE INDEX IF NOT EXISTS idx_sem_pinned ON semantic_notes(pinned) WHERE pinned = 1 AND valid_to IS NULL");
473
- stampVersion(10); // migration 10 added pinned
474
- }
475
- if (!semanticNoteCols.includes("thread_id")) {
476
- log.info("[memory] Self-heal: adding missing thread_id column to semantic_notes");
477
- db.exec("ALTER TABLE semantic_notes ADD COLUMN thread_id INTEGER");
478
- db.exec("CREATE INDEX IF NOT EXISTS idx_sem_thread ON semantic_notes(thread_id) WHERE valid_to IS NULL");
479
- stampVersion(4); // migration 4 added thread_id
480
- }
481
- if (!semanticNoteCols.includes("priority")) {
482
- log.info("[memory] Self-heal: adding missing priority column to semantic_notes");
483
- db.exec("ALTER TABLE semantic_notes ADD COLUMN priority INTEGER NOT NULL DEFAULT 0");
484
- db.exec("CREATE INDEX IF NOT EXISTS idx_sem_priority ON semantic_notes(priority DESC) WHERE valid_to IS NULL");
485
- stampVersion(3); // migration 3 added priority
486
- }
487
- // Also check episodes table for thread_id
488
- const episodeCols = db
489
- .prepare("PRAGMA table_info(episodes)")
490
- .all()
491
- .map((r) => r.name);
492
- if (!episodeCols.includes("thread_id")) {
493
- log.info("[memory] Self-heal: adding missing thread_id column to episodes");
494
- db.exec("ALTER TABLE episodes ADD COLUMN thread_id INTEGER");
495
- // Do not stamp a schema version here: no numbered migration owns this
496
- // legacy repair, and stamping version 2 would incorrectly suppress the
497
- // note_embeddings migration on the next startup.
498
- }
499
- // Self-heal: ensure temporal_narratives table exists (migration 11)
500
- const hasTemporalNarratives = db
501
- .prepare("SELECT COUNT(*) as cnt FROM sqlite_master WHERE type='table' AND name='temporal_narratives'")
502
- .get();
503
- if (hasTemporalNarratives.cnt === 0) {
504
- log.info("[memory] Self-heal: creating missing temporal_narratives table");
505
- db.exec(`
506
- CREATE TABLE IF NOT EXISTS temporal_narratives (
507
- id INTEGER PRIMARY KEY AUTOINCREMENT,
508
- thread_id INTEGER NOT NULL,
509
- resolution TEXT NOT NULL CHECK(resolution IN ('day', 'week', 'month', 'quarter', 'half_year')),
510
- period_start TEXT NOT NULL,
511
- period_end TEXT NOT NULL,
512
- narrative TEXT NOT NULL,
513
- source_episode_count INTEGER NOT NULL DEFAULT 0,
514
- source_note_count INTEGER NOT NULL DEFAULT 0,
515
- model TEXT,
516
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
517
- UNIQUE(thread_id, resolution, period_start)
518
- );
519
- CREATE INDEX IF NOT EXISTS idx_narrative_thread_res ON temporal_narratives(thread_id, resolution, created_at DESC);
520
- `);
521
- stampVersion(11);
522
- }
523
- else {
524
- const temporalNarrativesSql = getTableSql("temporal_narratives");
525
- const hasExpandedNarrativeResolution = temporalNarrativesSql.includes("'quarter'") &&
526
- temporalNarrativesSql.includes("'half_year'");
527
- if (!hasExpandedNarrativeResolution) {
528
- log.info("[memory] Self-heal: widening temporal_narratives resolution CHECK");
529
- db.exec(`
530
- CREATE TABLE IF NOT EXISTS temporal_narratives_new (
531
- id INTEGER PRIMARY KEY AUTOINCREMENT,
532
- thread_id INTEGER NOT NULL,
533
- resolution TEXT NOT NULL CHECK(resolution IN ('day', 'week', 'month', 'quarter', 'half_year')),
534
- period_start TEXT NOT NULL,
535
- period_end TEXT NOT NULL,
536
- narrative TEXT NOT NULL,
537
- source_episode_count INTEGER NOT NULL DEFAULT 0,
538
- source_note_count INTEGER NOT NULL DEFAULT 0,
539
- model TEXT,
540
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
541
- UNIQUE(thread_id, resolution, period_start)
542
- );
543
- INSERT OR IGNORE INTO temporal_narratives_new SELECT * FROM temporal_narratives;
544
- DROP TABLE temporal_narratives;
545
- ALTER TABLE temporal_narratives_new RENAME TO temporal_narratives;
546
- CREATE INDEX IF NOT EXISTS idx_narrative_thread_res ON temporal_narratives(thread_id, resolution, created_at DESC);
547
- `);
548
- stampVersion(14);
549
- }
550
- }
551
- const hasThreadRegistry = db
552
- .prepare("SELECT COUNT(*) as cnt FROM sqlite_master WHERE type='table' AND name='thread_registry'")
553
- .get();
554
- if (hasThreadRegistry.cnt === 0) {
555
- log.info("[memory] Self-heal: creating missing thread_registry table");
556
- db.exec(`
557
- CREATE TABLE IF NOT EXISTS thread_registry (
558
- id INTEGER PRIMARY KEY AUTOINCREMENT,
559
- thread_id INTEGER NOT NULL UNIQUE,
560
- name TEXT NOT NULL,
561
- type TEXT NOT NULL CHECK(type IN ('root','daily','branch','worker')),
562
- root_thread_id INTEGER,
563
- badge TEXT NOT NULL DEFAULT 'root',
564
- client TEXT DEFAULT 'claude',
565
- max_retries INTEGER DEFAULT 5,
566
- cooldown_ms INTEGER DEFAULT 300000,
567
- keep_alive INTEGER DEFAULT 0,
568
- created_at TEXT NOT NULL,
569
- last_active_at TEXT,
570
- session_reset_at TEXT,
571
- status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived','expired','exited')),
572
- daily_rotation INTEGER NOT NULL DEFAULT 0,
573
- autonomous_mode INTEGER NOT NULL DEFAULT 0,
574
- telegram_topic_id INTEGER,
575
- identity_prompt TEXT,
576
- working_directory TEXT
577
- );
578
- CREATE INDEX IF NOT EXISTS idx_thread_reg_type ON thread_registry(type);
579
- CREATE INDEX IF NOT EXISTS idx_thread_reg_root ON thread_registry(root_thread_id);
580
- CREATE INDEX IF NOT EXISTS idx_thread_reg_status ON thread_registry(status);
581
- `);
582
- stampVersion(12);
583
- stampVersion(13);
584
- stampVersion(15);
585
- stampVersion(16);
586
- stampVersion(17);
587
- stampVersion(19);
588
- stampVersion(20);
589
- stampVersion(21);
590
- }
591
- else {
592
- const threadRegistryCols = db
593
- .prepare("PRAGMA table_info(thread_registry)")
594
- .all()
595
- .map((r) => r.name);
596
- if (!threadRegistryCols.includes("session_reset_at")) {
597
- log.info("[memory] Self-heal: adding missing session_reset_at column to thread_registry");
598
- db.exec("ALTER TABLE thread_registry ADD COLUMN session_reset_at TEXT");
599
- stampVersion(13);
600
- }
601
- const threadRegistrySql = getTableSql("thread_registry");
602
- if (!threadRegistrySql.includes("'exited'")) {
603
- log.info("[memory] Self-heal: widening thread_registry status CHECK");
604
- rebuildThreadRegistryWithExitedStatus(db, threadRegistryCols);
605
- stampVersion(15);
606
- }
607
- if (!threadRegistryCols.includes("daily_rotation")) {
608
- log.info("[memory] Self-heal: adding missing daily_rotation column to thread_registry");
609
- db.exec("ALTER TABLE thread_registry ADD COLUMN daily_rotation INTEGER NOT NULL DEFAULT 0");
610
- stampVersion(16);
611
- }
612
- if (!threadRegistryCols.includes("autonomous_mode")) {
613
- log.info("[memory] Self-heal: adding missing autonomous_mode column to thread_registry");
614
- db.exec("ALTER TABLE thread_registry ADD COLUMN autonomous_mode INTEGER NOT NULL DEFAULT 0");
615
- stampVersion(17);
616
- }
617
- if (!threadRegistryCols.includes("telegram_topic_id")) {
618
- log.info("[memory] Self-heal: adding missing telegram_topic_id column to thread_registry");
619
- db.exec("ALTER TABLE thread_registry ADD COLUMN telegram_topic_id INTEGER");
620
- stampVersion(19);
621
- }
622
- if (!threadRegistryCols.includes("identity_prompt")) {
623
- log.info("[memory] Self-heal: adding missing identity_prompt column to thread_registry");
624
- db.exec("ALTER TABLE thread_registry ADD COLUMN identity_prompt TEXT");
625
- stampVersion(20);
626
- }
627
- if (!threadRegistryCols.includes("working_directory")) {
628
- log.info("[memory] Self-heal: adding missing working_directory column to thread_registry");
629
- db.exec("ALTER TABLE thread_registry ADD COLUMN working_directory TEXT");
630
- stampVersion(21);
631
- }
632
- }
633
- }
634
- const SCHEMA_SQL = `
635
- CREATE TABLE IF NOT EXISTS episodes (
636
- episode_id TEXT PRIMARY KEY,
637
- session_id TEXT NOT NULL,
638
- thread_id INTEGER NOT NULL,
639
- timestamp TEXT NOT NULL,
640
- type TEXT NOT NULL CHECK(type IN ('operator_message','agent_action','system_event','operator_reaction')),
641
- modality TEXT NOT NULL CHECK(modality IN ('text','voice','photo','video_note','document','mixed','reaction')),
642
- content TEXT NOT NULL,
643
- topic_tags TEXT,
644
- importance REAL NOT NULL DEFAULT 0.5,
645
- consolidated INTEGER DEFAULT 0,
646
- accessed_count INTEGER DEFAULT 0,
647
- last_accessed TEXT,
648
- created_at TEXT NOT NULL
649
- );
650
-
651
- CREATE INDEX IF NOT EXISTS idx_ep_thread_time ON episodes(thread_id, timestamp DESC);
652
- CREATE INDEX IF NOT EXISTS idx_ep_importance ON episodes(importance DESC);
653
- CREATE INDEX IF NOT EXISTS idx_ep_uncons ON episodes(consolidated) WHERE consolidated = 0;
654
-
655
- CREATE TABLE IF NOT EXISTS semantic_notes (
656
- note_id TEXT PRIMARY KEY,
657
- type TEXT NOT NULL CHECK(type IN ('fact','preference','pattern','entity','relationship')),
658
- content TEXT NOT NULL,
659
- keywords TEXT NOT NULL,
660
- confidence REAL NOT NULL DEFAULT 0.5,
661
- source_episodes TEXT,
662
- linked_notes TEXT,
663
- link_reasons TEXT,
664
- valid_from TEXT NOT NULL,
665
- valid_to TEXT,
666
- superseded_by TEXT,
667
- access_count INTEGER DEFAULT 0,
668
- last_accessed TEXT,
669
- priority INTEGER NOT NULL DEFAULT 0,
670
- thread_id INTEGER,
671
- is_guardrail INTEGER NOT NULL DEFAULT 0,
672
- pinned INTEGER NOT NULL DEFAULT 0,
673
- created_at TEXT NOT NULL,
674
- updated_at TEXT NOT NULL
675
- );
676
-
677
- CREATE INDEX IF NOT EXISTS idx_sem_type ON semantic_notes(type);
678
- CREATE INDEX IF NOT EXISTS idx_sem_conf ON semantic_notes(confidence DESC);
679
- CREATE INDEX IF NOT EXISTS idx_sem_valid ON semantic_notes(valid_to) WHERE valid_to IS NULL;
680
- CREATE INDEX IF NOT EXISTS idx_sem_priority ON semantic_notes(priority DESC) WHERE valid_to IS NULL;
681
- CREATE INDEX IF NOT EXISTS idx_sem_thread ON semantic_notes(thread_id) WHERE valid_to IS NULL;
682
- CREATE INDEX IF NOT EXISTS idx_sem_guardrail ON semantic_notes(is_guardrail) WHERE is_guardrail = 1 AND valid_to IS NULL;
683
- CREATE INDEX IF NOT EXISTS idx_sem_pinned ON semantic_notes(pinned) WHERE pinned = 1 AND valid_to IS NULL;
684
-
685
- CREATE TABLE IF NOT EXISTS procedures (
686
- procedure_id TEXT PRIMARY KEY,
687
- name TEXT NOT NULL,
688
- type TEXT NOT NULL CHECK(type IN ('workflow','habit','tool_pattern','template')),
689
- description TEXT NOT NULL,
690
- steps TEXT,
691
- trigger_conditions TEXT,
692
- success_rate REAL DEFAULT 0.5,
693
- times_executed INTEGER DEFAULT 0,
694
- last_executed_at TEXT,
695
- learned_from TEXT,
696
- corrections TEXT,
697
- related_procedures TEXT,
698
- confidence REAL DEFAULT 0.5,
699
- created_at TEXT NOT NULL,
700
- updated_at TEXT NOT NULL
701
- );
702
-
703
- CREATE INDEX IF NOT EXISTS idx_proc_name ON procedures(name);
704
- CREATE INDEX IF NOT EXISTS idx_proc_type ON procedures(type);
705
-
706
- CREATE TABLE IF NOT EXISTS meta_topic_index (
707
- topic TEXT PRIMARY KEY,
708
- semantic_count INTEGER DEFAULT 0,
709
- procedural_count INTEGER DEFAULT 0,
710
- last_updated TEXT,
711
- avg_confidence REAL DEFAULT 0.5,
712
- total_accesses INTEGER DEFAULT 0
713
- );
714
-
715
- CREATE TABLE IF NOT EXISTS meta_consolidation_log (
716
- id INTEGER PRIMARY KEY AUTOINCREMENT,
717
- run_at TEXT NOT NULL,
718
- episodes_processed INTEGER,
719
- notes_created INTEGER,
720
- duration_ms INTEGER
721
- );
722
-
723
- CREATE TABLE IF NOT EXISTS voice_signatures (
724
- id INTEGER PRIMARY KEY AUTOINCREMENT,
725
- episode_id TEXT NOT NULL,
726
- emotion TEXT,
727
- arousal REAL,
728
- dominance REAL,
729
- valence REAL,
730
- speech_rate REAL,
731
- mean_pitch_hz REAL,
732
- pitch_std_hz REAL,
733
- jitter REAL,
734
- shimmer REAL,
735
- hnr_db REAL,
736
- audio_events TEXT,
737
- duration_sec REAL,
738
- created_at TEXT NOT NULL
739
- );
740
-
741
- CREATE INDEX IF NOT EXISTS idx_voice_ep ON voice_signatures(episode_id);
742
- CREATE INDEX IF NOT EXISTS idx_voice_time ON voice_signatures(created_at DESC);
743
-
744
- CREATE TABLE IF NOT EXISTS note_embeddings (
745
- note_id TEXT PRIMARY KEY,
746
- embedding BLOB NOT NULL,
747
- model TEXT NOT NULL DEFAULT 'text-embedding-3-small',
748
- created_at TEXT NOT NULL
749
- );
750
-
751
- CREATE INDEX IF NOT EXISTS idx_emb_note ON note_embeddings(note_id);
752
-
753
- CREATE TABLE IF NOT EXISTS sent_messages (
754
- message_id INTEGER PRIMARY KEY,
755
- thread_id INTEGER NOT NULL,
756
- created_at TEXT DEFAULT (datetime('now'))
757
- );
758
-
759
- CREATE INDEX IF NOT EXISTS idx_sent_messages_thread ON sent_messages(thread_id);
760
-
761
- CREATE TABLE IF NOT EXISTS topic_registry (
762
- chat_id TEXT NOT NULL,
763
- name TEXT NOT NULL COLLATE NOCASE,
764
- thread_id INTEGER NOT NULL,
765
- registered_at TEXT NOT NULL DEFAULT (datetime('now')),
766
- PRIMARY KEY (chat_id, name)
767
- );
768
-
769
- CREATE TABLE IF NOT EXISTS temporal_narratives (
770
- id INTEGER PRIMARY KEY AUTOINCREMENT,
771
- thread_id INTEGER NOT NULL,
772
- resolution TEXT NOT NULL CHECK(resolution IN ('day', 'week', 'month', 'quarter', 'half_year')),
773
- period_start TEXT NOT NULL,
774
- period_end TEXT NOT NULL,
775
- narrative TEXT NOT NULL,
776
- source_episode_count INTEGER NOT NULL DEFAULT 0,
777
- source_note_count INTEGER NOT NULL DEFAULT 0,
778
- model TEXT,
779
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
780
- UNIQUE(thread_id, resolution, period_start)
781
- );
782
- CREATE INDEX IF NOT EXISTS idx_narrative_thread_res ON temporal_narratives(thread_id, resolution, created_at DESC);
783
-
784
- CREATE TABLE IF NOT EXISTS thread_registry (
785
- id INTEGER PRIMARY KEY AUTOINCREMENT,
786
- thread_id INTEGER NOT NULL UNIQUE,
787
- name TEXT NOT NULL,
788
- type TEXT NOT NULL CHECK(type IN ('root','daily','branch','worker')),
789
- root_thread_id INTEGER,
790
- badge TEXT NOT NULL DEFAULT 'root',
791
- client TEXT DEFAULT 'claude',
792
- max_retries INTEGER DEFAULT 5,
793
- cooldown_ms INTEGER DEFAULT 300000,
794
- keep_alive INTEGER DEFAULT 0,
795
- created_at TEXT NOT NULL,
796
- last_active_at TEXT,
797
- session_reset_at TEXT,
798
- status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','archived','expired','exited')),
799
- daily_rotation INTEGER NOT NULL DEFAULT 0,
800
- autonomous_mode INTEGER NOT NULL DEFAULT 0,
801
- telegram_topic_id INTEGER,
802
- identity_prompt TEXT
803
- );
804
- CREATE INDEX IF NOT EXISTS idx_thread_reg_type ON thread_registry(type);
805
- CREATE INDEX IF NOT EXISTS idx_thread_reg_root ON thread_registry(root_thread_id);
806
- CREATE INDEX IF NOT EXISTS idx_thread_reg_status ON thread_registry(status);
807
-
808
- CREATE TABLE IF NOT EXISTS schema_version (
809
- version INTEGER PRIMARY KEY,
810
- applied_at TEXT NOT NULL
811
- );
812
- `;
813
- /**
814
- * Backward-compatibility migration: if thread_registry is empty but we have
815
- * episodes for known threads, register them as root threads so existing
816
- * deployments don't lose visibility after the thread_registry feature lands.
817
- */
17
+ export { cleanupOldSentMessages };
818
18
  function migrateExistingRootThreads(db) {
819
19
  const count = db.prepare("SELECT COUNT(*) as cnt FROM thread_registry").get().cnt;
820
20
  if (count > 0)
@@ -840,23 +40,14 @@ export function initMemoryDb() {
840
40
  const db = new BetterSqlite3(dbPath);
841
41
  db.pragma("journal_mode = WAL");
842
42
  db.pragma("foreign_keys = ON");
843
- // Create all tables
844
43
  db.exec(SCHEMA_SQL);
845
- // Record base schema version for brand-new databases only
846
44
  const versionCount = db.prepare("SELECT COUNT(*) as cnt FROM schema_version").get().cnt;
847
45
  if (versionCount === 0) {
848
- // New database — record version 1 as the base, then run all migrations up to SCHEMA_VERSION
849
46
  db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (1, ?)").run(nowISO());
850
47
  }
851
- // Run any pending migrations (will upgrade from stored version to SCHEMA_VERSION)
852
48
  runMigrations(db);
853
- // Self-heal: ensure all critical columns exist even if a migration failed.
854
- // After each successful column addition, record the corresponding migration
855
- // version so it isn't retried but also isn't force-stamped over failures.
856
49
  ensureSchemaIntegrity(db);
857
- // Auto-migrate existing threads as roots (backward compatibility)
858
50
  migrateExistingRootThreads(db);
859
- // Backfill missing thread names from topic_registry
860
51
  try {
861
52
  db.prepare(`UPDATE thread_registry
862
53
  SET name = (
@@ -866,26 +57,7 @@ export function initMemoryDb() {
866
57
  )
867
58
  WHERE (thread_registry.name IS NULL OR thread_registry.name = '')`).run();
868
59
  }
869
- catch { /* best-effort — topic_registry may not exist yet */ }
870
- // NOTE: We deliberately do NOT force-stamp schema_version to SCHEMA_VERSION
871
- // here. If a migration failed, its version is not recorded, so it will be
872
- // retried on the next startup. This prevents permanently skipping failed
873
- // migrations (which was the root cause of the "pinned column missing" bug).
60
+ catch { }
874
61
  return db;
875
62
  }
876
- /**
877
- * Delete sent_messages entries older than 7 days.
878
- * Safe to call periodically (e.g. during consolidation).
879
- */
880
- export function cleanupOldSentMessages(db) {
881
- try {
882
- const result = db.prepare(`DELETE FROM sent_messages WHERE created_at < datetime('now', '-7 days')`).run();
883
- if (result.changes > 0) {
884
- log.info(`[memory] Cleaned up ${result.changes} old sent_messages entries.`);
885
- }
886
- }
887
- catch (err) {
888
- log.warn(`[memory] sent_messages cleanup failed: ${errorMessage(err)}`);
889
- }
890
- }
891
63
  //# sourceMappingURL=schema.js.map