@johpaz/hive-sdk 0.1.4 → 0.1.6

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 (278) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/README.md +78 -23
  3. package/bun.lock +55 -29
  4. package/bunfig.toml +4 -2
  5. package/docs/API-AGENTS.md +78 -27
  6. package/docs/API-CONTEXT-COMPILER.md +31 -34
  7. package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
  8. package/docs/HIVE-HARNESS.md +1 -1
  9. package/docs/INDEX.md +4 -4
  10. package/docs/TEMPLATE-HIVE-APP.md +10 -10
  11. package/package.json +17 -12
  12. package/packages/cli/package.json +2 -2
  13. package/packages/cli/src/commands/create-app.test.ts +36 -7
  14. package/packages/cli/src/commands/init.ts +3 -3
  15. package/packages/cli/src/commands/run.ts +1 -1
  16. package/packages/cli/src/commands/test.ts +37 -25
  17. package/packages/cli/src/commands/trace.ts +30 -28
  18. package/packages/cli/templates/hive-app/.env.example +10 -2
  19. package/packages/cli/templates/hive-app/README.md +103 -0
  20. package/packages/cli/templates/hive-app/hive.config.ts +9 -3
  21. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
  22. package/packages/cli/templates/hive-app/src/main.ts +12 -19
  23. package/packages/core/package.json +13 -12
  24. package/packages/core/src/agent/acceptance-checks.ts +172 -0
  25. package/packages/core/src/agent/agent-catalog.ts +348 -0
  26. package/packages/core/src/agent/agent-loop.ts +1373 -0
  27. package/packages/core/src/agent/capability-search.ts +186 -0
  28. package/packages/core/src/agent/catalog-selector.ts +103 -0
  29. package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
  30. package/packages/core/src/agent/context-compiler.ts +689 -0
  31. package/packages/core/src/agent/conversation-store.ts +381 -0
  32. package/packages/core/src/agent/curator.ts +276 -0
  33. package/packages/core/src/agent/delegation-runtime.ts +241 -0
  34. package/packages/core/src/agent/goal-runner.ts +323 -0
  35. package/packages/core/src/agent/index.ts +17 -12
  36. package/packages/core/src/agent/llm-client.ts +266 -0
  37. package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
  38. package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
  39. package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
  40. package/packages/core/src/agent/llm-providers/groq.ts +5 -0
  41. package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
  42. package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
  43. package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
  44. package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
  45. package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
  46. package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
  47. package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
  48. package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
  49. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
  50. package/packages/core/src/agent/llm-providers/openai.ts +5 -0
  51. package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
  52. package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
  53. package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
  54. package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
  55. package/packages/core/src/agent/minimal-loadout.ts +47 -0
  56. package/packages/core/src/agent/playbook-selector.ts +119 -0
  57. package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
  58. package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
  59. package/packages/core/src/agent/providers/index.ts +35 -16
  60. package/packages/core/src/agent/reflector.ts +320 -0
  61. package/packages/core/src/agent/routing-intent.ts +22 -0
  62. package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
  63. package/packages/core/src/{harness → agent}/run-store.ts +142 -81
  64. package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
  65. package/packages/core/src/agent/skill-selector.ts +374 -0
  66. package/packages/core/src/agent/stuck-loop.ts +209 -0
  67. package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
  68. package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
  69. package/packages/core/src/api/createAgent.test.ts +139 -27
  70. package/packages/core/src/api/createAgent.ts +232 -44
  71. package/packages/core/src/artifacts/store.ts +162 -0
  72. package/packages/core/src/canvas/canvas-manager.ts +161 -0
  73. package/packages/core/src/canvas/canvas.test.ts +8 -4
  74. package/packages/core/src/canvas/emitter.ts +131 -80
  75. package/packages/core/src/canvas/index.ts +1 -3
  76. package/packages/core/src/channels/base.ts +9 -1
  77. package/packages/core/src/channels/discord.ts +5 -4
  78. package/packages/core/src/channels/manager.ts +122 -30
  79. package/packages/core/src/channels/slack.ts +5 -4
  80. package/packages/core/src/channels/telegram.ts +36 -6
  81. package/packages/core/src/channels/webchat.ts +11 -10
  82. package/packages/core/src/channels/whatsapp.ts +23 -7
  83. package/packages/core/src/config/index.ts +13 -2
  84. package/packages/core/src/config/loader.ts +76 -29
  85. package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
  86. package/packages/core/src/ethics/EthicsGuard.ts +51 -47
  87. package/packages/core/src/events/agent-bus.ts +44 -68
  88. package/packages/core/src/events/channel-narration.ts +150 -0
  89. package/packages/core/src/events/narration.ts +82 -0
  90. package/packages/core/src/events/tool-narration.ts +62 -0
  91. package/packages/core/src/gateway/delegation-groups.ts +258 -0
  92. package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
  93. package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
  94. package/packages/core/src/gateway/lane-queue.ts +173 -0
  95. package/packages/core/src/gateway/notification-inbox.ts +57 -0
  96. package/packages/core/src/gateway/server.ts +1 -1
  97. package/packages/core/src/harness/index.ts +46 -27
  98. package/packages/core/src/index.ts +33 -27
  99. package/packages/core/src/mcp/hot-reload.ts +32 -23
  100. package/packages/core/src/mcp/index.ts +6 -3
  101. package/packages/core/src/mcp/singleton.ts +1 -4
  102. package/packages/core/src/mcp/tool-sync.ts +138 -0
  103. package/packages/core/src/memory/Scratchpad.test.ts +39 -20
  104. package/packages/core/src/memory/Scratchpad.ts +27 -34
  105. package/packages/core/src/multimodal/vision-service.ts +44 -38
  106. package/packages/core/src/resilience/retry.ts +95 -0
  107. package/packages/core/src/scheduler/CronScheduler.ts +334 -287
  108. package/packages/core/src/scheduler/index.ts +9 -7
  109. package/packages/core/src/scheduler/integration.ts +46 -26
  110. package/packages/core/src/scheduler/scheduler.test.ts +9 -13
  111. package/packages/core/src/scheduler/types.ts +7 -2
  112. package/packages/core/src/security/Pairing.ts +1 -1
  113. package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
  114. package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
  115. package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
  116. package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
  117. package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
  118. package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
  119. package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
  120. package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
  121. package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
  122. package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
  123. package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
  124. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
  125. package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
  126. package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
  127. package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
  128. package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
  129. package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
  130. package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
  131. package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
  132. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
  133. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
  134. package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
  135. package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
  136. package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
  137. package/packages/core/src/skills/skills.test.ts +52 -11
  138. package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
  139. package/packages/core/src/storage/bootstrap.ts +151 -0
  140. package/packages/core/src/storage/causal-events.ts +84 -0
  141. package/packages/core/src/storage/collections.ts +680 -0
  142. package/packages/core/src/storage/crypto.ts +205 -74
  143. package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
  144. package/packages/core/src/storage/hivedb.ts +61 -0
  145. package/packages/core/src/storage/index.ts +111 -18
  146. package/packages/core/src/storage/model-id.ts +53 -0
  147. package/packages/core/src/storage/onboarding.ts +540 -972
  148. package/packages/core/src/storage/reconcile.ts +238 -0
  149. package/packages/core/src/storage/seed.ts +572 -406
  150. package/packages/core/src/storage/usage.ts +285 -225
  151. package/packages/core/src/storage/user-email.ts +11 -0
  152. package/packages/core/src/swarm/AgentExecutor.ts +1 -1
  153. package/packages/core/src/swarm/EventBridge.ts +1 -1
  154. package/packages/core/src/swarm/index.ts +12 -9
  155. package/packages/core/src/tool-runtime/index.ts +146 -23
  156. package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
  157. package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
  158. package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
  159. package/packages/core/src/tools/agents/get-available-models.ts +36 -54
  160. package/packages/core/src/tools/agents/index.ts +784 -292
  161. package/packages/core/src/tools/api/api-request.test.ts +164 -0
  162. package/packages/core/src/tools/api/api-request.ts +174 -0
  163. package/packages/core/src/tools/api/index.ts +16 -0
  164. package/packages/core/src/tools/cli/index.ts +4 -0
  165. package/packages/core/src/tools/core/index.ts +281 -112
  166. package/packages/core/src/tools/cron/index.ts +121 -124
  167. package/packages/core/src/tools/index.ts +63 -78
  168. package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
  169. package/packages/core/src/tools/types.ts +3 -1
  170. package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
  171. package/packages/core/src/tools/web/browser-backend.ts +129 -0
  172. package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
  173. package/packages/core/src/tools/web/browser-service.ts +80 -35
  174. package/packages/core/src/tools/web/browser-type.ts +3 -8
  175. package/packages/core/src/tools/web/index.ts +4 -4
  176. package/packages/core/src/tools/web/webview-backend.ts +412 -0
  177. package/packages/core/src/voice/index.ts +89 -63
  178. package/packages/core/src/workers/agent.worker.ts +2 -2
  179. package/packages/core/src/workers/workers.test.ts +3 -10
  180. package/scripts/bump-version.ts +248 -0
  181. package/scripts/generate-skill-bundle.ts +108 -0
  182. package/test/acceptance-checks.test.ts +403 -0
  183. package/test/agent-loop-terminal-synthesis.test.ts +32 -0
  184. package/test/browser-backend.test.ts +308 -0
  185. package/test/catalog-agents-stay-enabled.test.ts +117 -0
  186. package/test/causal-events.test.ts +117 -0
  187. package/test/compaction.test.ts +105 -0
  188. package/test/context-compiler.test.ts +269 -0
  189. package/test/curator.test.ts +130 -0
  190. package/test/durable-queue.test.ts +114 -0
  191. package/test/harness-barrel.test.ts +64 -0
  192. package/test/hive-helpers.test.ts +130 -0
  193. package/test/hivedb-search.test.ts +189 -0
  194. package/test/internal-turns.test.ts +166 -0
  195. package/test/job-idempotency.test.ts +68 -0
  196. package/test/job-retry-backoff.test.ts +184 -0
  197. package/test/job-store.test.ts +381 -0
  198. package/test/llm-retry.test.ts +97 -0
  199. package/test/memory-perf.test.ts +774 -0
  200. package/test/minimal-loadout.test.ts +78 -0
  201. package/test/model-catalog.test.ts +105 -0
  202. package/test/preload.ts +12 -0
  203. package/test/reflector.test.ts +320 -0
  204. package/test/retention-cap.test.ts +91 -0
  205. package/test/retired-capabilities-pruned.test.ts +192 -0
  206. package/test/run-store.test.ts +355 -0
  207. package/test/scratchpad.test.ts +74 -0
  208. package/test/secrets-durability.test.ts +119 -0
  209. package/test/seed-model-reseed.test.ts +155 -0
  210. package/test/setup-agent-seed.test.ts +264 -0
  211. package/test/tool-inventory.test.ts +65 -0
  212. package/test/tool-runtime.test.ts +258 -0
  213. package/test/tool-selector-runtime-tools.test.ts +117 -0
  214. package/test/toon.test.ts +429 -0
  215. package/tsconfig.json +2 -0
  216. package/packages/core/src/ace/Curator.ts +0 -158
  217. package/packages/core/src/ace/Reflector.ts +0 -200
  218. package/packages/core/src/ace/index.ts +0 -4
  219. package/packages/core/src/agent/AgentRunner.ts +0 -711
  220. package/packages/core/src/agent/ContextCompiler.ts +0 -567
  221. package/packages/core/src/agent/ContextGuard.ts +0 -91
  222. package/packages/core/src/agent/ConversationStore.ts +0 -254
  223. package/packages/core/src/agent/Hooks.ts +0 -166
  224. package/packages/core/src/agent/StuckLoop.ts +0 -133
  225. package/packages/core/src/agent/providers/LLMClient.ts +0 -149
  226. package/packages/core/src/agent/providers/anthropic.ts +0 -212
  227. package/packages/core/src/agent/providers/openai-compat.ts +0 -231
  228. package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
  229. package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
  230. package/packages/core/src/agent/selectors/index.ts +0 -6
  231. package/packages/core/src/auth/auth.ts +0 -121
  232. package/packages/core/src/auth/index.ts +0 -1
  233. package/packages/core/src/canvas/CanvasManager.ts +0 -390
  234. package/packages/core/src/canvas/canvas-tools.ts +0 -448
  235. package/packages/core/src/harness/collections.ts +0 -98
  236. package/packages/core/src/harness/goal-verifier.ts +0 -141
  237. package/packages/core/src/harness/harness.test.ts +0 -236
  238. package/packages/core/src/harness/reconcile.ts +0 -149
  239. package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
  240. package/packages/core/src/multimodal/VisionService.ts +0 -293
  241. package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
  242. package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
  243. package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
  244. package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
  245. package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
  246. package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
  247. package/packages/core/src/scheduler/dag/errors.ts +0 -37
  248. package/packages/core/src/scheduler/dag/index.ts +0 -26
  249. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
  250. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
  251. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
  252. package/packages/core/src/storage/HiveDBStorage.ts +0 -64
  253. package/packages/core/src/storage/SQLiteStorage.ts +0 -414
  254. package/packages/core/src/storage/hiveSeed.ts +0 -308
  255. package/packages/core/src/storage/hiveStorage.test.ts +0 -38
  256. package/packages/core/src/storage/schema.ts +0 -689
  257. package/packages/core/src/storage/storage.test.ts +0 -37
  258. package/packages/core/src/swarm/AgentBus.ts +0 -460
  259. package/packages/core/src/swarm/EventBus.ts +0 -169
  260. package/packages/core/src/swarm/WorkerPool.ts +0 -236
  261. package/packages/core/src/tools/bridge-events.ts +0 -26
  262. package/packages/core/src/tools/canvas/index.ts +0 -375
  263. package/packages/core/src/tools/codebridge/index.ts +0 -342
  264. package/packages/core/src/tools/meeting/index.ts +0 -353
  265. package/packages/core/src/tools/projects/index.ts +0 -37
  266. package/packages/core/src/tools/projects/project-create.ts +0 -94
  267. package/packages/core/src/tools/projects/project-done.ts +0 -66
  268. package/packages/core/src/tools/projects/project-fail.ts +0 -66
  269. package/packages/core/src/tools/projects/project-list.ts +0 -96
  270. package/packages/core/src/tools/projects/project-update.ts +0 -72
  271. package/packages/core/src/tools/projects/task-create.ts +0 -68
  272. package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
  273. package/packages/core/src/tools/projects/task-update.ts +0 -93
  274. package/packages/core/src/tools/voice/index.ts +0 -104
  275. package/packages/core/src/tools/web/api-request.test.ts +0 -170
  276. package/packages/core/src/tools/web/api-request.ts +0 -239
  277. package/test/setup-db.ts +0 -216
  278. /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
@@ -6,7 +6,8 @@ import { createDiscordChannel, type DiscordConfig } from "./discord.ts";
6
6
  import { createWebChatChannel, type WebChatConfig } from "./webchat.ts";
7
7
  import { createWhatsAppChannel, WhatsAppChannel, type WhatsAppConfig } from "./whatsapp.ts";
8
8
  import { createSlackChannel, type SlackConfig } from "./slack.ts";
9
- import { getDb } from "../storage/SQLiteStorage.ts";
9
+ import { col } from "../storage/hive.ts";
10
+ import type { ChannelDoc, AgentDoc, UserIdentityDoc } from "../storage/collections.ts";
10
11
  import { loadChannelConfig } from "../storage/crypto.ts";
11
12
 
12
13
  export class ChannelManager {
@@ -14,11 +15,44 @@ export class ChannelManager {
14
15
  private channels: Map<string, IChannel> = new Map();
15
16
  private messageHandler?: MessageHandler;
16
17
  private log = logger.child("channels");
18
+ /**
19
+ * `channel:sessionId` → accountId of the account that received the message.
20
+ *
21
+ * Outbound calls (agent reply, narration, notify tools, scheduler) only carry
22
+ * a channel name and a routing session id. With two accounts of the same type
23
+ * connected — two WhatsApp numbers — picking a channel by name alone sends the
24
+ * reply out through whichever account happens to be first, to a peer that may
25
+ * not even exist there. Every inbound message records its account here so the
26
+ * reply goes back out the way it came in.
27
+ *
28
+ * Known limit: if the same peer id talks to two accounts of one type, the
29
+ * routing session id is identical for both and the most recent inbound
30
+ * message wins. Distinguishing them would require the account in the session
31
+ * id itself.
32
+ */
33
+ private sessionAccounts: Map<string, string> = new Map();
17
34
 
18
35
  constructor(config: Config) {
19
36
  this.config = config;
20
37
  }
21
38
 
39
+ private sessionKey(channelName: string, sessionId: string): string {
40
+ return `${channelName}:${sessionId}`;
41
+ }
42
+
43
+ /**
44
+ * Resolves which account owns an outbound session: an explicit account wins,
45
+ * then the account that last received a message on it.
46
+ */
47
+ private resolveAccountId(
48
+ channelName: string,
49
+ sessionId: string,
50
+ explicit?: string
51
+ ): string | undefined {
52
+ if (explicit) return explicit;
53
+ return this.sessionAccounts.get(this.sessionKey(channelName, sessionId));
54
+ }
55
+
22
56
  onMessage(handler: MessageHandler): void {
23
57
  this.messageHandler = handler;
24
58
  }
@@ -32,16 +66,36 @@ export class ChannelManager {
32
66
  await this.initializeFromConfig();
33
67
  }
34
68
 
69
+ await this.seedSessionAccounts();
70
+
35
71
  this.log.info(`Initialized ${this.channels.size} channel(s)`);
36
72
  }
37
73
 
74
+ /**
75
+ * Restores session→account routing from persisted identities. Without this, a
76
+ * durable worker that finishes after a restart would have no record of which
77
+ * account its conversation came in on.
78
+ */
79
+ private async seedSessionAccounts(): Promise<void> {
80
+ try {
81
+ const identitiesCol = await col<UserIdentityDoc>("userIdentities");
82
+ for (const { doc } of await identitiesCol.scan({})) {
83
+ if (!doc.account_id || !doc.channel_user_id) continue;
84
+ // Skip identities whose account is gone (disconnected and replaced by
85
+ // another one) — pinning a session to it would only fail every send.
86
+ if (!this.channels.has(`${doc.channel}:${doc.account_id}`)) continue;
87
+ this.sessionAccounts.set(this.sessionKey(doc.channel, doc.channel_user_id), doc.account_id);
88
+ }
89
+ } catch (error) {
90
+ this.log.debug(`Could not seed session accounts: ${(error as Error).message}`);
91
+ }
92
+ }
93
+
38
94
  private async initializeFromDB(): Promise<void> {
39
95
  try {
40
- const db = getDb();
96
+ const channelsCol = await col<ChannelDoc>("channels");
41
97
  // Load all active channels - config may be empty for webchat
42
- const rows = db.query(`
43
- SELECT id, type, enabled, active FROM channels WHERE enabled = 1 AND active = 1
44
- `).all() as Array<{ id: string; type: string; enabled: number; active: number }>;
98
+ const rows = (await channelsCol.scan({})).filter(e => e.doc.enabled && e.doc.active).map(e => e.doc);
45
99
 
46
100
  for (const row of rows) {
47
101
  let config: Record<string, unknown> = {};
@@ -85,6 +139,26 @@ export class ChannelManager {
85
139
  }
86
140
  }
87
141
 
142
+ /**
143
+ * Wires a channel into the manager: inbound messages record which account
144
+ * received them before reaching the handler, so replies can be routed back.
145
+ */
146
+ private registerChannel(key: string, channel: IChannel): void {
147
+ channel.onMessage(async (message: IncomingMessage) => {
148
+ if (message.accountId && message.sessionId) {
149
+ this.sessionAccounts.set(
150
+ this.sessionKey(message.channel, message.sessionId),
151
+ message.accountId
152
+ );
153
+ }
154
+ if (this.messageHandler) {
155
+ await this.messageHandler(message);
156
+ }
157
+ });
158
+
159
+ this.channels.set(key, channel);
160
+ }
161
+
88
162
  private async createChannel(
89
163
  channelName: string,
90
164
  accountId: string,
@@ -125,7 +199,8 @@ export class ChannelManager {
125
199
  case "whatsapp": {
126
200
  let coordinatorId = "main";
127
201
  try {
128
- const coordinator = getDb().query(`SELECT id FROM agents WHERE role = 'coordinator' LIMIT 1`).get() as { id: string } | null;
202
+ const agentsCol = await col<AgentDoc>("agents");
203
+ const coordinator = (await agentsCol.findBy("role", "coordinator", { limit: 1 }))[0];
129
204
  if (coordinator?.id) coordinatorId = coordinator.id;
130
205
  } catch { /* fallback to "main" */ }
131
206
  channel = createWhatsAppChannel({
@@ -158,14 +233,8 @@ export class ChannelManager {
158
233
  return;
159
234
  }
160
235
 
161
- channel.onMessage(async (message: IncomingMessage) => {
162
- if (this.messageHandler) {
163
- await this.messageHandler(message);
164
- }
165
- });
166
-
167
236
  const key = `${channelName}:${accountId}`;
168
- this.channels.set(key, channel);
237
+ this.registerChannel(key, channel);
169
238
 
170
239
  this.log.info(`Created channel: ${key}`);
171
240
  } catch (error) {
@@ -217,22 +286,35 @@ export class ChannelManager {
217
286
 
218
287
  getChannel(channelName: string, accountId?: string): IChannel | undefined {
219
288
  if (accountId) {
220
- return this.channels.get(`${channelName}:${accountId}`);
221
- }
222
-
223
- for (const [key, channel] of this.channels) {
224
- if (key.startsWith(channelName)) {
225
- return channel;
289
+ const exact = this.channels.get(`${channelName}:${accountId}`);
290
+ // Deliberately no fallback: delivering through a different account would
291
+ // send someone else's conversation out of the wrong number.
292
+ if (!exact) {
293
+ this.log.warn(`Channel ${channelName}:${accountId} is not instantiated — refusing to fall back to another account`);
226
294
  }
295
+ return exact;
227
296
  }
228
297
 
229
- return undefined;
298
+ const matches = [...this.channels.entries()].filter(([key]) => key.startsWith(`${channelName}:`));
299
+ if (matches.length > 1) {
300
+ this.log.warn(
301
+ `${matches.length} "${channelName}" accounts are connected and no account was given — routing through ${matches[0]![0]}`
302
+ );
303
+ }
304
+ return matches[0]?.[1];
230
305
  }
231
306
 
232
307
  async removeChannel(channelName: string, accountId: string): Promise<void> {
233
308
  const key = `${channelName}:${accountId}`;
234
309
  await this.stopChannel(channelName, accountId);
235
310
  this.channels.delete(key);
311
+ // Drop sessions pinned to this account, otherwise every later send to them
312
+ // resolves to a channel that no longer exists.
313
+ for (const [sessionKey, account] of this.sessionAccounts) {
314
+ if (account === accountId && sessionKey.startsWith(`${channelName}:`)) {
315
+ this.sessionAccounts.delete(sessionKey);
316
+ }
317
+ }
236
318
  this.log.info(`Removed channel: ${key}`);
237
319
  }
238
320
 
@@ -398,12 +480,22 @@ export class ChannelManager {
398
480
  });
399
481
  }
400
482
 
483
+ /** Resolves the account that owns this session (see `sessionAccounts`). */
484
+ private channelForSession(
485
+ channelName: string,
486
+ sessionId: string,
487
+ accountId?: string
488
+ ): IChannel | undefined {
489
+ return this.getChannel(channelName, this.resolveAccountId(channelName, sessionId, accountId));
490
+ }
491
+
401
492
  async send(
402
493
  channelName: string,
403
494
  sessionId: string,
404
- message: unknown
495
+ message: unknown,
496
+ accountId?: string
405
497
  ): Promise<void> {
406
- const channel = this.getChannel(channelName);
498
+ const channel = this.channelForSession(channelName, sessionId, accountId);
407
499
 
408
500
  if (!channel) {
409
501
  throw new Error(`Channel not found: ${channelName}`);
@@ -412,29 +504,29 @@ export class ChannelManager {
412
504
  await channel.send(sessionId, message as any);
413
505
  }
414
506
 
415
- async startTyping(channelName: string, sessionId: string): Promise<void> {
416
- const channel = this.getChannel(channelName);
507
+ async startTyping(channelName: string, sessionId: string, accountId?: string): Promise<void> {
508
+ const channel = this.channelForSession(channelName, sessionId, accountId);
417
509
  if (channel?.startTyping) {
418
510
  await channel.startTyping(sessionId);
419
511
  }
420
512
  }
421
513
 
422
- async stopTyping(channelName: string, sessionId: string): Promise<void> {
423
- const channel = this.getChannel(channelName);
514
+ async stopTyping(channelName: string, sessionId: string, accountId?: string): Promise<void> {
515
+ const channel = this.channelForSession(channelName, sessionId, accountId);
424
516
  if (channel?.stopTyping) {
425
517
  await channel.stopTyping(sessionId);
426
518
  }
427
519
  }
428
520
 
429
- async markAsRead(channelName: string, sessionId: string, messageId?: string): Promise<void> {
430
- const channel = this.getChannel(channelName);
521
+ async markAsRead(channelName: string, sessionId: string, messageId?: string, accountId?: string): Promise<void> {
522
+ const channel = this.channelForSession(channelName, sessionId, accountId);
431
523
  if (channel?.markAsRead) {
432
524
  await channel.markAsRead(sessionId, messageId);
433
525
  }
434
526
  }
435
527
 
436
- async sendAudio(channelName: string, sessionId: string, audio: Buffer, mimeType: string): Promise<void> {
437
- const channel = this.getChannel(channelName);
528
+ async sendAudio(channelName: string, sessionId: string, audio: Buffer, mimeType: string, accountId?: string): Promise<void> {
529
+ const channel = this.channelForSession(channelName, sessionId, accountId);
438
530
  if (!channel) {
439
531
  throw new Error(`Channel not found: ${channelName}`);
440
532
  }
@@ -2,7 +2,8 @@ import { App, ExpressReceiver, type SlashCommand } from "@slack/bolt";
2
2
  import type { ChannelConfig, IncomingMessage, OutboundMessage } from "./base.ts";
3
3
  import { BaseChannel } from "./base.ts";
4
4
  import { logger } from "../utils/logger.ts";
5
- import { getDb } from "../storage/SQLiteStorage.ts";
5
+ import { updateDoc } from "../storage/hive.ts";
6
+ import type { ChannelDoc } from "../storage/collections.ts";
6
7
 
7
8
  export interface SlackConfig extends ChannelConfig {
8
9
  accountId?: string;
@@ -95,7 +96,7 @@ export class SlackChannel extends BaseChannel {
95
96
  this.connectionState.status = "connected";
96
97
  this.log.info(`Slack channel started on port ${port}`);
97
98
  try {
98
- getDb().query(`UPDATE channels SET status = 'connected' WHERE id = ?`).run(this.accountId);
99
+ await updateDoc<ChannelDoc>("channels", this.accountId, { status: "connected" });
99
100
  } catch { /* ignore DB errors */ }
100
101
 
101
102
  } catch (error) {
@@ -103,7 +104,7 @@ export class SlackChannel extends BaseChannel {
103
104
  this.connectionState.error = (error as Error).message;
104
105
  this.log.error(`Slack connection error: ${(error as Error).message}`);
105
106
  try {
106
- getDb().query(`UPDATE channels SET status = 'error' WHERE id = ?`).run(this.accountId);
107
+ await updateDoc<ChannelDoc>("channels", this.accountId, { status: "error" });
107
108
  } catch { /* ignore DB errors */ }
108
109
  throw error;
109
110
  }
@@ -124,7 +125,7 @@ export class SlackChannel extends BaseChannel {
124
125
  this.connectionState.status = "disconnected";
125
126
  this.log.info("Slack channel stopped");
126
127
  try {
127
- getDb().query(`UPDATE channels SET status = 'disconnected' WHERE id = ?`).run(this.accountId);
128
+ await updateDoc<ChannelDoc>("channels", this.accountId, { status: "disconnected" });
128
129
  } catch { /* ignore DB errors */ }
129
130
  }
130
131
 
@@ -1,7 +1,9 @@
1
1
  import { Bot, GrammyError, InputFile, type Context } from "grammy";
2
2
  import { BaseChannel, type ChannelConfig, type IncomingMessage, type OutboundMessage } from "./base.ts";
3
3
  import { logger } from "../utils/logger.ts";
4
- import { getDb } from "../storage/SQLiteStorage.ts";
4
+ import { col, updateDoc } from "../storage/hive.ts";
5
+ import type { ChannelDoc, UserIdentityDoc } from "../storage/collections.ts";
6
+ import { resolveUserId } from "../storage/onboarding";
5
7
 
6
8
  export interface TelegramConfig extends ChannelConfig {
7
9
  botToken: string;
@@ -55,22 +57,45 @@ export class TelegramChannel extends BaseChannel {
55
57
  });
56
58
 
57
59
  this.bot.start({
58
- onStart: () => {
60
+ onStart: async () => {
59
61
  this.running = true;
60
62
  this.log.info(`Telegram bot started: @${this.bot?.botInfo?.username ?? "unknown"}`);
61
63
  try {
62
- getDb().query(`UPDATE channels SET status = 'connected' WHERE id = ?`).run(this.accountId);
64
+ await updateDoc<ChannelDoc>("channels", this.accountId, { status: "connected" });
63
65
  } catch { /* ignore DB errors */ }
64
66
  },
65
- }).catch((error: Error) => {
67
+ }).catch(async (error: Error) => {
66
68
  this.log.error(`Telegram bot error: ${error.message}`);
67
69
  this.running = false;
68
70
  try {
69
- getDb().query(`UPDATE channels SET status = 'error' WHERE id = ?`).run(this.accountId);
71
+ await updateDoc<ChannelDoc>("channels", this.accountId, { status: "error" });
70
72
  } catch { /* ignore DB errors */ }
71
73
  });
72
74
  }
73
75
 
76
+ /**
77
+ * Ensures a userIdentities row links this Telegram chat to the single-user
78
+ * account, so cron/task notifications can route to Telegram even when the
79
+ * user's first-ever contact with the bot was a command (e.g. /start) that
80
+ * otherwise replies and returns before reaching the normal
81
+ * handleMessage() → resolveContext() auto-link path.
82
+ */
83
+ private async linkIdentity(channelUserId: string): Promise<void> {
84
+ try {
85
+ const userId = await resolveUserId({});
86
+ if (!userId) return;
87
+ const identitiesCol = await col<UserIdentityDoc>("userIdentities");
88
+ await identitiesCol.put(`${userId}:telegram`, {
89
+ user_id: userId,
90
+ channel: "telegram",
91
+ channel_user_id: channelUserId,
92
+ linked_at: Date.now(),
93
+ });
94
+ } catch (err) {
95
+ this.log.warn(`Failed to auto-link telegram identity: ${(err as Error).message}`);
96
+ }
97
+ }
98
+
74
99
  private async handleTelegramMessage(ctx: Context): Promise<void> {
75
100
  const message = ctx.message;
76
101
  if (!message) return;
@@ -100,6 +125,11 @@ export class TelegramChannel extends BaseChannel {
100
125
  if (now - ts > 60_000) this.recentlyProcessed.delete(id);
101
126
  }
102
127
 
128
+ // Auto-link this chat on first contact, before any command short-circuits.
129
+ if (!isGroup && this.isUserAllowed(chatId)) {
130
+ await this.linkIdentity(chatId);
131
+ }
132
+
103
133
  const text = message.text;
104
134
  const isCommand = text?.startsWith("/") ?? false;
105
135
 
@@ -306,7 +336,7 @@ export class TelegramChannel extends BaseChannel {
306
336
  this.running = false;
307
337
  this.log.info("Telegram bot stopped");
308
338
  try {
309
- getDb().query(`UPDATE channels SET status = 'disconnected' WHERE id = ?`).run(this.accountId);
339
+ await updateDoc<ChannelDoc>("channels", this.accountId, { status: "disconnected" });
310
340
  } catch { /* ignore DB errors */ }
311
341
  }
312
342
  }
@@ -19,17 +19,22 @@ export class WebChatChannel extends BaseChannel {
19
19
  accountId: string;
20
20
  config: WebChatConfig;
21
21
 
22
+ private explicitAccountId?: string;
22
23
  private connections: Map<string, ServerWebSocket<WebSocketData>> = new Map();
23
24
  private log = logger.child("webchat");
24
25
 
25
26
  constructor(config: WebChatConfig) {
26
27
  super();
27
28
  this.config = config;
28
- // Resolve accountId from database (single user) or use fallback
29
- this.accountId = config.accountId || resolveUserId({}) || "webchat";
29
+ this.explicitAccountId = config.accountId;
30
+ this.accountId = config.accountId || "webchat";
30
31
  }
31
32
 
32
33
  async start(): Promise<void> {
34
+ // Resolve accountId from database (single user) if not provided explicitly
35
+ if (!this.explicitAccountId) {
36
+ this.accountId = (await resolveUserId({})) || "webchat";
37
+ }
33
38
  this.running = true;
34
39
  this.log.info("WebChat channel ready");
35
40
  }
@@ -46,7 +51,8 @@ export class WebChatChannel extends BaseChannel {
46
51
  this.log.debug(`WebChat connection registered: ${data.sessionId}`);
47
52
  }
48
53
 
49
- unregisterConnection(sessionId: string): void {
54
+ unregisterConnection(sessionId: string, ws?: ServerWebSocket<WebSocketData>): void {
55
+ if (ws && this.connections.get(sessionId) !== ws) return;
50
56
  this.connections.delete(sessionId);
51
57
  this.log.debug(`WebChat connection unregistered: ${sessionId}`);
52
58
  }
@@ -86,15 +92,10 @@ export class WebChatChannel extends BaseChannel {
86
92
  const ws = this.connections.get(sessionId);
87
93
 
88
94
  if (!ws) {
89
- this.log.warn(`No WebChat connection for session: ${sessionId}`);
90
- return;
95
+ throw new Error(`No WebChat connection for session: ${sessionId}`);
91
96
  }
92
97
 
93
- try {
94
- ws.send(JSON.stringify(message));
95
- } catch (error) {
96
- this.log.error(`Failed to send WebChat message: ${(error as Error).message}`);
97
- }
98
+ ws.send(JSON.stringify(message));
98
99
  }
99
100
 
100
101
  async sendAudio(sessionId: string, audio: Buffer, mimeType: string): Promise<void> {
@@ -11,8 +11,10 @@ import type { ChannelConfig, IncomingMessage, OutboundMessage } from "./base.ts"
11
11
  import { BaseChannel } from "./base.ts";
12
12
  import { existsSync, mkdirSync, rmSync } from "node:fs";
13
13
  import * as path from "node:path";
14
+ import { homedir } from "node:os";
14
15
  import { logger } from "../utils/logger.ts";
15
- import { getDb } from "../storage/SQLiteStorage.ts";
16
+ import { updateDoc } from "../storage/hive.ts";
17
+ import type { ChannelDoc } from "../storage/collections.ts";
16
18
  // @ts-ignore — no type definitions for qrcode-terminal
17
19
  import qrcodeTerminal from "qrcode-terminal";
18
20
 
@@ -69,7 +71,7 @@ export class WhatsAppChannel extends BaseChannel {
69
71
  }
70
72
 
71
73
  private getAuthPath(agentId: string, accountId: string): string {
72
- const baseDir = process.env.HOME ?? "";
74
+ const baseDir = homedir();
73
75
  const authDir = path.join(baseDir, ".hive", "agents", agentId, "whatsapp", accountId);
74
76
 
75
77
  if (!existsSync(authDir)) {
@@ -117,11 +119,22 @@ export class WhatsAppChannel extends BaseChannel {
117
119
  this.connectionState.waVersion = version.join(".");
118
120
  this.log.info(`Using WhatsApp Web v${version.join(".")}`);
119
121
 
122
+ const baileysLogger = {
123
+ level: "silent",
124
+ child: () => baileysLogger,
125
+ trace: () => {},
126
+ debug: () => {},
127
+ info: (msg: unknown) => { if (typeof msg === "object" && msg !== null) this.log.debug((msg as any).msg ?? JSON.stringify(msg)); },
128
+ warn: (msg: unknown) => { if (typeof msg === "object" && msg !== null) this.log.warn((msg as any).msg ?? JSON.stringify(msg)); },
129
+ error: (msg: unknown) => { if (typeof msg === "object" && msg !== null) this.log.error((msg as any).msg ?? JSON.stringify(msg)); },
130
+ };
131
+
120
132
  this.socket = makeWASocket({
121
133
  version,
122
134
  auth: state,
123
135
  printQRInTerminal: false,
124
136
  syncFullHistory: false,
137
+ logger: baileysLogger as any,
125
138
  getMessage: async () => ({ conversation: "" }),
126
139
  });
127
140
 
@@ -169,8 +182,7 @@ export class WhatsAppChannel extends BaseChannel {
169
182
  this.log.warn(`WhatsApp disconnected: ${statusCode}`);
170
183
 
171
184
  try {
172
- getDb().query(`UPDATE channels SET status = ? WHERE id = ?`)
173
- .run(shouldReconnect ? "connecting" : "disconnected", this.accountId);
185
+ await updateDoc<ChannelDoc>("channels", this.accountId, { status: shouldReconnect ? "connecting" : "disconnected" });
174
186
  } catch { /* ignore DB errors */ }
175
187
 
176
188
  const needsSessionClear =
@@ -207,8 +219,7 @@ export class WhatsAppChannel extends BaseChannel {
207
219
  }
208
220
 
209
221
  try {
210
- getDb().query(`UPDATE channels SET status = 'connected', last_active = ? WHERE id = ?`)
211
- .run(Date.now(), this.accountId);
222
+ await updateDoc<ChannelDoc>("channels", this.accountId, { status: "connected", last_active: Date.now() });
212
223
  } catch { /* ignore DB errors */ }
213
224
  }
214
225
  }
@@ -471,7 +482,11 @@ export class WhatsAppChannel extends BaseChannel {
471
482
  throw new Error("WhatsApp not connected");
472
483
  }
473
484
 
474
- await this.stopTyping(sessionId);
485
+ // Progress narration arrives mid-turn. Clearing "typing…" on it would leave
486
+ // the user watching interim messages with no sign the agent is still
487
+ // working — only a final message ends the indicator.
488
+ const isInterim = message.type === "progress";
489
+ if (!isInterim) await this.stopTyping(sessionId);
475
490
 
476
491
  const text = message.content ?? message.chunk ?? "";
477
492
  if (!text) return;
@@ -479,6 +494,7 @@ export class WhatsAppChannel extends BaseChannel {
479
494
  const jid = this.getJid(sessionId);
480
495
 
481
496
  await this.socket.sendMessage(jid, { text });
497
+ if (isInterim) await this.startTyping(sessionId);
482
498
  this.log.debug(`Sent message to ${jid}`);
483
499
  }
484
500
 
@@ -1,2 +1,13 @@
1
- export { loadEnv, getHiveDir, loadConfig } from "./loader.ts";
2
- export type { Config } from "./loader.ts";
1
+ export {
2
+ loadEnv,
3
+ getHiveDir,
4
+ loadConfig,
5
+ expandConfigPath,
6
+ expandPath,
7
+ type Config,
8
+ type ProviderConfig,
9
+ type MCPServerConfig,
10
+ type AgentEntry,
11
+ type Binding,
12
+ type UserConfig as ConfigUserConfig
13
+ } from "./loader.ts";