@johpaz/hive-sdk 0.1.4 → 0.1.5

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 (272) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +78 -23
  3. package/bunfig.toml +4 -2
  4. package/docs/API-AGENTS.md +78 -27
  5. package/docs/API-CONTEXT-COMPILER.md +31 -34
  6. package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
  7. package/docs/HIVE-HARNESS.md +1 -1
  8. package/docs/INDEX.md +4 -4
  9. package/docs/TEMPLATE-HIVE-APP.md +10 -10
  10. package/package.json +9 -4
  11. package/packages/cli/package.json +2 -2
  12. package/packages/cli/src/commands/create-app.test.ts +36 -7
  13. package/packages/cli/src/commands/init.ts +3 -3
  14. package/packages/cli/src/commands/run.ts +1 -1
  15. package/packages/cli/src/commands/test.ts +37 -25
  16. package/packages/cli/src/commands/trace.ts +30 -28
  17. package/packages/cli/templates/hive-app/.env.example +10 -2
  18. package/packages/cli/templates/hive-app/README.md +103 -0
  19. package/packages/cli/templates/hive-app/hive.config.ts +9 -3
  20. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
  21. package/packages/cli/templates/hive-app/src/main.ts +12 -19
  22. package/packages/core/package.json +5 -4
  23. package/packages/core/src/agent/acceptance-checks.ts +166 -0
  24. package/packages/core/src/agent/agent-catalog.ts +348 -0
  25. package/packages/core/src/agent/agent-loop.ts +1373 -0
  26. package/packages/core/src/agent/capability-search.ts +186 -0
  27. package/packages/core/src/agent/catalog-selector.ts +103 -0
  28. package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
  29. package/packages/core/src/agent/context-compiler.ts +689 -0
  30. package/packages/core/src/agent/conversation-store.ts +381 -0
  31. package/packages/core/src/agent/curator.ts +276 -0
  32. package/packages/core/src/agent/delegation-runtime.ts +241 -0
  33. package/packages/core/src/agent/goal-runner.ts +323 -0
  34. package/packages/core/src/agent/index.ts +17 -12
  35. package/packages/core/src/agent/llm-client.ts +266 -0
  36. package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
  37. package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
  38. package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
  39. package/packages/core/src/agent/llm-providers/groq.ts +5 -0
  40. package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
  41. package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
  42. package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
  43. package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
  44. package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
  45. package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
  46. package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
  47. package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
  48. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
  49. package/packages/core/src/agent/llm-providers/openai.ts +5 -0
  50. package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
  51. package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
  52. package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
  53. package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
  54. package/packages/core/src/agent/minimal-loadout.ts +47 -0
  55. package/packages/core/src/agent/playbook-selector.ts +119 -0
  56. package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
  57. package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
  58. package/packages/core/src/agent/providers/index.ts +35 -16
  59. package/packages/core/src/agent/reflector.ts +320 -0
  60. package/packages/core/src/agent/routing-intent.ts +22 -0
  61. package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
  62. package/packages/core/src/{harness → agent}/run-store.ts +142 -81
  63. package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
  64. package/packages/core/src/agent/skill-selector.ts +374 -0
  65. package/packages/core/src/agent/stuck-loop.ts +209 -0
  66. package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
  67. package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
  68. package/packages/core/src/api/createAgent.test.ts +139 -27
  69. package/packages/core/src/api/createAgent.ts +232 -44
  70. package/packages/core/src/artifacts/store.ts +162 -0
  71. package/packages/core/src/canvas/canvas-manager.ts +161 -0
  72. package/packages/core/src/canvas/canvas.test.ts +8 -4
  73. package/packages/core/src/canvas/emitter.ts +131 -80
  74. package/packages/core/src/canvas/index.ts +1 -3
  75. package/packages/core/src/channels/base.ts +9 -1
  76. package/packages/core/src/channels/discord.ts +5 -4
  77. package/packages/core/src/channels/manager.ts +122 -30
  78. package/packages/core/src/channels/slack.ts +5 -4
  79. package/packages/core/src/channels/telegram.ts +36 -6
  80. package/packages/core/src/channels/webchat.ts +11 -10
  81. package/packages/core/src/channels/whatsapp.ts +23 -7
  82. package/packages/core/src/config/index.ts +13 -2
  83. package/packages/core/src/config/loader.ts +71 -29
  84. package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
  85. package/packages/core/src/ethics/EthicsGuard.ts +51 -47
  86. package/packages/core/src/events/agent-bus.ts +44 -68
  87. package/packages/core/src/events/channel-narration.ts +150 -0
  88. package/packages/core/src/events/narration.ts +82 -0
  89. package/packages/core/src/events/tool-narration.ts +62 -0
  90. package/packages/core/src/gateway/delegation-groups.ts +258 -0
  91. package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
  92. package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
  93. package/packages/core/src/gateway/lane-queue.ts +173 -0
  94. package/packages/core/src/gateway/notification-inbox.ts +57 -0
  95. package/packages/core/src/gateway/server.ts +1 -1
  96. package/packages/core/src/harness/index.ts +46 -27
  97. package/packages/core/src/index.ts +33 -27
  98. package/packages/core/src/mcp/hot-reload.ts +32 -23
  99. package/packages/core/src/mcp/index.ts +6 -3
  100. package/packages/core/src/mcp/singleton.ts +1 -4
  101. package/packages/core/src/mcp/tool-sync.ts +138 -0
  102. package/packages/core/src/memory/Scratchpad.test.ts +39 -20
  103. package/packages/core/src/memory/Scratchpad.ts +27 -34
  104. package/packages/core/src/multimodal/vision-service.ts +44 -38
  105. package/packages/core/src/resilience/retry.ts +95 -0
  106. package/packages/core/src/scheduler/CronScheduler.ts +334 -287
  107. package/packages/core/src/scheduler/index.ts +9 -7
  108. package/packages/core/src/scheduler/integration.ts +46 -26
  109. package/packages/core/src/scheduler/scheduler.test.ts +9 -13
  110. package/packages/core/src/scheduler/types.ts +7 -2
  111. package/packages/core/src/security/Pairing.ts +1 -1
  112. package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
  113. package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
  114. package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
  115. package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
  116. package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
  117. package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
  118. package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
  119. package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
  120. package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
  121. package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
  122. package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
  123. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
  124. package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
  125. package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
  126. package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
  127. package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
  128. package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
  129. package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
  130. package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
  131. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
  132. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
  133. package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
  134. package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
  135. package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
  136. package/packages/core/src/skills/skills.test.ts +52 -11
  137. package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
  138. package/packages/core/src/storage/bootstrap.ts +151 -0
  139. package/packages/core/src/storage/causal-events.ts +84 -0
  140. package/packages/core/src/storage/collections.ts +680 -0
  141. package/packages/core/src/storage/crypto.ts +205 -74
  142. package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
  143. package/packages/core/src/storage/hivedb.ts +61 -0
  144. package/packages/core/src/storage/index.ts +111 -18
  145. package/packages/core/src/storage/model-id.ts +53 -0
  146. package/packages/core/src/storage/onboarding.ts +540 -972
  147. package/packages/core/src/storage/reconcile.ts +238 -0
  148. package/packages/core/src/storage/seed.ts +572 -406
  149. package/packages/core/src/storage/usage.ts +285 -225
  150. package/packages/core/src/storage/user-email.ts +11 -0
  151. package/packages/core/src/swarm/AgentExecutor.ts +1 -1
  152. package/packages/core/src/swarm/EventBridge.ts +1 -1
  153. package/packages/core/src/swarm/index.ts +12 -9
  154. package/packages/core/src/tool-runtime/index.ts +146 -23
  155. package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
  156. package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
  157. package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
  158. package/packages/core/src/tools/agents/get-available-models.ts +36 -54
  159. package/packages/core/src/tools/agents/index.ts +784 -292
  160. package/packages/core/src/tools/api/api-request.test.ts +164 -0
  161. package/packages/core/src/tools/api/api-request.ts +174 -0
  162. package/packages/core/src/tools/api/index.ts +16 -0
  163. package/packages/core/src/tools/cli/index.ts +4 -0
  164. package/packages/core/src/tools/core/index.ts +281 -112
  165. package/packages/core/src/tools/cron/index.ts +121 -124
  166. package/packages/core/src/tools/index.ts +63 -78
  167. package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
  168. package/packages/core/src/tools/types.ts +3 -1
  169. package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
  170. package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
  171. package/packages/core/src/tools/web/browser-service.ts +5 -0
  172. package/packages/core/src/tools/web/browser-type.ts +3 -8
  173. package/packages/core/src/tools/web/index.ts +4 -4
  174. package/packages/core/src/voice/index.ts +89 -63
  175. package/packages/core/src/workers/agent.worker.ts +2 -2
  176. package/packages/core/src/workers/workers.test.ts +3 -10
  177. package/scripts/bump-version.ts +248 -0
  178. package/scripts/generate-skill-bundle.ts +108 -0
  179. package/test/agent-loop-terminal-synthesis.test.ts +32 -0
  180. package/test/catalog-agents-stay-enabled.test.ts +117 -0
  181. package/test/causal-events.test.ts +117 -0
  182. package/test/compaction.test.ts +105 -0
  183. package/test/context-compiler.test.ts +269 -0
  184. package/test/curator.test.ts +130 -0
  185. package/test/durable-queue.test.ts +114 -0
  186. package/test/harness-barrel.test.ts +64 -0
  187. package/test/hive-helpers.test.ts +130 -0
  188. package/test/hivedb-search.test.ts +189 -0
  189. package/test/internal-turns.test.ts +166 -0
  190. package/test/job-idempotency.test.ts +68 -0
  191. package/test/job-retry-backoff.test.ts +184 -0
  192. package/test/job-store.test.ts +381 -0
  193. package/test/llm-retry.test.ts +97 -0
  194. package/test/memory-perf.test.ts +774 -0
  195. package/test/minimal-loadout.test.ts +78 -0
  196. package/test/model-catalog.test.ts +105 -0
  197. package/test/preload.ts +12 -0
  198. package/test/reflector.test.ts +320 -0
  199. package/test/retention-cap.test.ts +91 -0
  200. package/test/retired-capabilities-pruned.test.ts +192 -0
  201. package/test/run-store.test.ts +355 -0
  202. package/test/scratchpad.test.ts +74 -0
  203. package/test/secrets-durability.test.ts +119 -0
  204. package/test/seed-model-reseed.test.ts +155 -0
  205. package/test/setup-agent-seed.test.ts +264 -0
  206. package/test/tool-inventory.test.ts +65 -0
  207. package/test/tool-runtime.test.ts +258 -0
  208. package/test/toon.test.ts +429 -0
  209. package/tsconfig.json +2 -0
  210. package/packages/core/src/ace/Curator.ts +0 -158
  211. package/packages/core/src/ace/Reflector.ts +0 -200
  212. package/packages/core/src/ace/index.ts +0 -4
  213. package/packages/core/src/agent/AgentRunner.ts +0 -711
  214. package/packages/core/src/agent/ContextCompiler.ts +0 -567
  215. package/packages/core/src/agent/ContextGuard.ts +0 -91
  216. package/packages/core/src/agent/ConversationStore.ts +0 -254
  217. package/packages/core/src/agent/Hooks.ts +0 -166
  218. package/packages/core/src/agent/StuckLoop.ts +0 -133
  219. package/packages/core/src/agent/providers/LLMClient.ts +0 -149
  220. package/packages/core/src/agent/providers/anthropic.ts +0 -212
  221. package/packages/core/src/agent/providers/openai-compat.ts +0 -231
  222. package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
  223. package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
  224. package/packages/core/src/agent/selectors/index.ts +0 -6
  225. package/packages/core/src/auth/auth.ts +0 -121
  226. package/packages/core/src/auth/index.ts +0 -1
  227. package/packages/core/src/canvas/CanvasManager.ts +0 -390
  228. package/packages/core/src/canvas/canvas-tools.ts +0 -448
  229. package/packages/core/src/harness/collections.ts +0 -98
  230. package/packages/core/src/harness/goal-verifier.ts +0 -141
  231. package/packages/core/src/harness/harness.test.ts +0 -236
  232. package/packages/core/src/harness/reconcile.ts +0 -149
  233. package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
  234. package/packages/core/src/multimodal/VisionService.ts +0 -293
  235. package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
  236. package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
  237. package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
  238. package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
  239. package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
  240. package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
  241. package/packages/core/src/scheduler/dag/errors.ts +0 -37
  242. package/packages/core/src/scheduler/dag/index.ts +0 -26
  243. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
  244. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
  245. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
  246. package/packages/core/src/storage/HiveDBStorage.ts +0 -64
  247. package/packages/core/src/storage/SQLiteStorage.ts +0 -414
  248. package/packages/core/src/storage/hiveSeed.ts +0 -308
  249. package/packages/core/src/storage/hiveStorage.test.ts +0 -38
  250. package/packages/core/src/storage/schema.ts +0 -689
  251. package/packages/core/src/storage/storage.test.ts +0 -37
  252. package/packages/core/src/swarm/AgentBus.ts +0 -460
  253. package/packages/core/src/swarm/EventBus.ts +0 -169
  254. package/packages/core/src/swarm/WorkerPool.ts +0 -236
  255. package/packages/core/src/tools/bridge-events.ts +0 -26
  256. package/packages/core/src/tools/canvas/index.ts +0 -375
  257. package/packages/core/src/tools/codebridge/index.ts +0 -342
  258. package/packages/core/src/tools/meeting/index.ts +0 -353
  259. package/packages/core/src/tools/projects/index.ts +0 -37
  260. package/packages/core/src/tools/projects/project-create.ts +0 -94
  261. package/packages/core/src/tools/projects/project-done.ts +0 -66
  262. package/packages/core/src/tools/projects/project-fail.ts +0 -66
  263. package/packages/core/src/tools/projects/project-list.ts +0 -96
  264. package/packages/core/src/tools/projects/project-update.ts +0 -72
  265. package/packages/core/src/tools/projects/task-create.ts +0 -68
  266. package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
  267. package/packages/core/src/tools/projects/task-update.ts +0 -93
  268. package/packages/core/src/tools/voice/index.ts +0 -104
  269. package/packages/core/src/tools/web/api-request.test.ts +0 -170
  270. package/packages/core/src/tools/web/api-request.ts +0 -239
  271. package/test/setup-db.ts +0 -216
  272. /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Conversation Store — persists message history in the `conversations` HiveDB collection.
3
+ * Replaces the LangGraph BunSqliteSaver + lg_checkpoints approach.
4
+ *
5
+ * Also manages: summaries and scratchpad, both HiveDB document collections.
6
+ */
7
+
8
+ import { col, nextId, bumpRollup } from "../storage/hive"
9
+ import { getHiveDb } from "../storage/hivedb"
10
+ import { logger } from "../utils/logger"
11
+ import type { LLMMessage, ContentPart } from "./llm-client"
12
+ import { estimateTokens } from "../utils/toon"
13
+ import type { ConversationDoc, SummaryDoc, MessageSource } from "../storage/collections"
14
+
15
+ const log = logger.child("conv-store")
16
+
17
+ // ─── Types ────────────────────────────────────────────────────────────────────
18
+
19
+ export interface StoredMessage {
20
+ /** Per-thread monotonic sequence number (NOT globally unique — scope every comparison to a single threadId). */
21
+ id: number
22
+ thread_id: string
23
+ channel: string
24
+ role: "user" | "assistant" | "tool"
25
+ /** Provenance of this turn. Never null — legacy rows are normalized to "legacy_internal" on read. */
26
+ source: MessageSource
27
+ content: string
28
+ tool_calls_json: string | null
29
+ tool_call_id: string | null
30
+ reasoning_content: string | null // Kimi K2 thinking — must be round-tripped
31
+ content_multimodal: string | null // JSON array of ContentPart[]
32
+ token_count: number
33
+ created_at: number
34
+ }
35
+
36
+ // ─── Internal events (delegation fan-in, etc.) ────────────────────────────────
37
+ //
38
+ // These are system-authored turns (async delegation outcomes) that must reach
39
+ // the model as input but must never be persisted with role:"system" — doing so
40
+ // causes every LLM provider to hoist them permanently into the system
41
+ // instruction on every subsequent turn (see gemini.ts/anthropic.ts, which
42
+ // concatenate ALL role:"system" messages into systemInstruction/system). They
43
+ // are persisted as role:"user" + a source tag instead, and wrapped with a
44
+ // framing marker only at serialization time (toAPIMessages), so stored content
45
+ // stays clean and the wording can evolve without a migration.
46
+
47
+ export const INTERNAL_SOURCES: ReadonlySet<string> =
48
+ new Set(["task_complete", "delegation_summary", "legacy_internal"])
49
+
50
+ export function isInternalSource(source: string | null | undefined): boolean {
51
+ return !!source && INTERNAL_SOURCES.has(source)
52
+ }
53
+
54
+ export function formatInternalEvent(source: string, content: string): string {
55
+ return `<hive:internal_event source="${source}">\n` +
56
+ `Evento interno del sistema — NO es un mensaje del usuario. No lo cites literalmente, no expongas IDs internos (task_id, worker_id) ni JSON crudo. Respondé al usuario de forma natural y breve.\n\n` +
57
+ `${content}\n` +
58
+ `</hive:internal_event>`
59
+ }
60
+
61
+ function storageId(threadId: string, seq: number): string {
62
+ return `${threadId}:${String(seq).padStart(15, "0")}`
63
+ }
64
+
65
+ function toStoredMessage(id: string, doc: ConversationDoc): StoredMessage {
66
+ const seq = parseInt(id.slice(id.lastIndexOf(":") + 1), 10)
67
+ // Legacy rows (written before `source` existed) used role:"system" as the
68
+ // sole marker for internal events. Normalize them here so every downstream
69
+ // reader (getRecentMessages, compaction, context-compiler) sees a single
70
+ // consistent shape and never has to special-case role:"system" again.
71
+ const legacyInternal = doc.role === "system"
72
+ return {
73
+ id: seq,
74
+ thread_id: doc.thread_id,
75
+ channel: doc.channel,
76
+ role: legacyInternal ? "user" : (doc.role as StoredMessage["role"]),
77
+ source: doc.source ?? (legacyInternal ? "legacy_internal" : "message"),
78
+ content: doc.content,
79
+ tool_calls_json: doc.tool_calls_json,
80
+ tool_call_id: doc.tool_call_id,
81
+ reasoning_content: doc.reasoning_content,
82
+ content_multimodal: doc.content_multimodal,
83
+ token_count: doc.token_count,
84
+ created_at: doc.created_at,
85
+ }
86
+ }
87
+
88
+ // ─── Message operations ───────────────────────────────────────────────────────
89
+
90
+ const recentMessageTimestamps: number[] = []
91
+
92
+ export function getRecentMessageCount(windowMs = 5 * 60_000): number {
93
+ const cutoff = Date.now() - windowMs
94
+ while (recentMessageTimestamps.length && recentMessageTimestamps[0] < cutoff) {
95
+ recentMessageTimestamps.shift()
96
+ }
97
+ return recentMessageTimestamps.length
98
+ }
99
+
100
+ export async function addMessage(
101
+ threadId: string,
102
+ role: StoredMessage["role"],
103
+ content: string | ContentPart[],
104
+ opts?: {
105
+ channel?: string
106
+ tool_calls?: LLMMessage["tool_calls"]
107
+ tool_call_id?: string
108
+ reasoning_content?: string
109
+ source?: MessageSource
110
+ }
111
+ ): Promise<number> {
112
+ // Handle multimodal content by extracting text for the content column
113
+ const textContent = typeof content === "string"
114
+ ? content
115
+ : Array.isArray(content)
116
+ ? content.filter(p => p.type === "text").map(p => (p as any).text).join("\n")
117
+ : String(content)
118
+
119
+ const content_multimodal = Array.isArray(content) ? JSON.stringify(content) : null
120
+ const tool_calls_json = opts?.tool_calls ? JSON.stringify(opts.tool_calls) : null
121
+
122
+ const paddedSeq = await nextId(`conversations:${threadId}`)
123
+ const seq = parseInt(paddedSeq, 10)
124
+ const now = Date.now()
125
+
126
+ const conversationsCol = await col<ConversationDoc>("conversations")
127
+ await conversationsCol.put(storageId(threadId, seq), {
128
+ id: storageId(threadId, seq),
129
+ thread_id: threadId,
130
+ channel: opts?.channel ?? "webchat",
131
+ role,
132
+ content: textContent,
133
+ content_multimodal,
134
+ tool_calls_json,
135
+ tool_call_id: opts?.tool_call_id ?? null,
136
+ reasoning_content: opts?.reasoning_content ?? null,
137
+ source: opts?.source ?? "message",
138
+ // Estimate tokens: content + tool_calls JSON
139
+ token_count: Math.max(1, estimateTokens(textContent) + estimateTokens(tool_calls_json ?? "")),
140
+ created_at: now,
141
+ updated_at: now,
142
+ }, { expectedVersion: 0 })
143
+
144
+ // Fire-and-forget — never block message persistence on the activity chart rollup.
145
+ const hour = new Date(now).toISOString().slice(0, 13)
146
+ bumpRollup("activityRollups", hour, { messageCount: 1 }).catch(() => {})
147
+ recentMessageTimestamps.push(now)
148
+
149
+ return seq
150
+ }
151
+
152
+ /**
153
+ * Returns all messages for the thread ordered oldest → newest.
154
+ */
155
+ export async function getHistory(threadId: string, limit = 200): Promise<StoredMessage[]> {
156
+ const conversationsCol = await col<ConversationDoc>("conversations")
157
+ const entries = await conversationsCol.scan({ prefix: `${threadId}:`, limit })
158
+ return entries.map(e => toStoredMessage(e.id, e.doc))
159
+ }
160
+
161
+ /**
162
+ * Returns only the last N messages (oldest → newest order),
163
+ * with leading orphaned tool messages stripped from the window start.
164
+ *
165
+ * A tool message is "orphaned" when the assistant message that issued its
166
+ * tool_call_id is not present in the loaded window (it was compacted away).
167
+ * Sending orphaned tool messages to the LLM causes provider errors.
168
+ */
169
+ export async function getRecentMessages(threadId: string, n: number): Promise<StoredMessage[]> {
170
+ const conversationsCol = await col<ConversationDoc>("conversations")
171
+ const entries = await conversationsCol.scan({ prefix: `${threadId}:`, reverse: true })
172
+ const nonTool = entries.filter(e => e.doc.role !== "tool").slice(0, n)
173
+ const rows = nonTool.map(e => toStoredMessage(e.id, e.doc)).reverse()
174
+ return stripLeadingOrphanedTools(rows)
175
+ }
176
+
177
+ function stripLeadingOrphanedTools(rows: StoredMessage[]): StoredMessage[] {
178
+ // Collect all tool_call_ids referenced by assistant messages in this window
179
+ const knownIds = new Set<string>()
180
+ for (const r of rows) {
181
+ if (r.role === "assistant" && r.tool_calls_json) {
182
+ try {
183
+ const tcs = JSON.parse(r.tool_calls_json) as Array<{ id: string }>
184
+ for (const tc of tcs) knownIds.add(tc.id)
185
+ } catch { /* ignore malformed JSON */ }
186
+ }
187
+ }
188
+
189
+ // Drop tool messages at the start of the window whose assistant is missing
190
+ let start = 0
191
+ while (
192
+ start < rows.length &&
193
+ rows[start].role === "tool" &&
194
+ rows[start].tool_call_id !== null &&
195
+ !knownIds.has(rows[start].tool_call_id!)
196
+ ) {
197
+ start++
198
+ }
199
+
200
+ if (start > 0) {
201
+ log.warn(`[conv-store] Stripped ${start} leading orphaned tool message(s) from window (tool_call_ids outside window)`)
202
+ }
203
+ return start > 0 ? rows.slice(start) : rows
204
+ }
205
+
206
+ export async function getMessageCount(threadId: string): Promise<number> {
207
+ const conversationsCol = await col<ConversationDoc>("conversations")
208
+ const entries = await conversationsCol.scan({ prefix: `${threadId}:` })
209
+ return entries.length
210
+ }
211
+
212
+ export async function getTotalTokens(threadId: string): Promise<number> {
213
+ const conversationsCol = await col<ConversationDoc>("conversations")
214
+ const entries = await conversationsCol.scan({ prefix: `${threadId}:` })
215
+ return entries.reduce((sum, e) => sum + e.doc.token_count, 0)
216
+ }
217
+
218
+ /**
219
+ * Messages after a given message ID (for incremental summary updates).
220
+ */
221
+ export async function getMessagesAfter(threadId: string, afterId: number): Promise<StoredMessage[]> {
222
+ const conversationsCol = await col<ConversationDoc>("conversations")
223
+ const entries = await conversationsCol.scan({ prefix: `${threadId}:` })
224
+ return entries
225
+ .map(e => toStoredMessage(e.id, e.doc))
226
+ .filter(m => m.id > afterId)
227
+ }
228
+
229
+ // ─── Convert stored messages → LLMMessage array ───────────────────────────────
230
+
231
+ export function toAPIMessages(rows: StoredMessage[]): LLMMessage[] {
232
+ return rows.map((r) => {
233
+ let content: string | ContentPart[] = r.content
234
+ if (r.content_multimodal) {
235
+ try { content = JSON.parse(r.content_multimodal) } catch { /* ignore */ }
236
+ }
237
+ // Internal events (delegation fan-in) are wrapped at serialization time —
238
+ // never at authoring time — so stored content stays clean and legacy rows
239
+ // (normalized to source:"legacy_internal" in toStoredMessage) get wrapped
240
+ // for free. Internal events are never multimodal in practice.
241
+ if (isInternalSource(r.source) && typeof content === "string") {
242
+ content = formatInternalEvent(r.source, content)
243
+ }
244
+ const msg: LLMMessage = { role: r.role, content }
245
+ // Note: tool_calls and tool_call_id are NOT reconstructed from DB.
246
+ // Tool results are kept in-memory during iteration but not persisted,
247
+ // so historical messages only contain text conversation.
248
+ if (r.reasoning_content) msg.reasoning_content = r.reasoning_content
249
+ return msg
250
+ })
251
+ }
252
+
253
+ // ─── Summaries ────────────────────────────────────────────────────────────────
254
+
255
+ export interface Summary {
256
+ summary: string
257
+ last_message_id: number
258
+ messages_covered: number
259
+ }
260
+
261
+ export async function getSummary(threadId: string): Promise<Summary | null> {
262
+ const summariesCol = await col<SummaryDoc>("summaries")
263
+ const entry = await summariesCol.get(threadId)
264
+ if (!entry) return null
265
+ return {
266
+ summary: entry.doc.summary,
267
+ last_message_id: entry.doc.last_message_id ? parseInt(entry.doc.last_message_id, 10) : 0,
268
+ messages_covered: entry.doc.messages_covered,
269
+ }
270
+ }
271
+
272
+ export async function saveSummary(
273
+ threadId: string,
274
+ summary: string,
275
+ messagesCovered: number,
276
+ lastMessageId: number
277
+ ): Promise<void> {
278
+ const summariesCol = await col<SummaryDoc>("summaries")
279
+ const existing = await summariesCol.get(threadId)
280
+ await summariesCol.put(threadId, {
281
+ thread_id: threadId,
282
+ summary,
283
+ messages_covered: messagesCovered,
284
+ last_message_id: String(lastMessageId),
285
+ }, existing ? { expectedVersion: existing.version } : { expectedVersion: 0 })
286
+ }
287
+
288
+ // ─── Scratchpad (HiveDB collection) ────────────────────────────────────────────
289
+ //
290
+ // Persistent key-value notes per conversation. Lives in a HiveDB document
291
+ // collection instead of SQLite: id = "<threadId>:<key>", so a per-thread
292
+ // listing is a prefix scan and no secondary index is needed.
293
+
294
+ export interface ScratchpadDoc {
295
+ threadId: string
296
+ key: string
297
+ value: string
298
+ source: string | null
299
+ createdAt: number
300
+ updatedAt: number
301
+ /** Monotonic per-process counter — tiebreaker for notes saved within the same clock tick. */
302
+ seq: number
303
+ }
304
+
305
+ let scratchpadSeq = 0
306
+
307
+ /** Wire shape for the admin notes panel — mirrors the old SQLite row (snake_case, epoch seconds). */
308
+ export interface ScratchpadNoteRow {
309
+ id: string
310
+ thread_id: string
311
+ key: string
312
+ value: string
313
+ source: string | null
314
+ created_at: number
315
+ updated_at: number
316
+ }
317
+
318
+ function scratchpadNoteId(threadId: string, key: string): string {
319
+ return `${threadId}:${key}`
320
+ }
321
+
322
+ async function scratchpadCollection() {
323
+ const db = await getHiveDb()
324
+ return db.collection<ScratchpadDoc>("scratchpad")
325
+ }
326
+
327
+ export async function saveScratchpadNote(
328
+ threadId: string,
329
+ key: string,
330
+ value: string,
331
+ source?: string
332
+ ): Promise<void> {
333
+ const col = await scratchpadCollection()
334
+ const id = scratchpadNoteId(threadId, key)
335
+ const existing = await col.get(id)
336
+ const now = Date.now()
337
+ await col.put(id, {
338
+ threadId,
339
+ key,
340
+ value,
341
+ source: source ?? null,
342
+ createdAt: existing?.doc.createdAt ?? now,
343
+ updatedAt: now,
344
+ seq: scratchpadSeq++,
345
+ })
346
+ }
347
+
348
+ function byMostRecent(a: { doc: ScratchpadDoc }, b: { doc: ScratchpadDoc }): number {
349
+ return b.doc.updatedAt - a.doc.updatedAt || b.doc.seq - a.doc.seq
350
+ }
351
+
352
+ export async function getScratchpad(threadId: string): Promise<Array<{ key: string; value: string }>> {
353
+ const col = await scratchpadCollection()
354
+ const entries = await col.scan({ prefix: `${threadId}:` })
355
+ return entries
356
+ .sort(byMostRecent)
357
+ .map((e) => ({ key: e.doc.key, value: e.doc.value }))
358
+ }
359
+
360
+ /** All notes across every thread, most recently updated first — used by the admin notes panel. */
361
+ export async function listAllScratchpadNotes(limit: number): Promise<ScratchpadNoteRow[]> {
362
+ const col = await scratchpadCollection()
363
+ const entries = await col.scan({})
364
+ return entries
365
+ .sort(byMostRecent)
366
+ .slice(0, limit)
367
+ .map((e) => ({
368
+ id: e.id,
369
+ thread_id: e.doc.threadId,
370
+ key: e.doc.key,
371
+ value: e.doc.value,
372
+ source: e.doc.source,
373
+ created_at: Math.floor(e.doc.createdAt / 1000),
374
+ updated_at: Math.floor(e.doc.updatedAt / 1000),
375
+ }))
376
+ }
377
+
378
+ export async function deleteScratchpadNote(threadId: string, key: string): Promise<void> {
379
+ const col = await scratchpadCollection()
380
+ await col.delete(scratchpadNoteId(threadId, key))
381
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * ACE Curator — converts reflections into playbook rules.
3
+ *
4
+ * Runs after the Reflector. Performs incremental edits to the playbook:
5
+ * - New insights → new rules
6
+ * - Repeated patterns → increment helpful_count
7
+ * - Contradicted rules → increment harmful_count or deactivate
8
+ * - Deactivate rules where harmful_count > helpful_count
9
+ *
10
+ * Never rewrites the whole playbook — only incremental edits.
11
+ *
12
+ * "Last processed reflection" is tracked via a `cursors` collection doc
13
+ * (id="curator:lastReflection") instead of SQL's MAX(source_reflection_id).
14
+ */
15
+
16
+ import { logger } from "../utils/logger"
17
+ import { col, nextId, toIndexable, fromIndexable } from "../storage/hive"
18
+ import type {
19
+ ReflectionDoc,
20
+ PlaybookDoc,
21
+ AgentDoc,
22
+ CursorDoc,
23
+ AgentProposalDoc,
24
+ TraceDoc,
25
+ } from "../storage/collections"
26
+
27
+ const log = logger.child("curator")
28
+
29
+ const MAX_HARMFUL_BEFORE_PRUNE = 3
30
+ const CURSOR_ID = "curator:lastReflection"
31
+
32
+ /** Entry point — called by reflector.ts after it inserts new reflections */
33
+ export async function runCurator(): Promise<void> {
34
+ try {
35
+ const cursorsCol = await col<CursorDoc>("cursors")
36
+ const playbookCol = await col<PlaybookDoc>("playbook")
37
+ const reflectionsCol = await col<ReflectionDoc>("reflections")
38
+ // Process unprocessed reflections (those newer than last run)
39
+ const cursorEntry = await cursorsCol.get(CURSOR_ID)
40
+ const lastProcessed = cursorEntry?.doc.value ?? null
41
+
42
+ let candidates = lastProcessed
43
+ ? await reflectionsCol.scan({ start: lastProcessed })
44
+ : await reflectionsCol.scan({})
45
+ if (lastProcessed && candidates[0]?.id === lastProcessed) candidates = candidates.slice(1)
46
+
47
+ if (candidates.length === 0) {
48
+ log.debug("[curator] No new reflections to process")
49
+ } else {
50
+ log.info(`[curator] Processing ${candidates.length} new reflections`)
51
+ const allPlaybook = await playbookCol.scan({})
52
+ for (const entry of candidates) {
53
+ await processReflection(playbookCol, allPlaybook, entry.doc)
54
+ }
55
+ const newCursor = candidates[candidates.length - 1].id
56
+ await cursorsCol.put(CURSOR_ID, { value: newCursor }, cursorEntry ? { expectedVersion: cursorEntry.version } : { expectedVersion: 0 })
57
+ }
58
+
59
+ // Prune rules where harmful > helpful (consistently bad rules)
60
+ const allActive = (await playbookCol.scan({})).filter(e => e.doc.active)
61
+ for (const entry of allActive) {
62
+ if (entry.doc.harmful_count > entry.doc.helpful_count && entry.doc.harmful_count >= MAX_HARMFUL_BEFORE_PRUNE) {
63
+ await playbookCol.put(entry.id, { ...entry.doc, active: false, updated_at: Date.now() }, { expectedVersion: entry.version })
64
+ }
65
+ }
66
+
67
+ await curateAgentStructure()
68
+
69
+ log.info("[curator] Playbook updated")
70
+ } catch (err) {
71
+ log.warn("[curator] Error:", err)
72
+ }
73
+ }
74
+
75
+ async function curateAgentStructure(): Promise<void> {
76
+ const agentsCol = await col<AgentDoc>("agents")
77
+ const proposalsCol = await col<AgentProposalDoc>("agentProposals")
78
+ const tracesCol = await col<TraceDoc>("traces")
79
+ const now = Date.now()
80
+
81
+ // A catalog agent is a capability, not a disposable worker: disabling
82
+ // `workspace_file_operator` takes filesystem work away from the whole hive
83
+ // and nothing replaces it, so a bad streak can never switch one off by
84
+ // itself. It raises a proposal for a human to decide instead — and the ACE
85
+ // counters keep accumulating either way, so routing still sees the signal.
86
+ for (const entry of await agentsCol.findBy("source", "catalog")) {
87
+ if (!entry.doc.enabled) continue
88
+ const helpful = entry.doc.helpful_count ?? 0
89
+ const harmful = entry.doc.harmful_count ?? 0
90
+ if (harmful > helpful && harmful >= MAX_HARMFUL_BEFORE_PRUNE) {
91
+ await ensureAgentProposal(proposalsCol, {
92
+ type: "disable_agent",
93
+ agentId: entry.id,
94
+ change: { active: false, reason: "harmful_count exceeded helpful_count" },
95
+ evidence: [],
96
+ confidence: 1,
97
+ })
98
+ log.warn(`[curator] Catalog agent '${entry.doc.name}' (${entry.id}) is failing verification (helpful=${helpful}, harmful=${harmful}) — proposed for review, left enabled`)
99
+ }
100
+ }
101
+
102
+ const traces = (await tracesCol.scan({})).map((entry) => entry.doc)
103
+ const agents = new Map((await agentsCol.scan({})).map((entry) => [entry.id, entry.doc]))
104
+
105
+ // Recurrent successful free workers are candidates for a learned template.
106
+ const freeSuccess = new Map<string, TraceDoc[]>()
107
+ for (const trace of traces) {
108
+ const agent = agents.get(trace.agent_id)
109
+ if (!trace.success || trace.tool_used || trace.catalog_agent_id || agent?.role !== "worker" || agent.source === "catalog") continue
110
+ const bucket = freeSuccess.get(trace.agent_id) ?? []
111
+ bucket.push(trace)
112
+ freeSuccess.set(trace.agent_id, bucket)
113
+ }
114
+ for (const [agentId, evidence] of freeSuccess) {
115
+ if (evidence.length < 5) continue
116
+ const agent = agents.get(agentId)!
117
+ await ensureAgentProposal(proposalsCol, {
118
+ type: "create_agent",
119
+ agentId: `candidate:${agentId}`,
120
+ change: {
121
+ suggested_name: agent.name,
122
+ suggested_description: agent.description,
123
+ observed_tasks: evidence.slice(-5).map((trace) => trace.input_summary),
124
+ },
125
+ evidence: evidence.slice(-5).map((trace) => trace.id),
126
+ confidence: Math.min(0.95, 0.5 + evidence.length * 0.05),
127
+ })
128
+ }
129
+
130
+ // Repeated failures identify a loadout mismatch. This remains a proposal;
131
+ // permissions are never changed automatically.
132
+ const failures = new Map<string, TraceDoc[]>()
133
+ for (const trace of traces) {
134
+ if (trace.success || !trace.catalog_agent_id || !trace.tool_used) continue
135
+ const key = `${trace.catalog_agent_id}\0${trace.tool_used}`
136
+ const bucket = failures.get(key) ?? []
137
+ bucket.push(trace)
138
+ failures.set(key, bucket)
139
+ }
140
+ for (const [key, evidence] of failures) {
141
+ if (evidence.length < 3) continue
142
+ const [agentId, tool] = key.split("\0")
143
+ await ensureAgentProposal(proposalsCol, {
144
+ type: "move_tool",
145
+ agentId,
146
+ change: { tool, reason: "repeated failures; review ownership or remove from loadout" },
147
+ evidence: evidence.slice(-10).map((trace) => trace.id),
148
+ confidence: Math.min(0.95, 0.55 + evidence.length * 0.05),
149
+ })
150
+ }
151
+
152
+ const { syncCatalogAgentsToIndex } = await import("./catalog-selector")
153
+ await syncCatalogAgentsToIndex()
154
+ }
155
+
156
+ async function ensureAgentProposal(
157
+ proposalsCol: Awaited<ReturnType<typeof col<AgentProposalDoc>>>,
158
+ input: {
159
+ type: AgentProposalDoc["type"]
160
+ agentId: string
161
+ change: unknown
162
+ evidence: string[]
163
+ confidence: number
164
+ },
165
+ ): Promise<void> {
166
+ const existing = (await proposalsCol.findBy("agent_id", input.agentId))
167
+ .find((entry) => entry.doc.type === input.type && entry.doc.status === "proposed")
168
+ if (existing) return
169
+ const id = await nextId("agentProposals")
170
+ const now = Date.now()
171
+ await proposalsCol.put(id, {
172
+ id,
173
+ type: input.type,
174
+ agent_id: input.agentId,
175
+ proposed_change_json: JSON.stringify(input.change),
176
+ evidence_trace_ids_json: JSON.stringify(input.evidence),
177
+ confidence: input.confidence,
178
+ status: "proposed",
179
+ created_at: now,
180
+ updated_at: now,
181
+ }, { expectedVersion: 0 })
182
+ }
183
+
184
+ // ─── Process a single reflection ─────────────────────────────────────────────
185
+
186
+ async function processReflection(
187
+ playbookCol: Awaited<ReturnType<typeof col<PlaybookDoc>>>,
188
+ allPlaybook: Array<{ id: string; version: number; doc: PlaybookDoc }>,
189
+ reflection: ReflectionDoc
190
+ ): Promise<void> {
191
+ const category = mapInsightTypeToCategory(reflection.insight_type)
192
+ const applicable: string[] = reflection.affected_tools ? JSON.parse(reflection.affected_tools) : []
193
+ if (reflection.affected_agents) {
194
+ const agentsCol = await col<AgentDoc>("agents")
195
+ for (const agentId of JSON.parse(reflection.affected_agents) as string[]) {
196
+ const agent = await agentsCol.get(agentId)
197
+ if (agent?.doc.source === "catalog") applicable.push(`agent:${agent.doc.id}`)
198
+ }
199
+ }
200
+ const applicableTo = applicable.length ? JSON.stringify([...new Set(applicable)]) : null
201
+
202
+ // Check if a similar rule already exists (fuzzy check by first 60 chars)
203
+ const prefix = reflection.description.substring(0, 60)
204
+ const existing = allPlaybook.find(e => e.doc.active && e.doc.rule.startsWith(prefix))
205
+
206
+ if (existing) {
207
+ // Reinforce existing rule
208
+ await playbookCol.put(existing.id, { ...existing.doc, helpful_count: existing.doc.helpful_count + 1, updated_at: Date.now() }, { expectedVersion: existing.version })
209
+ return
210
+ }
211
+
212
+ // Insert new rule
213
+ const id = await nextId("playbook")
214
+ const now = Date.now()
215
+ await playbookCol.put(id, {
216
+ id,
217
+ rule: reflection.description,
218
+ category,
219
+ applicable_to: applicableTo,
220
+ helpful_count: 1,
221
+ harmful_count: 0,
222
+ active: true,
223
+ source_reflection_id: toIndexable(reflection.id),
224
+ created_at: now,
225
+ updated_at: now,
226
+ }, { expectedVersion: 0 })
227
+ allPlaybook.push({ id, version: 1, doc: { id, rule: reflection.description, category, applicable_to: applicableTo, helpful_count: 1, harmful_count: 0, active: true, source_reflection_id: toIndexable(reflection.id), created_at: now, updated_at: now } })
228
+ }
229
+
230
+ function mapInsightTypeToCategory(
231
+ type: string
232
+ ): "tool_selection" | "response_quality" | "error_avoidance" | "optimization" | "agent_creation" {
233
+ const map: Record<string, any> = {
234
+ success_pattern: "tool_selection",
235
+ failure_pattern: "error_avoidance",
236
+ optimization: "optimization",
237
+ ethics_violation: "error_avoidance",
238
+ // G9 evaluateHarness() insights (reflector.ts's analyzeCausalThreads)
239
+ root_cause: "error_avoidance",
240
+ learning_proposal: "response_quality",
241
+ }
242
+ return map[type] ?? "optimization"
243
+ }
244
+
245
+ async function addOrUpdateRule(
246
+ playbookCol: Awaited<ReturnType<typeof col<PlaybookDoc>>>,
247
+ allPlaybook: Array<{ id: string; version: number; doc: PlaybookDoc }>,
248
+ opts: {
249
+ rule: string
250
+ category: string
251
+ applicable_to: string | null
252
+ sourceReflectionId: string | null
253
+ }
254
+ ): Promise<void> {
255
+ const prefix = opts.rule.substring(0, 60)
256
+ const existing = allPlaybook.find(e => e.doc.rule.startsWith(prefix))
257
+
258
+ if (existing) {
259
+ await playbookCol.put(existing.id, { ...existing.doc, helpful_count: existing.doc.helpful_count + 1, updated_at: Date.now() }, { expectedVersion: existing.version })
260
+ } else {
261
+ const id = await nextId("playbook")
262
+ const now = Date.now()
263
+ await playbookCol.put(id, {
264
+ id,
265
+ rule: opts.rule,
266
+ category: opts.category as PlaybookDoc["category"],
267
+ applicable_to: opts.applicable_to,
268
+ helpful_count: 1,
269
+ harmful_count: 0,
270
+ active: true,
271
+ source_reflection_id: toIndexable(opts.sourceReflectionId),
272
+ created_at: now,
273
+ updated_at: now,
274
+ }, { expectedVersion: 0 })
275
+ }
276
+ }