@johpaz/hive-sdk 0.1.6 → 0.2.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 (185) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/README.md +11 -1
  3. package/package.json +18 -2
  4. package/packages/core/src/agent/acceptance-checks.ts +9 -9
  5. package/packages/core/src/agent/agent-catalog.ts +3 -3
  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 +10 -9
  10. package/packages/core/src/agent/context-compiler.ts +58 -34
  11. package/packages/core/src/agent/conversation-store.ts +31 -7
  12. package/packages/core/src/agent/curator.ts +4 -4
  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 +1 -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 +9 -5
  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 +4 -4
  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 +4 -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 +6 -6
  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 +250 -0
  49. package/packages/core/src/agent/tool-selector.ts +7 -5
  50. package/packages/core/src/agent/tracer.ts +5 -5
  51. package/packages/core/src/api/createAgent.ts +1 -1
  52. package/packages/core/src/artifacts/store.ts +84 -3
  53. package/packages/core/src/canvas/emitter.ts +2 -2
  54. package/packages/core/src/channels/telegram.ts +1 -1
  55. package/packages/core/src/channels/webchat.ts +1 -1
  56. package/packages/core/src/config/loader.ts +14 -5
  57. package/packages/core/src/events/agent-bus.ts +3 -3
  58. package/packages/core/src/events/channel-narration.ts +3 -3
  59. package/packages/core/src/events/event-bus.ts +1 -1
  60. package/packages/core/src/events/narration.ts +3 -3
  61. package/packages/core/src/gateway/delegation-groups.ts +4 -4
  62. package/packages/core/src/gateway/durable-queue.ts +5 -5
  63. package/packages/core/src/gateway/job-store.ts +5 -5
  64. package/packages/core/src/gateway/notification-inbox.ts +2 -2
  65. package/packages/core/src/gateway/server.ts +2 -2
  66. package/packages/core/src/mcp/MCPClient.ts +3 -3
  67. package/packages/core/src/mcp/hot-reload.ts +5 -5
  68. package/packages/core/src/mcp/tool-sync.ts +5 -5
  69. package/packages/core/src/mcp/transports/index.ts +2 -2
  70. package/packages/core/src/mcp/transports/sse.ts +1 -1
  71. package/packages/core/src/models/index.ts +36 -0
  72. package/packages/core/src/multimodal/index.ts +2 -2
  73. package/packages/core/src/multimodal/vision-service.ts +6 -6
  74. package/packages/core/src/plugins/loader.ts +4 -1
  75. package/packages/core/src/resilience/circuit-breaker.ts +16 -5
  76. package/packages/core/src/resilience/retry.ts +1 -1
  77. package/packages/core/src/scheduler/CronScheduler.ts +6 -6
  78. package/packages/core/src/scheduler/integration.ts +9 -9
  79. package/packages/core/src/sessions/index.ts +266 -0
  80. package/packages/core/src/storage/bootstrap.ts +34 -8
  81. package/packages/core/src/storage/causal-events.ts +1 -1
  82. package/packages/core/src/storage/collections.ts +32 -1
  83. package/packages/core/src/storage/crypto.ts +16 -2
  84. package/packages/core/src/storage/hive.ts +1 -1
  85. package/packages/core/src/storage/hivedb.ts +10 -1
  86. package/packages/core/src/storage/onboarding.ts +6 -6
  87. package/packages/core/src/storage/reconcile.ts +5 -5
  88. package/packages/core/src/storage/seed.ts +103 -13
  89. package/packages/core/src/storage/usage.ts +3 -3
  90. package/packages/core/src/swarm/AgentExecutor.ts +2 -2
  91. package/packages/core/src/swarm/Coordinator.ts +8 -8
  92. package/packages/core/src/swarm/EventBridge.ts +2 -2
  93. package/packages/core/src/swarm/RoleSwarm.ts +234 -0
  94. package/packages/core/src/swarm/TaskGraph.ts +2 -2
  95. package/packages/core/src/swarm/index.ts +7 -0
  96. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +2 -2
  97. package/packages/core/src/swarm/presets/ResearchPreset.ts +2 -2
  98. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +1 -1
  99. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +3 -3
  100. package/packages/core/src/tools/ToolExecutor.ts +7 -3
  101. package/packages/core/src/tools/core/index.ts +2 -2
  102. package/packages/core/src/tools/cron/index.ts +4 -4
  103. package/packages/core/src/tools/web/artifact-inspect.ts +2 -2
  104. package/packages/core/src/tools/web/artifact-read.ts +162 -0
  105. package/packages/core/src/tools/web/browser-backend.ts +141 -44
  106. package/packages/core/src/tools/web/browser-click.ts +2 -2
  107. package/packages/core/src/tools/web/browser-extract.ts +2 -2
  108. package/packages/core/src/tools/web/browser-navigate.ts +2 -2
  109. package/packages/core/src/tools/web/browser-screenshot.ts +12 -5
  110. package/packages/core/src/tools/web/browser-script.ts +2 -2
  111. package/packages/core/src/tools/web/browser-service.ts +63 -384
  112. package/packages/core/src/tools/web/browser-session.ts +125 -0
  113. package/packages/core/src/tools/web/browser-type.ts +2 -2
  114. package/packages/core/src/tools/web/browser-wait.ts +2 -2
  115. package/packages/core/src/tools/web/computer-use.ts +553 -0
  116. package/packages/core/src/tools/web/index.ts +8 -1
  117. package/packages/core/src/tools/web/webview-backend.ts +460 -21
  118. package/packages/core/src/utils/index.ts +1 -0
  119. package/packages/core/src/utils/logger.ts +12 -4
  120. package/packages/core/src/utils/redact-binary.ts +17 -0
  121. package/packages/core/src/utils/toon.ts +1 -1
  122. package/packages/core/src/voice/index.ts +6 -6
  123. package/bun.lock +0 -859
  124. package/bunfig.toml +0 -9
  125. package/docs/API-AGENTS.md +0 -367
  126. package/docs/API-CONTEXT-COMPILER.md +0 -249
  127. package/docs/API-DAG-SCHEDULER.md +0 -273
  128. package/docs/API-TOOLS-SKILLS-CHANNELS.md +0 -446
  129. package/docs/API-WORKERS-EVENTS.md +0 -299
  130. package/docs/HIVE-HARNESS.md +0 -113
  131. package/docs/INDEX.md +0 -190
  132. package/docs/TEMPLATE-HIVE-APP.md +0 -360
  133. package/packages/cli/package.json +0 -17
  134. package/packages/cli/src/commands/create-app.test.ts +0 -180
  135. package/packages/core/package.json +0 -70
  136. package/packages/core/src/api/createAgent.test.ts +0 -160
  137. package/packages/core/src/canvas/canvas.test.ts +0 -36
  138. package/packages/core/src/channels/channels.test.ts +0 -18
  139. package/packages/core/src/ethics/EthicsGuard.test.ts +0 -108
  140. package/packages/core/src/gateway/gateway.test.ts +0 -38
  141. package/packages/core/src/memory/Scratchpad.test.ts +0 -68
  142. package/packages/core/src/scheduler/scheduler.test.ts +0 -15
  143. package/packages/core/src/skills/skills.test.ts +0 -62
  144. package/packages/core/src/swarm/swarm.test.ts +0 -24
  145. package/packages/core/src/tool-runtime/tool-runtime.test.ts +0 -99
  146. package/packages/core/src/tools/ToolRegistry.test.ts +0 -98
  147. package/packages/core/src/tools/api/api-request.test.ts +0 -164
  148. package/packages/core/src/tools/web/browser-service.test.ts +0 -83
  149. package/packages/core/src/workers/workers.test.ts +0 -41
  150. package/scripts/bump-version.ts +0 -248
  151. package/scripts/generate-skill-bundle.ts +0 -108
  152. package/test/acceptance-checks.test.ts +0 -403
  153. package/test/agent-loop-terminal-synthesis.test.ts +0 -32
  154. package/test/browser-backend.test.ts +0 -308
  155. package/test/catalog-agents-stay-enabled.test.ts +0 -117
  156. package/test/causal-events.test.ts +0 -117
  157. package/test/compaction.test.ts +0 -105
  158. package/test/context-compiler.test.ts +0 -269
  159. package/test/curator.test.ts +0 -130
  160. package/test/durable-queue.test.ts +0 -114
  161. package/test/harness-barrel.test.ts +0 -64
  162. package/test/hive-helpers.test.ts +0 -130
  163. package/test/hivedb-search.test.ts +0 -189
  164. package/test/internal-turns.test.ts +0 -166
  165. package/test/job-idempotency.test.ts +0 -68
  166. package/test/job-retry-backoff.test.ts +0 -184
  167. package/test/job-store.test.ts +0 -381
  168. package/test/llm-retry.test.ts +0 -97
  169. package/test/memory-perf.test.ts +0 -774
  170. package/test/minimal-loadout.test.ts +0 -78
  171. package/test/model-catalog.test.ts +0 -105
  172. package/test/preload.ts +0 -12
  173. package/test/reflector.test.ts +0 -320
  174. package/test/retention-cap.test.ts +0 -91
  175. package/test/retired-capabilities-pruned.test.ts +0 -192
  176. package/test/run-store.test.ts +0 -355
  177. package/test/scratchpad.test.ts +0 -74
  178. package/test/secrets-durability.test.ts +0 -119
  179. package/test/seed-model-reseed.test.ts +0 -155
  180. package/test/setup-agent-seed.test.ts +0 -264
  181. package/test/tool-inventory.test.ts +0 -65
  182. package/test/tool-runtime.test.ts +0 -258
  183. package/test/tool-selector-runtime-tools.test.ts +0 -117
  184. package/test/toon.test.ts +0 -429
  185. package/tsconfig.json +0 -42
@@ -11,12 +11,12 @@
11
11
  * trick the SQLite version used — HiveDB has no equivalent primitive.
12
12
  */
13
13
 
14
- import { logger } from "../utils/logger"
15
- import { col, nextId } from "../storage/hive"
16
- import { getHiveDb } from "../storage/hivedb"
17
- import { loadConfig } from "../config/loader"
14
+ import { logger } from "../utils/logger.ts"
15
+ import { col, nextId } from "../storage/hive.ts"
16
+ import { getHiveDb } from "../storage/hivedb.ts"
17
+ import { loadConfig } from "../config/loader.ts"
18
18
  import type { HiveDB, ToolStats } from "@johpaz/hive-db"
19
- import type { TraceDoc, ReflectionDoc, CursorDoc } from "../storage/collections"
19
+ import type { TraceDoc, ReflectionDoc, CursorDoc } from "../storage/collections.ts"
20
20
 
21
21
  const log = logger.child("reflector")
22
22
 
@@ -88,7 +88,7 @@ export async function runReflector(): Promise<void> {
88
88
  await cursorsCol.put(CURSOR_ID, { value: newCursor }, cursorEntry ? { expectedVersion: cursorEntry.version } : { expectedVersion: 0 })
89
89
 
90
90
  // Trigger curator
91
- const { runCurator } = await import("./curator")
91
+ const { runCurator } = await import("./curator.ts")
92
92
  await runCurator()
93
93
 
94
94
  log.info(`[reflector] Reflection cycle completed successfully`)
@@ -10,14 +10,14 @@
10
10
  * should write to a run; single-writer pattern keeps contention minimal.
11
11
  */
12
12
 
13
- import { col, updateDoc, nextId, toIndexable } from "../storage/hive";
14
- import type { AgentRunDoc } from "../storage/collections";
15
- import { getBootId } from "../storage/boot-id";
16
- import { logger } from "../utils/logger";
17
- import { loadConfig } from "../config/loader";
18
- import type { LLMMessage } from "./llm-client";
19
- import type { RunEpoch } from "./run-epoch";
20
- import { formatInternalEvent } from "./conversation-store";
13
+ import { col, updateDoc, nextId, toIndexable } from "../storage/hive.ts";
14
+ import type { AgentRunDoc } from "../storage/collections.ts";
15
+ import { getBootId } from "../storage/boot-id.ts";
16
+ import { logger } from "../utils/logger.ts";
17
+ import { loadConfig } from "../config/loader.ts";
18
+ import type { LLMMessage } from "./llm-client.ts";
19
+ import type { RunEpoch } from "./run-epoch.ts";
20
+ import { formatInternalEvent } from "./conversation-store.ts";
21
21
 
22
22
  const log = logger.child("run-store");
23
23
 
@@ -12,15 +12,15 @@
12
12
  * - Eventos (cron, etc.)
13
13
  */
14
14
 
15
- import { logger } from "../utils/logger"
16
- import { buildSystemPromptWithProjects } from "./prompt-builder"
17
- import { getAgentLoop, rebuildAgentLoop } from "./agent-loop"
15
+ import { logger } from "../utils/logger.ts"
16
+ import { buildSystemPromptWithProjects } from "./prompt-builder.ts"
17
+ import { getAgentLoop, rebuildAgentLoop } from "./agent-loop.ts"
18
18
  import type { MCPClientManager } from "../mcp/index.ts"
19
- import { resolveAgentId, resolveUserId } from "../storage/onboarding"
20
- import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton"
21
- import type { ContentPart } from "./llm-client"
22
- import { col, fromIndexable } from "../storage/hive"
23
- import type { AgentDoc, EthicsDoc } from "../storage/collections"
19
+ import { resolveAgentId, resolveUserId } from "../storage/onboarding.ts"
20
+ import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton.ts"
21
+ import type { ContentPart } from "./llm-client.ts"
22
+ import { col, fromIndexable } from "../storage/hive.ts"
23
+ import type { AgentDoc, EthicsDoc } from "../storage/collections.ts"
24
24
 
25
25
  const log = logger.child("agent-service")
26
26
 
@@ -161,7 +161,7 @@ export class AgentService {
161
161
  */
162
162
  async reloadSkills(): Promise<void> {
163
163
  log.info("Reloading skills...")
164
- const { syncSkillsToIndex } = await import("./context-compiler")
164
+ const { syncSkillsToIndex } = await import("./context-compiler.ts")
165
165
  await syncSkillsToIndex()
166
166
  log.info("Skills reloaded")
167
167
  }
@@ -252,7 +252,7 @@ export class AgentService {
252
252
  * Ejecuta un agente con un mensaje
253
253
  */
254
254
  async runAgent(message: string | ContentPart[], threadId: string, userId?: string): Promise<string> {
255
- const { runAgentIsolated } = await import("./agent-loop")
255
+ const { runAgentIsolated } = await import("./agent-loop.ts")
256
256
  const result = await runAgentIsolated({
257
257
  agentId: this.agentId,
258
258
  taskDescription: message,
@@ -16,17 +16,17 @@
16
16
  * 5. Returns skill content for injection into system prompt
17
17
  */
18
18
 
19
- import { col } from "../storage/hive"
20
- import type { SkillDoc } from "../storage/collections"
21
- import { logger } from "../utils/logger"
22
- import { isMinimalSkill } from "./minimal-loadout"
23
- import { isCalendarOperation } from "./routing-intent"
19
+ import { col } from "../storage/hive.ts"
20
+ import type { SkillDoc } from "../storage/collections.ts"
21
+ import { logger } from "../utils/logger.ts"
22
+ import { isMinimalSkill } from "./minimal-loadout.ts"
23
+ import { isCalendarOperation } from "./routing-intent.ts"
24
24
  import {
25
25
  searchCapabilities,
26
26
  applyRelativeCutoff,
27
27
  replaceCapabilityDocs,
28
28
  type CapabilityDoc,
29
- } from "./capability-search"
29
+ } from "./capability-search.ts"
30
30
 
31
31
  const log = logger.child("skill-selector")
32
32
 
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Thread identity — `${userId}/${channel}/${peerId}`.
3
+ *
4
+ * Un hilo por canal Y por contacto: el chat privado de Telegram, cada grupo de
5
+ * WhatsApp y cada conversación de la web tienen historial propio. Antes había un
6
+ * único hilo por usuario (`threadId = userId`) compartido por todos los canales.
7
+ *
8
+ * Dos reglas de formato que no son cosméticas:
9
+ *
10
+ * 1. El separador es "/" y NO ":". Los mensajes viven en `conversations` con id
11
+ * `${threadId}:${seq}` y se leen por prefijo (`scan({ prefix: "${threadId}:" })`),
12
+ * sin índice secundario. El hilo legacy (id = userId pelado) se lee con prefijo
13
+ * `${userId}:`; si los hilos nuevos fueran `${userId}:webchat:...` ese scan se los
14
+ * tragaría enteros. Con "/" el historial viejo queda aislado sin migrar una fila.
15
+ *
16
+ * 2. Ningún segmento puede contener ":" — se sustituye por "_". Sin esto los peerIds
17
+ * compuestos rompen el aislamiento por prefijo: el DM de Telegram `12345` y el grupo
18
+ * `12345:678` (telegram.ts arma así el peerId de grupo) producirían los prefijos
19
+ * `.../12345:` y `.../12345:678:`, y el primero es prefijo del segundo — el DM leería
20
+ * los mensajes del grupo. Colapsar ":" hace que `${threadId}:` sea siempre un
21
+ * separador inequívoco, y mantiene válido el `lastIndexOf(":")` con que
22
+ * conversation-store extrae el número de secuencia.
23
+ */
24
+
25
+ export const THREAD_SEP = "/"
26
+
27
+ /** Canal sintético de las tareas programadas: un hilo estable por cron job. */
28
+ export const CRON_CHANNEL = "cron"
29
+
30
+ export interface ThreadParts {
31
+ userId: string
32
+ channel: string
33
+ peerId: string
34
+ }
35
+
36
+ /**
37
+ * Deja un segmento apto para formar parte de un threadId: sin "/" (rompería el
38
+ * parseo) y sin ":" (rompería el aislamiento por prefijo — ver cabecera).
39
+ */
40
+ export function sanitizeSegment(value: string): string {
41
+ return value.replace(/[/:]/g, "_").trim()
42
+ }
43
+
44
+ export function makeThreadId(userId: string, channel: string, peerId: string): string {
45
+ const parts = [userId, channel, peerId].map((p) => sanitizeSegment(p || "") || "default")
46
+ return parts.join(THREAD_SEP)
47
+ }
48
+
49
+ /**
50
+ * Descompone un threadId con formato. Devuelve null para todo lo que no lo tenga:
51
+ * el hilo legacy (`userId` pelado), los hilos aislados de los workers
52
+ * (`task-<id>-<agente>`) y cualquier id heredado. Quien llame debe conservar su
53
+ * comportamiento anterior ante un null, nunca inventar partes.
54
+ */
55
+ export function parseThreadId(threadId: string): ThreadParts | null {
56
+ if (!threadId) return null
57
+ const parts = threadId.split(THREAD_SEP)
58
+ if (parts.length !== 3) return null
59
+ const [userId, channel, peerId] = parts
60
+ if (!userId || !channel || !peerId) return null
61
+ return { userId, channel, peerId }
62
+ }
63
+
64
+ export function isStructuredThreadId(threadId: string): boolean {
65
+ return parseThreadId(threadId) !== null
66
+ }
67
+
68
+ /** Id de una conversación nueva de la web. No lo elige el cliente. */
69
+ export function newWebConversationId(): string {
70
+ return `conv-${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`
71
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Thread store — el registro de conversaciones (`conversationThreads`).
3
+ *
4
+ * `conversations` no tiene índice secundario: los mensajes se leen por prefijo del
5
+ * threadId. Esta colección es el catálogo que permite listar las conversaciones de
6
+ * un usuario, ponerles título y borrarlas sin escanear todos los mensajes.
7
+ *
8
+ * El id de cada fila ES el threadId. La única fila cuyo id no tiene formato
9
+ * `${user}/${canal}/${peer}` es la del hilo legacy anterior a la separación por
10
+ * canal (ver ensureLegacyThread).
11
+ */
12
+
13
+ import { col, updateDoc } from "../storage/hive.ts"
14
+ import { logger } from "../utils/logger.ts"
15
+ import type { ConversationThreadDoc, ConversationDoc, SummaryDoc } from "../storage/collections.ts"
16
+ import { makeThreadId, parseThreadId, newWebConversationId } from "./thread-id.ts"
17
+
18
+ const log = logger.child("thread-store")
19
+
20
+ const TITLE_MAX_CHARS = 60
21
+
22
+ /** Reintentos del contador ante mensajes concurrentes del mismo hilo. */
23
+ const TOUCH_MAX_RETRIES = 5
24
+
25
+ export interface EnsureThreadInput {
26
+ userId: string
27
+ channel: string
28
+ peerId: string
29
+ peerKind?: "direct" | "group"
30
+ }
31
+
32
+ async function threadsCol() {
33
+ return col<ConversationThreadDoc>("conversationThreads")
34
+ }
35
+
36
+ export async function getThread(threadId: string): Promise<ConversationThreadDoc | null> {
37
+ const c = await threadsCol()
38
+ const entry = await c.get(threadId)
39
+ return entry?.doc ?? null
40
+ }
41
+
42
+ /**
43
+ * Crea la fila de la conversación si aún no existe y devuelve su threadId.
44
+ * Idempotente: se llama en cada turno desde resolveContext.
45
+ */
46
+ export async function ensureThread(input: EnsureThreadInput): Promise<string> {
47
+ const threadId = makeThreadId(input.userId, input.channel, input.peerId)
48
+ const c = await threadsCol()
49
+ const existing = await c.get(threadId)
50
+ if (existing) return threadId
51
+
52
+ const now = Date.now()
53
+ try {
54
+ await c.put(threadId, {
55
+ id: threadId,
56
+ user_id: input.userId,
57
+ channel: input.channel,
58
+ peer_id: input.peerId,
59
+ peer_kind: input.peerKind ?? "direct",
60
+ title: null,
61
+ archived: false,
62
+ created_at: now,
63
+ last_message_at: now,
64
+ message_count: 0,
65
+ }, { expectedVersion: 0 })
66
+ } catch {
67
+ // Otro turno del mismo canal la creó primero — ambos quieren lo mismo.
68
+ }
69
+ return threadId
70
+ }
71
+
72
+ /** Conversación nueva de la web. El id lo elige el servidor, nunca el cliente. */
73
+ export async function createWebConversation(userId: string, title?: string): Promise<ConversationThreadDoc> {
74
+ const peerId = newWebConversationId()
75
+ const threadId = await ensureThread({ userId, channel: "webchat", peerId, peerKind: "direct" })
76
+ if (title?.trim()) await renameThread(threadId, title)
77
+ const doc = await getThread(threadId)
78
+ if (!doc) throw new Error(`No pude crear la conversación ${threadId}`)
79
+ return doc
80
+ }
81
+
82
+ /**
83
+ * Título derivado del primer mensaje del usuario. El contenido llega con el
84
+ * prefijo `[Timestamp: ...]` que le añaden el gateway y las rutas REST: esa línea
85
+ * no describe nada, así que se descarta antes de recortar.
86
+ */
87
+ export function deriveTitle(text: string): string | null {
88
+ const withoutTimestamp = text.replace(/^\[Timestamp:[^\]]*\]\s*/, "").trim()
89
+ const firstLine = withoutTimestamp.split("\n").find((l) => l.trim().length > 0)?.trim()
90
+ if (!firstLine) return null
91
+ return firstLine.length > TITLE_MAX_CHARS
92
+ ? `${firstLine.slice(0, TITLE_MAX_CHARS - 1)}…`
93
+ : firstLine
94
+ }
95
+
96
+ /**
97
+ * Registra actividad en el hilo: contador, fecha del último mensaje y —la primera
98
+ * vez que habla el usuario— el título.
99
+ *
100
+ * Si el hilo tiene formato y no existe la fila, la crea (es el caso de un hilo que
101
+ * nació fuera de resolveContext, como el de una tarea programada). Si no lo tiene
102
+ * —hilo legacy, hilos aislados `task-*`— solo actualiza una fila ya existente:
103
+ * nunca inventa conversaciones para los hilos internos de los workers.
104
+ */
105
+ export async function touchThread(
106
+ threadId: string,
107
+ opts: { role: "user" | "assistant" | "tool"; text?: string; internal?: boolean }
108
+ ): Promise<void> {
109
+ try {
110
+ const c = await threadsCol()
111
+ let existing = await c.get(threadId)
112
+
113
+ if (!existing) {
114
+ const parts = parseThreadId(threadId)
115
+ if (!parts) return
116
+ await ensureThread({ userId: parts.userId, channel: parts.channel, peerId: parts.peerId })
117
+ existing = await c.get(threadId)
118
+ if (!existing) return
119
+ }
120
+
121
+ // El contador se recalcula DENTRO del reintento, no fuera. `addMessage` llama
122
+ // acá sin esperar el resultado, así que dos mensajes seguidos se solapan: con
123
+ // el incremento calculado una sola vez, el reintento por conflicto de versión
124
+ // vuelve a escribir el valor viejo y el conteo se queda corto para siempre.
125
+ for (let attempt = 0; attempt < TOUCH_MAX_RETRIES; attempt++) {
126
+ const fresh = attempt === 0 ? existing : await c.get(threadId)
127
+ if (!fresh) return
128
+
129
+ const next: ConversationThreadDoc = {
130
+ ...fresh.doc,
131
+ last_message_at: Date.now(),
132
+ message_count: fresh.doc.message_count + 1,
133
+ }
134
+ // El título sale del primer mensaje humano: ni de la respuesta del agente ni
135
+ // de un evento interno (fan-in de delegación), que no describen el tema.
136
+ if (!fresh.doc.title && opts.role === "user" && !opts.internal && opts.text) {
137
+ const title = deriveTitle(opts.text)
138
+ if (title) next.title = title
139
+ }
140
+
141
+ try {
142
+ await c.put(threadId, next, { expectedVersion: fresh.version })
143
+ return
144
+ } catch {
145
+ // Conflicto de versión: otro mensaje del mismo hilo llegó primero.
146
+ }
147
+ }
148
+ log.warn(`demasiada contención actualizando el hilo ${threadId}`)
149
+ } catch (err) {
150
+ // El registro es un catálogo, no la fuente de verdad: nunca debe tumbar un turno.
151
+ log.warn(`no pude actualizar el hilo ${threadId}: ${(err as Error).message}`)
152
+ }
153
+ }
154
+
155
+ export async function listThreads(
156
+ userId: string,
157
+ opts?: { channel?: string; includeArchived?: boolean }
158
+ ): Promise<ConversationThreadDoc[]> {
159
+ const c = await threadsCol()
160
+ const rows = await c.findBy("user_id", userId)
161
+ return rows
162
+ .map((r) => r.doc)
163
+ .filter((d) => (opts?.channel ? d.channel === opts.channel : true))
164
+ .filter((d) => (opts?.includeArchived ? true : !d.archived))
165
+ .sort((a, b) => b.last_message_at - a.last_message_at)
166
+ }
167
+
168
+ /** La conversación de webchat en la que seguiría escribiendo el usuario. */
169
+ export async function mostRecentWebThread(userId: string): Promise<ConversationThreadDoc | null> {
170
+ const threads = await listThreads(userId, { channel: "webchat" })
171
+ return threads[0] ?? null
172
+ }
173
+
174
+ export async function renameThread(threadId: string, title: string): Promise<void> {
175
+ const clean = title.trim().slice(0, 200)
176
+ await updateDoc<ConversationThreadDoc>("conversationThreads", threadId, { title: clean || null })
177
+ }
178
+
179
+ /**
180
+ * Borra la conversación entera: mensajes, resumen, notas y la fila del registro.
181
+ * Los mensajes se localizan por prefijo, igual que los lee conversation-store.
182
+ */
183
+ export async function deleteThread(threadId: string): Promise<void> {
184
+ const conversations = await col<ConversationDoc>("conversations")
185
+ const messages = await conversations.scan({ prefix: `${threadId}:` })
186
+ for (const m of messages) await conversations.delete(m.id)
187
+
188
+ const summaries = await col<SummaryDoc>("summaries")
189
+ await summaries.delete(threadId).catch(() => {})
190
+
191
+ const scratchpad = await col<{ threadId: string }>("scratchpad")
192
+ const notes = await scratchpad.scan({ prefix: `${threadId}:` })
193
+ for (const n of notes) await scratchpad.delete(n.id)
194
+
195
+ const c = await threadsCol()
196
+ await c.delete(threadId).catch(() => {})
197
+
198
+ log.info(`conversación ${threadId} borrada (${messages.length} mensajes)`)
199
+ }
200
+
201
+ /**
202
+ * El hilo por el que hablarle a alguien en un canal cuando no venimos de un
203
+ * mensaje suyo (avisos de tareas programadas, por ejemplo).
204
+ *
205
+ * En la web es la conversación activa; en los demás canales, el contacto al que
206
+ * apunta `userIdentities`. Devuelve null si no hay ninguno, y quien llame decide
207
+ * el fallback.
208
+ */
209
+ export async function threadForChannel(userId: string, channel: string): Promise<string | null> {
210
+ if (channel === "webchat") return (await mostRecentWebThread(userId))?.id ?? null
211
+
212
+ const identities = await col<{ channel_user_id?: string }>("userIdentities")
213
+ const identity = await identities.get(`${userId}:${channel}`)
214
+ const peerId = identity?.doc.channel_user_id
215
+ return peerId ? makeThreadId(userId, channel, peerId) : null
216
+ }
217
+
218
+ /**
219
+ * Registra el hilo anterior a la separación por canal —`thread_id = userId`, un
220
+ * único hilo compartido por todos los canales— como una conversación más de la
221
+ * web, para que su historial siga siendo accesible. No mueve ni reescribe ningún
222
+ * mensaje: la fila apunta al mismo prefijo de siempre.
223
+ */
224
+ export async function ensureLegacyThread(userId: string): Promise<boolean> {
225
+ const c = await threadsCol()
226
+ if (await c.get(userId)) return false
227
+
228
+ const conversations = await col<ConversationDoc>("conversations")
229
+ const sample = await conversations.scan({ prefix: `${userId}:`, limit: 1 })
230
+ if (sample.length === 0) return false
231
+
232
+ const all = await conversations.scan({ prefix: `${userId}:` })
233
+ const lastAt = all.reduce((max, e) => Math.max(max, e.doc.created_at), 0)
234
+
235
+ await c.put(userId, {
236
+ id: userId,
237
+ user_id: userId,
238
+ channel: "webchat",
239
+ peer_id: "legacy",
240
+ peer_kind: "direct",
241
+ title: "Conversación anterior",
242
+ archived: false,
243
+ created_at: all[0]?.doc.created_at ?? Date.now(),
244
+ last_message_at: lastAt || Date.now(),
245
+ message_count: all.length,
246
+ }, { expectedVersion: 0 }).catch(() => {})
247
+
248
+ log.info(`hilo legacy ${userId} registrado como conversación (${all.length} mensajes)`)
249
+ return true
250
+ }
@@ -40,16 +40,16 @@
40
40
  * - core (notify, report_progress, save_note)
41
41
  */
42
42
 
43
- import { col } from "../storage/hive"
44
- import type { ToolDoc } from "../storage/collections"
45
- import { logger } from "../utils/logger"
43
+ import { col } from "../storage/hive.ts"
44
+ import type { ToolDoc } from "../storage/collections.ts"
45
+ import { logger } from "../utils/logger.ts"
46
46
  import {
47
47
  searchCapabilities,
48
48
  applyRelativeCutoff,
49
49
  replaceCapabilityDocs,
50
50
  type CapabilityDoc,
51
- } from "./capability-search"
52
- import { isCalendarOperation } from "./routing-intent"
51
+ } from "./capability-search.ts"
52
+ import { isCalendarOperation } from "./routing-intent.ts"
53
53
 
54
54
  const log = logger.child("tool-selector")
55
55
 
@@ -200,7 +200,9 @@ export const CORE_TOOL_CATALOG: ToolDescriptor[] = [
200
200
  { name: "api_request", description: "Perform an authorized HTTP request against a REST endpoint and validate the response. Spanish keywords: llamar api, request rest, consumir endpoint, petición http, hacer get, hacer post", category: "api", abstractionLevel: "atomic" },
201
201
 
202
202
  // Artifacts
203
+ { name: "computer_use_task", description: "Operate the browser by looking at the screen: click by coordinates, type and navigate when no stable CSS selector exists (canvas, generated UIs, embedded viewers). Acts on Hive's own browser, never on the user screen. Spanish keywords: usar el navegador, hacer clic donde veas, operar una página, rellenar formulario, computer use, mirar la pantalla", category: "browser", abstractionLevel: "orchestration" },
203
204
  { name: "artifact_inspect", description: "Inspect a managed artifact's integrity and metadata without modifying it. Spanish keywords: inspeccionar artefacto, verificar archivo generado, metadatos artefacto, comprobar entrega", category: "web", abstractionLevel: "atomic" },
205
+ { name: "artifact_read", description: "Read a managed artifact's text content in slices, or search inside it — the way to open any artifact_ref a tool returned. Spanish keywords: leer artefacto, ver contenido del artefacto, abrir resultado grande, buscar dentro del artefacto", category: "web", abstractionLevel: "atomic" },
204
206
 
205
207
  // Office documents — read
206
208
  { name: "office_leer_pdf", description: "Read and extract text from a PDF document. Spanish keywords: leer pdf, extraer texto pdf, abrir pdf, contenido pdf", category: "office", abstractionLevel: "atomic" },
@@ -6,9 +6,9 @@
6
6
  * `agents.lastTraceAt` field used by the Curator's stale-worker detection.
7
7
  */
8
8
 
9
- import { logger } from "../utils/logger"
10
- import { col, nextId, updateDoc } from "../storage/hive"
11
- import type { TraceDoc, AgentDoc } from "../storage/collections"
9
+ import { logger } from "../utils/logger.ts"
10
+ import { col, nextId, updateDoc } from "../storage/hive.ts"
11
+ import type { TraceDoc, AgentDoc } from "../storage/collections.ts"
12
12
 
13
13
  const log = logger.child("tracer")
14
14
 
@@ -82,7 +82,7 @@ async function checkReflectorTrigger(): Promise<void> {
82
82
  _tracesSinceLastReflection = 0
83
83
 
84
84
  // Lazy import to avoid circular deps
85
- const { runReflector } = await import("./reflector")
85
+ const { runReflector } = await import("./reflector.ts")
86
86
  runReflector().catch((err) => {
87
87
  log.warn("[tracer] Reflector run failed:", err)
88
88
  })
@@ -98,7 +98,7 @@ export function recordLLMUsage(opts: {
98
98
  }): void {
99
99
  Promise.resolve().then(async () => {
100
100
  try {
101
- const { recordUsage } = await import("../storage/usage")
101
+ const { recordUsage } = await import("../storage/usage.ts")
102
102
  recordUsage({
103
103
  provider: opts.provider,
104
104
  model: opts.model,
@@ -92,7 +92,7 @@ export async function createAgent(config: AgentConfig): Promise<Agent> {
92
92
 
93
93
  const coreConfig = await loadConfig();
94
94
 
95
- // Browser automation (agent-browser) si está habilitado.
95
+ // Browser automation (Bun.WebView) si está habilitado.
96
96
  try {
97
97
  const { initializeBrowserService } = await import("../tools/web/browser-service.ts");
98
98
  const browserService = initializeBrowserService(coreConfig);
@@ -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
 
@@ -91,6 +91,87 @@ export async function createArtifact(input: {
91
91
  }
92
92
  }
93
93
 
94
+ /**
95
+ * Reads an artifact's bytes straight off disk — for server-side consumers
96
+ * that don't need an HTTP round trip (routes/artifacts.ts is for the
97
+ * browser; channels/*.ts send()s use this directly to build a Telegram/
98
+ * Discord/Slack/WhatsApp photo attachment from the same file).
99
+ */
100
+ export async function readArtifactBytes(
101
+ artifactId: string,
102
+ ): Promise<{ bytes: Buffer; mimeType: string } | null> {
103
+ const artifacts = await col<ArtifactDoc>("artifacts");
104
+ const entry = await artifacts.get(artifactId);
105
+ if (!entry || entry.doc.status !== "active") return null;
106
+ if (!existsSync(entry.doc.path)) return null;
107
+ return { bytes: readFileSync(entry.doc.path), mimeType: entry.doc.mime_type };
108
+ }
109
+
110
+ /** Beyond this an artifact stops being something an agent can read into a context window. */
111
+ const MAX_TEXT_ARTIFACT_BYTES = 25 * 1024 * 1024;
112
+
113
+ function looksBinary(mimeType: string, bytes: Buffer): boolean {
114
+ if (/^(image|audio|video)\//.test(mimeType)) return true;
115
+ if (mimeType === "application/octet-stream") return true;
116
+ // A NUL byte in the first KB is the classic "this is not text" tell — a
117
+ // mislabelled text/plain artifact would otherwise decode to garbage.
118
+ return bytes.subarray(0, 1024).includes(0x00);
119
+ }
120
+
121
+ export type ArtifactTextResult = {
122
+ ok: boolean;
123
+ text?: string;
124
+ artifact?: ArtifactDoc;
125
+ error?: string;
126
+ status?: string;
127
+ };
128
+
129
+ /**
130
+ * Decodes a text artifact for in-process consumers that need its *content*,
131
+ * not just its metadata (inspectArtifact) or its raw bytes (readArtifactBytes).
132
+ *
133
+ * This exists because mcp-result-normalizer.ts moves oversized MCP text results
134
+ * out of the context window and hands the model an `artifact_ref` instead. Until
135
+ * this, nothing could read that reference back: the agent held a receipt it
136
+ * could not cash, burned its iterations guessing, and the turn died on an empty
137
+ * synthesis. Ownership and status checks mirror inspectArtifact's.
138
+ */
139
+ export async function readArtifactText(
140
+ artifactId: string,
141
+ options: { userId?: string } = {},
142
+ ): Promise<ArtifactTextResult> {
143
+ const artifacts = await col<ArtifactDoc>("artifacts");
144
+ const entry = await artifacts.get(artifactId);
145
+ if (!entry) return { ok: false, error: "Artifact not found" };
146
+ const artifact = entry.doc;
147
+
148
+ if (options.userId && artifact.user_id && artifact.user_id !== options.userId) {
149
+ return { ok: false, error: "Artifact not accessible" };
150
+ }
151
+ if (artifact.status !== "active") {
152
+ return { ok: false, error: "Artifact binary has expired", status: artifact.status };
153
+ }
154
+ if (!existsSync(artifact.path)) {
155
+ return { ok: false, error: "Artifact binary is missing", status: "missing" };
156
+ }
157
+ if (artifact.size > MAX_TEXT_ARTIFACT_BYTES) {
158
+ return {
159
+ ok: false,
160
+ error: `Artifact is too large to read as text (${artifact.size} bytes, limit ${MAX_TEXT_ARTIFACT_BYTES})`,
161
+ };
162
+ }
163
+
164
+ const bytes = readFileSync(artifact.path);
165
+ if (looksBinary(detectedMime(bytes, artifact.mime_type), bytes)) {
166
+ return {
167
+ ok: false,
168
+ error: `Artifact is binary (${detectedMime(bytes, artifact.mime_type)}) — use artifact_inspect for its metadata`,
169
+ };
170
+ }
171
+
172
+ return { ok: true, text: bytes.toString("utf-8"), artifact };
173
+ }
174
+
94
175
  export async function inspectArtifact(
95
176
  artifactId: string,
96
177
  options: { userId?: string } = {},
@@ -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
@@ -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;