@johpaz/hive-sdk 0.1.5 → 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 (182) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/README.md +11 -1
  3. package/package.json +26 -10
  4. package/packages/core/src/agent/acceptance-checks.ts +16 -10
  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 +15 -1
  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 +226 -0
  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 +85 -366
  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 +851 -0
  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 -833
  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/agent-loop-terminal-synthesis.test.ts +0 -32
  153. package/test/catalog-agents-stay-enabled.test.ts +0 -117
  154. package/test/causal-events.test.ts +0 -117
  155. package/test/compaction.test.ts +0 -105
  156. package/test/context-compiler.test.ts +0 -269
  157. package/test/curator.test.ts +0 -130
  158. package/test/durable-queue.test.ts +0 -114
  159. package/test/harness-barrel.test.ts +0 -64
  160. package/test/hive-helpers.test.ts +0 -130
  161. package/test/hivedb-search.test.ts +0 -189
  162. package/test/internal-turns.test.ts +0 -166
  163. package/test/job-idempotency.test.ts +0 -68
  164. package/test/job-retry-backoff.test.ts +0 -184
  165. package/test/job-store.test.ts +0 -381
  166. package/test/llm-retry.test.ts +0 -97
  167. package/test/memory-perf.test.ts +0 -774
  168. package/test/minimal-loadout.test.ts +0 -78
  169. package/test/model-catalog.test.ts +0 -105
  170. package/test/preload.ts +0 -12
  171. package/test/reflector.test.ts +0 -320
  172. package/test/retention-cap.test.ts +0 -91
  173. package/test/retired-capabilities-pruned.test.ts +0 -192
  174. package/test/run-store.test.ts +0 -355
  175. package/test/scratchpad.test.ts +0 -74
  176. package/test/secrets-durability.test.ts +0 -119
  177. package/test/seed-model-reseed.test.ts +0 -155
  178. package/test/setup-agent-seed.test.ts +0 -264
  179. package/test/tool-inventory.test.ts +0 -65
  180. package/test/tool-runtime.test.ts +0 -258
  181. package/test/toon.test.ts +0 -429
  182. package/tsconfig.json +0 -42
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Sessions — la conversación de un usuario, como una sola cosa.
3
+ *
4
+ * Hasta acá "sesión" estaba repartida en cuatro capas que nadie unía:
5
+ *
6
+ * - `agent/thread-store.ts` — identidad y catálogo (`conversationThreads`)
7
+ * - `agent/conversation-store.ts`— los mensajes (`conversations`, por prefijo)
8
+ * - `agent/run-store.ts` — la ejecución: checkpoint, lease, resume
9
+ * - `state/store.ts` — un `Map` en memoria que muere con el proceso
10
+ *
11
+ * El resultado eran dos identificadores para lo mismo —`thread_id` para la
12
+ * conversación y `run_id` para la ejecución— y ninguna forma de preguntar "qué
13
+ * sesiones tiene este usuario" sin escanear mensajes.
14
+ *
15
+ * Este módulo **no agrega una tercera persistencia**: `Session` es una vista
16
+ * compuesta sobre las colecciones que ya existen. `Session.id` ES el `threadId`.
17
+ * Agregar una colección propia habría recreado exactamente la duplicación que
18
+ * este módulo viene a cerrar.
19
+ *
20
+ * `state/store.ts` queda para métricas efímeras; no es la sesión.
21
+ */
22
+
23
+ import type { ContentPart } from "../multimodal/types.ts"
24
+ import type { AgentRunDoc, ConversationThreadDoc } from "../storage/collections.ts"
25
+ import { updateDoc } from "../storage/hive.ts"
26
+ import {
27
+ addMessage,
28
+ getHistory,
29
+ type StoredMessage,
30
+ } from "../agent/conversation-store.ts"
31
+ import {
32
+ createWebConversation,
33
+ deleteThread,
34
+ ensureThread,
35
+ getThread,
36
+ listThreads,
37
+ mostRecentWebThread,
38
+ renameThread,
39
+ threadForChannel,
40
+ } from "../agent/thread-store.ts"
41
+ import {
42
+ deserializeCheckpoint,
43
+ findRunsByThread,
44
+ type RunCheckpointState,
45
+ } from "../agent/run-store.ts"
46
+
47
+ export * from "../agent/thread-id.ts"
48
+
49
+ /** El estado de ejecución más reciente del hilo, si alguna vez corrió. */
50
+ export interface SessionRun {
51
+ runId: string
52
+ agentId: string
53
+ status: AgentRunDoc["status"]
54
+ kind: AgentRunDoc["kind"]
55
+ updatedAt: number
56
+ /** true cuando quedó a medias y `resumeSession` puede retomarla. */
57
+ resumable: boolean
58
+ }
59
+
60
+ /**
61
+ * Una sesión: la identidad del hilo más, opcionalmente, su última ejecución.
62
+ * Es una vista, no una fila: se compone de `conversationThreads` + `agentRuns`.
63
+ */
64
+ export interface Session {
65
+ /** ES el threadId (`${user}/${canal}/${peer}`). */
66
+ id: string
67
+ userId: string
68
+ channel: string
69
+ peerId: string
70
+ peerKind: "direct" | "group"
71
+ title: string | null
72
+ archived: boolean
73
+ createdAt: number
74
+ lastMessageAt: number
75
+ messageCount: number
76
+ lastRun?: SessionRun
77
+ }
78
+
79
+ /** Una ejecución interrumpida sigue siendo retomable; una terminada no. */
80
+ const RESUMABLE_STATUSES: ReadonlySet<AgentRunDoc["status"]> = new Set([
81
+ "interrupted",
82
+ "running",
83
+ ])
84
+
85
+ function toSessionRun(run: AgentRunDoc): SessionRun {
86
+ return {
87
+ runId: run.id,
88
+ agentId: run.agent_id,
89
+ status: run.status,
90
+ kind: run.kind,
91
+ updatedAt: run.updated_at ?? run.created_at,
92
+ resumable: RESUMABLE_STATUSES.has(run.status),
93
+ }
94
+ }
95
+
96
+ function toSession(doc: ConversationThreadDoc, lastRun?: SessionRun): Session {
97
+ return {
98
+ id: doc.id,
99
+ userId: doc.user_id,
100
+ channel: doc.channel,
101
+ peerId: doc.peer_id,
102
+ peerKind: doc.peer_kind,
103
+ title: doc.title,
104
+ archived: doc.archived,
105
+ createdAt: doc.created_at,
106
+ lastMessageAt: doc.last_message_at,
107
+ messageCount: doc.message_count,
108
+ ...(lastRun ? { lastRun } : {}),
109
+ }
110
+ }
111
+
112
+ /** La ejecución más reciente del hilo, o undefined si nunca corrió. */
113
+ async function latestRun(threadId: string): Promise<SessionRun | undefined> {
114
+ const runs = await findRunsByThread(threadId)
115
+ if (runs.length === 0) return undefined
116
+ const newest = runs.reduce((a, b) =>
117
+ (b.updated_at ?? b.created_at) > (a.updated_at ?? a.created_at) ? b : a,
118
+ )
119
+ return toSessionRun(newest)
120
+ }
121
+
122
+ export interface CreateSessionInput {
123
+ userId: string
124
+ channel: string
125
+ peerId: string
126
+ peerKind?: "direct" | "group"
127
+ }
128
+
129
+ /**
130
+ * Abre la sesión de un usuario en un canal, o devuelve la que ya existía.
131
+ * Idempotente: se puede llamar en cada turno.
132
+ */
133
+ export async function createSession(input: CreateSessionInput): Promise<Session> {
134
+ const threadId = await ensureThread(input)
135
+ const doc = await getThread(threadId)
136
+ if (!doc) throw new Error(`No se pudo abrir la sesión ${threadId}`)
137
+ return toSession(doc)
138
+ }
139
+
140
+ /** Una conversación nueva de la web, con su propio id (no reusa la anterior). */
141
+ export async function createWebSession(userId: string, title?: string): Promise<Session> {
142
+ return toSession(await createWebConversation(userId, title))
143
+ }
144
+
145
+ export async function getSession(sessionId: string): Promise<Session | null> {
146
+ const doc = await getThread(sessionId)
147
+ if (!doc) return null
148
+ return toSession(doc, await latestRun(sessionId))
149
+ }
150
+
151
+ export interface ListSessionsOptions {
152
+ channel?: string
153
+ includeArchived?: boolean
154
+ /**
155
+ * Adjunta la última ejecución de cada sesión. Cuesta una consulta por sesión,
156
+ * así que está apagado por defecto: la lista de conversaciones no lo necesita.
157
+ */
158
+ withRuns?: boolean
159
+ }
160
+
161
+ /**
162
+ * Las sesiones de un usuario, de la más reciente a la más vieja.
163
+ *
164
+ * Es la consulta que antes no existía: el estado en memoria se perdía en cada
165
+ * reinicio y los mensajes sólo se podían leer por prefijo de un hilo conocido.
166
+ */
167
+ export async function listSessions(
168
+ userId: string,
169
+ opts?: ListSessionsOptions,
170
+ ): Promise<Session[]> {
171
+ const docs = await listThreads(userId, {
172
+ channel: opts?.channel,
173
+ includeArchived: opts?.includeArchived,
174
+ })
175
+ if (!opts?.withRuns) return docs.map((d) => toSession(d))
176
+ return Promise.all(docs.map(async (d) => toSession(d, await latestRun(d.id))))
177
+ }
178
+
179
+ /** La sesión de webchat en la que el usuario seguiría escribiendo. */
180
+ export async function mostRecentWebSession(userId: string): Promise<Session | null> {
181
+ const doc = await mostRecentWebThread(userId)
182
+ return doc ? toSession(doc) : null
183
+ }
184
+
185
+ /**
186
+ * La sesión por la que hablarle a alguien en un canal cuando no venimos de un
187
+ * mensaje suyo (un aviso de tarea programada, por ejemplo). null si no hay.
188
+ */
189
+ export async function sessionForChannel(
190
+ userId: string,
191
+ channel: string,
192
+ ): Promise<string | null> {
193
+ return threadForChannel(userId, channel)
194
+ }
195
+
196
+ /**
197
+ * Agrega un mensaje y mantiene al día el catálogo (título, orden, contador):
198
+ * `addMessage` ya llama a `touchThread`, así que no hay que hacerlo acá.
199
+ *
200
+ * Ojo: esa actualización del catálogo es deliberadamente *fire-and-forget* —
201
+ * persistir el mensaje nunca se bloquea por el contador. En la práctica eso
202
+ * significa que `messageCount` y `title` son de consistencia eventual: leer la
203
+ * sesión inmediatamente después de escribir puede devolver el valor anterior.
204
+ * El historial (`getSessionHistory`) sí es consistente al instante.
205
+ */
206
+ export async function appendMessage(
207
+ sessionId: string,
208
+ role: StoredMessage["role"],
209
+ content: string | ContentPart[],
210
+ opts?: Parameters<typeof addMessage>[3],
211
+ ): Promise<number> {
212
+ return addMessage(sessionId, role, content, opts)
213
+ }
214
+
215
+ export async function getSessionHistory(
216
+ sessionId: string,
217
+ limit?: number,
218
+ ): Promise<StoredMessage[]> {
219
+ return getHistory(sessionId, limit)
220
+ }
221
+
222
+ export interface ResumableSession {
223
+ run: SessionRun
224
+ checkpoint: RunCheckpointState
225
+ }
226
+
227
+ /**
228
+ * El trabajo a medias de una sesión, listo para retomar.
229
+ *
230
+ * Devuelve null cuando no hay nada que retomar — que es el caso normal. Un run
231
+ * `running` sin checkpoint tampoco sirve: murió antes de guardar estado.
232
+ */
233
+ export async function resumeSession(sessionId: string): Promise<ResumableSession | null> {
234
+ const runs = await findRunsByThread(sessionId)
235
+ const candidates = runs
236
+ .filter((r) => RESUMABLE_STATUSES.has(r.status))
237
+ .sort((a, b) => (b.updated_at ?? b.created_at) - (a.updated_at ?? a.created_at))
238
+
239
+ for (const run of candidates) {
240
+ const checkpoint = deserializeCheckpoint(run)
241
+ if (checkpoint) return { run: toSessionRun(run), checkpoint }
242
+ }
243
+ return null
244
+ }
245
+
246
+ export async function renameSession(sessionId: string, title: string): Promise<void> {
247
+ return renameThread(sessionId, title)
248
+ }
249
+
250
+ /**
251
+ * Cierra la sesión sin perder nada: sale de la lista pero el historial queda.
252
+ * Para borrarla de verdad, `deleteSession`.
253
+ */
254
+ export async function closeSession(sessionId: string): Promise<void> {
255
+ await updateDoc<ConversationThreadDoc>("conversationThreads", sessionId, { archived: true })
256
+ }
257
+
258
+ /** Reabre una sesión archivada. */
259
+ export async function reopenSession(sessionId: string): Promise<void> {
260
+ await updateDoc<ConversationThreadDoc>("conversationThreads", sessionId, { archived: false })
261
+ }
262
+
263
+ /** Borra la sesión entera: mensajes, resumen, notas y su fila del catálogo. */
264
+ export async function deleteSession(sessionId: string): Promise<void> {
265
+ return deleteThread(sessionId)
266
+ }
@@ -11,10 +11,11 @@
11
11
  * instead of `DELETE`+`INSERT`.
12
12
  */
13
13
 
14
- import { getHiveDb } from "./hivedb";
15
- import { col } from "./hive";
16
- import { seedAllData } from "./seed";
17
- import { ensureSecretsBackend } from "./crypto";
14
+ import { getHiveDb, getOpenHiveDb } from "./hivedb.ts";
15
+ import { col } from "./hive.ts";
16
+ import { seedAllData } from "./seed.ts";
17
+ import { ensureSecretsBackend } from "./crypto.ts";
18
+ import { ensureLegacyThread } from "../agent/thread-store.ts";
18
19
 
19
20
  interface IndexSpec {
20
21
  collection: string;
@@ -102,6 +103,9 @@ const INDEXES: IndexSpec[] = [
102
103
  // Stage 7 — meeting
103
104
  { collection: "meetingSessions", field: "user_id" },
104
105
  { collection: "meetingSessions", field: "status" },
106
+ { collection: "conversationThreads", field: "user_id" },
107
+ { collection: "conversationThreads", field: "channel" },
108
+ { collection: "conversationThreads", field: "archived" },
105
109
  ];
106
110
 
107
111
  async function ensureIndexes(): Promise<void> {
@@ -123,7 +127,27 @@ async function ensureSeedData(): Promise<void> {
123
127
  await seedAllData();
124
128
  }
125
129
 
126
- let bootstrapped = false;
130
+ // Bootstrap state belongs to a specific database instance. A boolean that
131
+ // survives closeHiveDb() can report a closed/replaced database as ready when
132
+ // tests or long-running processes open a fresh instance in the same runtime.
133
+ let bootstrappedDb: Awaited<ReturnType<typeof getHiveDb>> | null = null;
134
+
135
+ /**
136
+ * Antes de la separación por canal todos los canales compartían un solo hilo cuyo
137
+ * `thread_id` era el `userId`. Esa conversación se registra —sin mover un solo
138
+ * mensaje— como una conversación más de la web, para que siga siendo legible desde
139
+ * la lista. Idempotente: no hace nada si ya está registrada o si no hay historial.
140
+ */
141
+ async function ensureLegacyThreads(): Promise<void> {
142
+ try {
143
+ const users = await col<{ id: string }>("users");
144
+ for (const user of await users.scan({})) {
145
+ await ensureLegacyThread(user.id);
146
+ }
147
+ } catch {
148
+ // Base sin usuarios todavía (onboarding pendiente) — nada que registrar.
149
+ }
150
+ }
127
151
 
128
152
  /**
129
153
  * Idempotent entry point: opens the database, ensures indexes, reseeds the
@@ -131,7 +155,7 @@ let bootstrapped = false;
131
155
  * gateway boot.
132
156
  */
133
157
  export async function ensureHiveDb(): Promise<void> {
134
- await getHiveDb();
158
+ const db = await getHiveDb();
135
159
  await ensureIndexes();
136
160
  // Mint the master key before anything can save an API key, so a fresh
137
161
  // install is durable from the first keystroke rather than after a restart
@@ -139,13 +163,15 @@ export async function ensureHiveDb(): Promise<void> {
139
163
  ensureSecretsBackend();
140
164
  await ensureSeedData();
141
165
 
166
+ await ensureLegacyThreads();
167
+
142
168
  const meta = await col<{ value: number }>("meta");
143
169
  const existing = await meta.get("schemaVersion");
144
170
  if (!existing) await meta.put("schemaVersion", { value: 1 }, { expectedVersion: 0 });
145
171
 
146
- bootstrapped = true;
172
+ bootstrappedDb = db;
147
173
  }
148
174
 
149
175
  export function isBootstrapped(): boolean {
150
- return bootstrapped;
176
+ return bootstrappedDb !== null && bootstrappedDb === getOpenHiveDb();
151
177
  }
@@ -6,7 +6,7 @@
6
6
  * co-located with the DB singleton accessor.
7
7
  */
8
8
 
9
- import { getHiveDb } from "./hivedb"
9
+ import { getHiveDb } from "./hivedb.ts"
10
10
  import type { Event, EventPattern } from "@johpaz/hive-db"
11
11
 
12
12
  export type { Event as CausalEvent, EventPattern as CausalEventPattern }
@@ -40,7 +40,8 @@ export interface ModelDoc {
40
40
  id: string
41
41
  provider_id: string
42
42
  name: string
43
- model_type: "llm" | "stt" | "tts" | "vision" | "embedding"
43
+ /** "realtime": voz full-duplex (Live API), no confundir con stt+tts encadenados. */
44
+ model_type: "llm" | "stt" | "tts" | "vision" | "embedding" | "realtime"
44
45
  context_window: number
45
46
  capabilities: string | null
46
47
  enabled: boolean
@@ -269,6 +270,14 @@ export interface RefreshTokenDoc {
269
270
  export type TurnSource =
270
271
  | "message" | "audio" | "a2ui" | "canvas" | "api"
271
272
  | "task_complete" | "delegation_summary"
273
+ /** Turno hablado en una sesión de voz en tiempo real. A diferencia de "audio"
274
+ * (nota de voz: STT → turno → TTS) la voz la pone el modelo realtime, así que
275
+ * el turno nunca sintetiza audio al final. */
276
+ | "realtime"
277
+ /** Charla hablada que la voz YA resolvió sin tocar la colmena. Se guarda como
278
+ * contexto del hilo, nunca como pedido pendiente: tratarla como accionable
279
+ * hacía que el coordinador re-delegara todo lo que se había dicho en voz. */
280
+ | "realtime_chat"
272
281
 
273
282
  /** What gets persisted — adds the synthetic marker for rows written before `source` existed. */
274
283
  export type MessageSource = TurnSource | "legacy_internal"
@@ -298,6 +307,28 @@ export interface SummaryDoc {
298
307
  last_message_id: string | null
299
308
  }
300
309
 
310
+ /**
311
+ * Una conversación: el registro de un `conversations.thread_id`, con lo necesario
312
+ * para listarla en la web sin escanear los mensajes. `id` ES el threadId
313
+ * (`${user}/${canal}/${peer}` — ver agent/thread-id.ts), salvo la fila del hilo
314
+ * legacy anterior a la separación por canal, cuyo id es el userId pelado.
315
+ */
316
+ export interface ConversationThreadDoc {
317
+ id: string
318
+ user_id: string
319
+ channel: string
320
+ /** Contacto/grupo del canal, o el id de conversación en la web. */
321
+ peer_id: string
322
+ peer_kind: "direct" | "group"
323
+ /** null hasta el primer mensaje del usuario, de donde se deriva. */
324
+ title: string | null
325
+ /** Borrado suave: sigue en la base pero no se lista ni se reabre. */
326
+ archived: boolean
327
+ created_at: number
328
+ last_message_at: number
329
+ message_count: number
330
+ }
331
+
301
332
  export interface TraceDoc {
302
333
  id: string
303
334
  thread_id: string
@@ -2,8 +2,8 @@ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto"
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
3
3
  import * as path from "node:path"
4
4
  import { getHiveDir } from "../config/loader.ts"
5
- import { logger } from "../utils/logger"
6
- import { col } from "./hive"
5
+ import { logger } from "../utils/logger.ts"
6
+ import { col } from "./hive.ts"
7
7
 
8
8
  const log = logger.child("crypto")
9
9
  const SERVICE = "hive"
@@ -79,6 +79,20 @@ async function _readCollectionSecret(name: string): Promise<string | null> {
79
79
  }
80
80
  }
81
81
 
82
+ /**
83
+ * Olvida si el keychain del SO respondió o no.
84
+ *
85
+ * `_keychainOk` se cachea a nivel de módulo a propósito: en un servidor sin
86
+ * libsecret cada lectura tiraría y no tiene sentido reintentarlo. El costo es
87
+ * que el resultado del primer sondeo vale para todo el proceso, y un test que
88
+ * sustituya `Bun.secrets` por un doble queda cortocircuitado si algo ya sondeó
89
+ * antes y falló. Resetear acá lo vuelve a dejar sin probar.
90
+ */
91
+ export function resetKeychainProbe(): void {
92
+ _keychainOk = null
93
+ _mem.clear()
94
+ }
95
+
82
96
  async function _keychainGet(name: string): Promise<string | null> {
83
97
  if (_keychainOk === false) return null
84
98
  try {
@@ -7,7 +7,7 @@
7
7
  * SUM/AVG/GROUP BY (`bumpRollup`).
8
8
  */
9
9
 
10
- import { getHiveDb } from "./hivedb";
10
+ import { getHiveDb } from "./hivedb.ts";
11
11
 
12
12
  const MAX_RETRIES = 5;
13
13
 
@@ -15,7 +15,7 @@
15
15
  import path from "node:path";
16
16
  import { HiveDB } from "@johpaz/hive-db";
17
17
  import { getHiveDir } from "../config/loader.ts";
18
- import { logger } from "../utils/logger";
18
+ import { logger } from "../utils/logger.ts";
19
19
 
20
20
  const log = logger.child("hivedb");
21
21
 
@@ -48,6 +48,15 @@ export async function getHiveDb(): Promise<HiveDB> {
48
48
  return opening;
49
49
  }
50
50
 
51
+ /**
52
+ * Return the currently open database without opening one.
53
+ * Used by lifecycle-aware consumers that need to invalidate state after a
54
+ * close/reopen cycle.
55
+ */
56
+ export function getOpenHiveDb(): HiveDB | null {
57
+ return db;
58
+ }
59
+
51
60
  export function closeHiveDb(): void {
52
61
  if (db) {
53
62
  try {
@@ -1,19 +1,19 @@
1
1
  import { logger } from "../utils/logger.ts";
2
- import { ensureHiveDb } from "./bootstrap";
3
- import { col, toIndexable, fromIndexable } from "./hive";
2
+ import { ensureHiveDb } from "./bootstrap.ts";
3
+ import { col, toIndexable, fromIndexable } from "./hive.ts";
4
4
  import {
5
5
  storeProviderApiKey,
6
6
  storeChannelConfig,
7
7
  storeMcpEnv,
8
8
  loadProviderApiKey,
9
9
  loadChannelConfig,
10
- } from "./crypto";
10
+ } from "./crypto.ts";
11
11
  import { SkillLoader } from "../skills/index.ts";
12
12
  import type {
13
13
  UserDoc, ProviderDoc, ModelDoc, AgentDoc, ChannelDoc, McpServerDoc,
14
14
  UserIdentityDoc, OnboardingProgressDoc, EthicsDoc, SkillDoc, ToolDoc,
15
- } from "./collections";
16
- import { normalizeUserEmail } from "./user-email";
15
+ } from "./collections.ts";
16
+ import { normalizeUserEmail } from "./user-email.ts";
17
17
 
18
18
  export interface OnboardingSection {
19
19
  step: "user" | "skills" | "ethics" | "tools" | "provider" | "model" | "channel" | "mcp" | "agent" | "complete";
@@ -423,7 +423,7 @@ export async function propagateCoordinatorModel(
423
423
  providerId: string,
424
424
  modelId: string,
425
425
  ): Promise<number> {
426
- const { applyCoordinatorModel } = await import("../agent/agent-catalog");
426
+ const { applyCoordinatorModel } = await import("../agent/agent-catalog.ts");
427
427
  const updated = await applyCoordinatorModel({ userId, providerId, modelId, overwrite: true });
428
428
  if (updated > 0) {
429
429
  log.info(`✅ ${updated} agente(s) sincronizados con el modelo del coordinador`, { providerId, modelId });
@@ -12,12 +12,12 @@
12
12
  * durable queue are activated.
13
13
  */
14
14
 
15
- import { col, updateDoc } from "./hive";
16
- import type { TaskRunDoc, MeetingSessionDoc, AgentRunDoc, JobDoc, TaskDoc } from "./collections";
17
- import { logger } from "../utils/logger";
15
+ import { col, updateDoc } from "./hive.ts";
16
+ import type { TaskRunDoc, MeetingSessionDoc, AgentRunDoc, JobDoc, TaskDoc } from "./collections.ts";
17
+ import { logger } from "../utils/logger.ts";
18
18
  import { reclaimOrInterrupt } from "../gateway/job-store.ts";
19
- import { interruptRun } from "../agent/run-store";
20
- import { sendToUserChannel } from "../gateway/channel-notify";
19
+ import { interruptRun } from "../agent/run-store.ts";
20
+ import { sendToUserChannel } from "../gateway/channel-notify.ts";
21
21
 
22
22
  const log = logger.child("reconcile");
23
23