@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
@@ -0,0 +1,162 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ statSync,
7
+ unlinkSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { extname, join } from "node:path";
11
+ import { getHiveDir } from "../config/loader";
12
+ import { col, updateDoc } from "../storage/hive";
13
+ import type { ArtifactDoc } from "../storage/collections";
14
+
15
+ const ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
16
+
17
+ function extensionFor(mimeType: string): string {
18
+ if (mimeType === "image/png") return "png";
19
+ if (mimeType === "image/jpeg") return "jpg";
20
+ if (mimeType === "image/webp") return "webp";
21
+ return "bin";
22
+ }
23
+
24
+ function hash(bytes: Uint8Array): string {
25
+ return createHash("sha256").update(bytes).digest("hex");
26
+ }
27
+
28
+ function detectedMime(bytes: Uint8Array, fallback: string): string {
29
+ if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
30
+ return "image/png";
31
+ }
32
+ if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
33
+ return "image/jpeg";
34
+ }
35
+ if (
36
+ bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&
37
+ bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50
38
+ ) {
39
+ return "image/webp";
40
+ }
41
+ return fallback;
42
+ }
43
+
44
+ export async function createArtifact(input: {
45
+ bytes: Uint8Array;
46
+ mimeType: string;
47
+ kind: string;
48
+ userId: string;
49
+ runId?: string | null;
50
+ taskId?: string | null;
51
+ rootDir?: string;
52
+ width?: number | null;
53
+ height?: number | null;
54
+ now?: number;
55
+ }): Promise<ArtifactDoc> {
56
+ const now = input.now ?? Date.now();
57
+ const id = randomUUID();
58
+ const root = input.rootDir ?? getHiveDir();
59
+ const mimeType = detectedMime(input.bytes, input.mimeType);
60
+ const day = new Date(now).toISOString().slice(0, 10);
61
+ const dir = join(root, "artifacts", day);
62
+ mkdirSync(dir, { recursive: true });
63
+ const path = join(dir, `${id}.${extensionFor(mimeType)}`);
64
+ writeFileSync(path, input.bytes, { flag: "wx" });
65
+
66
+ const doc: ArtifactDoc = {
67
+ id,
68
+ run_id: input.runId ?? null,
69
+ task_id: input.taskId ?? null,
70
+ user_id: input.userId,
71
+ kind: input.kind,
72
+ path,
73
+ mime_type: mimeType,
74
+ size: input.bytes.byteLength,
75
+ sha256: hash(input.bytes),
76
+ width: input.width ?? null,
77
+ height: input.height ?? null,
78
+ status: "active",
79
+ created_at: now,
80
+ expires_at: now + ARTIFACT_RETENTION_MS,
81
+ expired_at: null,
82
+ };
83
+
84
+ try {
85
+ const artifacts = await col<ArtifactDoc>("artifacts");
86
+ await artifacts.put(id, doc, { expectedVersion: 0 });
87
+ return doc;
88
+ } catch (error) {
89
+ try { unlinkSync(path); } catch { /* best effort rollback */ }
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ export async function inspectArtifact(
95
+ artifactId: string,
96
+ options: { userId?: string } = {},
97
+ ): Promise<Record<string, unknown>> {
98
+ const artifacts = await col<ArtifactDoc>("artifacts");
99
+ const entry = await artifacts.get(artifactId);
100
+ if (!entry) return { ok: false, artifact_id: artifactId, error: "Artifact not found" };
101
+ const artifact = entry.doc;
102
+ if (options.userId && artifact.user_id && artifact.user_id !== options.userId) {
103
+ return { ok: false, artifact_id: artifactId, error: "Artifact not accessible" };
104
+ }
105
+ if (artifact.status !== "active") {
106
+ return {
107
+ ok: false,
108
+ artifact_id: artifactId,
109
+ status: artifact.status,
110
+ expired_at: artifact.expired_at,
111
+ error: "Artifact binary has expired",
112
+ };
113
+ }
114
+ if (!existsSync(artifact.path)) {
115
+ return {
116
+ ok: false,
117
+ artifact_id: artifactId,
118
+ status: "missing",
119
+ error: "Artifact binary is missing",
120
+ };
121
+ }
122
+
123
+ const bytes = readFileSync(artifact.path);
124
+ const actualHash = hash(bytes);
125
+ const stat = statSync(artifact.path);
126
+ return {
127
+ ok: actualHash === artifact.sha256 && stat.size === artifact.size,
128
+ artifact_id: artifact.id,
129
+ kind: artifact.kind,
130
+ mime_type: detectedMime(bytes, artifact.mime_type),
131
+ extension: extname(artifact.path),
132
+ size: stat.size,
133
+ expected_size: artifact.size,
134
+ sha256: actualHash,
135
+ sha256_matches: actualHash === artifact.sha256,
136
+ width: artifact.width,
137
+ height: artifact.height,
138
+ created_at: artifact.created_at,
139
+ expires_at: artifact.expires_at,
140
+ status: artifact.status,
141
+ };
142
+ }
143
+
144
+ export async function expireArtifacts(now = Date.now()): Promise<{ expired: number }> {
145
+ const artifacts = await col<ArtifactDoc>("artifacts");
146
+ const rows = await artifacts.scan({});
147
+ let expired = 0;
148
+ for (const row of rows) {
149
+ if (row.doc.status !== "active" || row.doc.expires_at > now) continue;
150
+ try {
151
+ if (existsSync(row.doc.path)) unlinkSync(row.doc.path);
152
+ } catch {
153
+ continue;
154
+ }
155
+ await updateDoc<ArtifactDoc>("artifacts", row.id, {
156
+ status: "expired",
157
+ expired_at: now,
158
+ });
159
+ expired++;
160
+ }
161
+ return { expired };
162
+ }
@@ -0,0 +1,161 @@
1
+ import { logger } from "../utils/logger.ts";
2
+ import { eventBus } from "../events/event-bus.ts";
3
+
4
+ export interface WebSocketLike {
5
+ readyState: number;
6
+ send(data: string | ArrayBuffer | Uint8Array): number | boolean;
7
+ close(code?: number, reason?: string): void;
8
+ }
9
+
10
+ export const WebSocketState = {
11
+ CONNECTING: 0,
12
+ OPEN: 1,
13
+ CLOSING: 2,
14
+ CLOSED: 3,
15
+ };
16
+
17
+ interface A2UISurfaceCache {
18
+ createData: Record<string, unknown>;
19
+ components?: unknown[];
20
+ dataModel?: Record<string, unknown>;
21
+ }
22
+
23
+ export class CanvasManager {
24
+ private sessions: Map<string, WebSocketLike> = new Map();
25
+ private a2uiCache: Map<string, A2UISurfaceCache> = new Map();
26
+ private log = logger.child("canvas");
27
+ private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
28
+
29
+ constructor() {
30
+ this.startHeartbeat();
31
+ }
32
+
33
+ private startHeartbeat(): void {
34
+ if (this.heartbeatInterval) return;
35
+
36
+ // Enviar ping a todas las sesiones cada 30 segundos
37
+ this.heartbeatInterval = setInterval(() => {
38
+ for (const [sessionId, ws] of this.sessions) {
39
+ if (ws.readyState === WebSocketState.OPEN) {
40
+ try {
41
+ ws.send(JSON.stringify({ type: "canvas:ping", sessionId }));
42
+ this.log.debug(`Heartbeat sent to ${sessionId}`);
43
+ } catch (e) {
44
+ this.log.error(`Failed to send heartbeat to ${sessionId}`);
45
+ }
46
+ }
47
+ }
48
+ }, 30000);
49
+ }
50
+
51
+ private stopHeartbeat(): void {
52
+ if (this.heartbeatInterval) {
53
+ clearInterval(this.heartbeatInterval);
54
+ this.heartbeatInterval = null;
55
+ }
56
+ }
57
+
58
+ registerSession(sessionId: string, ws: WebSocketLike): void {
59
+ this.sessions.set(sessionId, ws);
60
+ this.log.info(`Canvas session registered: ${sessionId}`);
61
+
62
+ eventBus.emit("tool:completed" as any, {
63
+ toolName: "canvas:session:register",
64
+ result: { sessionId },
65
+ duration: 0,
66
+ success: true,
67
+ });
68
+
69
+ // Notify the client that the session is registered
70
+ ws.send(JSON.stringify({ type: "canvas:connected", sessionId }));
71
+
72
+ // Replay any cached A2UI surfaces so late-connecting clients get current state
73
+ for (const [surfaceId, cache] of this.a2uiCache) {
74
+ try {
75
+ ws.send(JSON.stringify({ type: "a2ui:createSurface", data: cache.createData }));
76
+ if (cache.components && cache.components.length > 0) {
77
+ ws.send(JSON.stringify({ type: "a2ui:updateComponents", data: { surfaceId, components: cache.components } }));
78
+ }
79
+ if (cache.dataModel && Object.keys(cache.dataModel).length > 0) {
80
+ ws.send(JSON.stringify({ type: "a2ui:updateDataModel", data: { surfaceId, path: undefined, value: cache.dataModel } }));
81
+ }
82
+ this.log.debug(`Replayed A2UI surface '${surfaceId}' to session ${sessionId}`);
83
+ } catch (e) {
84
+ this.log.warn(`Failed to replay A2UI surface '${surfaceId}' to ${sessionId}`);
85
+ }
86
+ }
87
+ }
88
+
89
+ unregisterSession(sessionId: string, ws?: WebSocketLike): void {
90
+ if (ws && this.sessions.get(sessionId) !== ws) return;
91
+ this.sessions.delete(sessionId);
92
+ this.log.info(`Canvas session disconnected: ${sessionId}`);
93
+ }
94
+
95
+ async sendA2UIMessage(sessionId: string, messageType: string, data: Record<string, unknown>): Promise<void> {
96
+ // Update A2UI cache so late-connecting clients can receive current state
97
+ const surfaceId = data.surfaceId as string | undefined;
98
+ if (surfaceId) {
99
+ if (messageType === "a2ui:createSurface") {
100
+ this.a2uiCache.set(surfaceId, { createData: data });
101
+ } else if (messageType === "a2ui:updateComponents") {
102
+ const cached = this.a2uiCache.get(surfaceId);
103
+ if (cached) cached.components = data.components as unknown[];
104
+ } else if (messageType === "a2ui:updateDataModel") {
105
+ const cached = this.a2uiCache.get(surfaceId);
106
+ if (cached) {
107
+ const path = data.path as string | undefined;
108
+ const value = data.value as Record<string, unknown>;
109
+ if (!path || path === "/") {
110
+ cached.dataModel = value;
111
+ } else {
112
+ cached.dataModel = cached.dataModel ?? {};
113
+ // Store the full model snapshot when possible; partial paths accumulate
114
+ const key = path.replace(/^\//, "").split("/")[0];
115
+ if (key) cached.dataModel[key] = value;
116
+ }
117
+ }
118
+ } else if (messageType === "a2ui:deleteSurface") {
119
+ this.a2uiCache.delete(surfaceId);
120
+ }
121
+ }
122
+
123
+ const ws = this.sessions.get(sessionId);
124
+
125
+ if (!ws || ws.readyState !== WebSocketState.OPEN) {
126
+ const connected = this.getConnectedSessions();
127
+ this.log.warn(`Session ${sessionId} NOT connected for A2UI message. Cached for replay. Available: ${connected.join(", ")}`);
128
+ return;
129
+ }
130
+
131
+ ws.send(JSON.stringify({ type: messageType, data }));
132
+ this.log.debug(`Sent A2UI message '${messageType}' to session ${sessionId}`);
133
+ }
134
+
135
+ isSessionConnected(sessionId: string): boolean {
136
+ const ws = this.sessions.get(sessionId);
137
+ return ws !== undefined && ws.readyState === WebSocketState.OPEN;
138
+ }
139
+
140
+ getConnectedSessions(): string[] {
141
+ return Array.from(this.sessions.entries())
142
+ .filter(([_, ws]) => ws.readyState === WebSocketState.OPEN)
143
+ .map(([id]) => id);
144
+ }
145
+
146
+ getStats(): { totalSessions: number; activeSessions: number } {
147
+ return {
148
+ totalSessions: this.sessions.size,
149
+ activeSessions: this.getConnectedSessions().length,
150
+ };
151
+ }
152
+
153
+ clearAll(): void {
154
+ this.stopHeartbeat();
155
+ this.sessions.clear();
156
+ this.a2uiCache.clear();
157
+ this.log.info("Canvas manager cleared");
158
+ }
159
+ }
160
+
161
+ export const canvasManager = new CanvasManager();
@@ -1,6 +1,10 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
  import { emitCanvas, subscribeCanvas, unsubscribeCanvas } from "./emitter.ts";
3
3
 
4
+ // `canvas:render` desapareció en 0.1.5: el canvas dejó de emitir componentes
5
+ // sueltos y pasó a un modelo de grafo (`node_add` / `node_update` / `edge_*`),
6
+ // que es el que consume la vista de oficina 3D.
7
+
4
8
  describe("canvas emitter", () => {
5
9
  it("subscribes and unsubscribes websocket-like objects", () => {
6
10
  let received: any = null;
@@ -9,11 +13,11 @@ describe("canvas emitter", () => {
9
13
  };
10
14
 
11
15
  subscribeCanvas(ws);
12
- emitCanvas("canvas:render", { component: { id: "test", type: "card" } });
16
+ emitCanvas("canvas:node_update", { nodeId: "test", changes: { status: "thinking" } });
13
17
  unsubscribeCanvas(ws);
14
18
 
15
19
  expect(received).toBeDefined();
16
- expect(received.type).toBe("canvas:render");
20
+ expect(received.type).toBe("canvas:node_update");
17
21
  });
18
22
 
19
23
  it("stops receiving after unsubscribe", () => {
@@ -23,9 +27,9 @@ describe("canvas emitter", () => {
23
27
  };
24
28
 
25
29
  subscribeCanvas(ws);
26
- emitCanvas("canvas:render", { component: { id: "a", type: "card" } });
30
+ emitCanvas("canvas:node_update", { nodeId: "a", changes: { status: "thinking" } });
27
31
  unsubscribeCanvas(ws);
28
- emitCanvas("canvas:render", { component: { id: "b", type: "card" } });
32
+ emitCanvas("canvas:node_update", { nodeId: "b", changes: { status: "idle" } });
29
33
 
30
34
  expect(count).toBe(1);
31
35
  });
@@ -1,4 +1,5 @@
1
- import { getDb } from "../storage/SQLiteStorage"
1
+ import { col, fromIndexable } from "../storage/hive"
2
+ import type { AgentDoc, McpServerDoc } from "../storage/collections"
2
3
 
3
4
  export interface CanvasEvent {
4
5
  type: CanvasEventType
@@ -13,17 +14,48 @@ export type CanvasEventType =
13
14
  | "canvas:node_remove"
14
15
  | "canvas:edge_add"
15
16
  | "canvas:edge_remove"
16
- | "canvas:render"
17
- | "canvas:ask"
18
- | "canvas:confirm"
19
- | "canvas:clear"
17
+ | "canvas:work_event"
20
18
  | "ag-ui:event"
21
19
 
22
- const subscribers = new Set<{ send: (data: string) => void }>()
20
+ export type CanvasWorkPhase =
21
+ | "delegated"
22
+ | "review_passed"
23
+ | "review_failed"
24
+ | "completed"
25
+ | "failed"
26
+ | "aborted"
27
+ | "blocked"
28
+
29
+ export interface CanvasWorkEvent {
30
+ eventId: string
31
+ phase: CanvasWorkPhase
32
+ taskRef: string
33
+ taskName: string
34
+ actorId: string
35
+ targetId: string | null
36
+ toolName?: string | null
37
+ detail?: string | null
38
+ }
23
39
 
24
- interface AgentLiveState { status: string; currentTool: string | null }
40
+ const subscribers = new Set<{ send: (data: string) => void }>()
41
+ let workEventSequence = 0
42
+
43
+ interface AgentLiveState {
44
+ status: string
45
+ currentTool: string | null
46
+ currentTask: string | null
47
+ taskId: string | null
48
+ delegatedBy: string | null
49
+ }
25
50
  const agentLiveState = new Map<string, AgentLiveState>()
26
- const canvasComponents = new Map<string, unknown>()
51
+
52
+ const LIVE_DEFAULTS: AgentLiveState = {
53
+ status: "idle",
54
+ currentTool: null,
55
+ currentTask: null,
56
+ taskId: null,
57
+ delegatedBy: null,
58
+ }
27
59
 
28
60
  export function subscribeCanvas(ws: { send: (data: string) => void }) {
29
61
  subscribers.add(ws)
@@ -34,20 +66,16 @@ export function unsubscribeCanvas(ws: { send: (data: string) => void }) {
34
66
  }
35
67
 
36
68
  export function emitCanvas(type: CanvasEventType, data: any) {
37
- // Track canvas components for new subscribers
38
- if (type === "canvas:render" && data?.component?.id) {
39
- canvasComponents.set(data.component.id, data.component)
40
- }
41
- if (type === "canvas:clear") {
42
- canvasComponents.clear()
43
- }
44
-
45
69
  // Track live agent state for new subscribers
46
70
  if (type === "canvas:node_update" && data?.nodeId && data?.changes) {
47
- const prev = agentLiveState.get(data.nodeId) ?? { status: "idle", currentTool: null }
71
+ const prev = agentLiveState.get(data.nodeId) ?? { ...LIVE_DEFAULTS }
72
+ const c = data.changes
48
73
  agentLiveState.set(data.nodeId, {
49
- status: data.changes.status ?? prev.status,
50
- currentTool: "currentTool" in data.changes ? data.changes.currentTool : prev.currentTool,
74
+ status: c.status ?? prev.status,
75
+ currentTool: "currentTool" in c ? c.currentTool : prev.currentTool,
76
+ currentTask: "currentTask" in c ? c.currentTask : prev.currentTask,
77
+ taskId: "taskId" in c ? c.taskId : prev.taskId,
78
+ delegatedBy: "delegatedBy" in c ? c.delegatedBy : prev.delegatedBy,
51
79
  })
52
80
  }
53
81
 
@@ -62,17 +90,69 @@ export function emitCanvas(type: CanvasEventType, data: any) {
62
90
  }
63
91
  }
64
92
 
65
- export function removeCanvasComponent(id: string) {
66
- canvasComponents.delete(id);
93
+ /** Evento semántico y transitorio para presentar el trabajo sin inferir resultados. */
94
+ export function emitWorkEvent(event: Omit<CanvasWorkEvent, "eventId">): CanvasWorkEvent {
95
+ const emitted: CanvasWorkEvent = {
96
+ eventId: `work:${event.taskRef}:${event.phase}:${Date.now().toString(36)}:${++workEventSequence}`,
97
+ ...event,
98
+ taskName: event.taskName.slice(0, 120),
99
+ detail: event.detail?.slice(0, 180) ?? null,
100
+ }
101
+ emitCanvas("canvas:work_event", emitted)
102
+ return emitted
67
103
  }
68
104
 
69
- export function getCanvasSnapshot() {
70
- const db = getDb()
105
+ /**
106
+ * Marca visualmente el inicio de una delegación coordinador→worker:
107
+ * el worker muestra la tarea en curso y aparece el edge "delegates".
108
+ */
109
+ export function emitDelegationStarted(opts: {
110
+ workerId: string
111
+ parentAgentId: string
112
+ taskRef: string
113
+ taskName: string
114
+ }) {
115
+ emitWorkEvent({
116
+ phase: "delegated",
117
+ taskRef: opts.taskRef,
118
+ taskName: opts.taskName,
119
+ actorId: opts.parentAgentId,
120
+ targetId: opts.workerId,
121
+ })
122
+ emitCanvas("canvas:node_update", {
123
+ nodeId: opts.workerId,
124
+ changes: {
125
+ status: "thinking",
126
+ currentTask: opts.taskName,
127
+ taskId: opts.taskRef,
128
+ delegatedBy: opts.parentAgentId,
129
+ },
130
+ })
131
+ if (opts.parentAgentId) {
132
+ emitCanvas("canvas:edge_add", {
133
+ id: `deleg_${opts.taskRef}`,
134
+ source: opts.parentAgentId,
135
+ target: opts.workerId,
136
+ edgeType: "delegates",
137
+ data: { taskId: opts.taskRef, taskName: opts.taskName },
138
+ })
139
+ }
140
+ }
141
+
142
+ /** Limpia el estado visual de delegación (éxito, fallo o aborto). */
143
+ export function emitDelegationFinished(opts: { workerId: string; taskRef: string }) {
144
+ emitCanvas("canvas:node_update", {
145
+ nodeId: opts.workerId,
146
+ changes: { status: "idle", currentTool: null, currentTask: null, taskId: null, delegatedBy: null },
147
+ })
148
+ emitCanvas("canvas:edge_remove", { id: `deleg_${opts.taskRef}` })
149
+ }
71
150
 
72
- const agentNodes = db
73
- .query<any, []>("SELECT id, name, description, role, status FROM agents")
74
- .all()
75
- .map((a: any) => {
151
+ export async function getCanvasSnapshot() {
152
+ const agentsCol = await col<AgentDoc>("agents")
153
+ const agentNodes = (await agentsCol.scan({}))
154
+ .map(e => e.doc)
155
+ .map((a) => {
76
156
  const live = agentLiveState.get(a.id)
77
157
  return {
78
158
  id: a.id,
@@ -80,70 +160,41 @@ export function getCanvasSnapshot() {
80
160
  description: a.description,
81
161
  status: live?.status ?? a.status,
82
162
  type: "agent",
83
- data: { role: a.role, currentTool: live?.currentTool ?? null },
163
+ data: {
164
+ role: a.role,
165
+ currentTool: live?.currentTool ?? null,
166
+ currentTask: live?.currentTask ?? null,
167
+ taskId: live?.taskId ?? null,
168
+ delegatedBy: live?.delegatedBy ?? null,
169
+ source: a.source ?? null,
170
+ },
84
171
  }
85
172
  })
86
173
 
87
- const mcpNodes = db
88
- .query<any, []>("SELECT id, name, status FROM mcp_servers WHERE enabled = 1")
89
- .all()
90
- .map((m: any) => ({
174
+ const mcpServersCol = await col<McpServerDoc>("mcpServers")
175
+ const mcpNodes = (await mcpServersCol.scan({}))
176
+ .map(e => e.doc)
177
+ .filter(m => m.enabled)
178
+ .map((m) => ({
91
179
  id: `mcp:${m.id}`,
92
180
  name: m.name,
93
181
  status: m.status,
94
182
  type: "mcp",
95
183
  }))
96
184
 
97
- // Proyectos activos
98
- const projectNodes = db
99
- .query<any, []>("SELECT id, name, type, status, progress, agent_id FROM projects WHERE status IN ('active','pending','paused')")
100
- .all()
101
- .map((p: any) => ({
102
- id: `project_${p.id}`,
103
- name: p.name,
104
- status: p.status,
105
- type: "project",
106
- data: { progress: p.progress, projectType: p.type, agentId: p.agent_id },
107
- }))
108
-
109
- // Tareas de proyectos activos
110
- const taskNodes = db
111
- .query<any, []>(`
112
- SELECT t.id, t.name, t.status, t.progress, t.agent_id, t.project_id
113
- FROM tasks t
114
- INNER JOIN projects p ON t.project_id = p.id
115
- WHERE p.status IN ('active','pending','paused')
116
- `)
117
- .all()
118
- .map((t: any) => ({
119
- id: `task_${t.id}`,
120
- name: t.name,
121
- status: t.status,
122
- type: "task",
123
- data: { progress: t.progress, agentId: t.agent_id, projectId: t.project_id },
124
- }))
125
-
126
- // Edges: proyecto → tarea
127
- const projectTaskEdges = taskNodes.map((t: any) => ({
128
- id: `edge_proj_task_${t.id.replace("task_", "")}`,
129
- source: `project_${t.data.projectId}`,
130
- target: t.id,
131
- edgeType: "contains",
132
- }))
133
-
134
- // Edges: tarea → agente asignado
135
- const taskAgentEdges = taskNodes
136
- .filter((t: any) => t.data.agentId)
137
- .map((t: any) => ({
138
- id: `edge_task_agent_${t.id.replace("task_", "")}`,
139
- source: t.id,
140
- target: t.data.agentId,
141
- edgeType: "assigned_to",
185
+ // Edges: delegaciones activas coordinador → worker (sobreviven reconexiones)
186
+ const delegationEdges = Array.from(agentLiveState.entries())
187
+ .filter(([, s]) => s.delegatedBy && s.taskId)
188
+ .map(([workerId, s]) => ({
189
+ id: `deleg_${s.taskId}`,
190
+ source: s.delegatedBy as string,
191
+ target: workerId,
192
+ edgeType: "delegates",
193
+ data: { taskId: s.taskId, taskName: s.currentTask },
142
194
  }))
143
195
 
144
196
  return {
145
- nodes: [...agentNodes, ...mcpNodes, ...projectNodes, ...taskNodes],
146
- edges: [...projectTaskEdges, ...taskAgentEdges],
147
- components: Array.from(canvasComponents.values()),
197
+ nodes: [...agentNodes, ...mcpNodes],
198
+ edges: [...delegationEdges],
148
199
  }
149
200
  }
@@ -1,3 +1 @@
1
- export * from "./CanvasManager.ts";
2
- export * from "./canvas-tools.ts";
3
- export * from "./a2ui-tools.ts";
1
+ export * from "./canvas-manager.ts";
@@ -1,12 +1,18 @@
1
1
  export interface OutboundMessage {
2
- type: "message" | "stream" | "status" | "error" | "pong" | "command_result" | "log" | "typing" | "audio";
2
+ type: "message" | "stream" | "status" | "error" | "pong" | "command_result" | "log" | "typing" | "audio" | "process" | "progress" | "notification";
3
3
  sessionId: string;
4
4
  id?: string; // Message ID for streaming
5
+ messageId?: string;
5
6
  content?: string;
6
7
  chunk?: string;
7
8
  isChunk?: boolean; // True if this is a streaming chunk
8
9
  isLast?: boolean;
9
10
  isStep?: boolean;
11
+ processKind?: "analysis" | "tool" | "observation" | "writing";
12
+ processStatus?: "thinking" | "done" | "error";
13
+ label?: string;
14
+ detail?: string;
15
+ summary?: string;
10
16
  stepType?: "plan" | "tool_call" | "tool_result" | "text";
11
17
  audio?: {
12
18
  buffer?: Buffer;
@@ -20,6 +26,8 @@ export interface OutboundMessage {
20
26
  };
21
27
  error?: string;
22
28
  result?: unknown;
29
+ notificationId?: string;
30
+ createdAt?: number;
23
31
  logEntry?: {
24
32
  timestamp: string;
25
33
  level: string;
@@ -9,7 +9,8 @@ import {
9
9
  } from "discord.js";
10
10
  import { BaseChannel, type ChannelConfig, type IncomingMessage, type OutboundMessage } from "./base.ts";
11
11
  import { logger } from "../utils/logger.ts";
12
- import { getDb } from "../storage/SQLiteStorage.ts";
12
+ import { updateDoc } from "../storage/hive.ts";
13
+ import type { ChannelDoc } from "../storage/collections.ts";
13
14
 
14
15
  export interface DiscordConfig extends ChannelConfig {
15
16
  botToken: string;
@@ -56,11 +57,11 @@ export class DiscordChannel extends BaseChannel {
56
57
  this.log.error(`Discord client error: ${error.message}`);
57
58
  });
58
59
 
59
- this.client.once(Events.ClientReady, () => {
60
+ this.client.once(Events.ClientReady, async () => {
60
61
  this.log.info(`Discord bot started: ${this.client?.user?.tag ?? "unknown"}`);
61
62
  this.running = true;
62
63
  try {
63
- getDb().query(`UPDATE channels SET status = 'connected' WHERE id = ?`).run(this.accountId);
64
+ await updateDoc<ChannelDoc>("channels", this.accountId, { status: "connected" });
64
65
  } catch { /* ignore DB errors */ }
65
66
  });
66
67
 
@@ -137,7 +138,7 @@ export class DiscordChannel extends BaseChannel {
137
138
  this.running = false;
138
139
  this.log.info("Discord bot stopped");
139
140
  try {
140
- getDb().query(`UPDATE channels SET status = 'disconnected' WHERE id = ?`).run(this.accountId);
141
+ await updateDoc<ChannelDoc>("channels", this.accountId, { status: "disconnected" });
141
142
  } catch { /* ignore DB errors */ }
142
143
  }
143
144
  }