negotium 0.1.7 → 0.1.8

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 (256) hide show
  1. package/README.md +2 -2
  2. package/dist/chunk-0ay6cjdy.js +1331 -0
  3. package/dist/chunk-0ay6cjdy.js.map +16 -0
  4. package/dist/chunk-1k7t7bnx.js +97 -0
  5. package/dist/chunk-1k7t7bnx.js.map +10 -0
  6. package/dist/chunk-5ahkew9c.js +806 -0
  7. package/dist/chunk-5ahkew9c.js.map +17 -0
  8. package/dist/chunk-6cpn7ht2.js +287 -0
  9. package/dist/chunk-6cpn7ht2.js.map +13 -0
  10. package/dist/chunk-6kctn20e.js +3529 -0
  11. package/dist/chunk-6kctn20e.js.map +20 -0
  12. package/dist/chunk-7xqj7aex.js +77 -0
  13. package/dist/chunk-7xqj7aex.js.map +11 -0
  14. package/dist/chunk-9p0ww3tk.js +41 -0
  15. package/dist/chunk-9p0ww3tk.js.map +10 -0
  16. package/dist/chunk-a92a7nrk.js +252 -0
  17. package/dist/chunk-a92a7nrk.js.map +10 -0
  18. package/dist/chunk-c500xe9z.js +33 -0
  19. package/dist/chunk-c500xe9z.js.map +10 -0
  20. package/dist/chunk-cds1qxsf.js +1644 -0
  21. package/dist/chunk-cds1qxsf.js.map +17 -0
  22. package/dist/chunk-e4407g8x.js +274 -0
  23. package/dist/chunk-e4407g8x.js.map +10 -0
  24. package/dist/chunk-e5pbggsh.js +363 -0
  25. package/dist/chunk-e5pbggsh.js.map +11 -0
  26. package/dist/chunk-edb07sjp.js +567 -0
  27. package/dist/chunk-edb07sjp.js.map +14 -0
  28. package/dist/chunk-exd0v74a.js +3012 -0
  29. package/dist/chunk-exd0v74a.js.map +32 -0
  30. package/dist/chunk-fgrme9tr.js +4398 -0
  31. package/dist/chunk-fgrme9tr.js.map +47 -0
  32. package/dist/chunk-fka9eqe1.js +581 -0
  33. package/dist/chunk-fka9eqe1.js.map +11 -0
  34. package/dist/chunk-g82jygez.js +108 -0
  35. package/dist/chunk-g82jygez.js.map +10 -0
  36. package/dist/chunk-ghs2j1fr.js +312 -0
  37. package/dist/chunk-ghs2j1fr.js.map +10 -0
  38. package/dist/chunk-gmy11efb.js +125 -0
  39. package/dist/chunk-gmy11efb.js.map +10 -0
  40. package/dist/chunk-gxma4c70.js +110 -0
  41. package/dist/chunk-gxma4c70.js.map +10 -0
  42. package/dist/chunk-hkdqhm34.js +61 -0
  43. package/dist/chunk-hkdqhm34.js.map +10 -0
  44. package/dist/chunk-hpa0cpzh.js +134 -0
  45. package/dist/chunk-hpa0cpzh.js.map +10 -0
  46. package/dist/chunk-hwy3n31w.js +21 -0
  47. package/dist/chunk-hwy3n31w.js.map +9 -0
  48. package/dist/chunk-j2behpy7.js +214 -0
  49. package/dist/chunk-j2behpy7.js.map +10 -0
  50. package/dist/chunk-jc0at0tc.js +102 -0
  51. package/dist/chunk-jc0at0tc.js.map +10 -0
  52. package/dist/chunk-jkqq5ef6.js +243 -0
  53. package/dist/chunk-jkqq5ef6.js.map +11 -0
  54. package/dist/chunk-jzkw2f01.js +51 -0
  55. package/dist/chunk-jzkw2f01.js.map +10 -0
  56. package/dist/chunk-jzz91jwh.js +70 -0
  57. package/dist/chunk-jzz91jwh.js.map +10 -0
  58. package/dist/chunk-k9z47mab.js +247 -0
  59. package/dist/chunk-k9z47mab.js.map +12 -0
  60. package/dist/chunk-n17yxpa8.js +23 -0
  61. package/dist/chunk-n17yxpa8.js.map +10 -0
  62. package/dist/chunk-naa09ags.js +2143 -0
  63. package/dist/chunk-naa09ags.js.map +15 -0
  64. package/dist/chunk-ns3ke88n.js +142 -0
  65. package/dist/chunk-ns3ke88n.js.map +10 -0
  66. package/dist/chunk-p3cr2ejg.js +1626 -0
  67. package/dist/chunk-p3cr2ejg.js.map +15 -0
  68. package/dist/chunk-q5bcpf23.js +2925 -0
  69. package/dist/chunk-q5bcpf23.js.map +20 -0
  70. package/dist/chunk-rafc55bs.js +424 -0
  71. package/dist/chunk-rafc55bs.js.map +11 -0
  72. package/dist/chunk-rbrxjvbj.js +2830 -0
  73. package/dist/chunk-rbrxjvbj.js.map +23 -0
  74. package/dist/chunk-sa874j5c.js +9 -0
  75. package/dist/chunk-sa874j5c.js.map +10 -0
  76. package/dist/chunk-sgsz2fpq.js +289 -0
  77. package/dist/chunk-sgsz2fpq.js.map +10 -0
  78. package/dist/chunk-sm6fh5c5.js +569 -0
  79. package/dist/chunk-sm6fh5c5.js.map +14 -0
  80. package/dist/chunk-t9zcg8f1.js +499 -0
  81. package/dist/chunk-t9zcg8f1.js.map +13 -0
  82. package/dist/chunk-vp2db42d.js +121 -0
  83. package/dist/chunk-vp2db42d.js.map +10 -0
  84. package/dist/chunk-w13jjf4a.js +46 -0
  85. package/dist/chunk-w13jjf4a.js.map +10 -0
  86. package/dist/chunk-x1arr7ze.js +63 -0
  87. package/dist/chunk-x1arr7ze.js.map +10 -0
  88. package/dist/chunk-xfq68rch.js +583 -0
  89. package/dist/chunk-xfq68rch.js.map +11 -0
  90. package/dist/main.js +132 -20
  91. package/dist/main.js.map +4 -4
  92. package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
  93. package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
  94. package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
  95. package/dist/runtime/src/agents/archiver.ts +396 -0
  96. package/dist/runtime/src/agents/auth-check.ts +64 -0
  97. package/dist/runtime/src/agents/claude-provider.ts +737 -0
  98. package/dist/runtime/src/agents/claude-registry.ts +115 -0
  99. package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
  100. package/dist/runtime/src/agents/codex-provider.ts +785 -0
  101. package/dist/runtime/src/agents/codex-registry.ts +115 -0
  102. package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
  103. package/dist/runtime/src/agents/contracts.ts +69 -0
  104. package/dist/runtime/src/agents/deep-map.ts +13 -0
  105. package/dist/runtime/src/agents/execution-host.ts +91 -0
  106. package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
  107. package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
  108. package/dist/runtime/src/agents/fork.ts +65 -0
  109. package/dist/runtime/src/agents/hosted-agent.ts +48 -0
  110. package/dist/runtime/src/agents/idle-archiver.ts +128 -0
  111. package/dist/runtime/src/agents/index.ts +205 -0
  112. package/dist/runtime/src/agents/maestro-provider.ts +206 -0
  113. package/dist/runtime/src/agents/maestro-registry.ts +31 -0
  114. package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
  115. package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
  116. package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
  117. package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
  118. package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
  119. package/dist/runtime/src/agents/model-catalog.ts +151 -0
  120. package/dist/runtime/src/agents/registry.ts +24 -0
  121. package/dist/runtime/src/agents/rollout/claude.ts +331 -0
  122. package/dist/runtime/src/agents/rollout/codex.ts +569 -0
  123. package/dist/runtime/src/agents/rollout/shared.ts +205 -0
  124. package/dist/runtime/src/agents/self-config-core.ts +415 -0
  125. package/dist/runtime/src/agents/task-events.ts +44 -0
  126. package/dist/runtime/src/agents/tool-format.ts +352 -0
  127. package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
  128. package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
  129. package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
  130. package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
  131. package/dist/runtime/src/application/submit-user-message.ts +47 -0
  132. package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
  133. package/dist/runtime/src/application/switch-topic-model.ts +79 -0
  134. package/dist/runtime/src/application/topic-service.ts +139 -0
  135. package/dist/runtime/src/application/vault-command.ts +170 -0
  136. package/dist/runtime/src/bus.ts +319 -0
  137. package/dist/runtime/src/index.ts +333 -0
  138. package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
  139. package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
  140. package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
  141. package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
  142. package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
  143. package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
  144. package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
  145. package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
  146. package/dist/runtime/src/mcp/runtime-spec.ts +134 -0
  147. package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
  148. package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
  149. package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
  150. package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
  151. package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
  152. package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
  153. package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
  154. package/dist/runtime/src/mcp/system-health-server.ts +130 -0
  155. package/dist/runtime/src/mcp/task-server.ts +173 -0
  156. package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
  157. package/dist/runtime/src/mcp/vault-http.ts +219 -0
  158. package/dist/runtime/src/mcp/vault-run.ts +153 -0
  159. package/dist/runtime/src/mcp/vault-server.ts +104 -0
  160. package/dist/runtime/src/mcp/wiki-server.ts +755 -0
  161. package/dist/runtime/src/media/file-events.ts +33 -0
  162. package/dist/runtime/src/media/text-extractor.ts +312 -0
  163. package/dist/runtime/src/media/video.ts +92 -0
  164. package/dist/runtime/src/outbox/file-ops.ts +210 -0
  165. package/dist/runtime/src/outbox/index.ts +8 -0
  166. package/dist/runtime/src/outbox/utils.ts +51 -0
  167. package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
  168. package/dist/runtime/src/platform/config.ts +371 -0
  169. package/dist/runtime/src/platform/constants.ts +18 -0
  170. package/dist/runtime/src/platform/debug-users.ts +14 -0
  171. package/dist/runtime/src/platform/delay.ts +1 -0
  172. package/dist/runtime/src/platform/error.ts +9 -0
  173. package/dist/runtime/src/platform/file-utils.ts +16 -0
  174. package/dist/runtime/src/platform/jsonl.ts +202 -0
  175. package/dist/runtime/src/platform/lifecycle.ts +174 -0
  176. package/dist/runtime/src/platform/logger.ts +31 -0
  177. package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
  178. package/dist/runtime/src/platform/mcp-config.ts +865 -0
  179. package/dist/runtime/src/platform/modules.ts +145 -0
  180. package/dist/runtime/src/platform/node-plugins.ts +46 -0
  181. package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
  182. package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
  183. package/dist/runtime/src/prompts/builders.ts +357 -0
  184. package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
  185. package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
  186. package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
  187. package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
  188. package/dist/runtime/src/query/active-rooms.ts +532 -0
  189. package/dist/runtime/src/query/control.ts +313 -0
  190. package/dist/runtime/src/query/index.ts +24 -0
  191. package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
  192. package/dist/runtime/src/query/session-inbox-path.ts +42 -0
  193. package/dist/runtime/src/query/state.ts +94 -0
  194. package/dist/runtime/src/query/types.ts +83 -0
  195. package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
  196. package/dist/runtime/src/runtime/attachments.ts +151 -0
  197. package/dist/runtime/src/runtime/background-sessions.ts +83 -0
  198. package/dist/runtime/src/runtime/channel-context.ts +107 -0
  199. package/dist/runtime/src/runtime/errors.ts +69 -0
  200. package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
  201. package/dist/runtime/src/runtime/file-hooks.ts +81 -0
  202. package/dist/runtime/src/runtime/footer.ts +23 -0
  203. package/dist/runtime/src/runtime/inbox.ts +946 -0
  204. package/dist/runtime/src/runtime/tasks.ts +56 -0
  205. package/dist/runtime/src/runtime/topic-config.ts +29 -0
  206. package/dist/runtime/src/runtime/turn-runner.ts +2286 -0
  207. package/dist/runtime/src/runtime/usage-alert.ts +84 -0
  208. package/dist/runtime/src/runtime/visual-html.ts +109 -0
  209. package/dist/runtime/src/runtime/visual-store.ts +430 -0
  210. package/dist/runtime/src/runtime/visuals.ts +171 -0
  211. package/dist/runtime/src/security/sanitize.ts +27 -0
  212. package/dist/runtime/src/security/sensitive-path.ts +34 -0
  213. package/dist/runtime/src/storage/activity-log.ts +152 -0
  214. package/dist/runtime/src/storage/api-messages.ts +617 -0
  215. package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
  216. package/dist/runtime/src/storage/api-topic-config.ts +192 -0
  217. package/dist/runtime/src/storage/api-topics.ts +844 -0
  218. package/dist/runtime/src/storage/app-settings.ts +44 -0
  219. package/dist/runtime/src/storage/browser-profiles.ts +133 -0
  220. package/dist/runtime/src/storage/conversations.ts +268 -0
  221. package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
  222. package/dist/runtime/src/storage/forum/index.ts +4 -0
  223. package/dist/runtime/src/storage/forum/repository.ts +283 -0
  224. package/dist/runtime/src/storage/forum/schema.ts +300 -0
  225. package/dist/runtime/src/storage/forum-db.ts +23 -0
  226. package/dist/runtime/src/storage/runtime-events.ts +130 -0
  227. package/dist/runtime/src/storage/runtime-leases.ts +189 -0
  228. package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
  229. package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
  230. package/dist/runtime/src/storage/runtime-turn-requests.ts +300 -0
  231. package/dist/runtime/src/storage/self-schedules.ts +298 -0
  232. package/dist/runtime/src/storage/session-asks.ts +347 -0
  233. package/dist/runtime/src/storage/sqlite.ts +129 -0
  234. package/dist/runtime/src/storage/tasks.ts +179 -0
  235. package/dist/runtime/src/storage/token-stats.ts +144 -0
  236. package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
  237. package/dist/runtime/src/storage/topic-archive.ts +74 -0
  238. package/dist/runtime/src/storage/topic-settings.ts +67 -0
  239. package/dist/runtime/src/storage/topic-transcript.ts +98 -0
  240. package/dist/runtime/src/storage/vault-crypto.ts +65 -0
  241. package/dist/runtime/src/storage/vault.ts +225 -0
  242. package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
  243. package/dist/runtime/src/storage/wiki.ts +85 -0
  244. package/dist/runtime/src/topics/create.ts +123 -0
  245. package/dist/runtime/src/topics/derive.ts +398 -0
  246. package/dist/runtime/src/topics/lifecycle.ts +311 -0
  247. package/dist/runtime/src/topics/links.ts +7 -0
  248. package/dist/runtime/src/topics/personal-general.ts +65 -0
  249. package/dist/runtime/src/topics/session.ts +381 -0
  250. package/dist/runtime/src/types/api.ts +497 -0
  251. package/dist/runtime/src/types.ts +255 -0
  252. package/dist/runtime/src/version.ts +1 -0
  253. package/dist/runtime/tsconfig.json +13 -0
  254. package/package.json +16 -3
  255. package/dist/main.d.ts +0 -3
  256. package/dist/main.d.ts.map +0 -1
@@ -0,0 +1,283 @@
1
+ import { SESSION_AGENT } from "#platform/config";
2
+ import { type AgentKind, isAgentKind } from "#types";
3
+ import {
4
+ db,
5
+ type ForumTopicInfo,
6
+ logger,
7
+ rowToTopic,
8
+ type TopicRow,
9
+ type UserForumConfig,
10
+ type UserRow,
11
+ } from "./schema";
12
+
13
+ export function getUserConfig(userId: number): UserForumConfig | null {
14
+ const user = db.query<UserRow, string>("SELECT * FROM users WHERE id = ?").get(String(userId));
15
+ if (!user) return null;
16
+
17
+ const topicRows = db
18
+ .query<TopicRow, string>("SELECT * FROM topics WHERE user_id = ?")
19
+ .all(String(userId));
20
+ const topics: { [name: string]: ForumTopicInfo } = {};
21
+ for (const row of topicRows) topics[row.name] = rowToTopic(row);
22
+
23
+ return {
24
+ ...(user.dm_session_id && { dmSessionId: user.dm_session_id }),
25
+ ...(user.communicate_thread_id != null && { communicateThreadId: user.communicate_thread_id }),
26
+ topics,
27
+ };
28
+ }
29
+
30
+ export function getTopicByName(userId: number, name: string): ForumTopicInfo | null {
31
+ const row = db
32
+ .query<TopicRow, [string, string]>("SELECT * FROM topics WHERE user_id = ? AND name = ?")
33
+ .get(String(userId), name);
34
+ return row ? rowToTopic(row) : null;
35
+ }
36
+
37
+ export function findUserByThread(
38
+ threadId: number,
39
+ ): { userId: number; topic: ForumTopicInfo } | null {
40
+ const row = db
41
+ .query<TopicRow, number>("SELECT * FROM topics WHERE message_thread_id = ?")
42
+ .get(threadId);
43
+ if (!row) return null;
44
+ return { userId: Number(row.user_id), topic: rowToTopic(row) };
45
+ }
46
+
47
+ export function getSessionForTopic(userId: number, topicName: string): string | null {
48
+ const row = db
49
+ .query<{ session_id: string | null }, [string, string]>(
50
+ "SELECT session_id FROM topics WHERE user_id = ? AND name = ?",
51
+ )
52
+ .get(String(userId), topicName);
53
+ return row?.session_id || null;
54
+ }
55
+
56
+ export function getTopicNames(userId: number): string[] {
57
+ const rows = db
58
+ .query<{ name: string }, string>("SELECT name FROM topics WHERE user_id = ?")
59
+ .all(String(userId));
60
+ return rows.map((r) => r.name);
61
+ }
62
+
63
+ export function getAllTopics(userId: number): ForumTopicInfo[] {
64
+ const rows = db
65
+ .query<TopicRow, string>("SELECT * FROM topics WHERE user_id = ?")
66
+ .all(String(userId));
67
+ return rows.map(rowToTopic);
68
+ }
69
+
70
+ export function getAllUserIds(): number[] {
71
+ const rows = db.query<{ id: string }, []>("SELECT id FROM users").all();
72
+ return rows.map((r) => Number(r.id)).filter((n) => !Number.isNaN(n));
73
+ }
74
+
75
+ export function getCommunicateThreadId(userId: number): number | null {
76
+ const row = db
77
+ .query<{ communicate_thread_id: number | null }, string>(
78
+ "SELECT communicate_thread_id FROM users WHERE id = ?",
79
+ )
80
+ .get(String(userId));
81
+ return row?.communicate_thread_id ?? null;
82
+ }
83
+
84
+ export function getDmSessionId(userId: number): string | null {
85
+ const row = db
86
+ .query<{ dm_session_id: string | null }, string>("SELECT dm_session_id FROM users WHERE id = ?")
87
+ .get(String(userId));
88
+ return row?.dm_session_id ?? null;
89
+ }
90
+
91
+ export function getTopicDescription(userId: number, topicName: string): string | null {
92
+ const row = db
93
+ .query<{ description: string | null }, [string, string]>(
94
+ "SELECT description FROM topics WHERE user_id = ? AND name = ?",
95
+ )
96
+ .get(String(userId), topicName);
97
+ return row?.description || null;
98
+ }
99
+
100
+ export function getTopicMcpConfig(
101
+ userId: number,
102
+ topicName: string,
103
+ ): { enabled: string[] | null; extra: Record<string, unknown> } {
104
+ const row = db
105
+ .query<{ mcp_enabled: string | null; mcp_extra: string | null }, [string, string]>(
106
+ "SELECT mcp_enabled, mcp_extra FROM topics WHERE user_id = ? AND name = ?",
107
+ )
108
+ .get(String(userId), topicName);
109
+ let enabled: string[] | null = null;
110
+ let extra: Record<string, unknown> = {};
111
+ try {
112
+ if (row?.mcp_enabled) enabled = JSON.parse(row.mcp_enabled);
113
+ } catch (e) {
114
+ logger.warn({ err: e }, "getTopicMcpConfig: failed to parse mcp_enabled");
115
+ }
116
+ try {
117
+ if (row?.mcp_extra) extra = JSON.parse(row.mcp_extra);
118
+ } catch (e) {
119
+ logger.warn({ err: e }, "getTopicMcpConfig: failed to parse mcp_extra");
120
+ }
121
+ return { enabled, extra };
122
+ }
123
+
124
+ export function addTopic(
125
+ userId: number,
126
+ name: string,
127
+ messageThreadId: number,
128
+ sessionId?: string,
129
+ createdAt?: string,
130
+ ): boolean {
131
+ if (name.startsWith("__")) {
132
+ logger.warn({ name }, "addTopic: refusing reserved __ prefix");
133
+ return false;
134
+ }
135
+ db.query("INSERT OR IGNORE INTO users (id) VALUES (?)").run(String(userId));
136
+ db.query(`
137
+ INSERT INTO topics (user_id, name, message_thread_id, session_id, created_at, agent)
138
+ VALUES (?, ?, ?, ?, ?, ?)
139
+ ON CONFLICT(user_id, name) DO UPDATE SET
140
+ message_thread_id = excluded.message_thread_id,
141
+ session_id = COALESCE(excluded.session_id, topics.session_id),
142
+ created_at = excluded.created_at
143
+ `).run(
144
+ String(userId),
145
+ name,
146
+ messageThreadId,
147
+ sessionId ?? null,
148
+ createdAt ?? new Date().toISOString(),
149
+ SESSION_AGENT,
150
+ );
151
+ return true;
152
+ }
153
+
154
+ export function renameTopic(
155
+ userId: number,
156
+ oldName: string,
157
+ newName: string,
158
+ ): { ok: true } | { ok: false; reason: "not_found" | "name_taken" } {
159
+ if (oldName === newName) return { ok: true };
160
+ if (getTopicByName(userId, newName)) return { ok: false, reason: "name_taken" };
161
+ const result = db
162
+ .prepare("UPDATE topics SET name = ? WHERE user_id = ? AND name = ?")
163
+ .run(newName, String(userId), oldName);
164
+ if (result.changes === 0) return { ok: false, reason: "not_found" };
165
+ return { ok: true };
166
+ }
167
+
168
+ export function removeTopic(userId: number, name: string) {
169
+ db.query("DELETE FROM topics WHERE user_id = ? AND name = ?").run(String(userId), name);
170
+ }
171
+
172
+ export function setSessionForTopic(userId: number, topicName: string, sessionId: string) {
173
+ db.query("UPDATE topics SET session_id = ? WHERE user_id = ? AND name = ?").run(
174
+ sessionId,
175
+ String(userId),
176
+ topicName,
177
+ );
178
+ }
179
+
180
+ export function clearSessionForTopic(userId: number, topicName: string) {
181
+ db.query("UPDATE topics SET session_id = NULL WHERE user_id = ? AND name = ?").run(
182
+ String(userId),
183
+ topicName,
184
+ );
185
+ }
186
+
187
+ export function setDmSessionId(userId: number, sessionId: string) {
188
+ db.query(`
189
+ INSERT INTO users (id, dm_session_id) VALUES (?, ?)
190
+ ON CONFLICT(id) DO UPDATE SET dm_session_id = excluded.dm_session_id
191
+ `).run(String(userId), sessionId);
192
+ }
193
+
194
+ export function clearDmSessionId(userId: number) {
195
+ db.query("UPDATE users SET dm_session_id = NULL WHERE id = ?").run(String(userId));
196
+ }
197
+
198
+ export function setTopicDescription(
199
+ userId: number,
200
+ topicName: string,
201
+ description: string,
202
+ ): boolean {
203
+ const result = db
204
+ .query("UPDATE topics SET description = ? WHERE user_id = ? AND name = ?")
205
+ .run(description, String(userId), topicName);
206
+ return result.changes > 0;
207
+ }
208
+
209
+ export function getLastShownConfig(
210
+ userId: number,
211
+ topicName: string,
212
+ ): { agent: AgentKind | null; model: string | null; effort: string | null } | null {
213
+ type LastShownRow = {
214
+ last_shown_agent: string | null;
215
+ last_shown_model: string | null;
216
+ last_shown_effort: string | null;
217
+ };
218
+ const row = db
219
+ .query<LastShownRow, [string, string]>(
220
+ "SELECT last_shown_agent, last_shown_model, last_shown_effort FROM topics WHERE user_id = ? AND name = ?",
221
+ )
222
+ .get(String(userId), topicName);
223
+ if (!row) return null;
224
+ const agent =
225
+ row.last_shown_agent && isAgentKind(row.last_shown_agent) ? row.last_shown_agent : null;
226
+ return { agent, model: row.last_shown_model, effort: row.last_shown_effort };
227
+ }
228
+
229
+ export function setLastShownConfig(
230
+ userId: number,
231
+ topicName: string,
232
+ agent: AgentKind,
233
+ model: string,
234
+ effort: string | undefined,
235
+ ): boolean {
236
+ const result = db
237
+ .query(
238
+ "UPDATE topics SET last_shown_agent = ?, last_shown_model = ?, last_shown_effort = ? WHERE user_id = ? AND name = ?",
239
+ )
240
+ .run(agent, model, effort ?? null, String(userId), topicName);
241
+ return result.changes > 0;
242
+ }
243
+
244
+ export function setTopicForkOrigin(userId: number, topicName: string, origin: string): boolean {
245
+ const result = db
246
+ .query("UPDATE topics SET fork_origin = ? WHERE user_id = ? AND name = ?")
247
+ .run(origin, String(userId), topicName);
248
+ return result.changes > 0;
249
+ }
250
+
251
+ export function updateTopicThreadId(
252
+ userId: number,
253
+ topicName: string,
254
+ newThreadId: number,
255
+ ): boolean {
256
+ const result = db
257
+ .query("UPDATE topics SET message_thread_id = ? WHERE user_id = ? AND name = ?")
258
+ .run(newThreadId, String(userId), topicName);
259
+ return result.changes > 0;
260
+ }
261
+
262
+ export function setTopicMcpEnabled(
263
+ userId: number,
264
+ topicName: string,
265
+ enabled: string[] | null,
266
+ ): boolean {
267
+ const value = enabled !== null ? JSON.stringify(enabled) : null;
268
+ const result = db
269
+ .query("UPDATE topics SET mcp_enabled = ? WHERE user_id = ? AND name = ?")
270
+ .run(value, String(userId), topicName);
271
+ return result.changes > 0;
272
+ }
273
+
274
+ export function setTopicMcpExtra(
275
+ userId: number,
276
+ topicName: string,
277
+ extra: Record<string, unknown>,
278
+ ): boolean {
279
+ const result = db
280
+ .query("UPDATE topics SET mcp_extra = ? WHERE user_id = ? AND name = ?")
281
+ .run(JSON.stringify(extra), String(userId), topicName);
282
+ return result.changes > 0;
283
+ }
@@ -0,0 +1,300 @@
1
+ import { errMsg } from "#platform/error";
2
+ import { logger } from "#platform/logger";
3
+ import { db } from "#storage/forum-db";
4
+ import { type AgentKind, isAgentKind } from "#types";
5
+
6
+ export { db };
7
+
8
+ export interface ForumTopicInfo {
9
+ messageThreadId: number;
10
+ sessionId: string;
11
+ createdAt: string;
12
+ name: string;
13
+ description?: string;
14
+ forkOrigin?: string;
15
+ agent: AgentKind;
16
+ }
17
+
18
+ export interface UserForumConfig {
19
+ communicateThreadId?: number;
20
+ dmSessionId?: string;
21
+ topics: { [topicName: string]: ForumTopicInfo };
22
+ }
23
+
24
+ export type TopicRow = {
25
+ user_id: string;
26
+ name: string;
27
+ message_thread_id: number;
28
+ session_id: string | null;
29
+ created_at: string;
30
+ description: string | null;
31
+ fork_origin: string | null;
32
+ agent: string | null;
33
+ };
34
+
35
+ export type UserRow = {
36
+ id: string;
37
+ dm_session_id: string | null;
38
+ communicate_thread_id: number | null;
39
+ };
40
+
41
+ db.exec(`
42
+ CREATE TABLE IF NOT EXISTS users (
43
+ id TEXT PRIMARY KEY,
44
+ dm_session_id TEXT,
45
+ communicate_thread_id INTEGER
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS topics (
49
+ user_id TEXT NOT NULL REFERENCES users(id),
50
+ name TEXT NOT NULL,
51
+ message_thread_id INTEGER NOT NULL,
52
+ session_id TEXT,
53
+ created_at TEXT NOT NULL,
54
+ description TEXT,
55
+ fork_origin TEXT,
56
+ agent TEXT NOT NULL DEFAULT 'claude',
57
+ mcp_enabled TEXT,
58
+ mcp_extra TEXT,
59
+ last_shown_model TEXT,
60
+ last_shown_effort TEXT,
61
+ last_shown_agent TEXT,
62
+ PRIMARY KEY (user_id, name),
63
+ UNIQUE (user_id, message_thread_id)
64
+ );
65
+
66
+ CREATE INDEX IF NOT EXISTS idx_topics_lookup ON topics(user_id, message_thread_id);
67
+ `);
68
+
69
+ function tryMigrate(sql: string, expectedMsg?: string): void {
70
+ try {
71
+ db.exec(sql);
72
+ } catch (e) {
73
+ if (expectedMsg && errMsg(e).includes(expectedMsg)) return;
74
+ logger.error({ err: e, sql }, "DB migration failed");
75
+ throw e;
76
+ }
77
+ }
78
+
79
+ type SqlValue = string | number | bigint | boolean | null | Uint8Array;
80
+
81
+ function sqlValue(value: unknown): SqlValue {
82
+ if (value === undefined || value === null) return null;
83
+ if (
84
+ typeof value === "string" ||
85
+ typeof value === "number" ||
86
+ typeof value === "bigint" ||
87
+ typeof value === "boolean"
88
+ ) {
89
+ return value;
90
+ }
91
+ if (value instanceof Uint8Array) return value;
92
+ return String(value);
93
+ }
94
+
95
+ function columnExists(table: string, column: string): boolean {
96
+ return db
97
+ .query<{ name: string }, []>(`PRAGMA table_info(${table})`)
98
+ .all()
99
+ .some((c) => c.name === column);
100
+ }
101
+
102
+ function tableSql(table: string): string | null {
103
+ return (
104
+ db
105
+ .query<{ sql: string }, string>(
106
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
107
+ )
108
+ .get(table)?.sql ?? null
109
+ );
110
+ }
111
+
112
+ function dropColumnIfExists(table: string, column: string): void {
113
+ if (columnExists(table, column)) {
114
+ tryMigrate(`ALTER TABLE ${table} DROP COLUMN ${column}`);
115
+ }
116
+ }
117
+
118
+ function topicTableNeedsRebuild(): boolean {
119
+ const sql = tableSql("topics");
120
+ if (!sql) return false;
121
+ const cols = db.query<{ name: string }, []>("PRAGMA table_info(topics)").all();
122
+ const names = new Set(cols.map((c) => c.name));
123
+ return (
124
+ names.has("forum_group_id") ||
125
+ names.has("system_prompt_extra") ||
126
+ names.has("model") ||
127
+ names.has("effort") ||
128
+ names.has("model_pinned") ||
129
+ names.has("effort_pinned") ||
130
+ names.has("memory_files") ||
131
+ names.has("memory_summary") ||
132
+ names.has("privacy_mode") ||
133
+ names.has("advisor_enabled") ||
134
+ names.has("agent_settings") ||
135
+ !/PRIMARY KEY \(\s*user_id\s*,\s*name\s*\)/i.test(sql)
136
+ );
137
+ }
138
+
139
+ function normalizeStoredAgent(value: unknown): AgentKind {
140
+ if (isAgentKind(value)) return value;
141
+ if (value === "hermes" || value === "alpha") return "maestro";
142
+ return "claude";
143
+ }
144
+
145
+ function rebuildTopicsTableIfNeeded(): void {
146
+ if (!topicTableNeedsRebuild()) return;
147
+
148
+ const cols = db.query<{ name: string }, []>("PRAGMA table_info(topics)").all();
149
+ const has = new Set(cols.map((c) => c.name));
150
+ const pick = (row: Record<string, unknown>, column: string, fallback: unknown = null) =>
151
+ has.has(column) ? (row[column] ?? fallback) : fallback;
152
+ const descriptionFor = (row: Record<string, unknown>) =>
153
+ pick(row, "description", pick(row, "system_prompt_extra", null));
154
+
155
+ const rows = db
156
+ .query<Record<string, unknown>, []>("SELECT rowid AS __rowid, * FROM topics")
157
+ .all()
158
+ .sort((a, b) => {
159
+ const aCreated = String(pick(a, "created_at", ""));
160
+ const bCreated = String(pick(b, "created_at", ""));
161
+ const byDate = bCreated.localeCompare(aCreated);
162
+ if (byDate !== 0) return byDate;
163
+ return Number(b.__rowid ?? 0) - Number(a.__rowid ?? 0);
164
+ });
165
+
166
+ const previousForeignKeys = db
167
+ .query<{ foreign_keys: number }, []>("PRAGMA foreign_keys")
168
+ .get()?.foreign_keys;
169
+ db.exec("PRAGMA foreign_keys = OFF");
170
+ try {
171
+ db.transaction(() => {
172
+ db.exec("DROP TABLE IF EXISTS topics_new");
173
+ db.exec(`
174
+ CREATE TABLE topics_new (
175
+ user_id TEXT NOT NULL REFERENCES users(id),
176
+ name TEXT NOT NULL,
177
+ message_thread_id INTEGER NOT NULL,
178
+ session_id TEXT,
179
+ created_at TEXT NOT NULL,
180
+ description TEXT,
181
+ fork_origin TEXT,
182
+ agent TEXT NOT NULL DEFAULT 'claude',
183
+ mcp_enabled TEXT,
184
+ mcp_extra TEXT,
185
+ last_shown_model TEXT,
186
+ last_shown_effort TEXT,
187
+ last_shown_agent TEXT,
188
+ PRIMARY KEY (user_id, name),
189
+ UNIQUE (user_id, message_thread_id)
190
+ )
191
+ `);
192
+
193
+ const insert = db.query(`
194
+ INSERT OR IGNORE INTO topics_new
195
+ (user_id, name, message_thread_id, session_id, created_at, description,
196
+ fork_origin, agent, mcp_enabled, mcp_extra, last_shown_model,
197
+ last_shown_effort, last_shown_agent)
198
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
199
+ `);
200
+ let inserted = 0;
201
+ const fallbackCreatedAt = new Date().toISOString();
202
+ for (const row of rows) {
203
+ const result = insert.run(
204
+ String(pick(row, "user_id", "")),
205
+ String(pick(row, "name", "")),
206
+ Number(pick(row, "message_thread_id", 0)),
207
+ sqlValue(pick(row, "session_id", null)),
208
+ String(pick(row, "created_at", fallbackCreatedAt)),
209
+ sqlValue(descriptionFor(row)),
210
+ sqlValue(pick(row, "fork_origin", null)),
211
+ normalizeStoredAgent(pick(row, "agent", "claude")),
212
+ sqlValue(pick(row, "mcp_enabled", null)),
213
+ sqlValue(pick(row, "mcp_extra", null)),
214
+ sqlValue(pick(row, "last_shown_model", null)),
215
+ sqlValue(pick(row, "last_shown_effort", null)),
216
+ sqlValue(pick(row, "last_shown_agent", null)),
217
+ );
218
+ if (Number(result.changes ?? 0) > 0) inserted += 1;
219
+ }
220
+
221
+ db.exec("DROP TABLE topics");
222
+ db.exec("ALTER TABLE topics_new RENAME TO topics");
223
+ db.exec("CREATE INDEX IF NOT EXISTS idx_topics_lookup ON topics(user_id, message_thread_id)");
224
+ logger.info(
225
+ { migrated: inserted, skippedConflicts: rows.length - inserted },
226
+ "topics schema migrated to current user-scoped schema",
227
+ );
228
+ })();
229
+ } finally {
230
+ db.exec(`PRAGMA foreign_keys = ${previousForeignKeys ? "ON" : "OFF"}`);
231
+ }
232
+ }
233
+
234
+ rebuildTopicsTableIfNeeded();
235
+
236
+ tryMigrate("ALTER TABLE topics ADD COLUMN fork_origin TEXT", "duplicate column");
237
+ tryMigrate(
238
+ "ALTER TABLE topics ADD COLUMN agent TEXT NOT NULL DEFAULT 'claude'",
239
+ "duplicate column",
240
+ );
241
+ tryMigrate("ALTER TABLE topics ADD COLUMN mcp_enabled TEXT", "duplicate column");
242
+ tryMigrate("ALTER TABLE topics ADD COLUMN mcp_extra TEXT", "duplicate column");
243
+ tryMigrate("ALTER TABLE topics ADD COLUMN last_shown_model TEXT", "duplicate column");
244
+ tryMigrate("ALTER TABLE topics ADD COLUMN last_shown_effort TEXT", "duplicate column");
245
+ tryMigrate("ALTER TABLE topics ADD COLUMN last_shown_agent TEXT", "duplicate column");
246
+
247
+ for (const column of ["privacy_mode", "advisor_enabled", "agent_settings"]) {
248
+ dropColumnIfExists("topics", column);
249
+ }
250
+ dropColumnIfExists("users", "manager_session_id");
251
+
252
+ tryMigrate("DROP INDEX idx_topics_lookup", "no such index");
253
+ tryMigrate("CREATE INDEX IF NOT EXISTS idx_topics_lookup ON topics(user_id, message_thread_id)");
254
+
255
+ function assertColumn(table: string, column: string): void {
256
+ if (!columnExists(table, column)) {
257
+ throw new Error(`Schema migration failed: ${table}.${column} is missing`);
258
+ }
259
+ }
260
+
261
+ for (const column of ["id", "dm_session_id", "communicate_thread_id"]) {
262
+ assertColumn("users", column);
263
+ }
264
+ for (const column of [
265
+ "user_id",
266
+ "name",
267
+ "message_thread_id",
268
+ "session_id",
269
+ "created_at",
270
+ "description",
271
+ "fork_origin",
272
+ "agent",
273
+ "mcp_enabled",
274
+ "mcp_extra",
275
+ "last_shown_model",
276
+ "last_shown_effort",
277
+ "last_shown_agent",
278
+ ]) {
279
+ assertColumn("topics", column);
280
+ }
281
+
282
+ export function rowToTopic(row: TopicRow): ForumTopicInfo {
283
+ if (!isAgentKind(row.agent)) throw new Error(`Invalid agent in DB: ${row.agent}`);
284
+ const agent: AgentKind = row.agent;
285
+ return {
286
+ messageThreadId: row.message_thread_id,
287
+ sessionId: row.session_id ?? "",
288
+ createdAt: row.created_at,
289
+ name: row.name,
290
+ agent,
291
+ ...(row.description && { description: row.description }),
292
+ ...(row.fork_origin && { forkOrigin: row.fork_origin }),
293
+ };
294
+ }
295
+
296
+ export function flushSessionCache() {
297
+ db.close();
298
+ }
299
+
300
+ export { logger };
@@ -0,0 +1,23 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { SESSIONS_DB } from "#platform/config";
4
+ import { Database } from "#storage/sqlite";
5
+
6
+ mkdirSync(dirname(SESSIONS_DB), { recursive: true });
7
+
8
+ export const db = new Database(SESSIONS_DB, { create: true });
9
+ db.exec("PRAGMA journal_mode = WAL");
10
+ db.exec("PRAGMA foreign_keys = ON");
11
+ db.exec("PRAGMA busy_timeout = 5000");
12
+ // Auto-checkpoint every ~1000 WAL pages (~4 MB at 4KB page size) to keep the
13
+ // WAL file from growing unbounded during long-running pm2 sessions. The
14
+ // default is 1000 but we set it explicitly so the policy is auditable.
15
+ db.exec("PRAGMA wal_autocheckpoint = 1000");
16
+ // Also run a TRUNCATE checkpoint on startup to reclaim any WAL space
17
+ // accumulated while the bot was stopped (checkpointer doesn't run when
18
+ // no connections are open).
19
+ try {
20
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
21
+ } catch {
22
+ // Non-fatal; a concurrent writer may hold the WAL briefly.
23
+ }