@johpaz/hive-sdk 0.4.5 → 0.4.7

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.
package/CHANGELOG.md CHANGED
@@ -17,9 +17,26 @@
17
17
  procedencia, hash y una prueba funcional del OOXML generado.
18
18
  - Añadidas las guías `docs/UPGRADING.md` y
19
19
  `docs/SECURITY-GUARDRAILS.md` para operación y auditoría.
20
+ - **Requiere `@johpaz/hive-db` ^0.5.1** (antes ^0.4.0). Trae las lecturas del
21
+ log causal acotadas por agente (`agents` en `causalThread`, `toolStats` y
22
+ `buildAgentContext`), de las que depende el aislamiento entre inquilinos del
23
+ log causal descrito en *Corregido*.
20
24
 
21
25
  ### Corregido
22
26
 
27
+ - **Con un tenant activo, el log causal se apagaba en lugar de acotarse.**
28
+ `causalThread`, `toolStats` y `buildAgentContext` recorrían todos los shards
29
+ de la base, así que con un tenant en scope `causalReadsEnabled()` las apagaba:
30
+ en un host multi-inquilino el reflector G9 y el contexto causal del
31
+ compilador no corrían nunca. Ahora las tres lecturas van siempre acotadas a
32
+ los agentes que corresponden —el agente del turno, o los del lote de trazas— y
33
+ el apagado desaparece. Además el shard de cada evento pasa a ser
34
+ `causalAgentKey(agentId)`: con tenant lleva el tenant delante (`t_…:agente`,
35
+ la misma forma que los ids del índice BM25), así que dos inquilinos con un
36
+ agente del mismo id ya no comparten shard. Una lista de agentes vacía se salta
37
+ la lectura en vez de pasarse al motor, que la trataría como "todos los
38
+ shards". Cubierto por `test/causal-tenant-scope.test.ts`.
39
+
23
40
  - **`browser_scrape` extraía con una tool que no ve lo que el navegador
24
41
  renderizó.** La skill existe para sitios dinámicos, y su paso de extracción
25
42
  usaba `web_fetch`, que vuelve a pedir la URL al servidor y recibe el HTML sin
@@ -194,6 +211,11 @@
194
211
 
195
212
  ### Cambiado
196
213
 
214
+ - **El `toolStats` del reflector se acota a los agentes del lote**, también sin
215
+ tenant. Antes sumaba el historial de la tool de todos los agentes de la base;
216
+ ahora el de los agentes cuyas trazas se están analizando. Es la misma
217
+ semántica con y sin tenant, y no recorre el log entero.
218
+
197
219
  - **Los tests que manejan un navegador real son opt-in (`BROWSER_TESTS=1`).**
198
220
  Su guarda era `isWebViewSupported()`, que sólo comprueba que exista un binario
199
221
  de Chromium — no que arranque. En un runner de CI (contenedor, a menudo root)
package/README.md CHANGED
@@ -271,4 +271,4 @@ npm view @johpaz/hive-sdk dist-tags # verificar después del release
271
271
 
272
272
  ---
273
273
 
274
- *Hive SDK v0.4.5 — MIT*
274
+ *Hive SDK v0.4.7 — MIT*
@@ -444,8 +444,8 @@ unsubscribeCanvas(handler);
444
444
 
445
445
  ## Storage
446
446
 
447
- HiveDB (`@johpaz/hive-db`), un motor embebido con colecciones de documentos e
448
- índice BM25. Reemplazó a SQLite + FTS5 en 0.1.5.
447
+ HiveDB (`@johpaz/hive-db` 0.5.1 o posterior), un motor embebido con colecciones
448
+ de documentos e índice BM25. Reemplazó a SQLite + FTS5 en 0.1.5.
449
449
 
450
450
  ```typescript
451
451
  import { ensureHiveDb, col } from "@johpaz/hive-sdk";
package/docs/UPGRADING.md CHANGED
@@ -54,6 +54,26 @@ Estos adaptadores están en la frontera con el runtime. No deben reemplazarse po
54
54
  `any`, `@ts-ignore` o `@ts-expect-error`: hacerlo convertiría una incompatibilidad
55
55
  real de plataforma en un falso resultado verde.
56
56
 
57
+ ## hive-db 0.5.1 y log causal por tenant
58
+
59
+ Hive SDK requiere **`@johpaz/hive-db` 0.5.1 o posterior**. Llega como
60
+ dependencia del SDK, así que una aplicación consumidora no la declara. Lo que
61
+ sigue sólo importa con el log causal encendido (`HIVE_CAUSAL_LOG=true` o
62
+ `causalLog.enabled`):
63
+
64
+ - Con un tenant activo (`runInTenant`) el reflector y el contexto causal del
65
+ compilador vuelven a funcionar. Antes se apagaban; ahora leen acotado a los
66
+ agentes del turno o del lote de trazas.
67
+ - La clave de shard de cada evento es `causalAgentKey(agentId)`: sin tenant, el
68
+ id del agente tal cual; con tenant, `t_…:agentId`. Los eventos que un host
69
+ haya escrito con tenant antes de esta versión quedaron con el id crudo y las
70
+ lecturas acotadas ya no los ven. Sin tenant no cambia nada.
71
+ - El `toolStats` del reflector cuenta el historial de los agentes del lote, no
72
+ el de toda la base, con y sin tenant.
73
+ - `watchCausalEvents` con tenant sigue exigiendo `agentId`: se le pasa el id
74
+ crudo y el SDK lo califica. Los eventos que entrega traen en `agentId` la
75
+ clave del shard; `formatCausalEvent` la muestra sin el tenant.
76
+
57
77
  ## Compatibilidad y CI
58
78
 
59
79
  Los workflows fijan Bun 1.4.2, instalan con `--frozen-lockfile`, ejecutan el
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johpaz/hive-sdk",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "private": false,
5
5
  "description": "Hive SDK — The Agent Harness SDK. Build, deploy, and scale AI agent applications with multi-channel support, context engineering, and swarm orchestration.",
6
6
  "license": "MIT",
@@ -99,7 +99,7 @@
99
99
  "drift": "bun scripts/check-drift.ts"
100
100
  },
101
101
  "dependencies": {
102
- "@johpaz/hive-db": "^0.4.0",
102
+ "@johpaz/hive-db": "^0.5.1",
103
103
  "@anthropic-ai/sdk": "^0.74.0",
104
104
  "@google/genai": "^1.43.0",
105
105
  "@modelcontextprotocol/sdk": "^1.26.0",
@@ -16,6 +16,7 @@
16
16
  import { logger } from "../utils/logger.ts"
17
17
  import { col, fromIndexable } from "../storage/hive.ts"
18
18
  import { getHiveDb } from "../storage/hivedb.ts"
19
+ import { causalAgentKey } from "../storage/causal-events.ts"
19
20
  import type { HiveDB, EventInput } from "@johpaz/hive-db"
20
21
  import type { AgentDoc, TurnSource } from "../storage/collections.ts"
21
22
  import { callLLM, resolveProviderConfig, getDefaultLLM, type LLMMessage, type ProviderCredentials } from "./llm-client.ts"
@@ -170,7 +171,8 @@ async function appendCausalEvent(
170
171
  ): Promise<number | undefined> {
171
172
  try {
172
173
  return await db.append({
173
- agentId: input.agentId,
174
+ // Shard calificado con el tenant: ver causalAgentKey en causal-events.ts.
175
+ agentId: causalAgentKey(input.agentId),
174
176
  streamId: input.streamId,
175
177
  kind: input.kind,
176
178
  payload: JSON.stringify(input.payload),
@@ -521,6 +523,7 @@ export async function* runAgent(
521
523
  messages: clearOldToolResults(messages) as LLMMessage[],
522
524
  tools: ctx.tools.length > 0 ? ctx.tools : undefined,
523
525
  signal: opts.signal,
526
+ sessionId: opts.threadId,
524
527
  onToken: opts.onToken && !delegationGroupAtCall
525
528
  ? (token: string) => {
526
529
  streamedThisCall = true
@@ -1133,6 +1136,7 @@ export async function* runAgent(
1133
1136
  ...providerCfg,
1134
1137
  messages: clearOldToolResults(messages) as LLMMessage[],
1135
1138
  tools: undefined, // no tools — force text response
1139
+ sessionId: opts.threadId,
1136
1140
  })
1137
1141
  if (synthesis.usage) {
1138
1142
  totalInputTokens += synthesis.usage.input_tokens
@@ -81,7 +81,8 @@ export async function searchCapabilities(
81
81
  const k = opts.k ?? 10;
82
82
  const types = opts.types?.length ? opts.types : undefined;
83
83
  const trimmed = query.trim();
84
- if (!trimmed) return [];
84
+ // hive-db >= 0.4 rejects k <= 0 instead of returning no hits.
85
+ if (!trimmed || k <= 0) return [];
85
86
 
86
87
  const startTime = performance.now();
87
88
  const db = await getHiveDb();
@@ -38,7 +38,7 @@ import { getMCPManager as getSingletonMCPManager } from "../mcp/singleton.ts"
38
38
  import { syncMCPToolsToDB, syncMCPToolsToIndex } from "../mcp/tool-sync.ts"
39
39
  import { getUserDate, getUserTime } from "../utils/date.ts"
40
40
  import { getHiveDb } from "../storage/hivedb.ts"
41
- import { causalReadsEnabled } from "../storage/causal-events.ts"
41
+ import { causalReadsEnabled, causalScope } from "../storage/causal-events.ts"
42
42
  import { listCatalogAgents, renderAgentRoutingCatalog } from "./catalog-selector.ts"
43
43
  import { expandToolAllowlist } from "./delegation-runtime.ts"
44
44
  import { MINIMAL_TOOLS } from "./minimal-loadout.ts"
@@ -588,7 +588,10 @@ export async function compileContext(opts: {
588
588
  // applies this turn (a real DB round-trip, not a per-turn cost) and
589
589
  // there's a causal stream to build it from. episodicSimilarity is omitted:
590
590
  // it requires embeddings hive doesn't generate anywhere yet.
591
- if (summaryApplies && opts.causalStreamId && causalReadsEnabled()) {
591
+ // Acotado al shard de este agente: el stream es de una sola invocación suya,
592
+ // así que el hilo es el mismo y no se recorre el log de nadie más.
593
+ const causalAgents = causalScope([opts.agentId])
594
+ if (summaryApplies && opts.causalStreamId && causalAgents && causalReadsEnabled()) {
592
595
  try {
593
596
  const causalDb = await getHiveDb()
594
597
  const objectiveSource = taskContext || userMessage
@@ -605,6 +608,7 @@ export async function compileContext(opts: {
605
608
  currentObjective: currentObjective.slice(0, 2000),
606
609
  maxTokens: causalMaxTokens,
607
610
  strategy: { causalAnchors: true, compressCompletedPhases: true },
611
+ agents: causalAgents,
608
612
  })) as AgentContextShape
609
613
 
610
614
  const causalLines = [...(causalCtx.items ?? []), ...(causalCtx.anomalies ?? [])]
@@ -97,6 +97,13 @@ export interface LLMCallOptions {
97
97
  signal?: AbortSignal
98
98
  /** Enable extended thinking for supported models (Anthropic Claude 3.7+). */
99
99
  thinking?: { enabled: boolean; budget_tokens?: number }
100
+ /**
101
+ * Stable id of the conversation this call belongs to (the agent loop's
102
+ * threadId). Providers that route or cache per session derive their own
103
+ * header from it — OpenCode Go's `x-opencode-session` — and never send it
104
+ * verbatim, since it carries user, channel and peer ids.
105
+ */
106
+ sessionId?: string
100
107
  }
101
108
 
102
109
  export interface LLMResponse {
@@ -214,6 +221,14 @@ export function describeProviderFailure(
214
221
  provider: string,
215
222
  cleanModel: string,
216
223
  ): string {
224
+ // NVIDIA usa 404 también para modelos que siguen en su catálogo público pero
225
+ // no están habilitados para esa key ("Function '…': Not found for account
226
+ // '…'"). Decir que "se retiró" mandaba a buscar un modelo que sí existe.
227
+ if (status === 404 && /not found for account/i.test((err as Error)?.message ?? "")) {
228
+ return `Tu cuenta de ${provider} no tiene habilitado el modelo "${cleanModel}" (HTTP 404): `
229
+ + `figura en su catálogo, pero no está disponible para esta API key. `
230
+ + `Elige otro modelo en Ajustes → Proveedores.`
231
+ }
217
232
  if (status === 404 || status === 410) {
218
233
  return `El modelo "${cleanModel}" ya no existe en ${provider} (HTTP ${status}). `
219
234
  + `El proveedor lo retiró de su catálogo; reintentar no sirve. `
@@ -82,8 +82,8 @@ export abstract class OpenAICompatBase implements LLMProvider {
82
82
  /** Override to true for providers running on localhost. */
83
83
  protected isLocalProvider(): boolean { return false }
84
84
 
85
- /** Override to customize the OpenAI client (e.g. strip unwanted headers, add custom fetch). */
86
- protected async resolveOpenAIClient(apiKey: string, baseURL: string | undefined): Promise<any> {
85
+ /** Override to customize the OpenAI client (e.g. strip unwanted headers, add custom fetch, per-call headers). */
86
+ protected async resolveOpenAIClient(apiKey: string, baseURL: string | undefined, _options?: LLMCallOptions): Promise<any> {
87
87
  const { default: OpenAI } = await import("openai")
88
88
  return new OpenAI({ apiKey, baseURL })
89
89
  }
@@ -135,7 +135,7 @@ export abstract class OpenAICompatBase implements LLMProvider {
135
135
  throw new Error(`API key missing for provider: ${this.providerName}. Configure it in Settings → Providers.`)
136
136
  }
137
137
 
138
- const client = await this.resolveOpenAIClient(apiKey, baseURL)
138
+ const client = await this.resolveOpenAIClient(apiKey, baseURL, options)
139
139
 
140
140
  const sanitized = sanitizeMessages(options.messages)
141
141
  const rawMessages = this.needsReasoningRoundtrip()
@@ -1,4 +1,29 @@
1
+ import { createHash, randomUUID } from "node:crypto"
2
+ import pkg from "../../../../../package.json"
1
3
  import { OpenAICompatBase } from "./openai-compat-base.ts"
4
+ import type { LLMCallOptions } from "../llm-client.ts"
5
+
6
+ /**
7
+ * OpenCode Go rechaza con 400 `MissingSessionID` toda petición sin
8
+ * `x-opencode-session` (verificado 2026-09-10 con una key real: sin el header
9
+ * 400, con él 200). Su documentación pide además que el cliente se identifique
10
+ * con su propio User-Agent en vez del genérico del SDK:
11
+ * https://opencode.ai/docs/go/#where-can-i-use-it
12
+ */
13
+ const USER_AGENT = `hive-sdk/${pkg.version}`
14
+
15
+ /** Sesión de respaldo para llamadas sin conversación (compactación, metas). */
16
+ const PROCESS_SESSION = randomUUID()
17
+
18
+ /**
19
+ * La sesión tiene que ser estable por conversación —OpenCode la usa para rutear
20
+ * y cachear el prompt—, pero el threadId lleva usuario, canal y contacto: a
21
+ * OpenCode sólo le llega un hash.
22
+ */
23
+ function sessionHeader(sessionId: string | undefined): string {
24
+ if (!sessionId) return PROCESS_SESSION
25
+ return createHash("sha256").update(`hive:${sessionId}`).digest("hex").slice(0, 32)
26
+ }
2
27
 
3
28
  export class OpenCodeGoProvider extends OpenAICompatBase {
4
29
  static readonly secretKey = "OPENCODE_GO_API_KEY"
@@ -6,4 +31,16 @@ export class OpenCodeGoProvider extends OpenAICompatBase {
6
31
  constructor() {
7
32
  super("opencode-go")
8
33
  }
34
+
35
+ protected async resolveOpenAIClient(apiKey: string, baseURL: string | undefined, options?: LLMCallOptions): Promise<any> {
36
+ const { default: OpenAI } = await import("openai")
37
+ return new OpenAI({
38
+ apiKey,
39
+ baseURL,
40
+ defaultHeaders: {
41
+ "x-opencode-session": sessionHeader(options?.sessionId),
42
+ "User-Agent": USER_AGENT,
43
+ },
44
+ })
45
+ }
9
46
  }
@@ -14,7 +14,8 @@
14
14
  import { logger } from "../utils/logger.ts"
15
15
  import { col, nextId } from "../storage/hive.ts"
16
16
  import { getHiveDb } from "../storage/hivedb.ts"
17
- import { causalReadsEnabled } from "../storage/causal-events.ts"
17
+ import { causalReadsEnabled, causalScope } from "../storage/causal-events.ts"
18
+ import { unqualifyDocId } from "../storage/tenant.ts"
18
19
  import type { HiveDB, ToolStats } from "@johpaz/hive-db"
19
20
  import type { TraceDoc, ReflectionDoc, CursorDoc } from "../storage/collections.ts"
20
21
  import { parseThreadId } from "./thread-id.ts"
@@ -183,10 +184,16 @@ async function analyzeCausalThreads(traces: TraceDoc[], causalDb: HiveDB | null)
183
184
 
184
185
  for (const streamId of streamIds) {
185
186
  try {
186
- const thread = (await causalDb.causalThread(streamId)) as CausalThreadShape
187
+ // El stream es de una sola invocación, así que sus trazas nombran a los
188
+ // agentes que escribieron en él. Sin ninguno no hay con qué acotar, y una
189
+ // lectura sin acotar recorre los shards de todos los inquilinos.
190
+ const subset = traces.filter((t) => t.causal_stream_id === streamId)
191
+ const agents = causalScope(subset.map((t) => t.agent_id))
192
+ if (!agents) continue
193
+
194
+ const thread = (await causalDb.causalThread(streamId, agents)) as CausalThreadShape
187
195
  if (!thread.decisions?.length && !thread.toolCalls?.length) continue
188
196
 
189
- const subset = traces.filter((t) => t.causal_stream_id === streamId)
190
197
  const originalIntent = subset[0]?.input_summary ?? ""
191
198
  const success = subset.every((t) => t.success)
192
199
 
@@ -204,13 +211,16 @@ async function analyzeCausalThreads(traces: TraceDoc[], causalDb: HiveDB | null)
204
211
  // underlying root cause mint a brand new playbook rule instead of
205
212
  // reinforcing one (confirmed via a local before/after canary run).
206
213
  if (evaluation.rootCause) {
214
+ // El log guarda la clave del shard (t_…:agente con tenant); a la regla de
215
+ // playbook y a affected_agents les llega el id que el host conoce.
216
+ const rootAgent = unqualifyDocId(evaluation.rootCause.agent)
207
217
  const decision = thread.decisions?.find((d) => d.seq === evaluation.rootCause!.seq)
208
218
  insights.push({
209
219
  type: "root_cause",
210
220
  description: decision
211
- ? `Root cause: decision "${decision.description}" (agent ${evaluation.rootCause.agent}) preceded a tool failure.`
212
- : `Root cause: a decision by agent ${evaluation.rootCause.agent} preceded a tool failure.`,
213
- affectedAgents: [evaluation.rootCause.agent],
221
+ ? `Root cause: decision "${decision.description}" (agent ${rootAgent}) preceded a tool failure.`
222
+ : `Root cause: a decision by agent ${rootAgent} preceded a tool failure.`,
223
+ affectedAgents: [rootAgent],
214
224
  confidence: 0.6,
215
225
  })
216
226
  }
@@ -248,14 +258,17 @@ async function analyzeCausalThreads(traces: TraceDoc[], causalDb: HiveDB | null)
248
258
  async function analyzeTracesLocally(traces: TraceDoc[], causalDb: HiveDB | null): Promise<Insight[]> {
249
259
  const insights: Insight[] = []
250
260
 
251
- // G9: whole-history stats per tool touched by this batch (undefined when
252
- // disabled, or when the tool has no events in the log yet).
261
+ // G9: historial completo, por tool, de los agentes de este lote (undefined si
262
+ // el log está apagado o la tool todavía no tiene eventos). Acotado a esos
263
+ // agentes: con tenant, sin esto se sumarían llamadas de otros inquilinos; sin
264
+ // tenant, es el historial de estos agentes y no el de toda la base.
253
265
  const statsByTool = new Map<string, ToolStats>()
254
- if (causalDb) {
266
+ const agents = causalDb ? causalScope(traces.map((t) => t.agent_id)) : null
267
+ if (causalDb && agents) {
255
268
  const distinctTools = new Set(traces.map((t) => t.tool_used).filter((t): t is string => !!t))
256
269
  for (const tool of distinctTools) {
257
270
  try {
258
- const stats = await causalDb.toolStats(tool)
271
+ const stats = await causalDb.toolStats(tool, agents)
259
272
  if (stats) statsByTool.set(tool, stats)
260
273
  } catch (err) {
261
274
  log.warn(`[reflector] toolStats(${tool}) failed: ${(err as Error).message}`)
@@ -3,32 +3,63 @@
3
3
  *
4
4
  * Read-side, separate from agent-loop.ts's write-side appendCausalEvent()
5
5
  * (module-private there, write-only). This is the read/watch counterpart,
6
- * co-located with the DB singleton accessor.
6
+ * co-located with the DB singleton accessor. It also owns the shard key both
7
+ * sides use (causalAgentKey) and the agent scope of every aggregated read
8
+ * (causalScope).
7
9
  */
8
10
 
9
11
  import { getHiveDb } from "./hivedb.ts"
10
- import { currentTenant } from "./tenant.ts"
12
+ import { currentTenant, qualifyDocId, unqualifyDocId } from "./tenant.ts"
11
13
  import { loadConfig } from "../config/loader.ts"
12
14
  import type { Event, EventPattern } from "@johpaz/hive-db"
13
15
 
14
16
  export type { Event as CausalEvent, EventPattern as CausalEventPattern }
15
17
 
16
18
  /**
17
- * ¿Se pueden hacer lecturas AGREGADAS del log causal en este contexto?
19
+ * Clave de shard de un agente en el log causal.
18
20
  *
19
- * `causalThread`, `buildAgentContext` y `toolStats` recorren todos los shards
20
- * de la base y mezclan sus resultados (`log.rs` — `read_stream_all_agents` y
21
- * `project::<P>()` para proyecciones con scope Agent). Sobre una base
22
- * compartida eso devolvería hilos y estadísticas de otros inquilinos. Mientras
23
- * el motor no exponga las variantes acotadas por agente, con un tenant activo
24
- * estas lecturas se apagan: perder una señal de reflexión es aceptable, cruzar
25
- * datos entre agencias no.
21
+ * El log no tiene colecciones que prefijar: cada evento va al shard de su
22
+ * `agentId`, y ahí está todo el aislamiento. Con un tenant activo la clave
23
+ * lleva el tenant delante (`t_…:agentId`, la misma forma que usa el índice
24
+ * BM25 vía `qualifyDocId`), así que dos inquilinos con un agente del mismo id
25
+ * no comparten shard. Sin tenant es la identidad y un log de un solo dueño no
26
+ * cambia.
26
27
  *
27
- * La ESCRITURA (`append`) no entra aquí: va al shard del propio agente, y los
28
- * agentes de Hive Cloud ya llevan el enjambre en el id.
28
+ * Escritura y lecturas tienen que pasar por aquí: un evento escrito con una
29
+ * clave y leído con otra simplemente no aparece.
30
+ */
31
+ export function causalAgentKey(agentId: string): string {
32
+ return qualifyDocId(agentId)
33
+ }
34
+
35
+ /**
36
+ * Lista `agents` para `causalThread`, `toolStats` y `buildAgentContext`.
37
+ *
38
+ * Devuelve `null` si no queda ningún agente, y quien llama se salta la
39
+ * lectura: hive-db trata una lista vacía igual que la ausencia de filtro y
40
+ * recorre TODOS los shards, que sobre una base compartida es leer los eventos
41
+ * de otros inquilinos.
42
+ */
43
+ export function causalScope(agentIds: Iterable<string | null | undefined>): string[] | null {
44
+ const keys = new Set<string>()
45
+ for (const id of agentIds) {
46
+ if (id) keys.add(causalAgentKey(id))
47
+ }
48
+ return keys.size > 0 ? [...keys] : null
49
+ }
50
+
51
+ /**
52
+ * ¿Se lee el log causal (reflector, contexto causal del compilador)?
53
+ *
54
+ * Con hive-db 0.4, `causalThread`, `buildAgentContext` y `toolStats` recorrían
55
+ * todos los shards y mezclaban sus resultados, así que con un tenant activo
56
+ * había que apagarlas para no devolver hilos y estadísticas de otros
57
+ * inquilinos. hive-db 0.5.1 —la mínima que pide el SDK— acepta `agents` en las
58
+ * tres, y el SDK las llama siempre acotadas con {@link causalScope}: basta con
59
+ * que el log esté encendido.
29
60
  */
30
61
  export function causalReadsEnabled(): boolean {
31
- return !!loadConfig().causalLog?.enabled && !currentTenant()
62
+ return !!loadConfig().causalLog?.enabled
32
63
  }
33
64
 
34
65
  /**
@@ -52,10 +83,10 @@ export function causalReadsEnabled(): boolean {
52
83
  export async function watchCausalEvents(
53
84
  pattern: EventPattern
54
85
  ): Promise<AsyncIterable<Event> & { close(): void }> {
55
- // El log de eventos no tiene colecciones que prefijar: el aislamiento entre
56
- // inquilinos lo da el shard por `agentId`. Un patrón sin `agentId` sobre una
57
- // base compartida entregaría los eventos de todos los enjambres, así que se
58
- // exige explícitamente en vez de filtrar a medias.
86
+ // Un patrón sin `agentId` sobre una base compartida entregaría los eventos de
87
+ // todos los enjambres, así que con un tenant activo se exige explícitamente en
88
+ // vez de filtrar a medias. Se recibe el id crudo del agente y su shard se
89
+ // busca por la clave calificada.
59
90
  if (currentTenant() && !pattern.agentId) {
60
91
  throw new Error(
61
92
  "watchCausalEvents: con un tenant activo el patrón debe fijar agentId; " +
@@ -63,7 +94,7 @@ export async function watchCausalEvents(
63
94
  )
64
95
  }
65
96
  const db = await getHiveDb()
66
- return db.events(pattern)
97
+ return db.events(pattern.agentId ? { ...pattern, agentId: causalAgentKey(pattern.agentId) } : pattern)
67
98
  }
68
99
 
69
100
  /** One-line human-readable summary of a causal event, keyed by its kindTag. */
@@ -76,7 +107,7 @@ export function formatCausalEvent(event: Event): string {
76
107
  }
77
108
 
78
109
  const streamShort = event.streamId.slice(0, 8)
79
- const header = `[${event.seq}] ${kindIcon(event.kindTag)} ${event.kindTag.padEnd(16)} agent=${event.agentId} stream=${streamShort}…`
110
+ const header = `[${event.seq}] ${kindIcon(event.kindTag)} ${event.kindTag.padEnd(16)} agent=${unqualifyDocId(event.agentId)} stream=${streamShort}…`
80
111
 
81
112
  switch (event.kindTag) {
82
113
  case "IntentLogged":
@@ -128,4 +128,4 @@ export { reconcileOnBoot } from "./reconcile.ts";
128
128
 
129
129
  // ─── Log causal (G9) ─────────────────────────────────────────────────────────
130
130
  export type { CausalEvent, CausalEventPattern } from "./causal-events.ts";
131
- export { watchCausalEvents, formatCausalEvent } from "./causal-events.ts";
131
+ export { watchCausalEvents, formatCausalEvent, causalAgentKey, causalScope } from "./causal-events.ts";
@@ -266,7 +266,9 @@ export const SEED_DATA: SeedData = {
266
266
  { id: "z-ai/glm-5.3-flash", providerId: "openrouter", name: "GLM 5.3 Flash (OR)", modelType: "llm", contextWindow: 1310720, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.075, outputPer1M: 0.25 },
267
267
  { id: "z-ai/glm-5.2", providerId: "openrouter", name: "GLM 5.2 (OR)", modelType: "llm", contextWindow: 1048576, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "code", "reasoning"]), inputPer1M: 0.966, outputPer1M: 3.036 },
268
268
  // Qwen
269
- { id: "qwen/qwen3.8-max", providerId: "openrouter", name: "Qwen3.8 Max (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 2, outputPer1M: 6 },
269
+ // OpenRouter renombró qwen/qwen3.8-max a su versión fechada: mismo contexto
270
+ // y precio, ahora también con imagen. Verificado 2026-09-10.
271
+ { id: "qwen/qwen3.8-max-0902", providerId: "openrouter", name: "Qwen3.8 Max (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 2, outputPer1M: 6 },
270
272
  { id: "qwen/qwen3.8-flash", providerId: "openrouter", name: "Qwen3.8 Flash (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0.15, outputPer1M: 0.47 },
271
273
  { id: "qwen/qwen3.7-flash", providerId: "openrouter", name: "Qwen3.7 Flash (OR)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "json_mode", "function_calling", "streaming"]), inputPer1M: 0.03, outputPer1M: 0.13 },
272
274
  // xAI
@@ -334,8 +336,10 @@ export const SEED_DATA: SeedData = {
334
336
  // provider `z-ai` directo, o el enrutado de OpenRouter.
335
337
  { id: "moonshotai/kimi-k3", providerId: "nvidia", name: "Kimi K3 (NVIDIA)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
336
338
  { id: "nvidia/nemotron-3.5-lightning-30b-a3b", providerId: "nvidia", name: "Nemotron 3.5 Lightning 30B", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
337
- { id: "moonshotai/kimi-k2.6", providerId: "nvidia", name: "Kimi K2.6 (NVIDIA)", modelType: "llm", contextWindow: 262144, capabilities: JSON.stringify(["chat", "code", "vision", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
338
- { id: "minimaxai/minimax-m3", providerId: "nvidia", name: "MiniMax M3 (NVIDIA)", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "vision", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
339
+ // MiniMax M3 (minimaxai/minimax-m3) se sacó: NVIDIA lo retiró (410 Gone, ya
340
+ // no figura en /v1/models). Kimi K2.6 (moonshotai/kimi-k2.6) se sacó por lo
341
+ // mismo que DeepSeek V4 Pro: 404 "Function not found for account" en dos
342
+ // cuentas distintas. Verificado 2026-09-10.
339
343
  { id: "nvidia/nemotron-3-ultra-550b-a55b", providerId: "nvidia", name: "Nemotron 3 Ultra 550B", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
340
344
  { id: "nvidia/nemotron-3-super-120b-a12b", providerId: "nvidia", name: "Nemotron 3 Super 120B", modelType: "llm", contextWindow: 1000000, capabilities: JSON.stringify(["chat", "code", "json_mode", "function_calling", "streaming", "reasoning"]), inputPer1M: 0, outputPer1M: 0 },
341
345
  // DeepSeek V4 Pro (deepseek-ai/deepseek-v4-pro) se sacó: devuelve 404