byterover-cli 0.3.4 → 0.4.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 (346) hide show
  1. package/README.md +119 -63
  2. package/bin/dev.js +8 -1
  3. package/bin/run.js +7 -0
  4. package/dist/commands/cipher-agent/run.d.ts +30 -0
  5. package/dist/commands/cipher-agent/run.js +123 -61
  6. package/dist/commands/cipher-agent/set-prompt.d.ts +2 -0
  7. package/dist/commands/cipher-agent/set-prompt.js +13 -8
  8. package/dist/commands/cipher-agent/show-prompt.d.ts +2 -0
  9. package/dist/commands/cipher-agent/show-prompt.js +17 -12
  10. package/dist/commands/curate.d.ts +3 -60
  11. package/dist/commands/curate.js +45 -341
  12. package/dist/commands/foo.d.ts +4 -2
  13. package/dist/commands/foo.js +21 -16
  14. package/dist/commands/main.d.ts +9 -0
  15. package/dist/commands/main.js +34 -0
  16. package/dist/commands/query.d.ts +2 -48
  17. package/dist/commands/query.js +19 -287
  18. package/dist/commands/status.d.ts +2 -13
  19. package/dist/commands/status.js +12 -91
  20. package/dist/commands/watch.d.ts +2 -0
  21. package/dist/commands/watch.js +23 -19
  22. package/dist/config/environment.d.ts +1 -1
  23. package/dist/config/environment.js +2 -2
  24. package/dist/constants.d.ts +4 -5
  25. package/dist/constants.js +5 -5
  26. package/dist/core/domain/cipher/errors/storage-error.d.ts +89 -0
  27. package/dist/core/domain/cipher/errors/storage-error.js +130 -0
  28. package/dist/core/domain/cipher/queue/types.d.ts +71 -0
  29. package/dist/core/domain/cipher/queue/types.js +9 -0
  30. package/dist/core/domain/cipher/storage/message-storage-types.d.ts +218 -0
  31. package/dist/core/domain/cipher/storage/message-storage-types.js +18 -0
  32. package/dist/core/domain/cipher/tools/constants.d.ts +1 -0
  33. package/dist/core/domain/cipher/tools/constants.js +1 -0
  34. package/dist/core/domain/entities/event.d.ts +1 -1
  35. package/dist/core/domain/entities/event.js +5 -0
  36. package/dist/core/domain/entities/global-config.d.ts +36 -0
  37. package/dist/core/domain/entities/global-config.js +66 -0
  38. package/dist/core/domain/knowledge/directory-manager.d.ts +10 -0
  39. package/dist/core/domain/knowledge/directory-manager.js +18 -0
  40. package/dist/core/domain/knowledge/markdown-writer.d.ts +9 -0
  41. package/dist/core/domain/knowledge/markdown-writer.js +51 -1
  42. package/dist/core/interfaces/cipher/i-agent-storage.d.ts +152 -0
  43. package/dist/core/interfaces/cipher/i-agent-storage.js +1 -0
  44. package/dist/core/interfaces/cipher/i-cipher-agent.d.ts +2 -0
  45. package/dist/core/interfaces/cipher/i-key-storage.d.ts +91 -0
  46. package/dist/core/interfaces/cipher/i-key-storage.js +1 -0
  47. package/dist/core/interfaces/i-global-config-store.d.ts +34 -0
  48. package/dist/core/interfaces/i-global-config-store.js +1 -0
  49. package/dist/core/interfaces/i-onboarding-preference-store.d.ts +20 -0
  50. package/dist/core/interfaces/i-onboarding-preference-store.js +1 -0
  51. package/dist/core/interfaces/i-terminal.d.ts +146 -0
  52. package/dist/core/interfaces/i-terminal.js +1 -0
  53. package/dist/core/interfaces/i-workspace-detector-service.d.ts +8 -0
  54. package/dist/core/interfaces/i-workspace-detector-service.js +1 -0
  55. package/dist/core/interfaces/usecase/i-clear-use-case.d.ts +6 -0
  56. package/dist/core/interfaces/usecase/i-clear-use-case.js +1 -0
  57. package/dist/core/interfaces/usecase/i-curate-use-case.d.ts +10 -0
  58. package/dist/core/interfaces/usecase/i-curate-use-case.js +1 -0
  59. package/dist/core/interfaces/usecase/i-generate-rules-use-case.d.ts +3 -0
  60. package/dist/core/interfaces/usecase/i-generate-rules-use-case.js +1 -0
  61. package/dist/core/interfaces/usecase/i-init-use-case.d.ts +5 -0
  62. package/dist/core/interfaces/usecase/i-init-use-case.js +1 -0
  63. package/dist/core/interfaces/usecase/i-login-use-case.d.ts +3 -0
  64. package/dist/core/interfaces/usecase/i-login-use-case.js +1 -0
  65. package/dist/core/interfaces/usecase/i-logout-use-case.d.ts +5 -0
  66. package/dist/core/interfaces/usecase/i-logout-use-case.js +1 -0
  67. package/dist/core/interfaces/usecase/i-pull-use-case.d.ts +5 -0
  68. package/dist/core/interfaces/usecase/i-pull-use-case.js +1 -0
  69. package/dist/core/interfaces/usecase/i-push-use-case.d.ts +6 -0
  70. package/dist/core/interfaces/usecase/i-push-use-case.js +1 -0
  71. package/dist/core/interfaces/usecase/i-query-use-case.d.ts +9 -0
  72. package/dist/core/interfaces/usecase/i-query-use-case.js +1 -0
  73. package/dist/core/interfaces/usecase/i-space-list-use-case.d.ts +3 -0
  74. package/dist/core/interfaces/usecase/i-space-list-use-case.js +1 -0
  75. package/dist/core/interfaces/usecase/i-space-switch-use-case.d.ts +3 -0
  76. package/dist/core/interfaces/usecase/i-space-switch-use-case.js +1 -0
  77. package/dist/core/interfaces/usecase/i-status-use-case.d.ts +5 -0
  78. package/dist/core/interfaces/usecase/i-status-use-case.js +1 -0
  79. package/dist/hooks/init/update-notifier.js +1 -5
  80. package/dist/hooks/init/welcome.js +1 -2
  81. package/dist/infra/cipher/agent-service-factory.d.ts +13 -6
  82. package/dist/infra/cipher/agent-service-factory.js +40 -16
  83. package/dist/infra/cipher/cipher-agent.js +4 -4
  84. package/dist/infra/cipher/consumer/consumer-lock.d.ts +20 -0
  85. package/dist/infra/cipher/consumer/consumer-lock.js +40 -0
  86. package/dist/infra/cipher/consumer/consumer-service.d.ts +99 -0
  87. package/dist/infra/cipher/consumer/consumer-service.js +165 -0
  88. package/dist/infra/cipher/consumer/execution-consumer.d.ts +121 -0
  89. package/dist/infra/cipher/consumer/execution-consumer.js +523 -0
  90. package/dist/infra/cipher/consumer/index.d.ts +33 -0
  91. package/dist/infra/cipher/consumer/index.js +33 -0
  92. package/dist/infra/cipher/consumer/queue-polling-service.d.ts +120 -0
  93. package/dist/infra/cipher/consumer/queue-polling-service.js +248 -0
  94. package/dist/infra/cipher/http/internal-llm-http-service.d.ts +94 -0
  95. package/dist/infra/cipher/http/internal-llm-http-service.js +118 -0
  96. package/dist/infra/cipher/llm/context/compaction/compaction-service.d.ts +106 -0
  97. package/dist/infra/cipher/llm/context/compaction/compaction-service.js +132 -0
  98. package/dist/infra/cipher/llm/context/compaction/index.d.ts +9 -0
  99. package/dist/infra/cipher/llm/context/compaction/index.js +9 -0
  100. package/dist/infra/cipher/llm/context/context-manager.d.ts +46 -2
  101. package/dist/infra/cipher/llm/context/context-manager.js +68 -4
  102. package/dist/infra/cipher/llm/context/rw-lock.d.ts +72 -0
  103. package/dist/infra/cipher/llm/context/rw-lock.js +145 -0
  104. package/dist/infra/cipher/llm/generators/byterover-content-generator.d.ts +7 -7
  105. package/dist/infra/cipher/llm/generators/byterover-content-generator.js +8 -8
  106. package/dist/infra/cipher/llm/internal-llm-service.js +2 -0
  107. package/dist/infra/cipher/session/session-manager.d.ts +4 -4
  108. package/dist/infra/cipher/session/session-manager.js +5 -5
  109. package/dist/infra/cipher/storage/agent-storage.d.ts +246 -0
  110. package/dist/infra/cipher/storage/agent-storage.js +956 -0
  111. package/dist/infra/cipher/storage/dual-format-history-storage.d.ts +77 -0
  112. package/dist/infra/cipher/storage/dual-format-history-storage.js +149 -0
  113. package/dist/infra/cipher/storage/granular-history-storage.d.ts +65 -0
  114. package/dist/infra/cipher/storage/granular-history-storage.js +118 -0
  115. package/dist/infra/cipher/storage/message-storage-service.d.ts +108 -0
  116. package/dist/infra/cipher/storage/message-storage-service.js +529 -0
  117. package/dist/infra/cipher/storage/process-utils.d.ts +16 -0
  118. package/dist/infra/cipher/storage/process-utils.js +43 -0
  119. package/dist/infra/cipher/storage/sqlite-key-storage.d.ts +105 -0
  120. package/dist/infra/cipher/storage/sqlite-key-storage.js +404 -0
  121. package/dist/infra/cipher/system-prompt/simple-prompt-factory.d.ts +1 -0
  122. package/dist/infra/cipher/system-prompt/simple-prompt-factory.js +7 -0
  123. package/dist/infra/cipher/tools/default-policy-rules.js +1 -1
  124. package/dist/infra/cipher/tools/implementations/curate-tool.d.ts +10 -0
  125. package/dist/infra/cipher/tools/implementations/curate-tool.js +371 -0
  126. package/dist/infra/cipher/tools/implementations/find-knowledge-topics-tool.js +11 -8
  127. package/dist/infra/cipher/tools/tool-manager.d.ts +8 -2
  128. package/dist/infra/cipher/tools/tool-manager.js +29 -2
  129. package/dist/infra/cipher/tools/tool-registry.js +7 -0
  130. package/dist/infra/http/authenticated-http-client.d.ts +21 -0
  131. package/dist/infra/http/authenticated-http-client.js +38 -0
  132. package/dist/infra/repl/commands/arg-parser.d.ts +97 -0
  133. package/dist/infra/repl/commands/arg-parser.js +129 -0
  134. package/dist/infra/repl/commands/clear-command.d.ts +5 -0
  135. package/dist/infra/repl/commands/clear-command.js +61 -0
  136. package/dist/infra/repl/commands/curate-command.d.ts +9 -0
  137. package/dist/infra/repl/commands/curate-command.js +88 -0
  138. package/dist/infra/repl/commands/gen-rules-command.d.ts +7 -0
  139. package/dist/infra/repl/commands/gen-rules-command.js +38 -0
  140. package/dist/infra/repl/commands/index.d.ts +8 -0
  141. package/dist/infra/repl/commands/index.js +36 -0
  142. package/dist/infra/repl/commands/init-command.d.ts +7 -0
  143. package/dist/infra/repl/commands/init-command.js +83 -0
  144. package/dist/infra/repl/commands/login-command.d.ts +7 -0
  145. package/dist/infra/repl/commands/login-command.js +50 -0
  146. package/dist/infra/repl/commands/logout-command.d.ts +5 -0
  147. package/dist/infra/repl/commands/logout-command.js +48 -0
  148. package/dist/infra/repl/commands/pull-command.d.ts +5 -0
  149. package/dist/infra/repl/commands/pull-command.js +61 -0
  150. package/dist/infra/repl/commands/push-command.d.ts +5 -0
  151. package/dist/infra/repl/commands/push-command.js +66 -0
  152. package/dist/infra/repl/commands/query-command.d.ts +5 -0
  153. package/dist/infra/repl/commands/query-command.js +66 -0
  154. package/dist/infra/repl/commands/space/index.d.ts +5 -0
  155. package/dist/infra/repl/commands/space/index.js +14 -0
  156. package/dist/infra/repl/commands/space/list-command.d.ts +5 -0
  157. package/dist/infra/repl/commands/space/list-command.js +70 -0
  158. package/dist/infra/repl/commands/space/switch-command.d.ts +5 -0
  159. package/dist/infra/repl/commands/space/switch-command.js +37 -0
  160. package/dist/infra/repl/commands/status-command.d.ts +5 -0
  161. package/dist/infra/repl/commands/status-command.js +39 -0
  162. package/dist/infra/repl/repl-startup.d.ts +18 -0
  163. package/dist/infra/repl/repl-startup.js +26 -0
  164. package/dist/infra/storage/file-global-config-store.d.ts +22 -0
  165. package/dist/infra/storage/file-global-config-store.js +65 -0
  166. package/dist/infra/storage/file-onboarding-preference-store.d.ts +10 -0
  167. package/dist/infra/storage/file-onboarding-preference-store.js +46 -0
  168. package/dist/infra/terminal/oclif-terminal.d.ts +19 -0
  169. package/dist/infra/terminal/oclif-terminal.js +60 -0
  170. package/dist/infra/terminal/repl-terminal.d.ts +31 -0
  171. package/dist/infra/terminal/repl-terminal.js +116 -0
  172. package/dist/infra/tracking/mixpanel-tracking-service.d.ts +11 -1
  173. package/dist/infra/tracking/mixpanel-tracking-service.js +18 -13
  174. package/dist/infra/usecase/clear-use-case.d.ts +20 -0
  175. package/dist/infra/usecase/clear-use-case.js +58 -0
  176. package/dist/infra/usecase/curate-use-case.d.ts +66 -0
  177. package/dist/infra/usecase/curate-use-case.js +283 -0
  178. package/dist/{commands/gen-rules.d.ts → infra/usecase/generate-rules-use-case.d.ts} +14 -20
  179. package/dist/{commands/gen-rules.js → infra/usecase/generate-rules-use-case.js} +59 -78
  180. package/dist/infra/usecase/init-use-case.d.ts +139 -0
  181. package/dist/{commands/init.js → infra/usecase/init-use-case.js} +184 -230
  182. package/dist/infra/usecase/login-use-case.d.ts +28 -0
  183. package/dist/infra/usecase/login-use-case.js +88 -0
  184. package/dist/infra/usecase/logout-use-case.d.ts +22 -0
  185. package/dist/infra/usecase/logout-use-case.js +51 -0
  186. package/dist/infra/usecase/pull-use-case.d.ts +35 -0
  187. package/dist/infra/usecase/pull-use-case.js +89 -0
  188. package/dist/infra/usecase/push-use-case.d.ts +37 -0
  189. package/dist/infra/usecase/push-use-case.js +124 -0
  190. package/dist/infra/usecase/query-use-case.d.ts +78 -0
  191. package/dist/infra/usecase/query-use-case.js +401 -0
  192. package/dist/infra/usecase/space-list-use-case.d.ts +27 -0
  193. package/dist/infra/usecase/space-list-use-case.js +64 -0
  194. package/dist/infra/usecase/space-switch-use-case.d.ts +36 -0
  195. package/dist/infra/usecase/space-switch-use-case.js +140 -0
  196. package/dist/infra/usecase/status-use-case.d.ts +27 -0
  197. package/dist/infra/usecase/status-use-case.js +97 -0
  198. package/dist/infra/workspace/workspace-detector-service.d.ts +3 -6
  199. package/dist/resources/prompts/curate-context-tree-curation.yml +23 -11
  200. package/dist/resources/prompts/query-context-tree-retrieval.yml +3 -4
  201. package/dist/resources/prompts/system-prompt.yml +1 -1
  202. package/dist/resources/prompts/tool-outputs.yml +4 -3
  203. package/dist/templates/sections/command-reference.md +12 -0
  204. package/dist/templates/sections/workflow.md +10 -1
  205. package/dist/tui/app.d.ts +9 -0
  206. package/dist/tui/app.js +26 -0
  207. package/dist/tui/components/enter-prompt.d.ts +13 -0
  208. package/dist/tui/components/enter-prompt.js +15 -0
  209. package/dist/tui/components/execution/execution-changes.d.ts +14 -0
  210. package/dist/tui/components/execution/execution-changes.js +15 -0
  211. package/dist/tui/components/execution/execution-content.d.ts +25 -0
  212. package/dist/tui/components/execution/execution-content.js +67 -0
  213. package/dist/tui/components/execution/execution-input.d.ts +12 -0
  214. package/dist/tui/components/execution/execution-input.js +16 -0
  215. package/dist/tui/components/execution/execution-progress.d.ts +21 -0
  216. package/dist/tui/components/execution/execution-progress.js +21 -0
  217. package/dist/tui/components/execution/execution-status.d.ts +13 -0
  218. package/dist/tui/components/execution/execution-status.js +19 -0
  219. package/dist/tui/components/execution/index.d.ts +11 -0
  220. package/dist/tui/components/execution/index.js +11 -0
  221. package/dist/tui/components/execution/log-item.d.ts +17 -0
  222. package/dist/tui/components/execution/log-item.js +25 -0
  223. package/dist/tui/components/footer.d.ts +5 -0
  224. package/dist/tui/components/footer.js +12 -0
  225. package/dist/tui/components/header.d.ts +18 -0
  226. package/dist/tui/components/header.js +18 -0
  227. package/dist/tui/components/index.d.ts +17 -0
  228. package/dist/tui/components/index.js +14 -0
  229. package/dist/tui/components/inline-prompts/index.d.ts +15 -0
  230. package/dist/tui/components/inline-prompts/index.js +10 -0
  231. package/dist/tui/components/inline-prompts/inline-confirm.d.ts +17 -0
  232. package/dist/tui/components/inline-prompts/inline-confirm.js +32 -0
  233. package/dist/tui/components/inline-prompts/inline-file-selector.d.ts +43 -0
  234. package/dist/tui/components/inline-prompts/inline-file-selector.js +185 -0
  235. package/dist/tui/components/inline-prompts/inline-input.d.ts +19 -0
  236. package/dist/tui/components/inline-prompts/inline-input.js +32 -0
  237. package/dist/tui/components/inline-prompts/inline-search.d.ts +20 -0
  238. package/dist/tui/components/inline-prompts/inline-search.js +50 -0
  239. package/dist/tui/components/inline-prompts/inline-select.d.ts +20 -0
  240. package/dist/tui/components/inline-prompts/inline-select.js +34 -0
  241. package/dist/tui/components/logo.d.ts +43 -0
  242. package/dist/tui/components/logo.js +103 -0
  243. package/dist/tui/components/message-item.d.ts +12 -0
  244. package/dist/tui/components/message-item.js +12 -0
  245. package/dist/tui/components/onboarding/copyable-prompt.d.ts +15 -0
  246. package/dist/tui/components/onboarding/copyable-prompt.js +65 -0
  247. package/dist/tui/components/onboarding/index.d.ts +7 -0
  248. package/dist/tui/components/onboarding/index.js +6 -0
  249. package/dist/tui/components/onboarding/onboarding-flow.d.ts +13 -0
  250. package/dist/tui/components/onboarding/onboarding-flow.js +304 -0
  251. package/dist/tui/components/onboarding/onboarding-step.d.ts +23 -0
  252. package/dist/tui/components/onboarding/onboarding-step.js +12 -0
  253. package/dist/tui/components/output-log.d.ts +14 -0
  254. package/dist/tui/components/output-log.js +13 -0
  255. package/dist/tui/components/scrollable-list.d.ts +30 -0
  256. package/dist/tui/components/scrollable-list.js +121 -0
  257. package/dist/tui/components/suggestions.d.ts +16 -0
  258. package/dist/tui/components/suggestions.js +162 -0
  259. package/dist/tui/components/tab-bar.d.ts +10 -0
  260. package/dist/tui/components/tab-bar.js +12 -0
  261. package/dist/tui/constants.d.ts +11 -0
  262. package/dist/tui/constants.js +13 -0
  263. package/dist/tui/contexts/auth-context.d.ts +30 -0
  264. package/dist/tui/contexts/auth-context.js +153 -0
  265. package/dist/tui/contexts/consumer.d.ts +31 -0
  266. package/dist/tui/contexts/consumer.js +56 -0
  267. package/dist/tui/contexts/index.d.ts +6 -0
  268. package/dist/tui/contexts/index.js +6 -0
  269. package/dist/tui/contexts/onboarding-context.d.ts +43 -0
  270. package/dist/tui/contexts/onboarding-context.js +181 -0
  271. package/dist/tui/contexts/services-context.d.ts +29 -0
  272. package/dist/tui/contexts/services-context.js +20 -0
  273. package/dist/tui/contexts/use-commands.d.ts +29 -0
  274. package/dist/tui/contexts/use-commands.js +53 -0
  275. package/dist/tui/contexts/use-mode.d.ts +43 -0
  276. package/dist/tui/contexts/use-mode.js +76 -0
  277. package/dist/tui/contexts/use-theme.d.ts +53 -0
  278. package/dist/tui/contexts/use-theme.js +60 -0
  279. package/dist/tui/hooks/index.d.ts +17 -0
  280. package/dist/tui/hooks/index.js +14 -0
  281. package/dist/tui/hooks/use-activity-logs.d.ts +26 -0
  282. package/dist/tui/hooks/use-activity-logs.js +90 -0
  283. package/dist/tui/hooks/use-consumer.d.ts +12 -0
  284. package/dist/tui/hooks/use-consumer.js +50 -0
  285. package/dist/tui/hooks/use-onboarding.d.ts +7 -0
  286. package/dist/tui/hooks/use-onboarding.js +6 -0
  287. package/dist/tui/hooks/use-queue-polling.d.ts +31 -0
  288. package/dist/tui/hooks/use-queue-polling.js +90 -0
  289. package/dist/tui/hooks/use-slash-command-processor.d.ts +16 -0
  290. package/dist/tui/hooks/use-slash-command-processor.js +132 -0
  291. package/dist/tui/hooks/use-slash-completion.d.ts +30 -0
  292. package/dist/tui/hooks/use-slash-completion.js +230 -0
  293. package/dist/tui/hooks/use-tab-navigation.d.ts +10 -0
  294. package/dist/tui/hooks/use-tab-navigation.js +35 -0
  295. package/dist/tui/hooks/use-visible-window.d.ts +22 -0
  296. package/dist/tui/hooks/use-visible-window.js +37 -0
  297. package/dist/tui/index.d.ts +1 -0
  298. package/dist/tui/index.js +1 -0
  299. package/dist/tui/providers/app-providers.d.ts +25 -0
  300. package/dist/tui/providers/app-providers.js +9 -0
  301. package/dist/tui/types/commands.d.ts +252 -0
  302. package/dist/tui/types/commands.js +16 -0
  303. package/dist/tui/types/dialogs.d.ts +37 -0
  304. package/dist/tui/types/dialogs.js +4 -0
  305. package/dist/tui/types/index.d.ts +11 -0
  306. package/dist/tui/types/index.js +7 -0
  307. package/dist/tui/types/messages.d.ts +55 -0
  308. package/dist/tui/types/messages.js +4 -0
  309. package/dist/tui/types/prompts.d.ts +100 -0
  310. package/dist/tui/types/prompts.js +4 -0
  311. package/dist/tui/types/ui.d.ts +14 -0
  312. package/dist/tui/types/ui.js +4 -0
  313. package/dist/tui/types.d.ts +1 -0
  314. package/dist/tui/types.js +1 -0
  315. package/dist/tui/views/command-view.d.ts +12 -0
  316. package/dist/tui/views/command-view.js +451 -0
  317. package/dist/tui/views/index.d.ts +6 -0
  318. package/dist/tui/views/index.js +6 -0
  319. package/dist/tui/views/login-view.d.ts +10 -0
  320. package/dist/tui/views/login-view.js +30 -0
  321. package/dist/tui/views/logs-view.d.ts +11 -0
  322. package/dist/tui/views/logs-view.js +73 -0
  323. package/dist/utils/file-validator.d.ts +16 -0
  324. package/dist/utils/file-validator.js +81 -0
  325. package/dist/utils/global-config-path.d.ts +15 -0
  326. package/dist/utils/global-config-path.js +38 -0
  327. package/oclif.manifest.json +29 -315
  328. package/package.json +11 -4
  329. package/dist/commands/clear.d.ts +0 -19
  330. package/dist/commands/clear.js +0 -78
  331. package/dist/commands/init.d.ts +0 -130
  332. package/dist/commands/login.d.ts +0 -22
  333. package/dist/commands/login.js +0 -108
  334. package/dist/commands/logout.d.ts +0 -16
  335. package/dist/commands/logout.js +0 -61
  336. package/dist/commands/pull.d.ts +0 -33
  337. package/dist/commands/pull.js +0 -115
  338. package/dist/commands/push.d.ts +0 -35
  339. package/dist/commands/push.js +0 -160
  340. package/dist/commands/space/list.d.ts +0 -25
  341. package/dist/commands/space/list.js +0 -114
  342. package/dist/commands/space/switch.d.ts +0 -36
  343. package/dist/commands/space/switch.js +0 -160
  344. package/dist/infra/cipher/grpc/internal-llm-grpc-service.d.ts +0 -149
  345. package/dist/infra/cipher/grpc/internal-llm-grpc-service.js +0 -364
  346. package/dist/infra/cipher/grpc/internal-llm-grpc.proto +0 -94
@@ -0,0 +1,956 @@
1
+ import Database from 'better-sqlite3';
2
+ import { randomUUID } from 'node:crypto';
3
+ import * as fsSync from 'node:fs';
4
+ import * as fs from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+ import { BLOBS_DIR, BRV_DIR } from '../../../constants.js';
7
+ import { isProcessRunning } from './process-utils.js';
8
+ // Query execution timeout (5 minutes of no activity)
9
+ const QUERY_ACTIVITY_TIMEOUT_MS = 5 * 60 * 1000;
10
+ // ==================== SQL SCHEMA ====================
11
+ const SCHEMA_SQL = `
12
+ -- Consumer Locks: track active consumers for orphan detection
13
+ CREATE TABLE IF NOT EXISTS consumer_locks (
14
+ id TEXT PRIMARY KEY,
15
+ pid INTEGER NOT NULL,
16
+ started_at INTEGER NOT NULL,
17
+ last_heartbeat INTEGER NOT NULL
18
+ );
19
+
20
+ -- Executions: job queue (status='queued') + execution tracking
21
+ CREATE TABLE IF NOT EXISTS executions (
22
+ id TEXT PRIMARY KEY,
23
+ type TEXT NOT NULL,
24
+ input TEXT NOT NULL,
25
+ status TEXT NOT NULL,
26
+ consumer_id TEXT,
27
+ pid INTEGER,
28
+ result TEXT,
29
+ error TEXT,
30
+ created_at INTEGER NOT NULL,
31
+ started_at INTEGER,
32
+ completed_at INTEGER,
33
+ updated_at INTEGER NOT NULL
34
+ );
35
+
36
+ -- Tool Calls: tool call details for UI polling
37
+ CREATE TABLE IF NOT EXISTS tool_calls (
38
+ id TEXT PRIMARY KEY,
39
+ execution_id TEXT NOT NULL,
40
+ name TEXT NOT NULL,
41
+ description TEXT,
42
+ args TEXT,
43
+ status TEXT NOT NULL,
44
+ result TEXT,
45
+ result_summary TEXT,
46
+ started_at INTEGER NOT NULL,
47
+ completed_at INTEGER,
48
+ -- Metrics for FE display (raw int values)
49
+ duration_ms INTEGER,
50
+ lines_count INTEGER,
51
+ chars_count INTEGER,
52
+ FOREIGN KEY (execution_id) REFERENCES executions(id) ON DELETE CASCADE
53
+ );
54
+
55
+ -- Indexes for performance
56
+ CREATE INDEX IF NOT EXISTS idx_executions_status ON executions(status);
57
+ CREATE INDEX IF NOT EXISTS idx_executions_type ON executions(type);
58
+ CREATE INDEX IF NOT EXISTS idx_executions_updated ON executions(updated_at);
59
+ CREATE INDEX IF NOT EXISTS idx_executions_consumer_id ON executions(consumer_id);
60
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_execution ON tool_calls(execution_id);
61
+ `;
62
+ // Migration SQL for existing databases (add new columns if missing)
63
+ const MIGRATION_SQL = `
64
+ -- Add new columns to tool_calls if they don't exist
65
+ ALTER TABLE tool_calls ADD COLUMN duration_ms INTEGER;
66
+ ALTER TABLE tool_calls ADD COLUMN lines_count INTEGER;
67
+ ALTER TABLE tool_calls ADD COLUMN chars_count INTEGER;
68
+ -- Add consumer_id to executions for orphan tracking
69
+ ALTER TABLE executions ADD COLUMN consumer_id TEXT;
70
+ -- Add pid to executions for query orphan detection
71
+ ALTER TABLE executions ADD COLUMN pid INTEGER;
72
+ -- Add index for consumer_id lookups (orphan detection queries)
73
+ CREATE INDEX IF NOT EXISTS idx_executions_consumer_id ON executions(consumer_id);
74
+ `;
75
+ // ==================== CLASS ====================
76
+ /**
77
+ * AgentStorage - SQLite-based storage for execution queue and tool calls
78
+ *
79
+ * Features:
80
+ * - Single database file at .brv/blobs/agent.db
81
+ * - Job queue via executions.status = 'queued'
82
+ * - Tool call tracking for UI polling
83
+ * - Prepared statement caching (no memory leak)
84
+ * - WAL mode for concurrent read/write
85
+ * - Orphan cleanup on startup
86
+ * - Old execution cleanup (max 100)
87
+ */
88
+ export class AgentStorage {
89
+ initialized = false;
90
+ db = null;
91
+ // Track DB file inode to detect if file was replaced
92
+ dbFileInode = null;
93
+ dbPath;
94
+ inMemory;
95
+ stmtAddToolCall = null;
96
+ stmtCleanupOrphans = null;
97
+ stmtCountCompletedFailed = null;
98
+ // Cached prepared statements (lazy init, reuse to avoid memory leak)
99
+ stmtCreateExecution = null;
100
+ stmtDeleteConsumerLock = null;
101
+ stmtDeleteOldExecutions = null;
102
+ stmtDequeueBatchSelect = null;
103
+ stmtDequeueSelect = null;
104
+ stmtFailQuery = null;
105
+ stmtGetExecution = null;
106
+ stmtGetExecutionsSince = null;
107
+ stmtGetQueuedExecutions = null;
108
+ stmtGetRecentExecutions = null;
109
+ stmtGetRunningExecutions = null;
110
+ stmtGetToolCalls = null;
111
+ stmtOrphanFromConsumer = null;
112
+ stmtOrphanMissingConsumer = null;
113
+ stmtOrphanNullConsumer = null;
114
+ stmtUpdateStatus = null;
115
+ stmtUpdateToolCall = null;
116
+ storageDir;
117
+ constructor(config) {
118
+ this.inMemory = config?.inMemory ?? false;
119
+ this.storageDir = config?.storageDir || join(process.cwd(), BRV_DIR, BLOBS_DIR);
120
+ this.dbPath = this.inMemory ? ':memory:' : join(this.storageDir, 'agent.db');
121
+ }
122
+ // ==================== LIFECYCLE ====================
123
+ /**
124
+ * Acquire consumer lock (register this consumer)
125
+ * Only ONE consumer can run at a time - checks for any active consumer first
126
+ * @returns true if lock acquired, false if another consumer is already running
127
+ */
128
+ acquireConsumerLock(consumerId) {
129
+ this.ensureInitialized();
130
+ const now = Date.now();
131
+ const { pid } = process;
132
+ // Use transaction to ensure atomic check-then-insert
133
+ const acquireLock = this.getDb().transaction(() => {
134
+ // Check if any active consumer exists (with recent heartbeat)
135
+ const cutoff = now - 30_000; // 30 second timeout
136
+ const existing = this.getDb().prepare(`SELECT id FROM consumer_locks WHERE last_heartbeat >= ?`).get(cutoff);
137
+ if (existing) {
138
+ // Another consumer is active
139
+ return false;
140
+ }
141
+ // No active consumer - acquire lock
142
+ this.getDb()
143
+ .prepare(`INSERT INTO consumer_locks (id, pid, started_at, last_heartbeat)
144
+ VALUES (?, ?, ?, ?)`)
145
+ .run(consumerId, pid, now, now);
146
+ return true;
147
+ });
148
+ return acquireLock();
149
+ }
150
+ /**
151
+ * Add a tool call record
152
+ * @returns tool call id
153
+ */
154
+ addToolCall(executionId, info) {
155
+ this.ensureInitialized();
156
+ const id = randomUUID();
157
+ const now = Date.now();
158
+ if (!this.stmtAddToolCall) {
159
+ this.stmtAddToolCall = this.getDb().prepare(`
160
+ INSERT INTO tool_calls (id, execution_id, name, description, args, status, started_at)
161
+ VALUES (?, ?, ?, ?, ?, 'running', ?)
162
+ `);
163
+ }
164
+ this.stmtAddToolCall.run(id, executionId, info.name, info.description ?? null, JSON.stringify(info.args), now);
165
+ return id;
166
+ }
167
+ /**
168
+ * Cleanup old executions, keep only maxKeep most recent completed/failed
169
+ */
170
+ cleanupOldExecutions(maxKeep = 100) {
171
+ this.ensureInitialized();
172
+ // Count completed/failed executions
173
+ if (!this.stmtCountCompletedFailed) {
174
+ this.stmtCountCompletedFailed = this.getDb().prepare(`
175
+ SELECT COUNT(*) as count FROM executions
176
+ WHERE status IN ('completed', 'failed')
177
+ `);
178
+ }
179
+ const countResult = this.stmtCountCompletedFailed.get();
180
+ const toDelete = countResult.count - maxKeep;
181
+ if (toDelete <= 0) {
182
+ return 0;
183
+ }
184
+ // Delete oldest executions (tool_calls auto-deleted via CASCADE)
185
+ if (!this.stmtDeleteOldExecutions) {
186
+ this.stmtDeleteOldExecutions = this.getDb().prepare(`
187
+ DELETE FROM executions
188
+ WHERE id IN (
189
+ SELECT id FROM executions
190
+ WHERE status IN ('completed', 'failed')
191
+ ORDER BY created_at ASC
192
+ LIMIT ?
193
+ )
194
+ `);
195
+ }
196
+ const result = this.stmtDeleteOldExecutions.run(toDelete);
197
+ return result.changes;
198
+ }
199
+ /**
200
+ * Cleanup orphaned executions (status='running') from previous session crash
201
+ * Should be called on startup
202
+ */
203
+ cleanupOrphanedExecutions() {
204
+ this.ensureInitialized();
205
+ if (!this.stmtCleanupOrphans) {
206
+ this.stmtCleanupOrphans = this.getDb().prepare(`
207
+ UPDATE executions
208
+ SET status = 'failed',
209
+ error = 'Orphaned from previous session',
210
+ updated_at = ?
211
+ WHERE status = 'running'
212
+ `);
213
+ }
214
+ const now = Date.now();
215
+ const result = this.stmtCleanupOrphans.run(now);
216
+ return result.changes;
217
+ }
218
+ /**
219
+ * Cleanup stale consumers and orphan their executions
220
+ * A consumer is stale if its heartbeat is older than timeoutMs
221
+ * @param timeoutMs - heartbeat timeout (default 30 seconds)
222
+ * @returns number of orphaned executions
223
+ */
224
+ cleanupStaleConsumers(timeoutMs = 30_000) {
225
+ this.ensureInitialized();
226
+ const now = Date.now();
227
+ const cutoff = now - timeoutMs;
228
+ let totalOrphaned = 0;
229
+ // Case 1: Find consumers with stale heartbeat
230
+ const staleConsumers = this.getDb()
231
+ .prepare(`
232
+ SELECT id FROM consumer_locks WHERE last_heartbeat < ?
233
+ `)
234
+ .all(cutoff);
235
+ if (staleConsumers.length > 0) {
236
+ // Orphan executions from stale consumers (cached statement)
237
+ if (!this.stmtOrphanFromConsumer) {
238
+ this.stmtOrphanFromConsumer = this.getDb().prepare(`
239
+ UPDATE executions
240
+ SET status = 'failed',
241
+ error = 'Consumer died unexpectedly',
242
+ consumer_id = NULL,
243
+ updated_at = ?
244
+ WHERE consumer_id = ? AND status = 'running'
245
+ `);
246
+ }
247
+ // Delete stale consumer locks (cached statement)
248
+ if (!this.stmtDeleteConsumerLock) {
249
+ this.stmtDeleteConsumerLock = this.getDb().prepare(`
250
+ DELETE FROM consumer_locks WHERE id = ?
251
+ `);
252
+ }
253
+ for (const consumer of staleConsumers) {
254
+ const result = this.stmtOrphanFromConsumer.run(now, consumer.id);
255
+ totalOrphaned += result.changes;
256
+ this.stmtDeleteConsumerLock.run(consumer.id);
257
+ }
258
+ }
259
+ // Case 2: Find "running" executions whose consumer_id doesn't exist in consumer_locks
260
+ // This handles cases where consumer crashed without proper cleanup
261
+ if (!this.stmtOrphanMissingConsumer) {
262
+ this.stmtOrphanMissingConsumer = this.getDb().prepare(`
263
+ UPDATE executions
264
+ SET status = 'failed',
265
+ error = 'Consumer no longer exists',
266
+ completed_at = ?,
267
+ updated_at = ?
268
+ WHERE status = 'running'
269
+ AND consumer_id IS NOT NULL
270
+ AND consumer_id NOT IN (SELECT id FROM consumer_locks)
271
+ `);
272
+ }
273
+ const orphanedFromMissingConsumers = this.stmtOrphanMissingConsumer.run(now, now);
274
+ totalOrphaned += orphanedFromMissingConsumers.changes;
275
+ // Case 3: Find "running" CURATE executions with NULL consumer_id (orphaned from releaseConsumerLock)
276
+ // These are stuck executions where consumer stopped but didn't complete the job
277
+ // NOTE: Query runs inline (not via consumer), so consumer_id=NULL is normal for query
278
+ if (!this.stmtOrphanNullConsumer) {
279
+ this.stmtOrphanNullConsumer = this.getDb().prepare(`
280
+ UPDATE executions
281
+ SET status = 'failed',
282
+ error = 'Execution orphaned (no consumer)',
283
+ completed_at = ?,
284
+ updated_at = ?
285
+ WHERE status = 'running'
286
+ AND consumer_id IS NULL
287
+ AND type = 'curate'
288
+ `);
289
+ }
290
+ const orphanedNullConsumer = this.stmtOrphanNullConsumer.run(now, now);
291
+ totalOrphaned += orphanedNullConsumer.changes;
292
+ // Case 4: Stuck query executions
293
+ // Two-layer detection:
294
+ // 1. PID dead → immediately fail
295
+ // 2. PID alive but no activity for 2 min → fail (process hung)
296
+ const runningQueries = this.getDb()
297
+ .prepare(`
298
+ SELECT
299
+ e.id,
300
+ e.pid,
301
+ e.started_at,
302
+ COALESCE(
303
+ MAX(tc.completed_at),
304
+ MAX(tc.started_at),
305
+ e.started_at
306
+ ) as last_activity
307
+ FROM executions e
308
+ LEFT JOIN tool_calls tc ON tc.execution_id = e.id
309
+ WHERE e.status = 'running'
310
+ AND e.consumer_id IS NULL
311
+ AND e.type = 'query'
312
+ GROUP BY e.id
313
+ `)
314
+ .all();
315
+ const activityCutoff = now - QUERY_ACTIVITY_TIMEOUT_MS;
316
+ // Cached statement for failing stuck queries
317
+ if (!this.stmtFailQuery) {
318
+ this.stmtFailQuery = this.getDb().prepare(`
319
+ UPDATE executions
320
+ SET status = 'failed',
321
+ error = ?,
322
+ completed_at = ?,
323
+ updated_at = ?
324
+ WHERE id = ?
325
+ `);
326
+ }
327
+ for (const query of runningQueries) {
328
+ let shouldFail = false;
329
+ let errorMessage = '';
330
+ // Check 1: PID-based detection (instant)
331
+ if (query.pid) {
332
+ const processAlive = isProcessRunning(query.pid);
333
+ if (processAlive === false) {
334
+ // Process is definitely dead
335
+ shouldFail = true;
336
+ errorMessage = `Query process died (PID ${query.pid} no longer exists)`;
337
+ }
338
+ }
339
+ // Check 2: Activity-based detection (fallback)
340
+ if (!shouldFail && query.last_activity < activityCutoff) {
341
+ shouldFail = true;
342
+ errorMessage = 'Query execution timed out (no activity for 2 minutes)';
343
+ }
344
+ if (shouldFail) {
345
+ this.stmtFailQuery.run(errorMessage, now, now, query.id);
346
+ totalOrphaned++;
347
+ }
348
+ }
349
+ return totalOrphaned;
350
+ }
351
+ /**
352
+ * Close database connection
353
+ */
354
+ close() {
355
+ if (this.db) {
356
+ this.db.close();
357
+ this.db = null;
358
+ this.initialized = false;
359
+ // Clear cached statements
360
+ this.stmtCreateExecution = null;
361
+ this.stmtDequeueBatchSelect = null;
362
+ this.stmtDequeueSelect = null;
363
+ this.stmtUpdateStatus = null;
364
+ this.stmtGetExecution = null;
365
+ this.stmtGetQueuedExecutions = null;
366
+ this.stmtGetRunningExecutions = null;
367
+ this.stmtGetRecentExecutions = null;
368
+ this.stmtAddToolCall = null;
369
+ this.stmtUpdateToolCall = null;
370
+ this.stmtGetToolCalls = null;
371
+ this.stmtCleanupOrphans = null;
372
+ this.stmtCountCompletedFailed = null;
373
+ this.stmtDeleteOldExecutions = null;
374
+ this.stmtGetExecutionsSince = null;
375
+ this.stmtOrphanFromConsumer = null;
376
+ this.stmtOrphanMissingConsumer = null;
377
+ this.stmtOrphanNullConsumer = null;
378
+ this.stmtDeleteConsumerLock = null;
379
+ this.stmtFailQuery = null;
380
+ }
381
+ }
382
+ /**
383
+ * Create a new execution
384
+ * @param type - 'curate' or 'query'
385
+ * @param input - content (curate) or query string (query)
386
+ * @returns execution id
387
+ */
388
+ createExecution(type, input) {
389
+ this.ensureInitialized();
390
+ const id = randomUUID();
391
+ const now = Date.now();
392
+ // curate starts as 'queued', query starts as 'running'
393
+ const status = type === 'curate' ? 'queued' : 'running';
394
+ const startedAt = type === 'query' ? now : null;
395
+ const { pid } = process; // Capture current process ID for orphan detection
396
+ if (!this.stmtCreateExecution) {
397
+ this.stmtCreateExecution = this.getDb().prepare(`
398
+ INSERT INTO executions (id, type, input, status, pid, created_at, started_at, updated_at)
399
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
400
+ `);
401
+ }
402
+ this.stmtCreateExecution.run(id, type, input, status, pid, now, startedAt, now);
403
+ return id;
404
+ }
405
+ /**
406
+ * Dequeue multiple executions at once (atomic batch SELECT + UPDATE)
407
+ * This is more efficient than calling dequeueExecution() multiple times
408
+ * and ensures all queued items are seen in a single transaction snapshot
409
+ * @param limit - max number of executions to dequeue
410
+ * @param consumerId - ID of the consumer claiming these executions
411
+ * @returns array of executions (may be empty if queue is empty)
412
+ */
413
+ dequeueBatch(limit, consumerId) {
414
+ this.ensureInitialized();
415
+ if (limit <= 0)
416
+ return [];
417
+ if (!this.stmtDequeueBatchSelect) {
418
+ // Use a parameterized LIMIT - better-sqlite3 handles this correctly
419
+ this.stmtDequeueBatchSelect = this.getDb().prepare(`
420
+ SELECT * FROM executions
421
+ WHERE status = 'queued'
422
+ ORDER BY created_at ASC
423
+ LIMIT ?
424
+ `);
425
+ }
426
+ // Capture for type safety inside transaction closure
427
+ const selectStmt = this.stmtDequeueBatchSelect;
428
+ // Use dynamic statement to handle optional consumer_id
429
+ const updateSql = consumerId
430
+ ? `UPDATE executions SET status = 'running', consumer_id = ?, started_at = ?, updated_at = ? WHERE id = ?`
431
+ : `UPDATE executions SET status = 'running', started_at = ?, updated_at = ? WHERE id = ?`;
432
+ // Use transaction for atomicity - all SELECTs and UPDATEs in one snapshot
433
+ const dequeueBatch = this.getDb().transaction((batchLimit) => {
434
+ const rows = selectStmt.all(batchLimit);
435
+ if (rows.length === 0) {
436
+ return [];
437
+ }
438
+ const now = Date.now();
439
+ const executions = [];
440
+ const updateStmt = this.getDb().prepare(updateSql);
441
+ for (const row of rows) {
442
+ if (consumerId) {
443
+ updateStmt.run(consumerId, now, now, row.id);
444
+ }
445
+ else {
446
+ updateStmt.run(now, now, row.id);
447
+ }
448
+ // eslint-disable-next-line camelcase
449
+ executions.push(this.rowToExecution({ ...row, started_at: now, status: 'running', updated_at: now }));
450
+ }
451
+ return executions;
452
+ });
453
+ return dequeueBatch(limit);
454
+ }
455
+ /**
456
+ * Dequeue next queued execution (atomic SELECT + UPDATE)
457
+ * @param consumerId - ID of the consumer claiming this execution
458
+ * @returns execution or null if queue is empty
459
+ */
460
+ dequeueExecution(consumerId) {
461
+ this.ensureInitialized();
462
+ if (!this.stmtDequeueSelect) {
463
+ this.stmtDequeueSelect = this.getDb().prepare(`
464
+ SELECT * FROM executions
465
+ WHERE status = 'queued'
466
+ ORDER BY created_at ASC
467
+ LIMIT 1
468
+ `);
469
+ }
470
+ // Capture for type safety inside transaction closure
471
+ const selectStmt = this.stmtDequeueSelect;
472
+ // Use dynamic statement to handle optional consumer_id
473
+ const updateSql = consumerId
474
+ ? `UPDATE executions SET status = 'running', consumer_id = ?, started_at = ?, updated_at = ? WHERE id = ?`
475
+ : `UPDATE executions SET status = 'running', started_at = ?, updated_at = ? WHERE id = ?`;
476
+ // Use transaction for atomicity
477
+ const dequeue = this.getDb().transaction(() => {
478
+ const row = selectStmt.get();
479
+ if (!row) {
480
+ return null;
481
+ }
482
+ const now = Date.now();
483
+ const updateStmt = this.getDb().prepare(updateSql);
484
+ if (consumerId) {
485
+ updateStmt.run(consumerId, now, now, row.id);
486
+ }
487
+ else {
488
+ updateStmt.run(now, now, row.id);
489
+ }
490
+ // eslint-disable-next-line camelcase
491
+ return this.rowToExecution({ ...row, started_at: now, status: 'running', updated_at: now });
492
+ });
493
+ return dequeue();
494
+ }
495
+ // ==================== EXECUTION METHODS ====================
496
+ /**
497
+ * Get execution by id
498
+ */
499
+ getExecution(id) {
500
+ this.ensureInitialized();
501
+ if (!this.stmtGetExecution) {
502
+ this.stmtGetExecution = this.getDb().prepare(`
503
+ SELECT * FROM executions WHERE id = ?
504
+ `);
505
+ }
506
+ const row = this.stmtGetExecution.get(id);
507
+ return row ? this.rowToExecution(row) : null;
508
+ }
509
+ /**
510
+ * Get executions updated since timestamp (for incremental polling)
511
+ */
512
+ getExecutionsSince(timestamp) {
513
+ this.ensureInitialized();
514
+ if (!this.stmtGetExecutionsSince) {
515
+ this.stmtGetExecutionsSince = this.getDb().prepare(`
516
+ SELECT * FROM executions
517
+ WHERE updated_at > ?
518
+ ORDER BY updated_at ASC
519
+ LIMIT 100
520
+ `);
521
+ }
522
+ const rows = this.stmtGetExecutionsSince.all(timestamp);
523
+ return rows.map((row) => this.rowToExecution(row));
524
+ }
525
+ /**
526
+ * Get execution with all its tool calls (for UI display)
527
+ */
528
+ getExecutionWithToolCalls(id) {
529
+ const execution = this.getExecution(id);
530
+ if (!execution) {
531
+ return null;
532
+ }
533
+ const toolCalls = this.getToolCalls(id);
534
+ return { execution, toolCalls };
535
+ }
536
+ /**
537
+ * Get all queued executions
538
+ */
539
+ getQueuedExecutions() {
540
+ this.ensureInitialized();
541
+ if (!this.stmtGetQueuedExecutions) {
542
+ this.stmtGetQueuedExecutions = this.getDb().prepare(`
543
+ SELECT * FROM executions
544
+ WHERE status = 'queued'
545
+ ORDER BY created_at ASC
546
+ LIMIT 100
547
+ `);
548
+ }
549
+ const rows = this.stmtGetQueuedExecutions.all();
550
+ return rows.map((row) => this.rowToExecution(row));
551
+ }
552
+ /**
553
+ * Get recent executions (for UI display)
554
+ */
555
+ getRecentExecutions(limit = 20) {
556
+ this.ensureInitialized();
557
+ if (!this.stmtGetRecentExecutions) {
558
+ this.stmtGetRecentExecutions = this.getDb().prepare(`
559
+ SELECT * FROM executions
560
+ ORDER BY created_at DESC
561
+ LIMIT ?
562
+ `);
563
+ }
564
+ const rows = this.stmtGetRecentExecutions.all(limit);
565
+ return rows.map((row) => this.rowToExecution(row));
566
+ }
567
+ /**
568
+ * Get all running executions
569
+ */
570
+ getRunningExecutions() {
571
+ this.ensureInitialized();
572
+ if (!this.stmtGetRunningExecutions) {
573
+ this.stmtGetRunningExecutions = this.getDb().prepare(`
574
+ SELECT * FROM executions
575
+ WHERE status = 'running'
576
+ ORDER BY started_at ASC
577
+ LIMIT 100
578
+ `);
579
+ }
580
+ const rows = this.stmtGetRunningExecutions.all();
581
+ return rows.map((row) => this.rowToExecution(row));
582
+ }
583
+ /**
584
+ * Get all executions belonging to a specific consumer session.
585
+ * Returns executions ordered by created_at ASC (oldest first, newest last).
586
+ * Includes:
587
+ * - Curate executions with matching consumer_id
588
+ * - Query executions created after consumer started (no consumer_id)
589
+ */
590
+ getSessionExecutions(consumerId) {
591
+ this.ensureInitialized();
592
+ // Get the consumer's start time from locks table
593
+ const lockRow = this.getDb().prepare(`SELECT started_at FROM consumer_locks WHERE id = ?`).get(consumerId);
594
+ if (!lockRow) {
595
+ // Consumer not found, return empty
596
+ return [];
597
+ }
598
+ // Get all executions belonging to this session:
599
+ // 1. Curate executions with this consumer_id
600
+ // 2. Query executions created after consumer started (have NULL consumer_id)
601
+ const rows = this.getDb()
602
+ .prepare(`
603
+ SELECT * FROM executions
604
+ WHERE consumer_id = ? OR (consumer_id IS NULL AND created_at >= ?)
605
+ ORDER BY created_at ASC
606
+ `)
607
+ .all(consumerId, lockRow.started_at);
608
+ return rows.map((row) => this.rowToExecution(row));
609
+ }
610
+ /**
611
+ * Get queue statistics (queries DB directly for accurate counts)
612
+ */
613
+ getStats() {
614
+ this.ensureInitialized();
615
+ const result = this.getDb()
616
+ .prepare(`
617
+ SELECT
618
+ COUNT(*) as total,
619
+ SUM(CASE WHEN status = 'queued' THEN 1 ELSE 0 END) as queued,
620
+ SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) as running,
621
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
622
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed
623
+ FROM executions
624
+ `)
625
+ .get();
626
+ return result;
627
+ }
628
+ /**
629
+ * Get all tool calls for an execution
630
+ */
631
+ getToolCalls(executionId) {
632
+ this.ensureInitialized();
633
+ if (!this.stmtGetToolCalls) {
634
+ this.stmtGetToolCalls = this.getDb().prepare(`
635
+ SELECT * FROM tool_calls
636
+ WHERE execution_id = ?
637
+ ORDER BY started_at ASC
638
+ `);
639
+ }
640
+ const rows = this.stmtGetToolCalls.all(executionId);
641
+ return rows.map((row) => this.rowToToolCall(row));
642
+ }
643
+ /**
644
+ * Check if any consumer is currently active (has recent heartbeat)
645
+ * @param timeoutMs - heartbeat timeout (default 30 seconds)
646
+ */
647
+ hasActiveConsumer(timeoutMs = 30_000) {
648
+ this.ensureInitialized();
649
+ const now = Date.now();
650
+ const cutoff = now - timeoutMs;
651
+ const result = this.getDb()
652
+ .prepare(`
653
+ SELECT COUNT(*) as count FROM consumer_locks WHERE last_heartbeat >= ?
654
+ `)
655
+ .get(cutoff);
656
+ return result.count > 0;
657
+ }
658
+ /**
659
+ * Check if a specific consumer lock exists in the database
660
+ * Used by Consumer to verify its lock is still valid after DB reconnection
661
+ */
662
+ hasConsumerLock(consumerId) {
663
+ this.ensureInitialized();
664
+ const result = this.getDb().prepare(`SELECT 1 FROM consumer_locks WHERE id = ?`).get(consumerId);
665
+ return result !== undefined;
666
+ }
667
+ /**
668
+ * Initialize storage - create tables, enable WAL
669
+ * @param options - Initialization options
670
+ * @param options.cleanupOrphans - If true, cleanup orphaned executions (only Consumer should set this)
671
+ */
672
+ async initialize(options) {
673
+ if (this.initialized) {
674
+ return;
675
+ }
676
+ // Ensure storage directory exists (skip for in-memory)
677
+ if (!this.inMemory) {
678
+ await fs.mkdir(this.storageDir, { recursive: true });
679
+ }
680
+ // Open/create database
681
+ this.db = new Database(this.dbPath);
682
+ // Enable WAL mode for better concurrent performance
683
+ this.db.pragma('journal_mode = WAL');
684
+ // Enable foreign keys
685
+ this.db.pragma('foreign_keys = ON');
686
+ // Set busy timeout to avoid SQLITE_BUSY errors
687
+ this.db.pragma('busy_timeout = 5000');
688
+ // Create schema
689
+ this.db.exec(SCHEMA_SQL);
690
+ // Run migrations for existing databases (ignore errors for columns that already exist)
691
+ for (const line of MIGRATION_SQL.split('\n')) {
692
+ const trimmed = line.trim();
693
+ if (trimmed && !trimmed.startsWith('--')) {
694
+ try {
695
+ this.db.exec(trimmed);
696
+ }
697
+ catch {
698
+ // Column already exists, ignore
699
+ }
700
+ }
701
+ }
702
+ // Capture inode to detect if file is replaced later
703
+ if (!this.inMemory) {
704
+ try {
705
+ const stat = fsSync.statSync(this.dbPath);
706
+ this.dbFileInode = stat.ino;
707
+ }
708
+ catch {
709
+ // File may not exist yet, ignore
710
+ }
711
+ }
712
+ this.initialized = true;
713
+ // Cleanup orphaned executions from previous session (only if requested)
714
+ // IMPORTANT: Only Consumer should set cleanupOrphans=true
715
+ // Dashboard/other readers should NOT cleanup, or they will mark consumer's running executions as failed
716
+ if (options?.cleanupOrphans) {
717
+ this.cleanupOrphanedExecutions();
718
+ }
719
+ }
720
+ /**
721
+ * Check if the DB file has been replaced (different inode)
722
+ * Returns true if DB needs reconnection
723
+ */
724
+ isDbFileChanged() {
725
+ if (this.inMemory || !this.dbFileInode) {
726
+ return false;
727
+ }
728
+ try {
729
+ const stat = fsSync.statSync(this.dbPath);
730
+ return stat.ino !== this.dbFileInode;
731
+ }
732
+ catch {
733
+ // File doesn't exist - definitely changed
734
+ return true;
735
+ }
736
+ }
737
+ /**
738
+ * Reconnect to the database (close and reinitialize)
739
+ * Use when DB file has been replaced by another process (e.g., brv init)
740
+ */
741
+ async reconnect() {
742
+ // Close existing connection
743
+ this.close();
744
+ // Reinitialize (will create new connection and capture new inode)
745
+ await this.initialize();
746
+ }
747
+ // ==================== TOOL CALL METHODS ====================
748
+ /**
749
+ * Release consumer lock (unregister this consumer)
750
+ */
751
+ releaseConsumerLock(consumerId) {
752
+ this.ensureInitialized();
753
+ // First, clear consumer_id from any running executions
754
+ const now = Date.now();
755
+ this.getDb()
756
+ .prepare(`
757
+ UPDATE executions
758
+ SET consumer_id = NULL, updated_at = ?
759
+ WHERE consumer_id = ? AND status = 'running'
760
+ `)
761
+ .run(now, consumerId);
762
+ // Then delete the lock
763
+ this.getDb()
764
+ .prepare(`
765
+ DELETE FROM consumer_locks WHERE id = ?
766
+ `)
767
+ .run(consumerId);
768
+ }
769
+ /**
770
+ * Update consumer heartbeat
771
+ */
772
+ updateConsumerHeartbeat(consumerId) {
773
+ this.ensureInitialized();
774
+ const now = Date.now();
775
+ this.getDb()
776
+ .prepare(`
777
+ UPDATE consumer_locks SET last_heartbeat = ? WHERE id = ?
778
+ `)
779
+ .run(now, consumerId);
780
+ }
781
+ /**
782
+ * Update execution status
783
+ */
784
+ updateExecutionStatus(id, status, result, error) {
785
+ this.ensureInitialized();
786
+ if (!this.stmtUpdateStatus) {
787
+ this.stmtUpdateStatus = this.getDb().prepare(`
788
+ UPDATE executions
789
+ SET status = ?, result = ?, error = ?, completed_at = ?, updated_at = ?
790
+ WHERE id = ?
791
+ `);
792
+ }
793
+ const now = Date.now();
794
+ const completedAt = status === 'completed' || status === 'failed' ? now : null;
795
+ this.stmtUpdateStatus.run(status, result ?? null, error ?? null, completedAt, now, id);
796
+ }
797
+ /**
798
+ * Update tool call status and result
799
+ */
800
+ updateToolCall(id, status, options) {
801
+ this.ensureInitialized();
802
+ if (!this.stmtUpdateToolCall) {
803
+ this.stmtUpdateToolCall = this.getDb().prepare(`
804
+ UPDATE tool_calls
805
+ SET status = ?, result = ?, result_summary = ?, completed_at = ?,
806
+ duration_ms = ?, lines_count = ?, chars_count = ?
807
+ WHERE id = ?
808
+ `);
809
+ }
810
+ const now = Date.now();
811
+ const completedAt = status === 'completed' || status === 'failed' ? now : null;
812
+ // Get started_at to calculate duration
813
+ const getStarted = this.getDb().prepare('SELECT started_at FROM tool_calls WHERE id = ?');
814
+ const row = getStarted.get(id);
815
+ const durationMs = row && completedAt ? completedAt - row.started_at : null;
816
+ this.stmtUpdateToolCall.run(status, options?.result ?? null, options?.resultSummary ?? null, completedAt, durationMs, options?.linesCount ?? null, options?.charsCount ?? null, id);
817
+ }
818
+ // ==================== PRIVATE HELPERS ====================
819
+ /**
820
+ * Ensure storage has been initialized
821
+ */
822
+ ensureInitialized() {
823
+ if (!this.initialized || !this.db) {
824
+ throw new Error('AgentStorage not initialized. Call initialize() first.');
825
+ }
826
+ }
827
+ /**
828
+ * Get database instance with type safety (throws if not initialized)
829
+ * Use this instead of this.getDb() for proper type narrowing
830
+ */
831
+ getDb() {
832
+ if (!this.db) {
833
+ throw new Error('AgentStorage not initialized. Call initialize() first.');
834
+ }
835
+ return this.db;
836
+ }
837
+ /**
838
+ * Convert database row to Execution object
839
+ */
840
+ rowToExecution(row) {
841
+ const execution = {
842
+ createdAt: row.created_at,
843
+ id: row.id,
844
+ input: row.input,
845
+ status: row.status,
846
+ type: row.type,
847
+ updatedAt: row.updated_at,
848
+ };
849
+ if (row.result)
850
+ execution.result = row.result;
851
+ if (row.error)
852
+ execution.error = row.error;
853
+ if (row.started_at)
854
+ execution.startedAt = row.started_at;
855
+ if (row.completed_at)
856
+ execution.completedAt = row.completed_at;
857
+ if (row.pid)
858
+ execution.pid = row.pid;
859
+ return execution;
860
+ }
861
+ /**
862
+ * Convert database row to ToolCall object
863
+ */
864
+ rowToToolCall(row) {
865
+ const toolCall = {
866
+ args: row.args ?? '{}',
867
+ executionId: row.execution_id,
868
+ id: row.id,
869
+ name: row.name,
870
+ startedAt: row.started_at,
871
+ status: row.status,
872
+ };
873
+ if (row.description)
874
+ toolCall.description = row.description;
875
+ if (row.result)
876
+ toolCall.result = row.result;
877
+ if (row.result_summary)
878
+ toolCall.resultSummary = row.result_summary;
879
+ if (row.completed_at)
880
+ toolCall.completedAt = row.completed_at;
881
+ if (row.duration_ms)
882
+ toolCall.durationMs = row.duration_ms;
883
+ if (row.lines_count)
884
+ toolCall.linesCount = row.lines_count;
885
+ if (row.chars_count)
886
+ toolCall.charsCount = row.chars_count;
887
+ return toolCall;
888
+ }
889
+ }
890
+ // ==================== SINGLETON ====================
891
+ let instance = null;
892
+ let initPromise = null;
893
+ /**
894
+ * Get the singleton AgentStorage instance (auto-initializes if needed)
895
+ *
896
+ * This is the PRIMARY API - just call this and it handles everything.
897
+ * First call will initialize with provided config, subsequent calls return cached instance.
898
+ *
899
+ * @param config - Configuration options
900
+ * @param config.cleanupOrphans - Cleanup orphaned executions (only Consumer should set this)
901
+ * @param config.inMemory - Use in-memory database (for testing)
902
+ * @param config.storageDir - Directory for agent.db (default: .brv/blobs)
903
+ */
904
+ export async function getAgentStorage(config) {
905
+ // Already initialized - return immediately
906
+ if (instance?.initialized) {
907
+ return instance;
908
+ }
909
+ // Initialization in progress - wait for it
910
+ if (initPromise) {
911
+ return initPromise;
912
+ }
913
+ // Start initialization (only one concurrent init allowed)
914
+ initPromise = (async () => {
915
+ if (!instance) {
916
+ instance = new AgentStorage(config);
917
+ }
918
+ await instance.initialize({ cleanupOrphans: config?.cleanupOrphans });
919
+ return instance;
920
+ })();
921
+ try {
922
+ return await initPromise;
923
+ }
924
+ finally {
925
+ initPromise = null;
926
+ }
927
+ }
928
+ /**
929
+ * Get the singleton AgentStorage instance (sync version)
930
+ * THROWS if not initialized - use getAgentStorage() instead for auto-init
931
+ *
932
+ * Use this only when you KNOW storage is already initialized (e.g., in Consumer after start)
933
+ */
934
+ export function getAgentStorageSync() {
935
+ if (!instance?.initialized) {
936
+ throw new Error('AgentStorage not initialized. Use await getAgentStorage() instead.');
937
+ }
938
+ return instance;
939
+ }
940
+ /**
941
+ * Initialize the singleton AgentStorage instance
942
+ * @deprecated Use getAgentStorage() directly - it auto-initializes
943
+ */
944
+ export async function initializeAgentStorage(config) {
945
+ return getAgentStorage(config);
946
+ }
947
+ /**
948
+ * Close and clear the singleton AgentStorage instance
949
+ */
950
+ export function closeAgentStorage() {
951
+ if (instance) {
952
+ instance.close();
953
+ instance = null;
954
+ }
955
+ initPromise = null;
956
+ }