@johpaz/hive-sdk 0.1.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/CHANGELOG.md +441 -0
  2. package/README.md +21 -3
  3. package/package.json +27 -5
  4. package/packages/core/src/agent/acceptance-checks.ts +9 -9
  5. package/packages/core/src/agent/agent-catalog.ts +82 -25
  6. package/packages/core/src/agent/agent-loop.ts +115 -26
  7. package/packages/core/src/agent/capability-search.ts +2 -2
  8. package/packages/core/src/agent/catalog-selector.ts +4 -4
  9. package/packages/core/src/agent/compaction.ts +30 -10
  10. package/packages/core/src/agent/context-compiler.ts +63 -36
  11. package/packages/core/src/agent/conversation-store.ts +167 -9
  12. package/packages/core/src/agent/curator.ts +16 -7
  13. package/packages/core/src/agent/delegation-runtime.ts +5 -5
  14. package/packages/core/src/agent/goal-runner.ts +9 -9
  15. package/packages/core/src/agent/index.ts +1 -0
  16. package/packages/core/src/agent/llm-client.ts +98 -37
  17. package/packages/core/src/agent/llm-providers/anthropic.ts +4 -4
  18. package/packages/core/src/agent/llm-providers/deepseek.ts +1 -1
  19. package/packages/core/src/agent/llm-providers/gemini.ts +4 -4
  20. package/packages/core/src/agent/llm-providers/groq.ts +1 -1
  21. package/packages/core/src/agent/llm-providers/hiveagents.ts +3 -3
  22. package/packages/core/src/agent/llm-providers/interface.ts +2 -2
  23. package/packages/core/src/agent/llm-providers/kimi.ts +1 -1
  24. package/packages/core/src/agent/llm-providers/minimax.ts +1 -1
  25. package/packages/core/src/agent/llm-providers/mistral.ts +1 -1
  26. package/packages/core/src/agent/llm-providers/modelscope.ts +1 -1
  27. package/packages/core/src/agent/llm-providers/nvidia.ts +40 -1
  28. package/packages/core/src/agent/llm-providers/ollama.ts +4 -4
  29. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +47 -7
  30. package/packages/core/src/agent/llm-providers/openai.ts +1 -1
  31. package/packages/core/src/agent/llm-providers/opencode-go.ts +1 -1
  32. package/packages/core/src/agent/llm-providers/openrouter.ts +1 -1
  33. package/packages/core/src/agent/llm-providers/qwen.ts +1 -1
  34. package/packages/core/src/agent/llm-providers/z-ai.ts +1 -1
  35. package/packages/core/src/agent/mcp-result-normalizer.ts +192 -0
  36. package/packages/core/src/agent/playbook-selector.ts +22 -7
  37. package/packages/core/src/agent/prompt-builder.ts +5 -5
  38. package/packages/core/src/agent/proof-packet.ts +5 -5
  39. package/packages/core/src/agent/providers/index.ts +39 -4
  40. package/packages/core/src/agent/realtime-providers/gemini-live.ts +238 -0
  41. package/packages/core/src/agent/realtime-providers/index.ts +29 -0
  42. package/packages/core/src/agent/realtime-providers/interface.ts +108 -0
  43. package/packages/core/src/agent/reflector.ts +38 -15
  44. package/packages/core/src/agent/run-store.ts +8 -8
  45. package/packages/core/src/agent/service.ts +10 -10
  46. package/packages/core/src/agent/skill-selector.ts +6 -6
  47. package/packages/core/src/agent/thread-id.ts +71 -0
  48. package/packages/core/src/agent/thread-store.ts +293 -0
  49. package/packages/core/src/agent/tool-selector.ts +9 -5
  50. package/packages/core/src/agent/tracer.ts +5 -5
  51. package/packages/core/src/api/createAgent.ts +67 -2
  52. package/packages/core/src/artifacts/index.ts +15 -0
  53. package/packages/core/src/artifacts/store.ts +161 -5
  54. package/packages/core/src/canvas/emitter.ts +2 -2
  55. package/packages/core/src/canvas/index.ts +9 -0
  56. package/packages/core/src/channels/telegram.ts +1 -1
  57. package/packages/core/src/channels/webchat.ts +1 -1
  58. package/packages/core/src/config/loader.ts +14 -5
  59. package/packages/core/src/ethics/EthicsGuard.ts +7 -1
  60. package/packages/core/src/events/agent-bus.ts +3 -3
  61. package/packages/core/src/events/channel-narration.ts +3 -3
  62. package/packages/core/src/events/event-bus.ts +1 -1
  63. package/packages/core/src/events/index.ts +18 -0
  64. package/packages/core/src/events/narration.ts +3 -3
  65. package/packages/core/src/events/tool-narration.ts +4 -0
  66. package/packages/core/src/gateway/channel-notify.ts +103 -6
  67. package/packages/core/src/gateway/delegation-groups.ts +4 -4
  68. package/packages/core/src/gateway/durable-queue.ts +18 -6
  69. package/packages/core/src/gateway/index.ts +3 -0
  70. package/packages/core/src/gateway/job-store.ts +11 -5
  71. package/packages/core/src/gateway/notification-inbox.ts +2 -2
  72. package/packages/core/src/gateway/server.ts +2 -2
  73. package/packages/core/src/harness/executors.ts +493 -0
  74. package/packages/core/src/harness/index.ts +12 -2
  75. package/packages/core/src/hooks/index.ts +203 -0
  76. package/packages/core/src/images/index.ts +161 -0
  77. package/packages/core/src/index.ts +1 -0
  78. package/packages/core/src/mcp/MCPClient.ts +3 -3
  79. package/packages/core/src/mcp/hot-reload.ts +5 -5
  80. package/packages/core/src/mcp/tool-sync.ts +5 -5
  81. package/packages/core/src/mcp/transports/index.ts +2 -2
  82. package/packages/core/src/mcp/transports/sse.ts +1 -1
  83. package/packages/core/src/models/index.ts +36 -0
  84. package/packages/core/src/multimodal/index.ts +2 -2
  85. package/packages/core/src/multimodal/vision-service.ts +51 -19
  86. package/packages/core/src/plugins/loader.ts +4 -1
  87. package/packages/core/src/resilience/circuit-breaker.ts +16 -5
  88. package/packages/core/src/resilience/index.ts +13 -0
  89. package/packages/core/src/resilience/retry.ts +1 -1
  90. package/packages/core/src/scheduler/CronScheduler.ts +54 -27
  91. package/packages/core/src/scheduler/cron/expression.ts +165 -0
  92. package/packages/core/src/scheduler/cron/index.ts +10 -0
  93. package/packages/core/src/scheduler/cron/job.ts +339 -0
  94. package/packages/core/src/scheduler/cron/next-run.ts +121 -0
  95. package/packages/core/src/scheduler/cron/zoned-time.ts +138 -0
  96. package/packages/core/src/scheduler/index.ts +21 -3
  97. package/packages/core/src/scheduler/integration.ts +25 -14
  98. package/packages/core/src/scheduler/types.ts +3 -18
  99. package/packages/core/src/services/agents.ts +268 -0
  100. package/packages/core/src/services/cron.ts +257 -0
  101. package/packages/core/src/services/endpoints.ts +289 -0
  102. package/packages/core/src/services/ethics.ts +107 -0
  103. package/packages/core/src/services/images.ts +212 -0
  104. package/packages/core/src/services/index.ts +112 -0
  105. package/packages/core/src/services/mcp.ts +201 -0
  106. package/packages/core/src/services/memory.ts +133 -0
  107. package/packages/core/src/services/models.ts +179 -0
  108. package/packages/core/src/services/providers.ts +152 -0
  109. package/packages/core/src/services/setup.ts +222 -0
  110. package/packages/core/src/services/skills.ts +241 -0
  111. package/packages/core/src/services/swarms.ts +307 -0
  112. package/packages/core/src/services/tools.ts +106 -0
  113. package/packages/core/src/sessions/index.ts +268 -0
  114. package/packages/core/src/sessions/resolve.ts +108 -0
  115. package/packages/core/src/skills/SkillLoader.ts +8 -1
  116. package/packages/core/src/skills/bundled/artifacts/artifact_reader/SKILL.md +105 -0
  117. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +21 -11
  118. package/packages/core/src/skills/bundled/images/image_editor/SKILL.md +120 -0
  119. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +12 -3
  120. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +22 -7
  121. package/packages/core/src/skills/bundled-data.generated.ts +110 -12
  122. package/packages/core/src/storage/bootstrap.ts +107 -12
  123. package/packages/core/src/storage/causal-events.ts +1 -1
  124. package/packages/core/src/storage/collections.ts +138 -2
  125. package/packages/core/src/storage/crypto.ts +35 -4
  126. package/packages/core/src/storage/hive.ts +1 -1
  127. package/packages/core/src/storage/hivedb.ts +10 -1
  128. package/packages/core/src/storage/index.ts +2 -1
  129. package/packages/core/src/storage/onboarding.ts +61 -45
  130. package/packages/core/src/storage/reconcile.ts +11 -6
  131. package/packages/core/src/storage/seed.ts +191 -23
  132. package/packages/core/src/storage/usage.ts +3 -3
  133. package/packages/core/src/swarm/AgentExecutor.ts +2 -2
  134. package/packages/core/src/swarm/Coordinator.ts +8 -8
  135. package/packages/core/src/swarm/EventBridge.ts +2 -2
  136. package/packages/core/src/swarm/RoleSwarm.ts +234 -0
  137. package/packages/core/src/swarm/TaskGraph.ts +2 -2
  138. package/packages/core/src/swarm/index.ts +7 -0
  139. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +2 -2
  140. package/packages/core/src/swarm/presets/ResearchPreset.ts +2 -2
  141. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +1 -1
  142. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +3 -3
  143. package/packages/core/src/swarm/types.ts +3 -18
  144. package/packages/core/src/tool-runtime/embedded-worker.generated.ts +21 -0
  145. package/packages/core/src/tool-runtime/index.ts +129 -14
  146. package/packages/core/src/tools/ToolExecutor.ts +7 -3
  147. package/packages/core/src/tools/agents/index.ts +18 -60
  148. package/packages/core/src/tools/cli/index.ts +55 -0
  149. package/packages/core/src/tools/core/index.ts +52 -4
  150. package/packages/core/src/tools/cron/index.ts +8 -8
  151. package/packages/core/src/tools/images/index.ts +130 -0
  152. package/packages/core/src/tools/index.ts +14 -1
  153. package/packages/core/src/tools/office/office-escribir-xlsx.ts +2 -1
  154. package/packages/core/src/tools/office/office-leer-xlsx.ts +2 -1
  155. package/packages/core/src/tools/office/xlsx-loader.ts +19 -0
  156. package/packages/core/src/tools/web/artifact-inspect.ts +2 -2
  157. package/packages/core/src/tools/web/artifact-read.ts +162 -0
  158. package/packages/core/src/tools/web/browser-backend.ts +141 -44
  159. package/packages/core/src/tools/web/browser-click.ts +2 -2
  160. package/packages/core/src/tools/web/browser-extract.ts +2 -2
  161. package/packages/core/src/tools/web/browser-navigate.ts +2 -2
  162. package/packages/core/src/tools/web/browser-screenshot.ts +12 -5
  163. package/packages/core/src/tools/web/browser-script.ts +2 -2
  164. package/packages/core/src/tools/web/browser-service.ts +63 -384
  165. package/packages/core/src/tools/web/browser-session.ts +125 -0
  166. package/packages/core/src/tools/web/browser-type.ts +2 -2
  167. package/packages/core/src/tools/web/browser-wait.ts +2 -2
  168. package/packages/core/src/tools/web/computer-use.ts +553 -0
  169. package/packages/core/src/tools/web/index.ts +8 -1
  170. package/packages/core/src/tools/web/webview-backend.ts +460 -21
  171. package/packages/core/src/utils/index.ts +1 -0
  172. package/packages/core/src/utils/logger.ts +12 -4
  173. package/packages/core/src/utils/redact-binary.ts +17 -0
  174. package/packages/core/src/utils/toon.ts +1 -1
  175. package/packages/core/src/voice/index.ts +6 -6
  176. package/bun.lock +0 -859
  177. package/bunfig.toml +0 -9
  178. package/docs/API-AGENTS.md +0 -367
  179. package/docs/API-CONTEXT-COMPILER.md +0 -249
  180. package/docs/API-DAG-SCHEDULER.md +0 -273
  181. package/docs/API-TOOLS-SKILLS-CHANNELS.md +0 -446
  182. package/docs/API-WORKERS-EVENTS.md +0 -299
  183. package/docs/HIVE-HARNESS.md +0 -113
  184. package/docs/INDEX.md +0 -190
  185. package/docs/TEMPLATE-HIVE-APP.md +0 -360
  186. package/packages/cli/package.json +0 -17
  187. package/packages/cli/src/commands/create-app.test.ts +0 -180
  188. package/packages/core/package.json +0 -70
  189. package/packages/core/src/api/createAgent.test.ts +0 -160
  190. package/packages/core/src/canvas/canvas.test.ts +0 -36
  191. package/packages/core/src/channels/channels.test.ts +0 -18
  192. package/packages/core/src/ethics/EthicsGuard.test.ts +0 -108
  193. package/packages/core/src/gateway/gateway.test.ts +0 -38
  194. package/packages/core/src/memory/Scratchpad.test.ts +0 -68
  195. package/packages/core/src/scheduler/scheduler.test.ts +0 -15
  196. package/packages/core/src/skills/skills.test.ts +0 -62
  197. package/packages/core/src/swarm/swarm.test.ts +0 -24
  198. package/packages/core/src/tool-runtime/tool-runtime.test.ts +0 -99
  199. package/packages/core/src/tools/ToolRegistry.test.ts +0 -98
  200. package/packages/core/src/tools/api/api-request.test.ts +0 -164
  201. package/packages/core/src/tools/web/browser-service.test.ts +0 -83
  202. package/packages/core/src/workers/workers.test.ts +0 -41
  203. package/scripts/bump-version.ts +0 -248
  204. package/scripts/generate-skill-bundle.ts +0 -108
  205. package/test/acceptance-checks.test.ts +0 -403
  206. package/test/agent-loop-terminal-synthesis.test.ts +0 -32
  207. package/test/browser-backend.test.ts +0 -308
  208. package/test/catalog-agents-stay-enabled.test.ts +0 -117
  209. package/test/causal-events.test.ts +0 -117
  210. package/test/compaction.test.ts +0 -105
  211. package/test/context-compiler.test.ts +0 -269
  212. package/test/curator.test.ts +0 -130
  213. package/test/durable-queue.test.ts +0 -114
  214. package/test/harness-barrel.test.ts +0 -64
  215. package/test/hive-helpers.test.ts +0 -130
  216. package/test/hivedb-search.test.ts +0 -189
  217. package/test/internal-turns.test.ts +0 -166
  218. package/test/job-idempotency.test.ts +0 -68
  219. package/test/job-retry-backoff.test.ts +0 -184
  220. package/test/job-store.test.ts +0 -381
  221. package/test/llm-retry.test.ts +0 -97
  222. package/test/memory-perf.test.ts +0 -774
  223. package/test/minimal-loadout.test.ts +0 -78
  224. package/test/model-catalog.test.ts +0 -105
  225. package/test/preload.ts +0 -12
  226. package/test/reflector.test.ts +0 -320
  227. package/test/retention-cap.test.ts +0 -91
  228. package/test/retired-capabilities-pruned.test.ts +0 -192
  229. package/test/run-store.test.ts +0 -355
  230. package/test/scratchpad.test.ts +0 -74
  231. package/test/secrets-durability.test.ts +0 -119
  232. package/test/seed-model-reseed.test.ts +0 -155
  233. package/test/setup-agent-seed.test.ts +0 -264
  234. package/test/tool-inventory.test.ts +0 -65
  235. package/test/tool-runtime.test.ts +0 -258
  236. package/test/tool-selector-runtime-tools.test.ts +0 -117
  237. package/test/toon.test.ts +0 -429
  238. package/tsconfig.json +0 -42
@@ -8,9 +8,9 @@ import {
8
8
  writeFileSync,
9
9
  } from "node:fs";
10
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";
11
+ import { getHiveDir } from "../config/loader.ts";
12
+ import { col, updateDoc } from "../storage/hive.ts";
13
+ import type { ArtifactDoc } from "../storage/collections.ts";
14
14
 
15
15
  const ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
16
16
 
@@ -52,6 +52,11 @@ export async function createArtifact(input: {
52
52
  width?: number | null;
53
53
  height?: number | null;
54
54
  now?: number;
55
+ /**
56
+ * Cuándo caduca. `null` = nunca, para lo que es del usuario y no basura
57
+ * transitoria. Por omisión, los 7 días de siempre.
58
+ */
59
+ expiresAt?: number | null;
55
60
  }): Promise<ArtifactDoc> {
56
61
  const now = input.now ?? Date.now();
57
62
  const id = randomUUID();
@@ -77,7 +82,7 @@ export async function createArtifact(input: {
77
82
  height: input.height ?? null,
78
83
  status: "active",
79
84
  created_at: now,
80
- expires_at: now + ARTIFACT_RETENTION_MS,
85
+ expires_at: input.expiresAt !== undefined ? input.expiresAt : now + ARTIFACT_RETENTION_MS,
81
86
  expired_at: null,
82
87
  };
83
88
 
@@ -91,6 +96,87 @@ export async function createArtifact(input: {
91
96
  }
92
97
  }
93
98
 
99
+ /**
100
+ * Reads an artifact's bytes straight off disk — for server-side consumers
101
+ * that don't need an HTTP round trip (routes/artifacts.ts is for the
102
+ * browser; channels/*.ts send()s use this directly to build a Telegram/
103
+ * Discord/Slack/WhatsApp photo attachment from the same file).
104
+ */
105
+ export async function readArtifactBytes(
106
+ artifactId: string,
107
+ ): Promise<{ bytes: Buffer; mimeType: string } | null> {
108
+ const artifacts = await col<ArtifactDoc>("artifacts");
109
+ const entry = await artifacts.get(artifactId);
110
+ if (!entry || entry.doc.status !== "active") return null;
111
+ if (!existsSync(entry.doc.path)) return null;
112
+ return { bytes: readFileSync(entry.doc.path), mimeType: entry.doc.mime_type };
113
+ }
114
+
115
+ /** Beyond this an artifact stops being something an agent can read into a context window. */
116
+ const MAX_TEXT_ARTIFACT_BYTES = 25 * 1024 * 1024;
117
+
118
+ function looksBinary(mimeType: string, bytes: Buffer): boolean {
119
+ if (/^(image|audio|video)\//.test(mimeType)) return true;
120
+ if (mimeType === "application/octet-stream") return true;
121
+ // A NUL byte in the first KB is the classic "this is not text" tell — a
122
+ // mislabelled text/plain artifact would otherwise decode to garbage.
123
+ return bytes.subarray(0, 1024).includes(0x00);
124
+ }
125
+
126
+ export type ArtifactTextResult = {
127
+ ok: boolean;
128
+ text?: string;
129
+ artifact?: ArtifactDoc;
130
+ error?: string;
131
+ status?: string;
132
+ };
133
+
134
+ /**
135
+ * Decodes a text artifact for in-process consumers that need its *content*,
136
+ * not just its metadata (inspectArtifact) or its raw bytes (readArtifactBytes).
137
+ *
138
+ * This exists because mcp-result-normalizer.ts moves oversized MCP text results
139
+ * out of the context window and hands the model an `artifact_ref` instead. Until
140
+ * this, nothing could read that reference back: the agent held a receipt it
141
+ * could not cash, burned its iterations guessing, and the turn died on an empty
142
+ * synthesis. Ownership and status checks mirror inspectArtifact's.
143
+ */
144
+ export async function readArtifactText(
145
+ artifactId: string,
146
+ options: { userId?: string } = {},
147
+ ): Promise<ArtifactTextResult> {
148
+ const artifacts = await col<ArtifactDoc>("artifacts");
149
+ const entry = await artifacts.get(artifactId);
150
+ if (!entry) return { ok: false, error: "Artifact not found" };
151
+ const artifact = entry.doc;
152
+
153
+ if (options.userId && artifact.user_id && artifact.user_id !== options.userId) {
154
+ return { ok: false, error: "Artifact not accessible" };
155
+ }
156
+ if (artifact.status !== "active") {
157
+ return { ok: false, error: "Artifact binary has expired", status: artifact.status };
158
+ }
159
+ if (!existsSync(artifact.path)) {
160
+ return { ok: false, error: "Artifact binary is missing", status: "missing" };
161
+ }
162
+ if (artifact.size > MAX_TEXT_ARTIFACT_BYTES) {
163
+ return {
164
+ ok: false,
165
+ error: `Artifact is too large to read as text (${artifact.size} bytes, limit ${MAX_TEXT_ARTIFACT_BYTES})`,
166
+ };
167
+ }
168
+
169
+ const bytes = readFileSync(artifact.path);
170
+ if (looksBinary(detectedMime(bytes, artifact.mime_type), bytes)) {
171
+ return {
172
+ ok: false,
173
+ error: `Artifact is binary (${detectedMime(bytes, artifact.mime_type)}) — use artifact_inspect for its metadata`,
174
+ };
175
+ }
176
+
177
+ return { ok: true, text: bytes.toString("utf-8"), artifact };
178
+ }
179
+
94
180
  export async function inspectArtifact(
95
181
  artifactId: string,
96
182
  options: { userId?: string } = {},
@@ -141,12 +227,82 @@ export async function inspectArtifact(
141
227
  };
142
228
  }
143
229
 
230
+ export interface ListArtifactsOptions {
231
+ /** Filtra por tipo: `image`, `document`… */
232
+ kind?: string;
233
+ includeExpired?: boolean;
234
+ limit?: number;
235
+ }
236
+
237
+ /**
238
+ * Los artefactos de un usuario, del más reciente al más viejo.
239
+ *
240
+ * No existía, y sin esto una interfaz no puede mostrarle a alguien lo que tiene
241
+ * guardado — ni una galería de imágenes ni la lista de adjuntos de una
242
+ * conversación.
243
+ */
244
+ export async function listArtifacts(
245
+ userId: string,
246
+ opts: ListArtifactsOptions = {},
247
+ ): Promise<ArtifactDoc[]> {
248
+ const artifacts = await col<ArtifactDoc>("artifacts");
249
+ const rows = await artifacts.findBy("user_id", userId);
250
+ return rows
251
+ .map((e) => e.doc)
252
+ .filter((d) => (opts.includeExpired ? true : d.status === "active"))
253
+ .filter((d) => (opts.kind ? d.kind === opts.kind : true))
254
+ .sort((a, b) => b.created_at - a.created_at)
255
+ .slice(0, opts.limit ?? Number.MAX_SAFE_INTEGER);
256
+ }
257
+
258
+ /**
259
+ * Cambia cuándo caduca un artefacto. `null` = conservarlo indefinidamente.
260
+ *
261
+ * Es lo que le da al usuario el control: puede marcar como permanente algo que
262
+ * nació temporal, o ponerle fecha a algo que ya no necesita.
263
+ */
264
+ export async function setArtifactRetention(
265
+ artifactId: string,
266
+ expiresAt: number | null,
267
+ ): Promise<ArtifactDoc | null> {
268
+ const artifacts = await col<ArtifactDoc>("artifacts");
269
+ const entry = await artifacts.get(artifactId);
270
+ if (!entry) return null;
271
+
272
+ const doc: ArtifactDoc = { ...entry.doc, expires_at: expiresAt };
273
+ await artifacts.put(artifactId, doc, { expectedVersion: entry.version });
274
+ return doc;
275
+ }
276
+
277
+ /**
278
+ * Borra el artefacto y su archivo, sin esperar a que caduque.
279
+ *
280
+ * A diferencia de `expireArtifacts`, que marca `status: "expired"` y conserva la
281
+ * fila como registro, esto la elimina: es un borrado pedido por el usuario, y
282
+ * dejar el rastro de algo que pidió borrar sería lo contrario de lo que pidió.
283
+ */
284
+ export async function deleteArtifact(artifactId: string): Promise<boolean> {
285
+ const artifacts = await col<ArtifactDoc>("artifacts");
286
+ const entry = await artifacts.get(artifactId);
287
+ if (!entry) return false;
288
+
289
+ try {
290
+ if (existsSync(entry.doc.path)) unlinkSync(entry.doc.path);
291
+ } catch {
292
+ // El archivo puede haber desaparecido; la fila igual se va.
293
+ }
294
+ await artifacts.delete(artifactId);
295
+ return true;
296
+ }
297
+
144
298
  export async function expireArtifacts(now = Date.now()): Promise<{ expired: number }> {
145
299
  const artifacts = await col<ArtifactDoc>("artifacts");
146
300
  const rows = await artifacts.scan({});
147
301
  let expired = 0;
148
302
  for (const row of rows) {
149
- if (row.doc.status !== "active" || row.doc.expires_at > now) continue;
303
+ // `null` es explícito: el usuario pidió conservarlo. Saltarlo ANTES de
304
+ // comparar fechas evita que un `null > now` (que es false) lo borre.
305
+ if (row.doc.status !== "active" || row.doc.expires_at === null || row.doc.expires_at > now) continue;
150
306
  try {
151
307
  if (existsSync(row.doc.path)) unlinkSync(row.doc.path);
152
308
  } catch {
@@ -1,5 +1,5 @@
1
- import { col, fromIndexable } from "../storage/hive"
2
- import type { AgentDoc, McpServerDoc } from "../storage/collections"
1
+ import { col, fromIndexable } from "../storage/hive.ts"
2
+ import type { AgentDoc, McpServerDoc } from "../storage/collections.ts"
3
3
 
4
4
  export interface CanvasEvent {
5
5
  type: CanvasEventType
@@ -1 +1,10 @@
1
+ /**
2
+ * Canvas — el estado visual de un enjambre corriendo.
3
+ *
4
+ * `canvas-manager.ts` guarda y sirve el snapshot; `emitter.ts` es por donde el
5
+ * runtime publica los cambios (un nodo que empieza a pensar, una delegación que
6
+ * arranca o termina). Quien construya una UI sobre el SDK consume ambos.
7
+ */
8
+
1
9
  export * from "./canvas-manager.ts";
10
+ export * from "./emitter.ts";
@@ -3,7 +3,7 @@ import { BaseChannel, type ChannelConfig, type IncomingMessage, type OutboundMes
3
3
  import { logger } from "../utils/logger.ts";
4
4
  import { col, updateDoc } from "../storage/hive.ts";
5
5
  import type { ChannelDoc, UserIdentityDoc } from "../storage/collections.ts";
6
- import { resolveUserId } from "../storage/onboarding";
6
+ import { resolveUserId } from "../storage/onboarding.ts";
7
7
 
8
8
  export interface TelegramConfig extends ChannelConfig {
9
9
  botToken: string;
@@ -1,7 +1,7 @@
1
1
  import type { ServerWebSocket } from "bun";
2
2
  import { BaseChannel, type ChannelConfig, type IncomingMessage, type OutboundMessage } from "./base.ts";
3
3
  import { logger } from "../utils/logger.ts";
4
- import { resolveUserId } from "../storage/onboarding";
4
+ import { resolveUserId } from "../storage/onboarding.ts";
5
5
 
6
6
  export interface WebChatConfig extends ChannelConfig {
7
7
  accountId?: string;
@@ -20,7 +20,14 @@ export function loadEnv(hiveDir: string): void {
20
20
  const [key, ...valueParts] = trimmed.split("=");
21
21
  if (key && valueParts.length > 0) {
22
22
  const value = valueParts.join("=").trim().replace(/^['"]|['"]$/g, "");
23
- process.env[key.trim()] = value;
23
+ const normalizedKey = key.trim();
24
+ // Explicit process environment values (for example the random
25
+ // port assigned by the desktop shell) must take precedence over the
26
+ // persisted .env file. This follows dotenv conventions and prevents
27
+ // a previous CLI port from breaking desktop startup.
28
+ if (process.env[normalizedKey] === undefined) {
29
+ process.env[normalizedKey] = value;
30
+ }
24
31
  }
25
32
  }
26
33
  } catch (e) {
@@ -120,11 +127,13 @@ const BrowserConfigSchema = z.object({
120
127
  headless: z.boolean().optional(),
121
128
  timeoutMs: z.number().optional(),
122
129
  sessionName: z.string().optional(),
123
- // "agent-browser" (default) usa Chrome via CLI y sirve headless/Docker.
124
- // "webview" usa Bun.WebView in-process mucho más rápido y sin instalación,
125
- // pero requiere Chrome instalado (o macOS con WebKit). "auto" toma webview
126
- // sólo si hay motor. Lo pisa HIVE_BROWSER_BACKEND.
130
+ // Queda un solo backend: Bun.WebView in-process. La clave sobrevive para no
131
+ // romper configs viejas —"agent-browser" se acepta, avisa y usa el WebView—
132
+ // y se puede quitar sin más. Lo pisa HIVE_BROWSER_BACKEND.
127
133
  backend: z.enum(["agent-browser", "webview", "auto"]).optional(),
134
+ // Guarda las cookies para que los logins sobrevivan a un reinicio. Default
135
+ // activo; apagarlo hace que cada arranque empiece sin historia.
136
+ persistSession: z.boolean().optional(),
128
137
  });
129
138
 
130
139
  const CanvasConfigSchema = z.object({
@@ -37,8 +37,12 @@ export class EthicsGuard {
37
37
  * Con `agentRole` filtra por las que lo declaran en `applicable_to`; si
38
38
  * ninguna coincide devuelve todas, para no dejar al agente sin capa por un
39
39
  * `applicable_to` mal cargado.
40
+ *
41
+ * `userId` acota lo aprendido a quien corresponde: entran las globales
42
+ * (`user_id === ""`, sembradas con el producto) y las que salieron de las
43
+ * trazas de ese mismo usuario. Omitirlo deja sólo las globales.
40
44
  */
41
- async getRules(agentRole?: string): Promise<EthicsRule[]> {
45
+ async getRules(agentRole?: string, userId?: string): Promise<EthicsRule[]> {
42
46
  const playbookCol = await col<PlaybookDoc>("playbook");
43
47
  const all = (await playbookCol.scan({}))
44
48
  .map((e) => ({
@@ -48,8 +52,10 @@ export class EthicsGuard {
48
52
  applicable_to: e.doc.applicable_to,
49
53
  helpful_count: e.doc.helpful_count ?? 0,
50
54
  active: e.doc.active,
55
+ user_id: e.doc.user_id ?? "",
51
56
  }))
52
57
  .filter((r) => r.active && r.category === RESPONSE_QUALITY)
58
+ .filter((r) => r.user_id === "" || r.user_id === (userId ?? ""))
53
59
  .sort(byUsefulness);
54
60
 
55
61
  if (!agentRole) return all;
@@ -10,9 +10,9 @@
10
10
  */
11
11
 
12
12
  import { EventEmitter } from "events";
13
- import { logger } from "../utils/logger";
14
- import { col, nextId, toIndexable, fromIndexable, BROADCAST } from "../storage/hive";
15
- import type { AgentBusMessageDoc, TaskDoc } from "../storage/collections";
13
+ import { logger } from "../utils/logger.ts";
14
+ import { col, nextId, toIndexable, fromIndexable, BROADCAST } from "../storage/hive.ts";
15
+ import type { AgentBusMessageDoc, TaskDoc } from "../storage/collections.ts";
16
16
 
17
17
  const log = logger.child("agent-bus");
18
18
 
@@ -6,9 +6,9 @@
6
6
  // messages before the actual answer — noise for the user and a rate-limit /
7
7
  // ban risk on WhatsApp. This module decides what reaches those channels.
8
8
 
9
- import { col } from "../storage/hive";
10
- import type { ChannelDoc, NarrationEventDoc } from "../storage/collections";
11
- import { logger } from "../utils/logger";
9
+ import { col } from "../storage/hive.ts";
10
+ import type { ChannelDoc, NarrationEventDoc } from "../storage/collections.ts";
11
+ import { logger } from "../utils/logger.ts";
12
12
 
13
13
  const log = logger.child("narration:channel");
14
14
 
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from "events";
2
- import { logger } from "../utils/logger";
2
+ import { logger } from "../utils/logger.ts";
3
3
 
4
4
  export interface EventMap {
5
5
  "message:received": {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Events — el bus de eventos del runtime y la narración de lo que hace un agente.
3
+ *
4
+ * Dos buses con propósitos distintos:
5
+ * - `eventBus`: eventos del proceso, tipados (`event-bus.ts`).
6
+ * - `agentBus`: mensajería entre workers de un enjambre, con respaldo
7
+ * persistente en HiveDB para que un worker lea lo que le dejaron mientras
8
+ * no estaba (`agent-bus.ts`).
9
+ *
10
+ * La narración traduce una tool call a una frase que se le puede mostrar a
11
+ * alguien ("Buscando en la web...") en vez del nombre crudo de la tool.
12
+ */
13
+
14
+ export * from "./event-bus.ts";
15
+ export * from "./agent-bus.ts";
16
+ export * from "./narration.ts";
17
+ export * from "./tool-narration.ts";
18
+ export * from "./channel-narration.ts";
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import { col } from "../storage/hive";
3
- import type { NarrationEventDoc } from "../storage/collections";
4
- import { logger } from "../utils/logger";
2
+ import { col } from "../storage/hive.ts";
3
+ import type { NarrationEventDoc } from "../storage/collections.ts";
4
+ import { logger } from "../utils/logger.ts";
5
5
 
6
6
  const log = logger.child("narration");
7
7
 
@@ -43,7 +43,11 @@ const TOOL_NARRATIONS: Record<string, string> = {
43
43
  browser_click: "Haciendo clic...",
44
44
  browser_type: "Escribiendo en la página...",
45
45
  browser_screenshot: "Tomando captura de pantalla...",
46
+ computer_use_task: "Operando el navegador...",
46
47
  browser_extract: "Extrayendo información de la página...",
48
+ // Artefactos
49
+ artifact_inspect: "Verificando el archivo generado...",
50
+ artifact_read: "Leyendo el resultado completo...",
47
51
  // Canvas
48
52
  canvas_add_node: "Actualizando canvas...",
49
53
  canvas_update: "Actualizando canvas...",
@@ -1,16 +1,110 @@
1
1
  /**
2
- * Channel Notify — stub for SDK compatibility.
3
- * In the full harness this sends messages back to channels.
2
+ * Channel Notify — el camino de salida hacia el usuario.
3
+ *
4
+ * Esto era un stub que sólo hacía `console.log`, y estaba en el camino real: la
5
+ * tool `notify`, los reportes de progreso, el aviso de que una tarea programada
6
+ * terminó, el de un turno interrumpido por un crash y el de compactación pasan
7
+ * todos por acá. Es decir, un agente sobre el SDK **no podía hablarle al
8
+ * usuario por ningún canal** — mientras `channels/manager.ts` tenía adaptadores
9
+ * funcionales de Slack, Discord, Telegram y WhatsApp, sin nada que los conectara.
10
+ *
11
+ * El cableado es explícito y opcional: la app registra su `ChannelManager` con
12
+ * `setChannelManager()`. Sin registro se conserva el comportamiento anterior
13
+ * —un log— porque un proceso que no maneja canales (un script, un test) no
14
+ * debería fallar por intentar notificar.
15
+ *
16
+ * Resolver a quién enviar es la otra mitad. `ChannelManager.send` necesita un
17
+ * `sessionId`, que es el contacto o grupo dentro del canal. Se obtiene del
18
+ * `threadId` (`${userId}/${canal}/${peer}`) cuando viene, y si no, buscando la
19
+ * conversación de ese usuario en ese canal. Sin eso el mensaje no sabe a qué
20
+ * chat volver.
4
21
  */
5
22
 
23
+ import { logger } from "../utils/logger.ts";
24
+ import { parseThreadId } from "../agent/thread-id.ts";
25
+ import { threadForChannel, listThreads } from "../agent/thread-store.ts";
26
+
27
+ const log = logger.child("channel-notify");
28
+
29
+ /** Lo mínimo que se necesita de un ChannelManager, para no atarse a su clase. */
30
+ export interface ChannelSender {
31
+ send(channelName: string, sessionId: string, message: unknown, accountId?: string): Promise<void>;
32
+ }
33
+
34
+ let _sender: ChannelSender | null = null;
35
+
36
+ /**
37
+ * Conecta el manager de canales. Llamalo una vez al arrancar, después de
38
+ * `channelManager.initialize()`.
39
+ */
40
+ export function setChannelManager(sender: ChannelSender | null): void {
41
+ _sender = sender;
42
+ log.info(sender ? "canales conectados: las notificaciones salen de verdad" : "canales desconectados");
43
+ }
44
+
45
+ export function getChannelManager(): ChannelSender | null {
46
+ return _sender;
47
+ }
48
+
49
+ /**
50
+ * A qué conversación del canal enviar.
51
+ *
52
+ * El `threadId` ya lleva el peer adentro, así que si viene se usa. Si no, se
53
+ * busca el hilo del usuario en ese canal; y como último recurso se usa el
54
+ * `userId`, que es lo que hacían las instalaciones anteriores a la separación
55
+ * por canal.
56
+ */
57
+ async function resolveSessionId(
58
+ channel: string,
59
+ userId: string,
60
+ threadId?: string,
61
+ ): Promise<string | null> {
62
+ if (threadId) {
63
+ const parts = parseThreadId(threadId);
64
+ if (parts?.peerId) return parts.peerId;
65
+ }
66
+ // `threadForChannel` mira `userIdentities`, que es el registro canónico de
67
+ // "por dónde se alcanza a este usuario".
68
+ const delCanal = await threadForChannel(userId, channel).catch(() => null);
69
+ if (delCanal) {
70
+ const parts = parseThreadId(delCanal);
71
+ if (parts?.peerId) return parts.peerId;
72
+ }
73
+
74
+ // Si no hay identidad registrada pero sí una conversación abierta en ese
75
+ // canal, ahí es donde responder: es evidencia igual de válida de dónde está
76
+ // el usuario, y evita perder el aviso por un registro que nadie llenó.
77
+ const hilos = await listThreads(userId, { channel }).catch(() => []);
78
+ const reciente = hilos[0];
79
+ if (reciente) {
80
+ const parts = parseThreadId(reciente.id);
81
+ if (parts?.peerId) return parts.peerId;
82
+ }
83
+
84
+ return userId || null;
85
+ }
86
+
6
87
  export async function notifyChannel(
7
88
  channel: string,
8
89
  userId: string,
9
90
  message: string,
10
91
  opts?: { threadId?: string; metadata?: Record<string, unknown> }
11
92
  ): Promise<void> {
12
- // TODO: integrate with ChannelManager for full functionality
13
- console.log(`[channel-notify] ${channel}: ${message}`);
93
+ if (!_sender) {
94
+ // Sin canales conectados no es un error: hay procesos que legítimamente no
95
+ // los tienen. Pero conviene que se note, porque un `notify` que no llega es
96
+ // silencioso por naturaleza.
97
+ log.warn(`sin ChannelManager conectado — el mensaje para ${channel} no sale: ${message.slice(0, 80)}`);
98
+ return;
99
+ }
100
+
101
+ const sessionId = await resolveSessionId(channel, userId, opts?.threadId);
102
+ if (!sessionId) {
103
+ log.warn(`no pude resolver a qué conversación de ${channel} enviarle a ${userId}`);
104
+ return;
105
+ }
106
+
107
+ await _sender.send(channel, sessionId, message);
14
108
  }
15
109
 
16
110
  export async function sendToUserChannel(
@@ -23,15 +117,18 @@ export async function sendToUserChannel(
23
117
  await notifyChannel(channel, userId, message, opts);
24
118
  return { ok: true };
25
119
  } catch (err) {
120
+ // Un canal caído no debe tumbar el turno que estaba notificando.
121
+ log.warn(`falló el envío a ${channel}: ${(err as Error).message}`);
26
122
  return { ok: false, error: (err as Error).message };
27
123
  }
28
124
  }
29
125
 
30
126
  export async function broadcastNotification(
31
127
  channels: string[],
32
- message: string
128
+ message: string,
129
+ userId = "",
33
130
  ): Promise<void> {
34
131
  for (const channel of channels) {
35
- await notifyChannel(channel, "", message);
132
+ await notifyChannel(channel, userId, message).catch(() => {});
36
133
  }
37
134
  }
@@ -1,10 +1,10 @@
1
- import { col } from "../storage/hive";
1
+ import { col } from "../storage/hive.ts";
2
2
  import type {
3
3
  DelegationGroupDoc,
4
4
  DelegationGroupOutcome,
5
- } from "../storage/collections";
6
- import { logger } from "../utils/logger";
7
- import { publishNarration } from "../events/narration";
5
+ } from "../storage/collections.ts";
6
+ import { logger } from "../utils/logger.ts";
7
+ import { publishNarration } from "../events/narration.ts";
8
8
 
9
9
  const log = logger.child("delegation-groups");
10
10
  const MAX_RETRIES = 8;
@@ -14,7 +14,7 @@
14
14
  * is preserved, while all queue state is mirrored to the DB.
15
15
  */
16
16
 
17
- import { logger } from "../utils/logger";
17
+ import { logger } from "../utils/logger.ts";
18
18
  import {
19
19
  createJob,
20
20
  claimJob,
@@ -30,9 +30,9 @@ import {
30
30
  loadJobRetryPolicy,
31
31
  DEFAULT_JOB_RETRY_POLICY,
32
32
  type JobRetryPolicy,
33
- } from "./job-store";
34
- import type { JobDoc } from "../storage/collections";
35
- import { getBootId } from "../storage/boot-id";
33
+ } from "./job-store.ts";
34
+ import type { JobDoc } from "../storage/collections.ts";
35
+ import { getBootId } from "../storage/boot-id.ts";
36
36
 
37
37
  const log = logger.child("durable-queue");
38
38
 
@@ -76,6 +76,17 @@ export function registerExecutor(type: JobType, executor: JobExecutor): void {
76
76
  log.info(`[registerExecutor] Registered executor for type=${type}`);
77
77
  }
78
78
 
79
+ /**
80
+ * Los tipos de job que este proceso sabe ejecutar.
81
+ *
82
+ * El registro era privado, así que no había forma de comprobar desde fuera si
83
+ * un tipo quedó cableado — y un job encolado sin ejecutor no falla al encolarse
84
+ * sino al tomarse, que es tarde y lejos de donde está el error.
85
+ */
86
+ export function getRegisteredExecutorTypes(): JobType[] {
87
+ return [...executors.keys()];
88
+ }
89
+
79
90
  export interface JobTerminalOutcome {
80
91
  ok: boolean;
81
92
  result?: unknown;
@@ -92,7 +103,8 @@ export function registerTerminalHook(type: JobType, hook: JobTerminalHook): void
92
103
  terminalHooks.set(type, hook);
93
104
  }
94
105
 
95
- async function runTerminalHook(job: JobDoc, outcome: JobTerminalOutcome): Promise<void> {
106
+ /** Exported so job-store.ts's reclaimOrInterrupt can fire it too (dynamic import there — see its call site). */
107
+ export async function runTerminalHook(job: JobDoc, outcome: JobTerminalOutcome): Promise<void> {
96
108
  const hook = terminalHooks.get(job.type);
97
109
  if (!hook) return;
98
110
  try {
@@ -287,7 +299,7 @@ export class DurableLaneQueue {
287
299
  let leasedHere = true;
288
300
  const leaseRenewer = setInterval(async () => {
289
301
  if (leasedHere) {
290
- const { renewLease } = await import("./job-store");
302
+ const { renewLease } = await import("./job-store.ts");
291
303
  await renewLease(job.id, this.bootId).catch(() => {});
292
304
  }
293
305
  }, 30_000);
@@ -1,2 +1,5 @@
1
1
  export { startGateway } from "./server.ts";
2
2
  export type { GatewayConfig } from "./server.ts";
3
+
4
+ // Salida hacia el usuario: la app conecta su ChannelManager con setChannelManager().
5
+ export { setChannelManager, getChannelManager, notifyChannel, sendToUserChannel, broadcastNotification, type ChannelSender } from "./channel-notify.ts";
@@ -5,11 +5,11 @@
5
5
  * path guarantees only one process wins the race for the same job.
6
6
  */
7
7
 
8
- import { col, nextId, updateDoc, toIndexable } from "../storage/hive";
9
- import type { JobDoc } from "../storage/collections";
10
- import { getBootId } from "../storage/boot-id";
11
- import { logger } from "../utils/logger";
12
- import { loadConfig } from "../config/loader";
8
+ import { col, nextId, updateDoc, toIndexable } from "../storage/hive.ts";
9
+ import type { JobDoc } from "../storage/collections.ts";
10
+ import { getBootId } from "../storage/boot-id.ts";
11
+ import { logger } from "../utils/logger.ts";
12
+ import { loadConfig } from "../config/loader.ts";
13
13
 
14
14
  const log = logger.child("job-store");
15
15
 
@@ -337,6 +337,12 @@ export async function reclaimOrInterrupt(jobId: string, opts?: { force?: boolean
337
337
  try {
338
338
  await c.put(jobId, updated, { expectedVersion: entry.version });
339
339
  log.warn(`[reclaimOrInterrupt] Job ${jobId} interrupted (attempts exhausted)`);
340
+ // Terminal via lease-expiry, not executeJob's normal fail path — that
341
+ // path already fires the hook, this one didn't before. Dynamic import
342
+ // avoids a static circular import (durable-queue.ts imports
343
+ // reclaimOrInterrupt from this module).
344
+ const { runTerminalHook } = await import("./durable-queue.ts");
345
+ await runTerminalHook(updated, { ok: false, error: updated.error ?? "Job interrupted after lease expiry" }).catch(() => {});
340
346
  return updated;
341
347
  } catch {
342
348
  await occRetryDelay(attempt);
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import type { NotificationDoc } from "../storage/collections";
3
- import { col, updateDoc } from "../storage/hive";
2
+ import type { NotificationDoc } from "../storage/collections.ts";
3
+ import { col, updateDoc } from "../storage/hive.ts";
4
4
 
5
5
  export async function createNotification(input: {
6
6
  userId: string;
@@ -7,9 +7,9 @@
7
7
  * - WebSocket /ws — real-time streaming
8
8
  */
9
9
 
10
- import { logger } from "../utils/logger";
10
+ import { logger } from "../utils/logger.ts";
11
11
  import { runAgent } from "../agent/agent-loop.ts";
12
- import type { MCPClientManager } from "../mcp/index";
12
+ import type { MCPClientManager } from "../mcp/index.ts";
13
13
 
14
14
  const log = logger.child("gateway");
15
15