@tenex-chat/backend 0.9.1

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 (427) hide show
  1. package/README.md +194 -0
  2. package/dist/backend-wrapper.cjs +3 -0
  3. package/dist/src/index.js +331928 -0
  4. package/package.json +103 -0
  5. package/src/agents/AgentRegistry.ts +418 -0
  6. package/src/agents/AgentStorage.ts +1133 -0
  7. package/src/agents/ConfigResolver.ts +229 -0
  8. package/src/agents/agent-installer.ts +236 -0
  9. package/src/agents/agent-loader.ts +241 -0
  10. package/src/agents/constants.ts +82 -0
  11. package/src/agents/errors.ts +48 -0
  12. package/src/agents/execution/AgentExecutor.ts +561 -0
  13. package/src/agents/execution/ExecutionContextFactory.ts +112 -0
  14. package/src/agents/execution/MessageCompiler.ts +597 -0
  15. package/src/agents/execution/MessageSyncer.ts +100 -0
  16. package/src/agents/execution/PostCompletionChecker.ts +278 -0
  17. package/src/agents/execution/ProgressMonitor.ts +50 -0
  18. package/src/agents/execution/RALResolver.ts +177 -0
  19. package/src/agents/execution/SessionManager.ts +181 -0
  20. package/src/agents/execution/StreamCallbacks.ts +312 -0
  21. package/src/agents/execution/StreamExecutionHandler.ts +579 -0
  22. package/src/agents/execution/StreamSetup.ts +313 -0
  23. package/src/agents/execution/ToolEventHandlers.ts +239 -0
  24. package/src/agents/execution/ToolExecutionTracker.ts +498 -0
  25. package/src/agents/execution/ToolResultUtils.ts +97 -0
  26. package/src/agents/execution/ToolSupervisionWrapper.ts +174 -0
  27. package/src/agents/execution/constants.ts +16 -0
  28. package/src/agents/execution/index.ts +3 -0
  29. package/src/agents/execution/types.ts +96 -0
  30. package/src/agents/execution/utils.ts +26 -0
  31. package/src/agents/index.ts +4 -0
  32. package/src/agents/script-installer.ts +266 -0
  33. package/src/agents/supervision/SupervisorLLMService.ts +253 -0
  34. package/src/agents/supervision/SupervisorOrchestrator.ts +471 -0
  35. package/src/agents/supervision/heuristics/ConsecutiveToolsWithoutTodoHeuristic.ts +73 -0
  36. package/src/agents/supervision/heuristics/DelegationClaimHeuristic.ts +80 -0
  37. package/src/agents/supervision/heuristics/HeuristicRegistry.ts +114 -0
  38. package/src/agents/supervision/heuristics/PendingTodosHeuristic.ts +93 -0
  39. package/src/agents/supervision/heuristics/SilentAgentHeuristic.ts +54 -0
  40. package/src/agents/supervision/heuristics/index.ts +5 -0
  41. package/src/agents/supervision/index.ts +28 -0
  42. package/src/agents/supervision/registerHeuristics.ts +110 -0
  43. package/src/agents/supervision/supervisionHealthCheck.ts +123 -0
  44. package/src/agents/supervision/types.ts +171 -0
  45. package/src/agents/tool-names.ts +46 -0
  46. package/src/agents/tool-normalization.ts +184 -0
  47. package/src/agents/types/index.ts +2 -0
  48. package/src/agents/types/runtime.ts +74 -0
  49. package/src/agents/types/storage.ts +145 -0
  50. package/src/commands/agent/import/index.ts +6 -0
  51. package/src/commands/agent/import/openclaw-distiller.ts +57 -0
  52. package/src/commands/agent/import/openclaw-reader.ts +141 -0
  53. package/src/commands/agent/import/openclaw.ts +154 -0
  54. package/src/commands/agent/index.ts +6 -0
  55. package/src/commands/agent.ts +215 -0
  56. package/src/commands/daemon.ts +198 -0
  57. package/src/commands/doctor.ts +134 -0
  58. package/src/commands/setup/embed.ts +228 -0
  59. package/src/commands/setup/global-system-prompt.ts +223 -0
  60. package/src/commands/setup/image.ts +179 -0
  61. package/src/commands/setup/index.ts +16 -0
  62. package/src/commands/setup/interactive.ts +95 -0
  63. package/src/commands/setup/llm.ts +38 -0
  64. package/src/commands/setup/onboarding.ts +294 -0
  65. package/src/commands/setup/providers.ts +27 -0
  66. package/src/constants.ts +34 -0
  67. package/src/conversations/ConversationDiskReader.ts +148 -0
  68. package/src/conversations/ConversationRegistry.ts +728 -0
  69. package/src/conversations/ConversationStore.ts +868 -0
  70. package/src/conversations/MessageBuilder.ts +866 -0
  71. package/src/conversations/executionTime.ts +62 -0
  72. package/src/conversations/formatters/DelegationXmlFormatter.ts +64 -0
  73. package/src/conversations/formatters/ThreadedConversationFormatter.ts +303 -0
  74. package/src/conversations/formatters/index.ts +9 -0
  75. package/src/conversations/formatters/utils/MessageFormatter.ts +46 -0
  76. package/src/conversations/formatters/utils/TimestampFormatter.ts +56 -0
  77. package/src/conversations/formatters/utils/TreeBuilder.ts +131 -0
  78. package/src/conversations/formatters/utils/TreeRenderer.ts +49 -0
  79. package/src/conversations/index.ts +2 -0
  80. package/src/conversations/persistence/ToolMessageStorage.ts +143 -0
  81. package/src/conversations/search/ConversationIndexManager.ts +393 -0
  82. package/src/conversations/search/QueryParser.ts +114 -0
  83. package/src/conversations/search/SearchEngine.ts +175 -0
  84. package/src/conversations/search/SnippetExtractor.ts +345 -0
  85. package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +484 -0
  86. package/src/conversations/search/embeddings/ConversationIndexingJob.ts +320 -0
  87. package/src/conversations/search/embeddings/IndexingStateManager.ts +338 -0
  88. package/src/conversations/search/embeddings/index.ts +18 -0
  89. package/src/conversations/search/index.ts +49 -0
  90. package/src/conversations/search/types.ts +124 -0
  91. package/src/conversations/services/CategoryManager.ts +160 -0
  92. package/src/conversations/services/ConversationResolver.ts +296 -0
  93. package/src/conversations/services/ConversationSummarizer.ts +234 -0
  94. package/src/conversations/services/MetadataDebounceManager.ts +188 -0
  95. package/src/conversations/services/index.ts +2 -0
  96. package/src/conversations/types.ts +148 -0
  97. package/src/conversations/utils/content-utils.ts +69 -0
  98. package/src/conversations/utils/image-placeholder.ts +281 -0
  99. package/src/conversations/utils/image-url-utils.ts +171 -0
  100. package/src/conversations/utils/multimodal-content.ts +90 -0
  101. package/src/conversations/utils/tool-result-truncator.ts +159 -0
  102. package/src/daemon/Daemon.ts +1883 -0
  103. package/src/daemon/ProjectRuntime.ts +657 -0
  104. package/src/daemon/RestartState.ts +152 -0
  105. package/src/daemon/RuntimeLifecycle.ts +268 -0
  106. package/src/daemon/SubscriptionManager.ts +305 -0
  107. package/src/daemon/UnixSocketTransport.ts +318 -0
  108. package/src/daemon/filters/SubscriptionFilterBuilder.ts +119 -0
  109. package/src/daemon/index.ts +9 -0
  110. package/src/daemon/routing/DaemonRouter.ts +491 -0
  111. package/src/daemon/types.ts +150 -0
  112. package/src/daemon/utils/routing-log.ts +76 -0
  113. package/src/daemon/utils/telemetry.ts +173 -0
  114. package/src/event-handler/agentDeletion.ts +383 -0
  115. package/src/event-handler/index.ts +749 -0
  116. package/src/event-handler/newConversation.ts +165 -0
  117. package/src/event-handler/project.ts +166 -0
  118. package/src/event-handler/reply.ts +18 -0
  119. package/src/events/NDKAgentDefinition.ts +292 -0
  120. package/src/events/NDKAgentLesson.ts +106 -0
  121. package/src/events/NDKEventMetadata.ts +34 -0
  122. package/src/events/NDKMCPTool.ts +60 -0
  123. package/src/events/NDKProjectStatus.ts +384 -0
  124. package/src/events/index.ts +4 -0
  125. package/src/index.ts +126 -0
  126. package/src/lib/agent-home.ts +334 -0
  127. package/src/lib/error-formatter.ts +200 -0
  128. package/src/lib/fs/filesystem.ts +128 -0
  129. package/src/lib/fs/index.ts +1 -0
  130. package/src/lib/json-parser.ts +30 -0
  131. package/src/lib/string.ts +15 -0
  132. package/src/lib/time.ts +74 -0
  133. package/src/llm/ChunkHandler.ts +277 -0
  134. package/src/llm/FinishHandler.ts +250 -0
  135. package/src/llm/LLMConfigEditor.ts +154 -0
  136. package/src/llm/LLMServiceFactory.ts +230 -0
  137. package/src/llm/MessageProcessor.ts +90 -0
  138. package/src/llm/RecordingState.ts +37 -0
  139. package/src/llm/StreamPublisher.ts +40 -0
  140. package/src/llm/TracingUtils.ts +77 -0
  141. package/src/llm/chunk-validators.ts +57 -0
  142. package/src/llm/constants.ts +6 -0
  143. package/src/llm/index.ts +12 -0
  144. package/src/llm/meta/MetaModelResolver.ts +352 -0
  145. package/src/llm/meta/index.ts +11 -0
  146. package/src/llm/middleware/flight-recorder.ts +188 -0
  147. package/src/llm/providers/MockProvider.ts +332 -0
  148. package/src/llm/providers/agent/ClaudeCodeProvider.ts +343 -0
  149. package/src/llm/providers/agent/ClaudeCodeToolsAdapter.ts +203 -0
  150. package/src/llm/providers/agent/CodexAppServerProvider.ts +214 -0
  151. package/src/llm/providers/agent/CodexAppServerToolsAdapter.ts +91 -0
  152. package/src/llm/providers/agent/index.ts +10 -0
  153. package/src/llm/providers/base/AgentProvider.ts +107 -0
  154. package/src/llm/providers/base/BaseProvider.ts +114 -0
  155. package/src/llm/providers/base/StandardProvider.ts +38 -0
  156. package/src/llm/providers/base/index.ts +9 -0
  157. package/src/llm/providers/index.ts +106 -0
  158. package/src/llm/providers/key-manager.ts +238 -0
  159. package/src/llm/providers/ollama-models.ts +105 -0
  160. package/src/llm/providers/openrouter-models.ts +102 -0
  161. package/src/llm/providers/provider-ids.ts +18 -0
  162. package/src/llm/providers/registry/ProviderRegistry.ts +414 -0
  163. package/src/llm/providers/registry/index.ts +7 -0
  164. package/src/llm/providers/standard/AnthropicProvider.ts +71 -0
  165. package/src/llm/providers/standard/OllamaProvider.ts +59 -0
  166. package/src/llm/providers/standard/OpenAIProvider.ts +44 -0
  167. package/src/llm/providers/standard/OpenRouterProvider.ts +103 -0
  168. package/src/llm/providers/standard/index.ts +10 -0
  169. package/src/llm/providers/types.ts +194 -0
  170. package/src/llm/providers/usage-metadata.ts +78 -0
  171. package/src/llm/service.ts +713 -0
  172. package/src/llm/types.ts +167 -0
  173. package/src/llm/utils/ConfigurationManager.ts +650 -0
  174. package/src/llm/utils/ConfigurationTester.ts +229 -0
  175. package/src/llm/utils/ModelSelector.ts +212 -0
  176. package/src/llm/utils/ProviderConfigUI.ts +177 -0
  177. package/src/llm/utils/claudeCodePromptCompiler.ts +141 -0
  178. package/src/llm/utils/codex-models.ts +53 -0
  179. package/src/llm/utils/context-window-cache.ts +30 -0
  180. package/src/llm/utils/models-dev-cache.ts +267 -0
  181. package/src/llm/utils/provider-setup.ts +50 -0
  182. package/src/llm/utils/tool-errors.ts +78 -0
  183. package/src/llm/utils/usage.ts +74 -0
  184. package/src/logging/EventRoutingLogger.ts +205 -0
  185. package/src/nostr/AgentEventDecoder.ts +357 -0
  186. package/src/nostr/AgentEventEncoder.ts +677 -0
  187. package/src/nostr/AgentProfilePublisher.ts +657 -0
  188. package/src/nostr/AgentPublisher.ts +437 -0
  189. package/src/nostr/BlossomService.ts +226 -0
  190. package/src/nostr/InterventionPublisher.ts +132 -0
  191. package/src/nostr/TagExtractor.ts +228 -0
  192. package/src/nostr/collectEvents.ts +83 -0
  193. package/src/nostr/constants.ts +38 -0
  194. package/src/nostr/encryption.ts +26 -0
  195. package/src/nostr/index.ts +31 -0
  196. package/src/nostr/keys.ts +17 -0
  197. package/src/nostr/kinds.ts +37 -0
  198. package/src/nostr/ndkClient.ts +72 -0
  199. package/src/nostr/relays.ts +43 -0
  200. package/src/nostr/trace-context.ts +39 -0
  201. package/src/nostr/types.ts +227 -0
  202. package/src/nostr/utils.ts +84 -0
  203. package/src/prompts/core/FragmentRegistry.ts +30 -0
  204. package/src/prompts/core/PromptBuilder.ts +98 -0
  205. package/src/prompts/core/index.ts +3 -0
  206. package/src/prompts/core/types.ts +13 -0
  207. package/src/prompts/fragments/00-global-system-prompt.ts +44 -0
  208. package/src/prompts/fragments/01-agent-identity.ts +69 -0
  209. package/src/prompts/fragments/02-agent-home-directory.ts +114 -0
  210. package/src/prompts/fragments/03-system-reminders-explanation.ts +14 -0
  211. package/src/prompts/fragments/04-relay-configuration.ts +38 -0
  212. package/src/prompts/fragments/05-delegation-chain.ts +45 -0
  213. package/src/prompts/fragments/06-agent-todos.ts +74 -0
  214. package/src/prompts/fragments/06-todo-usage-guidance.ts +34 -0
  215. package/src/prompts/fragments/07-meta-project-context.ts +234 -0
  216. package/src/prompts/fragments/08-active-conversations.ts +382 -0
  217. package/src/prompts/fragments/09-recent-conversations.ts +153 -0
  218. package/src/prompts/fragments/10-referenced-article.ts +21 -0
  219. package/src/prompts/fragments/11-nudges.ts +134 -0
  220. package/src/prompts/fragments/12-skills.ts +127 -0
  221. package/src/prompts/fragments/13-available-nudges.ts +122 -0
  222. package/src/prompts/fragments/15-available-agents.ts +53 -0
  223. package/src/prompts/fragments/16-stay-in-your-lane.ts +41 -0
  224. package/src/prompts/fragments/17-todo-before-delegation.ts +39 -0
  225. package/src/prompts/fragments/20-voice-mode.ts +62 -0
  226. package/src/prompts/fragments/22-scheduled-tasks.ts +175 -0
  227. package/src/prompts/fragments/24-retrieved-lessons.ts +26 -0
  228. package/src/prompts/fragments/25-rag-instructions.ts +333 -0
  229. package/src/prompts/fragments/26-mcp-resources.ts +237 -0
  230. package/src/prompts/fragments/27-memorized-reports.ts +77 -0
  231. package/src/prompts/fragments/28-agent-directed-monitoring.ts +32 -0
  232. package/src/prompts/fragments/29-rag-collections.ts +50 -0
  233. package/src/prompts/fragments/30-worktree-context.ts +98 -0
  234. package/src/prompts/fragments/31-agents-md-guidance.ts +96 -0
  235. package/src/prompts/fragments/32-process-metrics.ts +72 -0
  236. package/src/prompts/fragments/debug-mode.ts +48 -0
  237. package/src/prompts/fragments/delegation-completion.ts +44 -0
  238. package/src/prompts/fragments/index.ts +91 -0
  239. package/src/prompts/index.ts +21 -0
  240. package/src/prompts/utils/systemPromptBuilder.ts +777 -0
  241. package/src/scripts/migrate-prefix-index.ts +157 -0
  242. package/src/services/AgentDefinitionMonitor.ts +701 -0
  243. package/src/services/ConfigService.ts +723 -0
  244. package/src/services/CooldownRegistry.ts +199 -0
  245. package/src/services/LLMOperationsRegistry.ts +424 -0
  246. package/src/services/OwnerAgentListService.ts +354 -0
  247. package/src/services/PubkeyService.ts +308 -0
  248. package/src/services/agents/AgentMetadataStore.ts +72 -0
  249. package/src/services/agents/AgentResolution.ts +59 -0
  250. package/src/services/agents/EscalationService.ts +281 -0
  251. package/src/services/agents/NDKAgentDiscovery.ts +95 -0
  252. package/src/services/agents/index.ts +7 -0
  253. package/src/services/agents-md/AgentsMdService.ts +184 -0
  254. package/src/services/agents-md/SystemReminderInjector.ts +238 -0
  255. package/src/services/agents-md/index.ts +11 -0
  256. package/src/services/apns/APNsClient.ts +203 -0
  257. package/src/services/apns/APNsService.ts +358 -0
  258. package/src/services/apns/index.ts +11 -0
  259. package/src/services/apns/types.ts +80 -0
  260. package/src/services/compression/CompressionService.ts +445 -0
  261. package/src/services/compression/compression-schema.ts +28 -0
  262. package/src/services/compression/compression-types.ts +74 -0
  263. package/src/services/compression/compression-utils.ts +587 -0
  264. package/src/services/config/types.ts +394 -0
  265. package/src/services/dispatch/AgentDispatchService.ts +937 -0
  266. package/src/services/dispatch/AgentRouter.ts +181 -0
  267. package/src/services/dispatch/DelegationCompletionHandler.ts +232 -0
  268. package/src/services/embedding/EmbeddingProvider.ts +188 -0
  269. package/src/services/embedding/index.ts +5 -0
  270. package/src/services/event-context/EventContextService.ts +108 -0
  271. package/src/services/event-context/index.ts +2 -0
  272. package/src/services/heuristics/ContextBuilder.ts +106 -0
  273. package/src/services/heuristics/HeuristicEngine.ts +200 -0
  274. package/src/services/heuristics/formatters.ts +58 -0
  275. package/src/services/heuristics/index.ts +12 -0
  276. package/src/services/heuristics/rules/index.ts +25 -0
  277. package/src/services/heuristics/rules/todoBeforeDelegation.ts +69 -0
  278. package/src/services/heuristics/rules/todoReminderOnToolUse.ts +63 -0
  279. package/src/services/heuristics/types.ts +144 -0
  280. package/src/services/image/ImageGenerationService.ts +389 -0
  281. package/src/services/image/index.ts +12 -0
  282. package/src/services/intervention/InterventionService.ts +1352 -0
  283. package/src/services/intervention/index.ts +7 -0
  284. package/src/services/mcp/MCPManager.ts +683 -0
  285. package/src/services/mcp/McpNotificationDelivery.ts +139 -0
  286. package/src/services/mcp/McpSubscriptionService.ts +653 -0
  287. package/src/services/mcp/mcpInstaller.ts +130 -0
  288. package/src/services/nip46/Nip46SigningLog.ts +81 -0
  289. package/src/services/nip46/Nip46SigningService.ts +467 -0
  290. package/src/services/nip46/index.ts +4 -0
  291. package/src/services/nudge/NudgeService.ts +224 -0
  292. package/src/services/nudge/NudgeWhitelistService.ts +382 -0
  293. package/src/services/nudge/index.ts +5 -0
  294. package/src/services/nudge/types.ts +83 -0
  295. package/src/services/projects/ProjectContext.ts +672 -0
  296. package/src/services/projects/ProjectContextStore.ts +102 -0
  297. package/src/services/projects/index.ts +6 -0
  298. package/src/services/prompt-compiler/index.ts +15 -0
  299. package/src/services/prompt-compiler/prompt-compiler-service.ts +1143 -0
  300. package/src/services/pubkey-gate/PubkeyGateService.ts +93 -0
  301. package/src/services/pubkey-gate/index.ts +1 -0
  302. package/src/services/rag/EmbeddingProviderFactory.ts +292 -0
  303. package/src/services/rag/LanceDBMaintenanceService.ts +211 -0
  304. package/src/services/rag/RAGDatabaseService.ts +173 -0
  305. package/src/services/rag/RAGOperations.ts +682 -0
  306. package/src/services/rag/RAGService.ts +240 -0
  307. package/src/services/rag/RagSubscriptionService.ts +618 -0
  308. package/src/services/rag/rag-utils.ts +174 -0
  309. package/src/services/ral/PendingDelegationsRegistry.ts +168 -0
  310. package/src/services/ral/RALRegistry.ts +2782 -0
  311. package/src/services/ral/index.ts +4 -0
  312. package/src/services/ral/types.ts +292 -0
  313. package/src/services/reports/LocalReportStore.ts +380 -0
  314. package/src/services/reports/ReportEmbeddingService.ts +430 -0
  315. package/src/services/reports/ReportService.ts +440 -0
  316. package/src/services/reports/articleUtils.ts +52 -0
  317. package/src/services/reports/index.ts +7 -0
  318. package/src/services/scheduling/SchedulerService.ts +1057 -0
  319. package/src/services/scheduling/errors.ts +14 -0
  320. package/src/services/scheduling/index.ts +7 -0
  321. package/src/services/scheduling/utils.ts +77 -0
  322. package/src/services/search/SearchProviderRegistry.ts +78 -0
  323. package/src/services/search/UnifiedSearchService.ts +218 -0
  324. package/src/services/search/index.ts +47 -0
  325. package/src/services/search/projectFilter.ts +22 -0
  326. package/src/services/search/providers/ConversationSearchProvider.ts +48 -0
  327. package/src/services/search/providers/LessonSearchProvider.ts +75 -0
  328. package/src/services/search/providers/ReportSearchProvider.ts +49 -0
  329. package/src/services/search/types.ts +144 -0
  330. package/src/services/skill/SkillService.ts +482 -0
  331. package/src/services/skill/index.ts +2 -0
  332. package/src/services/skill/types.ts +70 -0
  333. package/src/services/status/OperationsStatusService.ts +276 -0
  334. package/src/services/status/ProjectStatusService.ts +522 -0
  335. package/src/services/status/index.ts +11 -0
  336. package/src/services/storage/PrefixKVStore.ts +242 -0
  337. package/src/services/storage/index.ts +1 -0
  338. package/src/services/system-reminder/SystemReminderUtils.ts +96 -0
  339. package/src/services/system-reminder/index.ts +7 -0
  340. package/src/services/trust-pubkeys/TrustPubkeyService.ts +325 -0
  341. package/src/services/trust-pubkeys/index.ts +2 -0
  342. package/src/telemetry/ConversationSpanManager.ts +111 -0
  343. package/src/telemetry/EventLoopMonitor.ts +206 -0
  344. package/src/telemetry/LLMSpanRegistry.ts +20 -0
  345. package/src/telemetry/NostrSpanProcessor.ts +89 -0
  346. package/src/telemetry/ToolCallSpanProcessor.ts +66 -0
  347. package/src/telemetry/diagnostics.ts +27 -0
  348. package/src/telemetry/setup.ts +120 -0
  349. package/src/tools/implementations/agents_discover.ts +121 -0
  350. package/src/tools/implementations/agents_hire.ts +127 -0
  351. package/src/tools/implementations/agents_list.ts +96 -0
  352. package/src/tools/implementations/agents_publish.ts +611 -0
  353. package/src/tools/implementations/agents_read.ts +173 -0
  354. package/src/tools/implementations/agents_write.ts +200 -0
  355. package/src/tools/implementations/ask.ts +411 -0
  356. package/src/tools/implementations/change_model.ts +141 -0
  357. package/src/tools/implementations/conversation_get.ts +661 -0
  358. package/src/tools/implementations/conversation_list.ts +377 -0
  359. package/src/tools/implementations/conversation_search.ts +370 -0
  360. package/src/tools/implementations/delegate.ts +327 -0
  361. package/src/tools/implementations/delegate_crossproject.ts +209 -0
  362. package/src/tools/implementations/delegate_followup.ts +300 -0
  363. package/src/tools/implementations/fs_edit.ts +162 -0
  364. package/src/tools/implementations/fs_glob.ts +182 -0
  365. package/src/tools/implementations/fs_grep.ts +513 -0
  366. package/src/tools/implementations/fs_read.ts +332 -0
  367. package/src/tools/implementations/fs_write.ts +113 -0
  368. package/src/tools/implementations/generate_image.ts +259 -0
  369. package/src/tools/implementations/home_fs.ts +515 -0
  370. package/src/tools/implementations/kill.ts +651 -0
  371. package/src/tools/implementations/learn.ts +166 -0
  372. package/src/tools/implementations/lesson-formatter.ts +38 -0
  373. package/src/tools/implementations/lesson_delete.ts +164 -0
  374. package/src/tools/implementations/lesson_get.ts +105 -0
  375. package/src/tools/implementations/lessons_list.ts +153 -0
  376. package/src/tools/implementations/mcp_resource_read.ts +161 -0
  377. package/src/tools/implementations/mcp_subscribe.ts +158 -0
  378. package/src/tools/implementations/mcp_subscription_stop.ts +85 -0
  379. package/src/tools/implementations/nostr_fetch.ts +149 -0
  380. package/src/tools/implementations/nostr_publish_as_user.ts +353 -0
  381. package/src/tools/implementations/project_list.ts +146 -0
  382. package/src/tools/implementations/rag_add_documents.ts +573 -0
  383. package/src/tools/implementations/rag_create_collection.ts +65 -0
  384. package/src/tools/implementations/rag_delete_collection.ts +68 -0
  385. package/src/tools/implementations/rag_list_collections.ts +77 -0
  386. package/src/tools/implementations/rag_query.ts +107 -0
  387. package/src/tools/implementations/rag_subscription_create.ts +105 -0
  388. package/src/tools/implementations/rag_subscription_delete.ts +80 -0
  389. package/src/tools/implementations/rag_subscription_get.ts +123 -0
  390. package/src/tools/implementations/rag_subscription_list.ts +128 -0
  391. package/src/tools/implementations/report_delete.ts +79 -0
  392. package/src/tools/implementations/report_read.ts +160 -0
  393. package/src/tools/implementations/report_write.ts +278 -0
  394. package/src/tools/implementations/reports_list.ts +77 -0
  395. package/src/tools/implementations/schedule_task.ts +104 -0
  396. package/src/tools/implementations/schedule_task_cancel.ts +62 -0
  397. package/src/tools/implementations/schedule_task_once.ts +128 -0
  398. package/src/tools/implementations/schedule_tasks_list.ts +79 -0
  399. package/src/tools/implementations/search.ts +160 -0
  400. package/src/tools/implementations/shell.ts +553 -0
  401. package/src/tools/implementations/todo.ts +260 -0
  402. package/src/tools/implementations/upload_blob.ts +381 -0
  403. package/src/tools/implementations/web_fetch.ts +153 -0
  404. package/src/tools/implementations/web_search.ts +250 -0
  405. package/src/tools/registry.ts +670 -0
  406. package/src/tools/types.ts +177 -0
  407. package/src/tools/utils.ts +256 -0
  408. package/src/types/event-ids.ts +320 -0
  409. package/src/types/index.ts +46 -0
  410. package/src/utils/agentFetcher.ts +107 -0
  411. package/src/utils/cli-error.ts +29 -0
  412. package/src/utils/conversation-id.ts +27 -0
  413. package/src/utils/conversation-utils.ts +1 -0
  414. package/src/utils/delegation-chain.ts +357 -0
  415. package/src/utils/error-handler.ts +42 -0
  416. package/src/utils/git/gitignore.ts +69 -0
  417. package/src/utils/git/index.ts +2 -0
  418. package/src/utils/git/initializeGitRepo.ts +204 -0
  419. package/src/utils/git/worktree.ts +260 -0
  420. package/src/utils/lessonFormatter.ts +70 -0
  421. package/src/utils/lessonTrust.ts +24 -0
  422. package/src/utils/lockfile.ts +123 -0
  423. package/src/utils/logger.ts +149 -0
  424. package/src/utils/nostr-entity-parser.ts +365 -0
  425. package/src/utils/process.ts +49 -0
  426. package/src/wrapper.ts +262 -0
  427. package/tsconfig.json +41 -0
@@ -0,0 +1,1883 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { EventRoutingLogger } from "@/logging/EventRoutingLogger";
4
+ import type { AgentInstance, AgentDefaultConfig } from "@/agents/types";
5
+ import { agentStorage } from "@/agents/AgentStorage";
6
+ import { NDKAgentLesson } from "@/events/NDKAgentLesson";
7
+ import { AgentEventDecoder } from "@/nostr/AgentEventDecoder";
8
+ import { TagExtractor } from "@/nostr/TagExtractor";
9
+ import { AgentProfilePublisher } from "@/nostr/AgentProfilePublisher";
10
+ import { getNDK, initNDK } from "@/nostr/ndkClient";
11
+ import { config } from "@/services/ConfigService";
12
+ import { prefixKVStore } from "@/services/storage";
13
+ import { Lockfile } from "@/utils/lockfile";
14
+ import { shouldTrustLesson } from "@/utils/lessonTrust";
15
+ import { logger } from "@/utils/logger";
16
+ import type { Hexpubkey, NDKEvent } from "@nostr-dev-kit/ndk";
17
+ import type NDK from "@nostr-dev-kit/ndk";
18
+ import { NDKProject } from "@nostr-dev-kit/ndk";
19
+ import { context as otelContext, trace, type Span } from "@opentelemetry/api";
20
+ import { getConversationSpanManager } from "@/telemetry/ConversationSpanManager";
21
+ import { shutdownTelemetry } from "@/telemetry/setup";
22
+ import type { RoutingDecision } from "./routing/DaemonRouter";
23
+ import type { ProjectRuntime } from "./ProjectRuntime";
24
+ import { RuntimeLifecycle } from "./RuntimeLifecycle";
25
+ import { SubscriptionManager } from "./SubscriptionManager";
26
+ import { DaemonRouter } from "./routing/DaemonRouter";
27
+ import type { DaemonStatus } from "./types";
28
+ import { createEventSpan, endSpanSuccess, endSpanError, addRoutingEvent } from "./utils/telemetry";
29
+ import { logDropped, logRouted } from "./utils/routing-log";
30
+ import { UnixSocketTransport } from "./UnixSocketTransport";
31
+ import { streamPublisher } from "@/llm";
32
+ import { getConversationIndexingJob } from "@/conversations/search/embeddings";
33
+ import { getLanceDBMaintenanceService } from "@/services/rag/LanceDBMaintenanceService";
34
+ import { ConversationStore } from "@/conversations/ConversationStore";
35
+ import { InterventionService, type AgentResolutionResult, type ActiveDelegationCheckerFn } from "@/services/intervention";
36
+ import { Nip46SigningService } from "@/services/nip46";
37
+ import { NudgeSkillWhitelistService } from "@/services/nudge";
38
+ import { OwnerAgentListService } from "@/services/OwnerAgentListService";
39
+ import { RALRegistry } from "@/services/ral/RALRegistry";
40
+ import { RestartState } from "./RestartState";
41
+ import { AgentDefinitionMonitor } from "@/services/AgentDefinitionMonitor";
42
+ import { APNsService } from "@/services/apns";
43
+ const lessonTracer = trace.getTracer("tenex.lessons");
44
+
45
+ /**
46
+ * Main daemon that manages all projects in a single process.
47
+ * Uses lazy loading - projects only start when they receive events.
48
+ *
49
+ * This class now focuses on orchestration, delegating specific responsibilities to:
50
+ * - RuntimeLifecycle: Runtime management (start/stop/restart)
51
+ * - DaemonRouter: Event routing decisions
52
+ * - SubscriptionFilterBuilder: Filter construction
53
+ * - AgentEventDecoder: Event classification
54
+ */
55
+ export class Daemon {
56
+ private ndk: NDK | null = null;
57
+ private subscriptionManager: SubscriptionManager | null = null;
58
+ private routingLogger: EventRoutingLogger;
59
+ private whitelistedPubkeys: Hexpubkey[] = [];
60
+ private projectsBase = "";
61
+ private daemonDir = "";
62
+ private isRunning = false;
63
+ private shutdownHandlers: Array<() => Promise<void>> = [];
64
+ private lockfile: Lockfile | null = null;
65
+ private streamTransport: UnixSocketTransport | null = null;
66
+
67
+ // Runtime management delegated to RuntimeLifecycle
68
+ private runtimeLifecycle: RuntimeLifecycle | null = null;
69
+
70
+ // Project management
71
+ private knownProjects = new Map<string, NDKProject>(); // All discovered projects
72
+
73
+ // Agent pubkey mapping for routing (pubkey -> project IDs)
74
+ private agentPubkeyToProjects = new Map<Hexpubkey, Set<string>>();
75
+
76
+ // Tracked agent definition IDs for lesson subscription sync
77
+ private trackedLessonDefinitionIds = new Set<string>();
78
+
79
+ // Auto-boot patterns - projects whose d-tag contains any of these patterns will be auto-started
80
+ private autoBootPatterns: string[] = [];
81
+
82
+ // Agent definition auto-upgrade monitor
83
+ private agentDefinitionMonitor: AgentDefinitionMonitor | null = null;
84
+
85
+ // Graceful restart state
86
+ private pendingRestart = false;
87
+ private restartInProgress = false;
88
+ private restartState: RestartState | null = null;
89
+ private supervisedMode = false;
90
+
91
+ // Projects pending auto-boot from restart state (populated by loadRestartState, consumed by handleProjectEvent)
92
+ private pendingRestartBootProjects: Set<string> = new Set();
93
+
94
+ // Shutdown function (set by setupShutdownHandlers, used by triggerGracefulRestart)
95
+ private shutdownFn: ((exitCode?: number, isGracefulRestart?: boolean) => Promise<void>) | null = null;
96
+
97
+ constructor() {
98
+ this.routingLogger = new EventRoutingLogger();
99
+ }
100
+
101
+ /**
102
+ * Set patterns for auto-booting projects on discovery
103
+ * Projects whose d-tag contains any of these patterns will be auto-started
104
+ */
105
+ setAutoBootPatterns(patterns: string[]): void {
106
+ this.autoBootPatterns = patterns;
107
+ logger.info("Auto-boot patterns configured", { patterns });
108
+ }
109
+
110
+ /**
111
+ * Enable supervised mode for graceful restart support.
112
+ * In supervised mode:
113
+ * - SIGHUP triggers deferred restart instead of immediate shutdown
114
+ * - Daemon waits for all RALs to complete before exiting
115
+ * - Booted projects are persisted for auto-boot on restart
116
+ */
117
+ setSupervisedMode(supervised: boolean): void {
118
+ this.supervisedMode = supervised;
119
+ logger.info("Supervised mode configured", { supervised });
120
+ }
121
+
122
+ /**
123
+ * Check if daemon is in supervised mode
124
+ */
125
+ isSupervisedMode(): boolean {
126
+ return this.supervisedMode;
127
+ }
128
+
129
+ /**
130
+ * Check if a restart is pending
131
+ */
132
+ isPendingRestart(): boolean {
133
+ return this.pendingRestart;
134
+ }
135
+
136
+ /**
137
+ * Initialize and start the daemon
138
+ */
139
+ async start(): Promise<void> {
140
+ if (this.isRunning) {
141
+ logger.warn("Daemon is already running");
142
+ return;
143
+ }
144
+
145
+ try {
146
+ // 1. Initialize base directories
147
+ logger.debug("Initializing base directories");
148
+ await this.initializeDirectories();
149
+
150
+ // 2. Acquire lockfile to prevent multiple daemon instances
151
+ logger.debug("Acquiring daemon lock");
152
+ await this.acquireDaemonLock();
153
+
154
+ // 3. Initialize routing logger
155
+ logger.debug("Initializing routing logger");
156
+ this.routingLogger.initialize(this.daemonDir);
157
+
158
+ // 4. Load configuration
159
+ logger.debug("Loading configuration");
160
+ const { config: loadedConfig } = await config.loadConfig();
161
+ const whitelistedPubkeys = loadedConfig.whitelistedPubkeys;
162
+ if (!whitelistedPubkeys) {
163
+ throw new Error("whitelistedPubkeys not configured");
164
+ }
165
+ this.whitelistedPubkeys = whitelistedPubkeys;
166
+ const projectsBase = config.getProjectsBase();
167
+ if (!loadedConfig.projectsBase) {
168
+ logger.warn(`projectsBase not configured, falling back to default: ${projectsBase}`);
169
+ }
170
+ this.projectsBase = projectsBase;
171
+
172
+ if (this.whitelistedPubkeys.length === 0) {
173
+ throw new Error("No whitelisted pubkeys configured. Run 'tenex setup' first.");
174
+ }
175
+
176
+ // 5. Initialize NDK
177
+ logger.debug("Initializing NDK (again)");
178
+ await initNDK();
179
+ this.ndk = getNDK();
180
+
181
+ // 6. Publish backend profile (kind:0)
182
+ logger.debug("Publishing backend profile");
183
+ const backendSigner = await config.getBackendSigner();
184
+ const backendName = loadedConfig.backendName || "tenex backend";
185
+ await AgentProfilePublisher.publishBackendProfile(backendSigner, backendName, this.whitelistedPubkeys);
186
+
187
+ // 6b. Initialize NIP-46 signing service (lazy — signers created on first use)
188
+ if (loadedConfig.nip46?.enabled) {
189
+ logger.info("NIP-46 remote signing enabled");
190
+ }
191
+
192
+ // 6c. Initialize OwnerAgentListService (global 14199 management)
193
+ const nip46Service = Nip46SigningService.getInstance();
194
+ const ownerPubkeys = nip46Service.isEnabled()
195
+ ? [...this.whitelistedPubkeys]
196
+ : [backendSigner.pubkey];
197
+ OwnerAgentListService.getInstance().initialize(ownerPubkeys);
198
+
199
+ // 6d. Initialize NudgeSkillWhitelistService (global nudge/skill whitelist)
200
+ // Nudges are user-scoped, not project-scoped — initialize once at daemon level
201
+ // with the same owner pubkeys used for agent list management.
202
+ NudgeSkillWhitelistService.getInstance().initialize(ownerPubkeys);
203
+
204
+ // 7. Initialize runtime lifecycle manager
205
+ logger.debug("Initializing runtime lifecycle manager");
206
+ this.runtimeLifecycle = new RuntimeLifecycle(this.projectsBase);
207
+
208
+ // 8. Initialize subscription manager (before discovery)
209
+ logger.debug("Initializing subscription manager");
210
+ this.subscriptionManager = new SubscriptionManager(
211
+ this.ndk,
212
+ this.handleIncomingEvent.bind(this), // Pass event handler
213
+ this.whitelistedPubkeys,
214
+ this.routingLogger
215
+ );
216
+
217
+ // 9. Start subscription immediately
218
+ // Projects will be discovered naturally as events arrive
219
+ logger.debug("Starting subscription manager");
220
+ await this.subscriptionManager.start();
221
+ logger.debug("Subscription manager started");
222
+
223
+ // 10. Start local streaming socket
224
+ logger.debug("Starting local streaming socket");
225
+ this.streamTransport = new UnixSocketTransport();
226
+ await this.streamTransport.start();
227
+ streamPublisher.setTransport(this.streamTransport);
228
+ logger.info("Local streaming socket started", { path: this.streamTransport.getSocketPath() });
229
+
230
+ // 11. Start automatic conversation indexing job
231
+ getConversationIndexingJob().start();
232
+ logger.info("Automatic conversation indexing job started");
233
+
234
+ // 11b. Start LanceDB maintenance service (periodic compaction)
235
+ getLanceDBMaintenanceService().start();
236
+ logger.info("LanceDB maintenance service started");
237
+
238
+ // 12. Initialize InterventionService (after projects are loaded)
239
+ // This must happen after subscriptions start so agent slugs can be resolved
240
+ logger.debug("Initializing intervention service");
241
+ const interventionService = InterventionService.getInstance();
242
+
243
+ // Wire the agent resolver - allows InterventionService (Layer 3) to resolve agents
244
+ // per-project without depending on @/daemon (Layer 4)
245
+ interventionService.setAgentResolver(this.createAgentResolver());
246
+
247
+ // Wire the active delegation checker - prevents premature intervention notifications
248
+ // when an agent has delegated work that is still running
249
+ interventionService.setActiveDelegationChecker(this.createActiveDelegationChecker());
250
+
251
+ await interventionService.initialize();
252
+
253
+ // 12b. Initialize APNs push notification service
254
+ logger.debug("Initializing APNs service");
255
+ await APNsService.getInstance().initialize();
256
+
257
+ // 13. Initialize restart state manager
258
+ logger.debug("Initializing restart state manager");
259
+ this.restartState = new RestartState(this.daemonDir);
260
+
261
+ // 14. Setup RAL completion listener for graceful restart
262
+ if (this.supervisedMode) {
263
+ this.setupRALCompletionListener();
264
+ }
265
+
266
+ // 15. Start agent definition monitor for auto-upgrades
267
+ logger.debug("Starting agent definition monitor");
268
+ this.agentDefinitionMonitor = new AgentDefinitionMonitor(
269
+ this.ndk,
270
+ { whitelistedPubkeys: this.whitelistedPubkeys },
271
+ () => this.runtimeLifecycle?.getActiveRuntimes() || new Map(),
272
+ );
273
+ await this.agentDefinitionMonitor.start();
274
+ logger.info("Agent definition monitor started");
275
+
276
+ // 16. Setup graceful shutdown
277
+ this.setupShutdownHandlers();
278
+
279
+ this.isRunning = true;
280
+
281
+ } catch (error) {
282
+ logger.error("Failed to start daemon", {
283
+ error: error instanceof Error ? error.message : String(error),
284
+ });
285
+
286
+ // Release lockfile on startup failure
287
+ if (this.lockfile) {
288
+ await this.lockfile.release().catch((releaseError) => {
289
+ logger.warn("Failed to release lockfile during error cleanup", {
290
+ error:
291
+ releaseError instanceof Error
292
+ ? releaseError.message
293
+ : String(releaseError),
294
+ });
295
+ });
296
+ }
297
+
298
+ throw error;
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Initialize required directories for daemon operations
304
+ */
305
+ private async initializeDirectories(): Promise<void> {
306
+ // Use global daemon directory instead of project-local .tenex
307
+ this.daemonDir = config.getConfigPath("daemon");
308
+
309
+ const dirs = [
310
+ this.daemonDir,
311
+ path.join(this.daemonDir, "logs"),
312
+ config.getConfigPath("agents"),
313
+ ];
314
+
315
+ for (const dir of dirs) {
316
+ await fs.mkdir(dir, { recursive: true });
317
+ }
318
+
319
+ }
320
+
321
+ /**
322
+ * Acquire daemon lockfile to prevent multiple instances
323
+ */
324
+ private async acquireDaemonLock(): Promise<void> {
325
+ const lockfilePath = path.join(config.getConfigPath("daemon"), "tenex.lock");
326
+ this.lockfile = new Lockfile(lockfilePath);
327
+ await this.lockfile.acquire();
328
+ }
329
+
330
+ /**
331
+ * Build project ID from event
332
+ */
333
+ private buildProjectId(event: NDKEvent): string {
334
+ const dTag = event.tags.find((t) => t[0] === "d")?.[1];
335
+ if (!dTag) {
336
+ throw new Error("Project event missing d tag");
337
+ }
338
+ return `31933:${event.pubkey}:${dTag}`;
339
+ }
340
+
341
+ /**
342
+ * Handle incoming events from the subscription (telemetry wrapper)
343
+ */
344
+ private async handleIncomingEvent(event: NDKEvent): Promise<void> {
345
+ // Check if this daemon should trace this event at all.
346
+ // This prevents noisy traces when multiple backends are running.
347
+ // Only trace events we'll actually process:
348
+ // - Project events from whitelisted authors (for discovery)
349
+ // - Other events only if we have a runtime OR can boot one
350
+ const knownAgentPubkeys = new Set(this.agentPubkeyToProjects.keys());
351
+ const activeRuntimes = this.runtimeLifecycle?.getActiveRuntimes() || new Map();
352
+ if (
353
+ !DaemonRouter.shouldTraceEvent(
354
+ event,
355
+ this.knownProjects,
356
+ knownAgentPubkeys,
357
+ this.whitelistedPubkeys,
358
+ activeRuntimes
359
+ )
360
+ ) {
361
+ // Not our event - drop silently without creating a span
362
+ return;
363
+ }
364
+
365
+ const span = createEventSpan(event);
366
+
367
+ return otelContext.with(trace.setSpan(otelContext.active(), span), async () => {
368
+ try {
369
+ await this.processIncomingEvent(event, span);
370
+ endSpanSuccess(span);
371
+ } catch (error) {
372
+ if (!event.id) {
373
+ throw new Error("Event ID not found", { cause: error });
374
+ }
375
+ if (!event.id) {
376
+ throw new Error("Event ID not found", { cause: error });
377
+ }
378
+ logger.error("Error handling incoming event", {
379
+ error: error instanceof Error ? error.message : String(error),
380
+ eventId: event.id,
381
+ });
382
+ endSpanError(span, error);
383
+ }
384
+ });
385
+ }
386
+
387
+ /**
388
+ * Process incoming event (pure business logic, telemetry-free)
389
+ */
390
+ private async processIncomingEvent(
391
+ event: NDKEvent,
392
+ span: Span
393
+ ): Promise<void> {
394
+ // Classify event type
395
+ const eventType = AgentEventDecoder.classifyForDaemon(event);
396
+
397
+ // Handle project events (kind 31933)
398
+ if (eventType === "project") {
399
+ addRoutingEvent(span, "project_event", { reason: "kind_31933" });
400
+ await this.handleProjectEvent(event);
401
+ await logDropped(this.routingLogger, event, "Project creation/update event");
402
+ return;
403
+ }
404
+
405
+ // Handle lesson events (kind 4129)
406
+ if (eventType === "lesson") {
407
+ addRoutingEvent(span, "lesson_event", { reason: "kind_4129" });
408
+ await this.handleLessonEvent(event);
409
+ await logDropped(this.routingLogger, event, "Lesson event - hydrated into active runtimes only");
410
+ return;
411
+ }
412
+
413
+ // Handle lesson comment events (kind 1111 with #K: ["4129"])
414
+ if (eventType === "lesson_comment") {
415
+ addRoutingEvent(span, "lesson_comment_event", { reason: "kind_1111_K_4129" });
416
+ await this.handleLessonCommentEvent(event);
417
+ await logDropped(this.routingLogger, event, "Lesson comment - routed to prompt compilers");
418
+ return;
419
+ }
420
+
421
+ // Handle global agent config updates (kind 24020 without a-tag) at daemon level.
422
+ // These update the agent's default config in storage and don't need project context.
423
+ // With a-tag: falls through to normal A-tag routing for project-scoped updates.
424
+ if (AgentEventDecoder.isConfigUpdate(event) && !event.tagValue("a")) {
425
+ addRoutingEvent(span, "agent_config_global", { reason: "kind_24020_no_a_tag" });
426
+ await this.handleGlobalAgentConfigUpdate(event);
427
+ return;
428
+ }
429
+
430
+ // Filter out agent events without p-tags (but allow root events)
431
+ // Root events are conversation starters and don't need p-tags
432
+ const isRootEvent = !AgentEventDecoder.getReplyTarget(event);
433
+ if (
434
+ DaemonRouter.isAgentEvent(event, this.agentPubkeyToProjects) &&
435
+ !DaemonRouter.hasPTagsToSystemEntities(event, this.whitelistedPubkeys, this.agentPubkeyToProjects) &&
436
+ !isRootEvent
437
+ ) {
438
+ addRoutingEvent(span, "dropped", { reason: "agent_event_without_p_tags" });
439
+ await logDropped(this.routingLogger, event, "Agent event without p-tags to system entities");
440
+ return;
441
+ }
442
+
443
+ // Determine target project
444
+ const activeRuntimes = this.runtimeLifecycle?.getActiveRuntimes() || new Map();
445
+ const routingResult = DaemonRouter.determineTargetProject(
446
+ event,
447
+ this.knownProjects,
448
+ this.agentPubkeyToProjects,
449
+ activeRuntimes
450
+ );
451
+
452
+ if (!routingResult.projectId) {
453
+ // Log routing failures for kind:1 events to diagnose agent "disappearing"
454
+ addRoutingEvent(span, "dropped", { reason: routingResult.reason });
455
+ await logDropped(this.routingLogger, event, routingResult.reason);
456
+ return;
457
+ }
458
+
459
+ addRoutingEvent(span, "route_to_project", {
460
+ projectId: routingResult.projectId,
461
+ method: routingResult.method
462
+ });
463
+
464
+ await this.routeEventToProject(event, routingResult, span);
465
+ }
466
+
467
+ /**
468
+ * Route event to a specific project (business logic)
469
+ */
470
+ private async routeEventToProject(
471
+ event: NDKEvent,
472
+ routingResult: RoutingDecision,
473
+ span: Span
474
+ ): Promise<void> {
475
+ if (!this.runtimeLifecycle) {
476
+ logger.error("RuntimeLifecycle not initialized");
477
+ return;
478
+ }
479
+
480
+ const projectId = routingResult.projectId;
481
+ if (!projectId) {
482
+ addRoutingEvent(span, "error", { error: "no_project_id" });
483
+ await logDropped(this.routingLogger, event, "No project ID in routing result");
484
+ return;
485
+ }
486
+
487
+ const project = this.knownProjects.get(projectId);
488
+ if (!project) {
489
+ addRoutingEvent(span, "error", { error: "unknown_project" });
490
+ await logDropped(this.routingLogger, event, "Project not found in known projects");
491
+ return;
492
+ }
493
+
494
+ // Check if runtime exists
495
+ let runtime = this.runtimeLifecycle.getRuntime(projectId);
496
+
497
+ if (!runtime) {
498
+ // Only kind:1 (Text) and kind:24000 (TenexBootProject) can boot projects
499
+ const canBootProject = event.kind === 1 || event.kind === 24000;
500
+
501
+ if (!canBootProject) {
502
+ addRoutingEvent(span, "dropped", { reason: "no_runtime_and_cannot_boot" });
503
+ await logDropped(
504
+ this.routingLogger,
505
+ event,
506
+ `Project not running and kind:${event.kind} cannot boot projects`
507
+ );
508
+ return;
509
+ }
510
+
511
+ // Start the project runtime
512
+ try {
513
+ addRoutingEvent(span, "project_runtime_start", {
514
+ title: project.tagValue("title") || "untitled",
515
+ bootKind: event.kind,
516
+ });
517
+ runtime = await this.runtimeLifecycle.startRuntime(projectId, project);
518
+ await this.updateSubscriptionWithProjectAgents(projectId, runtime);
519
+ } catch (error) {
520
+ logger.error("Failed to start runtime", { projectId, error });
521
+ await logDropped(this.routingLogger, event, "Failed to start runtime");
522
+ return;
523
+ }
524
+ }
525
+
526
+ // Log successful routing
527
+ if (!routingResult.matchedTags) {
528
+ throw new Error("Routing matchedTags not found");
529
+ }
530
+ if (!routingResult.method) {
531
+ throw new Error("Routing method not found");
532
+ }
533
+ if (!routingResult.matchedTags) {
534
+ throw new Error("Routing matchedTags not found");
535
+ }
536
+ if (!routingResult.method) {
537
+ throw new Error("Routing method not found");
538
+ }
539
+ if (routingResult.method !== "none") {
540
+ await logRouted(
541
+ this.routingLogger,
542
+ event,
543
+ projectId,
544
+ routingResult.method,
545
+ routingResult.matchedTags
546
+ );
547
+ }
548
+
549
+ // Handle the event with crash isolation
550
+ try {
551
+ if (!event.id) {
552
+ throw new Error("Event ID not found");
553
+ }
554
+ await runtime.handleEvent(event);
555
+
556
+ // Check for intervention triggers (completion or user response)
557
+ await this.checkInterventionTriggers(event, runtime, projectId);
558
+ } catch (error) {
559
+ logger.error("Project runtime crashed", { projectId, eventId: event.id });
560
+ await this.runtimeLifecycle.handleRuntimeCrash(projectId, runtime);
561
+ throw error; // Re-throw to mark span as error
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Check if an event triggers intervention logic.
567
+ *
568
+ * Completion detection:
569
+ * - Event is kind:1
570
+ * - Event author is an agent (not whitelisted user)
571
+ * - Event p-tags a whitelisted pubkey
572
+ * - That whitelisted pubkey is the author of the root event for this conversation
573
+ *
574
+ * User response detection:
575
+ * - Event is kind:1
576
+ * - Event author is a whitelisted user
577
+ * - Event is a reply in an existing conversation
578
+ */
579
+ private async checkInterventionTriggers(
580
+ event: NDKEvent,
581
+ runtime: ProjectRuntime,
582
+ projectId: string
583
+ ): Promise<void> {
584
+ const interventionService = InterventionService.getInstance();
585
+ if (!interventionService.isEnabled()) {
586
+ return;
587
+ }
588
+
589
+ // Only process kind:1 events
590
+ if (event.kind !== 1) {
591
+ return;
592
+ }
593
+
594
+ const context = runtime.getContext();
595
+ if (!context) {
596
+ return;
597
+ }
598
+
599
+ const eventTimestamp = (event.created_at || 0) * 1000; // Convert to ms
600
+
601
+ // Get conversation ID from the event (e-tag or reply target)
602
+ const replyTarget = AgentEventDecoder.getReplyTarget(event);
603
+ if (!replyTarget) {
604
+ // This is a root event, not a reply - no intervention needed
605
+ return;
606
+ }
607
+
608
+ // Find the conversation for this event
609
+ const conversation = ConversationStore.findByEventId(replyTarget);
610
+ if (!conversation) {
611
+ // Conversation not found - can't determine root author
612
+ return;
613
+ }
614
+
615
+ const conversationId = conversation.id || replyTarget;
616
+ const rootAuthorPubkey = conversation.getRootAuthorPubkey();
617
+ if (!rootAuthorPubkey) {
618
+ return;
619
+ }
620
+
621
+ // Set the project context for InterventionService per-event
622
+ // This ensures the service loads/saves state for the correct project
623
+ // Must await to prevent race conditions during project switch:
624
+ // - setProject flushes pending writes before updating currentProjectId
625
+ // - If not awaited, onUserResponse/onAgentCompletion could run under wrong project
626
+ try {
627
+ await interventionService.setProject(projectId);
628
+ } catch (error) {
629
+ logger.error("Failed to set intervention project context", {
630
+ projectId: projectId.substring(0, 12),
631
+ error: error instanceof Error ? error.message : String(error),
632
+ });
633
+ // Continue processing - intervention is optional, don't block event handling
634
+ }
635
+
636
+ const isUserEvent = this.whitelistedPubkeys.includes(event.pubkey);
637
+ const isAgentEvent = this.agentPubkeyToProjects.has(event.pubkey);
638
+
639
+ if (isUserEvent) {
640
+ // User response - potentially cancel intervention timer
641
+ interventionService.onUserResponse(
642
+ conversationId,
643
+ eventTimestamp,
644
+ event.pubkey
645
+ );
646
+ } else if (isAgentEvent) {
647
+ // Check if agent is p-tagging the root author (completion signal)
648
+ const pTags = event.tags.filter((t) => t[0] === "p").map((t) => t[1]);
649
+ const pTagsRootAuthor = pTags.includes(rootAuthorPubkey);
650
+
651
+ if (pTagsRootAuthor) {
652
+ // Agent completed work and notified the user
653
+ // Find the last user message timestamp in the conversation
654
+ // This is used to determine if the user was recently active
655
+ // We look for messages from the root author (conversation owner), not all whitelisted users
656
+ const messages = conversation.getAllMessages();
657
+ let lastUserMessageTime: number | undefined;
658
+ for (let i = messages.length - 1; i >= 0; i--) {
659
+ const msg = messages[i];
660
+ if (msg.pubkey === rootAuthorPubkey) {
661
+ if (msg.timestamp) {
662
+ // Convert from seconds to ms
663
+ lastUserMessageTime = msg.timestamp * 1000;
664
+ break;
665
+ }
666
+ // Message from root author without timestamp - continue searching
667
+ }
668
+ }
669
+
670
+ interventionService.onAgentCompletion(
671
+ conversationId,
672
+ eventTimestamp,
673
+ event.pubkey,
674
+ rootAuthorPubkey,
675
+ projectId,
676
+ lastUserMessageTime
677
+ );
678
+ }
679
+ }
680
+ }
681
+
682
+ /** Create an agent resolver for InterventionService to resolve agents per-project. */
683
+ private createAgentResolver(): (projectId: string, agentSlug: string) => AgentResolutionResult {
684
+ return (projectId: string, agentSlug: string): AgentResolutionResult => {
685
+ // Get active runtimes
686
+ const activeRuntimes = this.runtimeLifecycle?.getActiveRuntimes();
687
+ if (!activeRuntimes) {
688
+ // RuntimeLifecycle not initialized - transient failure
689
+ return { status: "runtime_unavailable" };
690
+ }
691
+
692
+ // Find the runtime for this project
693
+ const runtime = activeRuntimes.get(projectId);
694
+ if (!runtime) {
695
+ // Runtime not active for this project - transient failure
696
+ // (Project might not be booted yet, or was stopped)
697
+ return { status: "runtime_unavailable" };
698
+ }
699
+
700
+ // Get the project context
701
+ const context = runtime.getContext();
702
+ if (!context) {
703
+ // Context not available - transient failure
704
+ return { status: "runtime_unavailable" };
705
+ }
706
+
707
+ // Look up the agent by slug in the project's agent registry
708
+ const agent = context.agentRegistry.getAgent(agentSlug);
709
+ if (!agent) {
710
+ // Agent slug not found in this project - permanent failure
711
+ return { status: "agent_not_found" };
712
+ }
713
+
714
+ // Successfully resolved
715
+ return { status: "resolved", pubkey: agent.pubkey };
716
+ };
717
+ }
718
+
719
+ /**
720
+ * Create an active delegation checker function for InterventionService.
721
+ * This allows Layer 3 (InterventionService) to check if a conversation
722
+ * has active outgoing delegations without directly depending on RALRegistry.
723
+ *
724
+ * Returns a function that:
725
+ * - Returns true if the agent+conversation has pending delegations
726
+ * - Returns false otherwise
727
+ *
728
+ * CRITICAL: This prevents premature intervention notifications when an agent
729
+ * has delegated work that is still running. The intervention should only
730
+ * trigger when the entire delegation tree has completed.
731
+ */
732
+ private createActiveDelegationChecker(): ActiveDelegationCheckerFn {
733
+ return (agentPubkey: string, conversationId: string): boolean => {
734
+ const pendingDelegations = RALRegistry.getInstance().getConversationPendingDelegations(
735
+ agentPubkey,
736
+ conversationId
737
+ );
738
+ return pendingDelegations.length > 0;
739
+ };
740
+ }
741
+
742
+ /**
743
+ * Collect all agent pubkeys and definition IDs from active runtimes
744
+ */
745
+ private collectAgentData(): { pubkeys: Set<Hexpubkey>; definitionIds: Set<string> } {
746
+ const pubkeys = new Set<Hexpubkey>();
747
+ const definitionIds = new Set<string>();
748
+
749
+ if (!this.runtimeLifecycle) {
750
+ return { pubkeys, definitionIds };
751
+ }
752
+
753
+ const activeRuntimes = this.runtimeLifecycle.getActiveRuntimes();
754
+ for (const [pid, rt] of activeRuntimes) {
755
+ const context = rt.getContext();
756
+ if (!context) {
757
+ throw new Error(
758
+ `Runtime for project ${pid} has no context during agent collection`
759
+ );
760
+ }
761
+
762
+ const agents = context.agentRegistry.getAllAgents();
763
+ for (const agent of agents) {
764
+ pubkeys.add(agent.pubkey);
765
+
766
+ if (agent.eventId) {
767
+ definitionIds.add(agent.eventId);
768
+ }
769
+ }
770
+ }
771
+
772
+ return { pubkeys, definitionIds };
773
+ }
774
+
775
+ /**
776
+ * Update subscription with agent pubkeys and definition IDs from all active runtimes.
777
+ * Also sets up the onAgentAdded callback to keep routing synchronized when
778
+ * agents are created dynamically via agents_write tool.
779
+ */
780
+ private async updateSubscriptionWithProjectAgents(
781
+ projectId: string,
782
+ runtime: ProjectRuntime
783
+ ): Promise<void> {
784
+ if (!this.subscriptionManager) return;
785
+
786
+ try {
787
+ const { pubkeys: allAgentPubkeys, definitionIds: allAgentDefinitionIds } =
788
+ this.collectAgentData();
789
+
790
+ // Rebuild the routing map from scratch
791
+ this.agentPubkeyToProjects.clear();
792
+
793
+ // Track which projects each agent belongs to
794
+ const activeRuntimes = this.runtimeLifecycle?.getActiveRuntimes() || new Map();
795
+ for (const [pid, rt] of activeRuntimes) {
796
+ const context = rt.getContext();
797
+ if (!context) {
798
+ throw new Error(
799
+ `Runtime for project ${pid} has no context during subscription update`
800
+ );
801
+ }
802
+
803
+ const agents = context.agentRegistry.getAllAgents();
804
+ for (const agent of agents) {
805
+ if (!this.agentPubkeyToProjects.has(agent.pubkey)) {
806
+ this.agentPubkeyToProjects.set(agent.pubkey, new Set());
807
+ }
808
+
809
+ const projectSet = this.agentPubkeyToProjects.get(agent.pubkey);
810
+ if (!projectSet) {
811
+ throw new Error(
812
+ `Agent pubkey ${agent.pubkey.slice(0, 8)} missing from agentPubkeyToProjects after set`
813
+ );
814
+ }
815
+ projectSet.add(pid);
816
+ }
817
+ }
818
+
819
+ // Update agent mentions subscription
820
+ this.subscriptionManager.updateAgentMentions(Array.from(allAgentPubkeys));
821
+
822
+ // Sync per-agent lesson subscriptions: add new, remove stale
823
+ this.syncLessonSubscriptions(allAgentDefinitionIds);
824
+
825
+ // Set up callback for dynamic agent additions (e.g., via agents_write tool)
826
+ // This ensures new agents are immediately routable without requiring a restart
827
+ const context = runtime.getContext();
828
+ if (context) {
829
+ context.setOnAgentAdded((agent) => {
830
+ this.handleDynamicAgentAdded(projectId, agent);
831
+ });
832
+ }
833
+ } catch (error) {
834
+ logger.error("Failed to update subscription with project agents", {
835
+ projectId,
836
+ error: error instanceof Error ? error.message : String(error),
837
+ });
838
+ }
839
+ }
840
+
841
+ /**
842
+ * Sync per-agent lesson subscriptions: add subscriptions for new definition IDs,
843
+ * remove subscriptions for definition IDs no longer active.
844
+ */
845
+ private syncLessonSubscriptions(currentDefinitionIds: Set<string>): void {
846
+ if (!this.subscriptionManager) return;
847
+
848
+ // Collect existing lesson subscription IDs from the subscription manager
849
+ const existingIds = this.trackedLessonDefinitionIds;
850
+
851
+ // Add new
852
+ for (const id of currentDefinitionIds) {
853
+ if (!existingIds.has(id)) {
854
+ this.subscriptionManager.addLessonSubscription(id);
855
+ }
856
+ }
857
+
858
+ // Remove stale
859
+ for (const id of existingIds) {
860
+ if (!currentDefinitionIds.has(id)) {
861
+ this.subscriptionManager.removeLessonSubscription(id);
862
+ }
863
+ }
864
+
865
+ this.trackedLessonDefinitionIds = new Set(currentDefinitionIds);
866
+ }
867
+
868
+ /**
869
+ * Handle a dynamically added agent (e.g., created via agents_write tool).
870
+ * Updates the routing map and subscription to make the agent immediately routable.
871
+ */
872
+ private handleDynamicAgentAdded(projectId: string, agent: AgentInstance): void {
873
+ // Add to routing map
874
+ if (!this.agentPubkeyToProjects.has(agent.pubkey)) {
875
+ this.agentPubkeyToProjects.set(agent.pubkey, new Set());
876
+ }
877
+ const projectSet = this.agentPubkeyToProjects.get(agent.pubkey);
878
+ if (projectSet) {
879
+ projectSet.add(projectId);
880
+ }
881
+
882
+ // Update subscriptions
883
+ if (this.subscriptionManager) {
884
+ const allPubkeys = Array.from(this.agentPubkeyToProjects.keys());
885
+ this.subscriptionManager.updateAgentMentions(allPubkeys);
886
+
887
+ // Add lesson subscription if this agent has a definition ID
888
+ if (agent.eventId) {
889
+ this.subscriptionManager.addLessonSubscription(agent.eventId);
890
+ this.trackedLessonDefinitionIds.add(agent.eventId);
891
+ }
892
+ }
893
+
894
+ // Register with global 14199 service
895
+ const dTag = projectId.split(":").slice(2).join(":");
896
+ OwnerAgentListService.getInstance().registerAgents(dTag, [agent.pubkey]);
897
+
898
+ logger.info("Dynamic agent added to routing", {
899
+ projectId,
900
+ agentSlug: agent.slug,
901
+ agentPubkey: agent.pubkey.slice(0, 8),
902
+ });
903
+ }
904
+
905
+ /**
906
+ * Handle global agent config updates (kind 24020 without a-tag).
907
+ * Updates agent storage directly and reloads the agent in all running runtimes.
908
+ */
909
+ private async handleGlobalAgentConfigUpdate(event: NDKEvent): Promise<void> {
910
+ const agentPubkey = event.tagValue("p");
911
+ if (!agentPubkey) {
912
+ logger.warn("Global agent config update missing p-tag", { eventId: event.id });
913
+ return;
914
+ }
915
+
916
+ await agentStorage.initialize();
917
+ const storedAgent = await agentStorage.loadAgent(agentPubkey);
918
+ if (!storedAgent) {
919
+ logger.warn("Agent not found for global config update", {
920
+ agentPubkey: agentPubkey.substring(0, 8),
921
+ });
922
+ return;
923
+ }
924
+
925
+ // Extract config from event tags
926
+ const newModel = event.tagValue("model");
927
+ const toolTags = TagExtractor.getToolTags(event);
928
+ const newToolNames = toolTags.map((tool) => tool.name).filter(Boolean);
929
+ const hasPMTag = event.tags.some((tag) => tag[0] === "pm");
930
+
931
+ // Build default config update (partial update semantics)
932
+ const defaultUpdates: AgentDefaultConfig = {};
933
+
934
+ const hasModelTag = event.tags.some((tag) => tag[0] === "model");
935
+ if (hasModelTag && newModel) {
936
+ defaultUpdates.model = newModel;
937
+ }
938
+
939
+ const hasToolTags = event.tags.some((tag) => tag[0] === "tool");
940
+ if (hasToolTags) {
941
+ defaultUpdates.tools = newToolNames;
942
+ }
943
+
944
+ let configUpdated = false;
945
+
946
+ const defaultUpdated = await agentStorage.updateDefaultConfig(agentPubkey, defaultUpdates);
947
+ if (defaultUpdated) configUpdated = true;
948
+
949
+ // PM designation uses authoritative snapshot semantics
950
+ const pmUpdated = await agentStorage.updateAgentIsPM(agentPubkey, hasPMTag);
951
+ if (pmUpdated) configUpdated = true;
952
+
953
+ if (!configUpdated) {
954
+ logger.info("No config changes for global agent config update", {
955
+ agentSlug: storedAgent.slug,
956
+ agentPubkey: agentPubkey.substring(0, 8),
957
+ });
958
+ return;
959
+ }
960
+
961
+ logger.info("Applied global agent config update", {
962
+ agentSlug: storedAgent.slug,
963
+ agentPubkey: agentPubkey.substring(0, 8),
964
+ hasModel: !!newModel,
965
+ toolCount: newToolNames.length,
966
+ hasPM: hasPMTag,
967
+ });
968
+
969
+ // Reload agent in all running runtimes that have it
970
+ const activeRuntimes = this.runtimeLifecycle?.getActiveRuntimes() || new Map();
971
+ for (const [, runtime] of activeRuntimes) {
972
+ const context = runtime.getContext();
973
+ if (!context) continue;
974
+
975
+ const agent = context.getAgentByPubkey(agentPubkey);
976
+ if (!agent) continue;
977
+
978
+ await context.agentRegistry.reloadAgent(agentPubkey);
979
+
980
+ if (context.statusPublisher) {
981
+ await context.statusPublisher.publishImmediately();
982
+ }
983
+ }
984
+ }
985
+
986
+ /**
987
+ * Handle project creation/update events
988
+ */
989
+ private async handleProjectEvent(event: NDKEvent): Promise<void> {
990
+ const projectId = this.buildProjectId(event);
991
+
992
+ const isDeleted = event.tags.some((tag: string[]) => tag[0] === "deleted");
993
+ if (isDeleted) {
994
+ if (this.knownProjects.has(projectId)) {
995
+ this.knownProjects.delete(projectId);
996
+ this.pendingRestartBootProjects.delete(projectId);
997
+
998
+ if (this.runtimeLifecycle?.getRuntime(projectId)) {
999
+ try {
1000
+ await this.killRuntime(projectId);
1001
+ } catch (error) {
1002
+ logger.error("Failed to stop runtime for deleted project", {
1003
+ projectId,
1004
+ error: error instanceof Error ? error.message : String(error),
1005
+ });
1006
+ }
1007
+ }
1008
+
1009
+ if (this.subscriptionManager) {
1010
+ this.subscriptionManager.updateKnownProjects(Array.from(this.knownProjects.keys()));
1011
+ }
1012
+ }
1013
+
1014
+ logger.info("Ignored deleted project event", { projectId });
1015
+ return;
1016
+ }
1017
+
1018
+ const project = new NDKProject(getNDK(), event.rawEvent());
1019
+ const isNewProject = !this.knownProjects.has(projectId);
1020
+
1021
+ this.knownProjects.set(projectId, project);
1022
+
1023
+ // Update subscription for new projects
1024
+ if (isNewProject && this.subscriptionManager) {
1025
+ this.subscriptionManager.updateKnownProjects(Array.from(this.knownProjects.keys()));
1026
+ }
1027
+
1028
+ // Route to active runtime if exists
1029
+ let runtime = this.runtimeLifecycle?.getRuntime(projectId);
1030
+ if (runtime) {
1031
+ await runtime.handleEvent(event);
1032
+ await this.updateSubscriptionWithProjectAgents(projectId, runtime);
1033
+ }
1034
+
1035
+ // Auto-boot newly discovered projects that match boot patterns
1036
+ if (isNewProject && !runtime && this.autoBootPatterns.length > 0) {
1037
+ const dTag = event.tags.find((t) => t[0] === "d")?.[1] || "";
1038
+ const matchingPattern = this.autoBootPatterns.find((pattern) =>
1039
+ dTag.toLowerCase().includes(pattern.toLowerCase())
1040
+ );
1041
+
1042
+ if (matchingPattern && this.runtimeLifecycle) {
1043
+ const projectTitle = project.tagValue("title") || dTag;
1044
+ logger.info("Auto-booting project matching pattern", {
1045
+ projectId,
1046
+ projectTitle,
1047
+ dTag,
1048
+ matchedPattern: matchingPattern,
1049
+ });
1050
+
1051
+ try {
1052
+ runtime = await this.runtimeLifecycle.startRuntime(projectId, project);
1053
+ await this.updateSubscriptionWithProjectAgents(projectId, runtime);
1054
+ // Clear any pending restart boot entry since we've successfully started
1055
+ this.pendingRestartBootProjects.delete(projectId);
1056
+ logger.info("Auto-booted project successfully", { projectId, projectTitle });
1057
+ } catch (error) {
1058
+ logger.error("Failed to auto-boot project", {
1059
+ projectId,
1060
+ projectTitle,
1061
+ error: error instanceof Error ? error.message : String(error),
1062
+ });
1063
+ }
1064
+ }
1065
+ }
1066
+
1067
+ // Auto-boot projects from restart state when they are discovered or retried
1068
+ // Drop the isNewProject guard: already-known projects that failed to boot in loadRestartState
1069
+ // need another chance when their project event is re-processed
1070
+ if (!runtime && this.pendingRestartBootProjects.has(projectId)) {
1071
+ if (this.runtimeLifecycle) {
1072
+ const projectTitle = project.tagValue("title") || event.tags.find((t) => t[0] === "d")?.[1] || "untitled";
1073
+ logger.info("Auto-booting project from restart state (deferred)", {
1074
+ projectId,
1075
+ projectTitle,
1076
+ });
1077
+
1078
+ try {
1079
+ runtime = await this.runtimeLifecycle.startRuntime(projectId, project);
1080
+ await this.updateSubscriptionWithProjectAgents(projectId, runtime);
1081
+ this.pendingRestartBootProjects.delete(projectId);
1082
+ logger.info("Auto-booted project from restart state (deferred) successfully", {
1083
+ projectId,
1084
+ projectTitle,
1085
+ remainingPending: this.pendingRestartBootProjects.size,
1086
+ });
1087
+ } catch (error) {
1088
+ logger.error("Failed to auto-boot project from restart state (deferred)", {
1089
+ projectId,
1090
+ projectTitle,
1091
+ error: error instanceof Error ? error.message : String(error),
1092
+ });
1093
+ // Remove from pending to avoid repeated failures
1094
+ this.pendingRestartBootProjects.delete(projectId);
1095
+ }
1096
+ }
1097
+ }
1098
+ }
1099
+
1100
+ /**
1101
+ * Handle lesson events (kind 4129) - hydrate into active runtimes only
1102
+ * Does NOT start new project runtimes
1103
+ */
1104
+ private async handleLessonEvent(event: NDKEvent): Promise<void> {
1105
+ const span = lessonTracer.startSpan("tenex.lesson.received", {
1106
+ attributes: {
1107
+ "lesson.event_id": event.id?.substring(0, 16) || "unknown",
1108
+ "lesson.publisher": event.pubkey?.substring(0, 16) || "unknown",
1109
+ "lesson.created_at": event.created_at || 0,
1110
+ },
1111
+ });
1112
+
1113
+ try {
1114
+ const lesson = NDKAgentLesson.from(event);
1115
+ span.setAttribute("lesson.title", lesson.title || "untitled");
1116
+
1117
+ // Check if we should trust this lesson
1118
+ if (!shouldTrustLesson(lesson, event.pubkey)) {
1119
+ span.setAttribute("lesson.rejected", true);
1120
+ span.setAttribute("lesson.rejection_reason", "trust_check_failed");
1121
+ span.end();
1122
+ return;
1123
+ }
1124
+
1125
+ const agentDefinitionId = lesson.agentDefinitionId;
1126
+ const lessonAuthorPubkey = event.pubkey;
1127
+ span.setAttribute("lesson.agent_definition_id", agentDefinitionId?.substring(0, 16) || "none");
1128
+ span.setAttribute("lesson.author_pubkey", lessonAuthorPubkey?.substring(0, 16) || "unknown");
1129
+
1130
+ // Hydrate lesson into ACTIVE runtimes only (don't start new ones)
1131
+ const activeRuntimes = this.runtimeLifecycle?.getActiveRuntimes() || new Map();
1132
+ span.setAttribute("lesson.active_runtimes_count", activeRuntimes.size);
1133
+
1134
+ let totalMatches = 0;
1135
+ let totalAgentsChecked = 0;
1136
+
1137
+ for (const [projectId, runtime] of activeRuntimes) {
1138
+ try {
1139
+ const context = runtime.getContext();
1140
+ if (!context) {
1141
+ continue;
1142
+ }
1143
+
1144
+ const allAgents = context.agentRegistry.getAllAgents();
1145
+ totalAgentsChecked += allAgents.length;
1146
+
1147
+ // Match agents by EITHER:
1148
+ // 1. Author pubkey (the agent published this lesson)
1149
+ // 2. Definition eventId (lesson references agent's definition via e-tag)
1150
+ const matchingAgents = allAgents.filter((agent: AgentInstance) => {
1151
+ // Always match if the agent authored this lesson
1152
+ if (agent.pubkey === lessonAuthorPubkey) {
1153
+ return true;
1154
+ }
1155
+ // Also match if lesson references this agent's definition (and agent has an eventId)
1156
+ if (agentDefinitionId && agent.eventId === agentDefinitionId) {
1157
+ return true;
1158
+ }
1159
+ return false;
1160
+ });
1161
+
1162
+ if (matchingAgents.length === 0) {
1163
+ // Log all agent info for debugging
1164
+ const agentInfo = allAgents.map((a: AgentInstance) => ({
1165
+ slug: a.slug,
1166
+ pubkey: a.pubkey.substring(0, 16),
1167
+ eventId: a.eventId?.substring(0, 16) || "none",
1168
+ }));
1169
+ span.addEvent("no_matching_agents_in_project", {
1170
+ "project.id": projectId,
1171
+ "project.agent_count": allAgents.length,
1172
+ "project.agents": JSON.stringify(agentInfo),
1173
+ "lesson.agent_definition_id": agentDefinitionId?.substring(0, 16) || "none",
1174
+ "lesson.author_pubkey": lessonAuthorPubkey?.substring(0, 16) || "unknown",
1175
+ });
1176
+ continue;
1177
+ }
1178
+
1179
+ // Store the lesson for each matching agent
1180
+ for (const agent of matchingAgents) {
1181
+ const matchedByAuthor = agent.pubkey === lessonAuthorPubkey;
1182
+ const matchedByEventId = agentDefinitionId && agent.eventId === agentDefinitionId;
1183
+ const matchReason = matchedByAuthor && matchedByEventId
1184
+ ? "author_and_event_id"
1185
+ : matchedByAuthor
1186
+ ? "author_pubkey"
1187
+ : "event_id";
1188
+
1189
+ context.addLesson(agent.pubkey, lesson);
1190
+ totalMatches++;
1191
+ span.addEvent("lesson_stored", {
1192
+ "agent.slug": agent.slug,
1193
+ "agent.pubkey": agent.pubkey.substring(0, 16),
1194
+ "project.id": projectId,
1195
+ "lesson.title": lesson.title || "untitled",
1196
+ "match_reason": matchReason,
1197
+ });
1198
+ logger.info("Stored lesson for agent", {
1199
+ agentSlug: agent.slug,
1200
+ lessonTitle: lesson.title,
1201
+ lessonId: event.id?.substring(0, 8),
1202
+ matchReason,
1203
+ });
1204
+ }
1205
+ } catch (error) {
1206
+ span.addEvent("hydration_error", {
1207
+ "project.id": projectId,
1208
+ "error": error instanceof Error ? error.message : String(error),
1209
+ });
1210
+ logger.error("Failed to hydrate lesson into project", {
1211
+ projectId,
1212
+ error: error instanceof Error ? error.message : String(error),
1213
+ });
1214
+ }
1215
+ }
1216
+
1217
+ span.setAttribute("lesson.total_agents_checked", totalAgentsChecked);
1218
+ span.setAttribute("lesson.total_matches", totalMatches);
1219
+ span.setAttribute("lesson.stored", totalMatches > 0);
1220
+ span.end();
1221
+ } catch (error) {
1222
+ span.setAttribute("error", true);
1223
+ span.setAttribute("error.message", error instanceof Error ? error.message : String(error));
1224
+ span.end();
1225
+ throw error;
1226
+ }
1227
+ }
1228
+
1229
+ /**
1230
+ * Handle lesson comment events (kind 1111 with #K: ["4129"])
1231
+ * Routes comments to the appropriate PromptCompilerService for prompt refinement.
1232
+ *
1233
+ * The static subscription receives ALL lesson comments from whitelisted authors
1234
+ * (no #p pre-filtering). We use the e-tag (lesson event ID) to find which
1235
+ * agent the comment belongs to, falling back to p-tag if present.
1236
+ */
1237
+ private async handleLessonCommentEvent(event: NDKEvent): Promise<void> {
1238
+ const span = lessonTracer.startSpan("tenex.lesson_comment.received", {
1239
+ attributes: {
1240
+ "comment.event_id": event.id?.substring(0, 16) || "unknown",
1241
+ "comment.author": event.pubkey?.substring(0, 16) || "unknown",
1242
+ "comment.created_at": event.created_at || 0,
1243
+ },
1244
+ });
1245
+
1246
+ try {
1247
+ // Verify author is whitelisted
1248
+ if (!this.whitelistedPubkeys.includes(event.pubkey)) {
1249
+ span.setAttribute("comment.rejected", true);
1250
+ span.setAttribute("comment.rejection_reason", "not_whitelisted");
1251
+ span.end();
1252
+ return;
1253
+ }
1254
+
1255
+ // Extract the lesson event ID from the root 'e' tag (NIP-22)
1256
+ // Try uppercase E tag first (NIP-22 root reference), then lowercase e
1257
+ const upperETag = event.tags.find(
1258
+ (tag) => tag[0] === "E"
1259
+ );
1260
+ const rootETag = event.tags.find(
1261
+ (tag) => tag[0] === "e" && tag[3] === "root"
1262
+ );
1263
+ const anyETag = event.tags.find((tag) => tag[0] === "e");
1264
+ const lessonEventId = upperETag?.[1] || rootETag?.[1] || anyETag?.[1];
1265
+
1266
+ if (!lessonEventId) {
1267
+ span.setAttribute("comment.rejected", true);
1268
+ span.setAttribute("comment.rejection_reason", "no_lesson_reference");
1269
+ span.end();
1270
+ return;
1271
+ }
1272
+
1273
+ span.setAttribute("comment.lesson_event_id", lessonEventId.substring(0, 16));
1274
+
1275
+ // Build the LessonComment object
1276
+ const comment = {
1277
+ id: event.id || "",
1278
+ pubkey: event.pubkey,
1279
+ content: event.content,
1280
+ lessonEventId,
1281
+ createdAt: event.created_at || 0,
1282
+ };
1283
+
1284
+ // Route to active runtimes. Use p-tag if available for direct lookup,
1285
+ // otherwise scan agents to find those with matching lesson event IDs.
1286
+ const agentPubkey = event.tagValue("p");
1287
+ const activeRuntimes = this.runtimeLifecycle?.getActiveRuntimes() || new Map();
1288
+ let routedCount = 0;
1289
+
1290
+ for (const [projectId, runtime] of activeRuntimes) {
1291
+ const context = runtime.getContext();
1292
+ if (!context) continue;
1293
+
1294
+ if (agentPubkey) {
1295
+ // Direct lookup by p-tag
1296
+ const agent = context.getAgentByPubkey(agentPubkey);
1297
+ if (agent) {
1298
+ context.addComment(agentPubkey, comment);
1299
+ routedCount++;
1300
+ logger.debug("Stored lesson comment for agent", {
1301
+ projectId,
1302
+ agentSlug: agent.slug,
1303
+ commentId: event.id?.substring(0, 8),
1304
+ lessonEventId: lessonEventId.substring(0, 8),
1305
+ });
1306
+ }
1307
+ } else {
1308
+ // No p-tag: scan agents for those whose lessons match this event ID
1309
+ for (const agent of context.agentRegistry.getAllAgents()) {
1310
+ const lessons = context.getLessonsForAgent(agent.pubkey);
1311
+ if (lessons.some((l: NDKAgentLesson) => l.id === lessonEventId)) {
1312
+ context.addComment(agent.pubkey, comment);
1313
+ routedCount++;
1314
+ logger.debug("Stored lesson comment for agent (via lesson scan)", {
1315
+ projectId,
1316
+ agentSlug: agent.slug,
1317
+ commentId: event.id?.substring(0, 8),
1318
+ lessonEventId: lessonEventId.substring(0, 8),
1319
+ });
1320
+ }
1321
+ }
1322
+ }
1323
+ }
1324
+
1325
+ span.setAttribute("comment.routed_count", routedCount);
1326
+ span.end();
1327
+ } catch (error) {
1328
+ span.setAttribute("error", true);
1329
+ span.setAttribute("error.message", error instanceof Error ? error.message : String(error));
1330
+ span.end();
1331
+ throw error;
1332
+ }
1333
+ }
1334
+
1335
+ /**
1336
+ * Setup graceful shutdown handlers
1337
+ */
1338
+ private setupShutdownHandlers(): void {
1339
+ /**
1340
+ * Perform graceful shutdown of the daemon.
1341
+ * @param exitCode - Exit code to use (default: 0)
1342
+ * @param isGracefulRestart - If true, persist restart state before shutdown
1343
+ */
1344
+ const shutdown = async (exitCode: number = 0, isGracefulRestart: boolean = false): Promise<void> => {
1345
+ if (isGracefulRestart) {
1346
+ console.log("\n[Daemon] Triggering graceful restart...");
1347
+ } else {
1348
+ console.log("\nShutting down gracefully...");
1349
+ }
1350
+
1351
+ if (!this.isRunning) {
1352
+ process.exit(exitCode);
1353
+ }
1354
+
1355
+ this.isRunning = false;
1356
+
1357
+ try {
1358
+ // Persist booted projects for auto-boot on restart (only for graceful restart)
1359
+ if (isGracefulRestart && this.restartState && this.runtimeLifecycle) {
1360
+ const bootedProjects = this.runtimeLifecycle.getActiveProjectIds();
1361
+ await this.restartState.save(bootedProjects);
1362
+ console.log(`[Daemon] Saved ${bootedProjects.length} booted project(s) for restart`);
1363
+ }
1364
+
1365
+ if (this.streamTransport) {
1366
+ process.stdout.write("Stopping stream transport...");
1367
+ await this.streamTransport.stop();
1368
+ streamPublisher.setTransport(null);
1369
+ this.streamTransport = null;
1370
+ console.log(" done");
1371
+ }
1372
+
1373
+ // Stop conversation indexing job
1374
+ process.stdout.write("Stopping conversation indexing job...");
1375
+ getConversationIndexingJob().stop();
1376
+ console.log(" done");
1377
+
1378
+ // Stop LanceDB maintenance service
1379
+ process.stdout.write("Stopping LanceDB maintenance service...");
1380
+ getLanceDBMaintenanceService().stop();
1381
+ console.log(" done");
1382
+
1383
+ // Stop agent definition monitor
1384
+ if (this.agentDefinitionMonitor) {
1385
+ process.stdout.write("Stopping agent definition monitor...");
1386
+ this.agentDefinitionMonitor.stop();
1387
+ this.agentDefinitionMonitor = null;
1388
+ console.log(" done");
1389
+ }
1390
+
1391
+ // Stop intervention service
1392
+ process.stdout.write("Stopping intervention service...");
1393
+ InterventionService.getInstance().shutdown();
1394
+ console.log(" done");
1395
+
1396
+ // Stop owner agent list service
1397
+ process.stdout.write("Stopping owner agent list service...");
1398
+ OwnerAgentListService.getInstance().shutdown();
1399
+ console.log(" done");
1400
+
1401
+ // Stop NIP-46 signing service
1402
+ process.stdout.write("Stopping NIP-46 signing service...");
1403
+ await Nip46SigningService.getInstance().shutdown();
1404
+ console.log(" done");
1405
+
1406
+ if (this.subscriptionManager) {
1407
+ process.stdout.write("Stopping subscriptions...");
1408
+ this.subscriptionManager.stop();
1409
+ console.log(" done");
1410
+ }
1411
+
1412
+ if (this.runtimeLifecycle) {
1413
+ const stats = this.runtimeLifecycle.getStats();
1414
+ if (stats.activeCount > 0) {
1415
+ console.log(`Stopping ${stats.activeCount} project runtime(s)...`);
1416
+ }
1417
+ await this.runtimeLifecycle.stopAllRuntimes();
1418
+ }
1419
+
1420
+ // Close the global prefix KV store (after all runtimes are stopped)
1421
+ process.stdout.write("Closing storage...");
1422
+ await prefixKVStore.forceClose();
1423
+ console.log(" done");
1424
+
1425
+ if (this.shutdownHandlers.length > 0) {
1426
+ process.stdout.write("Running shutdown handlers...");
1427
+ for (const handler of this.shutdownHandlers) {
1428
+ await handler();
1429
+ }
1430
+ console.log(" done");
1431
+ }
1432
+
1433
+ if (this.lockfile) {
1434
+ await this.lockfile.release();
1435
+ }
1436
+
1437
+ process.stdout.write("Flushing telemetry...");
1438
+ const conversationSpanManager = getConversationSpanManager();
1439
+ conversationSpanManager.shutdown();
1440
+ await shutdownTelemetry();
1441
+ console.log(" done");
1442
+
1443
+ if (isGracefulRestart) {
1444
+ console.log("[Daemon] Graceful restart complete - exiting with code 0");
1445
+ } else {
1446
+ console.log("Shutdown complete.");
1447
+ }
1448
+ process.exit(exitCode);
1449
+ } catch (error) {
1450
+ logger.error("Error during shutdown", { error });
1451
+ process.exit(1);
1452
+ }
1453
+ };
1454
+
1455
+ // Store shutdown function for use by triggerGracefulRestart
1456
+ this.shutdownFn = shutdown;
1457
+
1458
+ // SIGHUP handler - deferred restart in supervised mode, immediate shutdown otherwise
1459
+ const handleSighup = async (): Promise<void> => {
1460
+ if (this.supervisedMode) {
1461
+ // Ignore duplicate SIGHUP if restart is already pending or in progress
1462
+ if (this.pendingRestart || this.restartInProgress) {
1463
+ logger.info("[Daemon] SIGHUP received but restart already pending/in progress, ignoring");
1464
+ console.log("Restart already pending, ignoring duplicate SIGHUP");
1465
+ return;
1466
+ }
1467
+
1468
+ this.pendingRestart = true;
1469
+ const activeRalCount = RALRegistry.getInstance().getTotalActiveCount();
1470
+
1471
+ console.log("\n[Daemon] SIGHUP received - initiating deferred restart");
1472
+ logger.info("[Daemon] SIGHUP received - initiating deferred restart", {
1473
+ activeRalCount,
1474
+ });
1475
+
1476
+ // If no active RALs, trigger restart immediately
1477
+ if (activeRalCount === 0) {
1478
+ console.log("[Daemon] No active RALs, triggering immediate graceful restart");
1479
+ await this.triggerGracefulRestart();
1480
+ } else {
1481
+ console.log(`[Daemon] Waiting for ${activeRalCount} active RAL(s) to complete before restart...`);
1482
+ // The RAL completion listener will trigger restart when count hits 0
1483
+ }
1484
+ } else {
1485
+ // Non-supervised mode: immediate shutdown
1486
+ shutdown();
1487
+ }
1488
+ };
1489
+
1490
+ process.on("SIGTERM", () => shutdown());
1491
+ process.on("SIGINT", () => shutdown());
1492
+ process.on("SIGHUP", () => handleSighup());
1493
+
1494
+ // Handle uncaught exceptions - exit with code 1 to trigger crash counter
1495
+ process.on("uncaughtException", (error) => {
1496
+ logger.error("Uncaught exception", {
1497
+ error: error.message,
1498
+ stack: error.stack,
1499
+ });
1500
+ // Use exit code 1 to indicate a crash, not a graceful restart
1501
+ // This ensures the wrapper's crash counter is incremented
1502
+ shutdown(1);
1503
+ });
1504
+
1505
+ process.on("unhandledRejection", (reason, promise) => {
1506
+ logger.error("Unhandled rejection", {
1507
+ reason: String(reason),
1508
+ promise: String(promise),
1509
+ });
1510
+ // Don't shutdown - most unhandled rejections are not critical
1511
+ // e.g., relay rejections like "replaced: have newer event"
1512
+ });
1513
+ }
1514
+
1515
+ /**
1516
+ * Setup listener for RAL completion events to trigger deferred restart.
1517
+ * Called when supervised mode is enabled.
1518
+ */
1519
+ private setupRALCompletionListener(): void {
1520
+ const ralRegistry = RALRegistry.getInstance();
1521
+
1522
+ // Subscribe to RAL updates
1523
+ ralRegistry.on("updated", (_projectId: string, _conversationId: string) => {
1524
+ // Only check if restart is pending
1525
+ if (!this.pendingRestart) {
1526
+ return;
1527
+ }
1528
+
1529
+ const activeRalCount = ralRegistry.getTotalActiveCount();
1530
+ logger.debug("[Daemon] RAL update received during pending restart", {
1531
+ activeRalCount,
1532
+ });
1533
+
1534
+ // When count hits 0, trigger graceful restart
1535
+ if (activeRalCount === 0) {
1536
+ console.log("[Daemon] All RALs completed, triggering graceful restart");
1537
+ this.triggerGracefulRestart().catch((error) => {
1538
+ logger.error("[Daemon] Failed to trigger graceful restart", {
1539
+ error: error instanceof Error ? error.message : String(error),
1540
+ });
1541
+ process.exit(1);
1542
+ });
1543
+ }
1544
+ });
1545
+
1546
+ logger.debug("[Daemon] RAL completion listener registered for supervised mode");
1547
+ }
1548
+
1549
+ /**
1550
+ * Trigger graceful restart: persist state and exit cleanly.
1551
+ * The wrapper process will respawn the daemon.
1552
+ */
1553
+ private async triggerGracefulRestart(): Promise<void> {
1554
+ // Guard against concurrent calls (race condition from multiple RAL updates)
1555
+ if (this.restartInProgress) {
1556
+ logger.debug("[Daemon] Graceful restart already in progress, ignoring duplicate trigger");
1557
+ return;
1558
+ }
1559
+ this.restartInProgress = true;
1560
+
1561
+ // Use the unified shutdown function with graceful restart flag
1562
+ if (this.shutdownFn) {
1563
+ await this.shutdownFn(0, true);
1564
+ } else {
1565
+ // Fallback if shutdown function not yet initialized (shouldn't happen)
1566
+ logger.error("[Daemon] Shutdown function not initialized, exiting with code 0");
1567
+ process.exit(0);
1568
+ }
1569
+ }
1570
+
1571
+ /**
1572
+ * Add a custom shutdown handler
1573
+ */
1574
+ addShutdownHandler(handler: () => Promise<void>): void {
1575
+ this.shutdownHandlers.push(handler);
1576
+ }
1577
+
1578
+ /**
1579
+ * Load restart state and queue previously booted projects for auto-boot.
1580
+ * Called after daemon is fully initialized to restore state from a graceful restart.
1581
+ *
1582
+ * Note: Projects may not be discovered yet via SubscriptionManager, so we store
1583
+ * the project IDs and attempt to boot them when they are discovered in handleProjectEvent.
1584
+ */
1585
+ async loadRestartState(): Promise<void> {
1586
+ if (!this.restartState) {
1587
+ return;
1588
+ }
1589
+
1590
+ const state = await this.restartState.load();
1591
+ if (!state) {
1592
+ return;
1593
+ }
1594
+
1595
+ console.log(`[Daemon] Found restart state from ${new Date(state.requestedAt).toISOString()}`);
1596
+ console.log(`[Daemon] Queuing ${state.bootedProjects.length} project(s) for auto-boot from restart state`);
1597
+
1598
+ // Store projects to boot - they will be booted when discovered via handleProjectEvent
1599
+ this.pendingRestartBootProjects = new Set(state.bootedProjects);
1600
+
1601
+ // Attempt to boot any projects that are already known
1602
+ // (This handles the case where some projects were discovered before loadRestartState was called)
1603
+ let bootedCount = 0;
1604
+
1605
+ for (const projectId of state.bootedProjects) {
1606
+ const project = this.knownProjects.get(projectId);
1607
+ if (!project) {
1608
+ // Project not yet discovered - will be booted when discovered
1609
+ logger.debug("[Daemon] Project from restart state not yet discovered, deferring boot", {
1610
+ projectId: projectId.substring(0, 20),
1611
+ });
1612
+ continue;
1613
+ }
1614
+
1615
+ if (!this.runtimeLifecycle) {
1616
+ logger.error("[Daemon] RuntimeLifecycle not initialized during restart state loading");
1617
+ break;
1618
+ }
1619
+
1620
+ // Already running? Skip
1621
+ if (this.runtimeLifecycle.getRuntime(projectId)) {
1622
+ this.pendingRestartBootProjects.delete(projectId);
1623
+ continue;
1624
+ }
1625
+
1626
+ try {
1627
+ const runtime = await this.runtimeLifecycle.startRuntime(projectId, project);
1628
+ await this.updateSubscriptionWithProjectAgents(projectId, runtime);
1629
+ this.pendingRestartBootProjects.delete(projectId);
1630
+ bootedCount++;
1631
+ logger.info("[Daemon] Auto-booted project from restart state", {
1632
+ projectId: projectId.substring(0, 20),
1633
+ });
1634
+ } catch (error) {
1635
+ logger.error("[Daemon] Failed to auto-boot project from restart state", {
1636
+ projectId: projectId.substring(0, 20),
1637
+ error: error instanceof Error ? error.message : String(error),
1638
+ });
1639
+ // Keep in pending set - will retry when project event is re-processed in handleProjectEvent
1640
+ }
1641
+ }
1642
+
1643
+ // Clear restart state file now that we've loaded it
1644
+ // (Pending boots are tracked in memory via pendingRestartBootProjects)
1645
+ await this.restartState.clear();
1646
+
1647
+ const pendingCount = this.pendingRestartBootProjects.size;
1648
+ if (pendingCount > 0) {
1649
+ console.log(`[Daemon] Restart state loaded: ${bootedCount} booted immediately, ${pendingCount} pending discovery`);
1650
+ } else {
1651
+ console.log(`[Daemon] Restart state processed: ${bootedCount} booted`);
1652
+ }
1653
+ }
1654
+
1655
+ /**
1656
+ * Get daemon status
1657
+ */
1658
+ getStatus(): DaemonStatus {
1659
+ // Count total agents across all known projects
1660
+ let totalAgents = 0;
1661
+ for (const project of this.knownProjects.values()) {
1662
+ const agentTags = project.tags.filter((t) => t[0] === "agent");
1663
+ totalAgents += agentTags.length;
1664
+ }
1665
+
1666
+ const runtimeStats = this.runtimeLifecycle?.getStats() || {
1667
+ activeCount: 0,
1668
+ startingCount: 0,
1669
+ };
1670
+
1671
+ return {
1672
+ running: this.isRunning,
1673
+ knownProjects: this.knownProjects.size,
1674
+ activeProjects: runtimeStats.activeCount,
1675
+ startingProjects: runtimeStats.startingCount,
1676
+ totalAgents,
1677
+ uptime: process.uptime(),
1678
+ memoryUsage: process.memoryUsage(),
1679
+ };
1680
+ }
1681
+
1682
+ /**
1683
+ * Get known projects
1684
+ */
1685
+ getKnownProjects(): Map<string, NDKProject> {
1686
+ return this.knownProjects;
1687
+ }
1688
+
1689
+ /**
1690
+ * Get active runtimes
1691
+ */
1692
+ getActiveRuntimes(): Map<string, ProjectRuntime> {
1693
+ return this.runtimeLifecycle?.getActiveRuntimes() || new Map();
1694
+ }
1695
+
1696
+ /**
1697
+ * Kill a specific project runtime
1698
+ * @param projectId - The project ID to kill
1699
+ * @throws Error if the runtime is not found or not running
1700
+ */
1701
+ async killRuntime(projectId: string): Promise<void> {
1702
+ if (!this.runtimeLifecycle) {
1703
+ throw new Error("RuntimeLifecycle not initialized");
1704
+ }
1705
+
1706
+
1707
+ try {
1708
+ await this.runtimeLifecycle.stopRuntime(projectId);
1709
+
1710
+ // Update subscription to remove this project's agent pubkeys
1711
+ await this.updateSubscriptionAfterRuntimeRemoved(projectId);
1712
+
1713
+ } catch (error) {
1714
+ logger.error(`Failed to kill project runtime: ${projectId}`, {
1715
+ error: error instanceof Error ? error.message : String(error),
1716
+ });
1717
+ throw error;
1718
+ }
1719
+ }
1720
+
1721
+ /**
1722
+ * Restart a specific project runtime
1723
+ * @param projectId - The project ID to restart
1724
+ * @throws Error if the runtime is not found or restart fails
1725
+ */
1726
+ async restartRuntime(projectId: string): Promise<void> {
1727
+ if (!this.runtimeLifecycle) {
1728
+ throw new Error("RuntimeLifecycle not initialized");
1729
+ }
1730
+
1731
+ const project = this.knownProjects.get(projectId);
1732
+ if (!project) {
1733
+ throw new Error(`Project not found: ${projectId}`);
1734
+ }
1735
+
1736
+
1737
+ try {
1738
+ const runtime = await this.runtimeLifecycle.restartRuntime(projectId, project);
1739
+
1740
+ // Update subscription with potentially new agent pubkeys
1741
+ await this.updateSubscriptionWithProjectAgents(projectId, runtime);
1742
+
1743
+ } catch (error) {
1744
+ logger.error(`Failed to restart project runtime: ${projectId}`, {
1745
+ error: error instanceof Error ? error.message : String(error),
1746
+ });
1747
+ throw error;
1748
+ }
1749
+ }
1750
+
1751
+ /**
1752
+ * Start a specific project runtime
1753
+ * @param projectId - The project ID to start
1754
+ * @throws Error if the project is not found or already running
1755
+ */
1756
+ async startRuntime(projectId: string): Promise<void> {
1757
+ if (!this.runtimeLifecycle) {
1758
+ throw new Error("RuntimeLifecycle not initialized");
1759
+ }
1760
+
1761
+ // Check if project exists in known projects
1762
+ const project = this.knownProjects.get(projectId);
1763
+ if (!project) {
1764
+ throw new Error(`Project not found: ${projectId}`);
1765
+ }
1766
+
1767
+ try {
1768
+ const runtime = await this.runtimeLifecycle.startRuntime(projectId, project);
1769
+
1770
+ // Update subscription with this project's agent pubkeys
1771
+ await this.updateSubscriptionWithProjectAgents(projectId, runtime);
1772
+
1773
+ } catch (error) {
1774
+ logger.error(`Failed to start project runtime: ${projectId}`, {
1775
+ error: error instanceof Error ? error.message : String(error),
1776
+ });
1777
+ throw error;
1778
+ }
1779
+ }
1780
+
1781
+ /**
1782
+ * Update subscription after a runtime has been removed
1783
+ */
1784
+ private async updateSubscriptionAfterRuntimeRemoved(projectId: string): Promise<void> {
1785
+ if (!this.subscriptionManager) return;
1786
+
1787
+ try {
1788
+ // Rebuild agent pubkey mapping without the removed project
1789
+ this.agentPubkeyToProjects.forEach((projectSet, agentPubkey) => {
1790
+ projectSet.delete(projectId);
1791
+ if (projectSet.size === 0) {
1792
+ this.agentPubkeyToProjects.delete(agentPubkey);
1793
+ }
1794
+ });
1795
+
1796
+ // Collect all agent pubkeys and definition IDs from remaining active runtimes
1797
+ const { pubkeys: allAgentPubkeys, definitionIds: allAgentDefinitionIds } =
1798
+ this.collectAgentData();
1799
+
1800
+ this.subscriptionManager.updateAgentMentions(Array.from(allAgentPubkeys));
1801
+ this.syncLessonSubscriptions(allAgentDefinitionIds);
1802
+ } catch (error) {
1803
+ logger.error("Failed to update subscription after runtime removed", {
1804
+ projectId,
1805
+ error: error instanceof Error ? error.message : String(error),
1806
+ });
1807
+ }
1808
+ }
1809
+
1810
+ /**
1811
+ * Stop the daemon
1812
+ */
1813
+ async stop(): Promise<void> {
1814
+ if (!this.isRunning) {
1815
+ logger.warn("Daemon is not running");
1816
+ return;
1817
+ }
1818
+
1819
+
1820
+ this.isRunning = false;
1821
+
1822
+ // Stop streaming socket
1823
+ if (this.streamTransport) {
1824
+ await this.streamTransport.stop();
1825
+ streamPublisher.setTransport(null);
1826
+ this.streamTransport = null;
1827
+ }
1828
+
1829
+ // Stop conversation indexing job
1830
+ getConversationIndexingJob().stop();
1831
+
1832
+ // Stop LanceDB maintenance service
1833
+ getLanceDBMaintenanceService().stop();
1834
+
1835
+ // Stop intervention service
1836
+ InterventionService.getInstance().shutdown();
1837
+
1838
+ // Stop owner agent list service
1839
+ OwnerAgentListService.getInstance().shutdown();
1840
+
1841
+ // Stop NIP-46 signing service
1842
+ await Nip46SigningService.getInstance().shutdown();
1843
+
1844
+ // Stop subscription
1845
+ if (this.subscriptionManager) {
1846
+ this.subscriptionManager.stop();
1847
+ }
1848
+
1849
+ // Stop all active project runtimes
1850
+ if (this.runtimeLifecycle) {
1851
+ await this.runtimeLifecycle.stopAllRuntimes();
1852
+ }
1853
+
1854
+ // Close the global prefix KV store (after all runtimes are stopped)
1855
+ await prefixKVStore.forceClose();
1856
+
1857
+ // Clear state
1858
+ this.knownProjects.clear();
1859
+
1860
+ // Release lockfile
1861
+ if (this.lockfile) {
1862
+ await this.lockfile.release();
1863
+ }
1864
+
1865
+ // Shutdown conversation span manager
1866
+ const conversationSpanManager = getConversationSpanManager();
1867
+ conversationSpanManager.shutdown();
1868
+
1869
+ }
1870
+ }
1871
+
1872
+ // Singleton instance
1873
+ let daemonInstance: Daemon | null = null;
1874
+
1875
+ /**
1876
+ * Get or create the daemon instance
1877
+ */
1878
+ export function getDaemon(): Daemon {
1879
+ if (!daemonInstance) {
1880
+ daemonInstance = new Daemon();
1881
+ }
1882
+ return daemonInstance;
1883
+ }