@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,653 @@
1
+ /**
2
+ * McpSubscriptionService - Manages MCP resource subscriptions with notification delivery
3
+ *
4
+ * Enables agents to subscribe to MCP resource updates within a conversation context.
5
+ * When a notification arrives, it delivers a system-reminder message to the agent
6
+ * in the existing conversation and triggers a new AgentExecutor run.
7
+ *
8
+ * Features:
9
+ * - Persistent subscriptions across restarts (JSON file)
10
+ * - Notification delivery as system-reminder messages
11
+ * - Automatic re-subscription on initialization
12
+ * - Per-conversation subscription tracking
13
+ */
14
+
15
+ import * as fs from "node:fs/promises";
16
+ import * as path from "node:path";
17
+ import { config } from "@/services/ConfigService";
18
+ import { getProjectContext, isProjectContextInitialized } from "@/services/projects";
19
+ import { logger } from "@/utils/logger";
20
+ import { trace } from "@opentelemetry/api";
21
+
22
+ export enum McpSubscriptionStatus {
23
+ ACTIVE = "ACTIVE",
24
+ ERROR = "ERROR",
25
+ STOPPED = "STOPPED",
26
+ }
27
+
28
+ export interface McpSubscription {
29
+ /** Unique subscription ID */
30
+ id: string;
31
+ /** Agent pubkey that created the subscription */
32
+ agentPubkey: string;
33
+ /** Agent slug for display purposes */
34
+ agentSlug: string;
35
+ /** MCP server name */
36
+ serverName: string;
37
+ /** Resource URI being subscribed to */
38
+ resourceUri: string;
39
+ /** Conversation ID where notifications should be delivered */
40
+ conversationId: string;
41
+ /** Root event ID of the conversation (for event routing) */
42
+ rootEventId: string;
43
+ /** Project ID (NIP-33 a-tag format) */
44
+ projectId: string;
45
+ /** Human-readable description */
46
+ description: string;
47
+ /** Current subscription status */
48
+ status: McpSubscriptionStatus;
49
+ /** Number of notifications received */
50
+ notificationsReceived: number;
51
+ /** Timestamp of last notification */
52
+ lastNotificationAt?: number;
53
+ /** Last error message if status is ERROR */
54
+ lastError?: string;
55
+ /** Creation timestamp */
56
+ createdAt: number;
57
+ /** Last update timestamp */
58
+ updatedAt: number;
59
+ }
60
+
61
+ /**
62
+ * Callback type for delivering notifications to conversations.
63
+ * Injected by the initialization layer to avoid circular dependencies.
64
+ */
65
+ export type NotificationDeliveryHandler = (
66
+ subscription: McpSubscription,
67
+ content: string
68
+ ) => Promise<void>;
69
+
70
+ /** Minimal MCPManager interface needed for reading resources */
71
+ interface MCPManagerLike {
72
+ readResource(serverName: string, uri: string): Promise<{ contents: Array<Record<string, unknown>> }>;
73
+ isServerRunning(serverName: string): boolean;
74
+ }
75
+
76
+ export class McpSubscriptionService {
77
+ private static instance: McpSubscriptionService;
78
+ private subscriptions: Map<string, McpSubscription> = new Map();
79
+ private persistencePath: string;
80
+ private isInitialized = false;
81
+ private notificationHandler: NotificationDeliveryHandler | null = null;
82
+ /** Per-subscription handler removal functions (returned by MCPManager.addResourceNotificationHandler) */
83
+ private handlerRemovers: Map<string, () => void> = new Map();
84
+ /** Ref-count of active subscriptions per "server::resource" key */
85
+ private resourceRefCounts: Map<string, number> = new Map();
86
+ /** Previously-seen content item IDs per subscription (for delta tracking) */
87
+ private contentSnapshots: Map<string, Set<string>> = new Map();
88
+
89
+ private constructor() {
90
+ const tenexDir = config.getConfigPath();
91
+ this.persistencePath = path.join(tenexDir, "mcp_subscriptions.json");
92
+ }
93
+
94
+ public static getInstance(): McpSubscriptionService {
95
+ if (!McpSubscriptionService.instance) {
96
+ McpSubscriptionService.instance = new McpSubscriptionService();
97
+ }
98
+ return McpSubscriptionService.instance;
99
+ }
100
+
101
+ /**
102
+ * Reset the singleton instance (for testing)
103
+ */
104
+ public static resetInstance(): void {
105
+ McpSubscriptionService.instance = undefined as unknown as McpSubscriptionService;
106
+ }
107
+
108
+ /**
109
+ * Set the notification delivery handler.
110
+ * Must be called before initialize() for notifications to work.
111
+ */
112
+ public setNotificationHandler(handler: NotificationDeliveryHandler): void {
113
+ this.notificationHandler = handler;
114
+ }
115
+
116
+ /**
117
+ * Initialize the service and restore subscriptions from disk.
118
+ * Re-subscribes all active subscriptions with the MCP servers.
119
+ */
120
+ public async initialize(): Promise<void> {
121
+ if (this.isInitialized) {
122
+ return;
123
+ }
124
+
125
+ try {
126
+ const tenexDir = path.dirname(this.persistencePath);
127
+ await fs.mkdir(tenexDir, { recursive: true });
128
+
129
+ await this.loadSubscriptions();
130
+
131
+ // Re-subscribe all active subscriptions
132
+ for (const subscription of this.subscriptions.values()) {
133
+ if (subscription.status === McpSubscriptionStatus.ACTIVE) {
134
+ try {
135
+ await this.setupMcpSubscription(subscription);
136
+ } catch (error) {
137
+ subscription.status = McpSubscriptionStatus.ERROR;
138
+ subscription.lastError = error instanceof Error ? error.message : String(error);
139
+ subscription.updatedAt = Date.now();
140
+ logger.warn(`Failed to re-establish subscription '${subscription.id}'`, {
141
+ error: subscription.lastError,
142
+ });
143
+ }
144
+ }
145
+ }
146
+
147
+ await this.saveSubscriptions();
148
+ this.isInitialized = true;
149
+
150
+ logger.info(`McpSubscriptionService initialized with ${this.subscriptions.size} subscriptions`);
151
+ trace.getActiveSpan()?.addEvent("mcp_subscription.initialized", {
152
+ "subscriptions.count": this.subscriptions.size,
153
+ });
154
+ } catch (error) {
155
+ logger.error("Failed to initialize McpSubscriptionService", { error });
156
+ throw error;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Create a new MCP resource subscription.
162
+ */
163
+ public async createSubscription(params: {
164
+ agentPubkey: string;
165
+ agentSlug: string;
166
+ serverName: string;
167
+ resourceUri: string;
168
+ conversationId: string;
169
+ rootEventId: string;
170
+ projectId: string;
171
+ description: string;
172
+ }): Promise<McpSubscription> {
173
+ const id = this.generateSubscriptionId();
174
+
175
+ const subscription: McpSubscription = {
176
+ id,
177
+ agentPubkey: params.agentPubkey,
178
+ agentSlug: params.agentSlug,
179
+ serverName: params.serverName,
180
+ resourceUri: params.resourceUri,
181
+ conversationId: params.conversationId,
182
+ rootEventId: params.rootEventId,
183
+ projectId: params.projectId,
184
+ description: params.description,
185
+ status: McpSubscriptionStatus.ACTIVE,
186
+ notificationsReceived: 0,
187
+ createdAt: Date.now(),
188
+ updatedAt: Date.now(),
189
+ };
190
+
191
+ // Setup MCP subscription with notification handler
192
+ await this.setupMcpSubscription(subscription);
193
+
194
+ this.subscriptions.set(id, subscription);
195
+ await this.saveSubscriptions();
196
+
197
+ logger.info(`Created MCP subscription '${id}'`, {
198
+ agent: params.agentSlug,
199
+ server: params.serverName,
200
+ resource: params.resourceUri,
201
+ conversation: params.conversationId.substring(0, 12),
202
+ });
203
+
204
+ trace.getActiveSpan()?.addEvent("mcp_subscription.created", {
205
+ "subscription.id": id,
206
+ "subscription.server": params.serverName,
207
+ "subscription.resource": params.resourceUri,
208
+ });
209
+
210
+ return subscription;
211
+ }
212
+
213
+ /**
214
+ * Stop and remove a subscription.
215
+ */
216
+ public async stopSubscription(subscriptionId: string, agentPubkey: string): Promise<boolean> {
217
+ const subscription = this.subscriptions.get(subscriptionId);
218
+
219
+ if (!subscription) {
220
+ return false;
221
+ }
222
+
223
+ // Authorization check
224
+ if (subscription.agentPubkey !== agentPubkey) {
225
+ return false;
226
+ }
227
+
228
+ try {
229
+ // Unsubscribe from MCP server
230
+ await this.teardownMcpSubscription(subscription);
231
+
232
+ // Remove from in-memory state
233
+ this.subscriptions.delete(subscriptionId);
234
+ this.contentSnapshots.delete(subscriptionId);
235
+ await this.saveSubscriptions();
236
+
237
+ logger.info(`Stopped MCP subscription '${subscriptionId}'`);
238
+ trace.getActiveSpan()?.addEvent("mcp_subscription.stopped", {
239
+ "subscription.id": subscriptionId,
240
+ });
241
+
242
+ return true;
243
+ } catch (error) {
244
+ logger.error(`Failed to stop subscription '${subscriptionId}'`, { error });
245
+ // Still remove from our tracking even if unsubscribe fails
246
+ this.subscriptions.delete(subscriptionId);
247
+ this.contentSnapshots.delete(subscriptionId);
248
+ await this.saveSubscriptions();
249
+ return true;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Get all active subscriptions for an agent in a conversation.
255
+ */
256
+ public getSubscriptionsForAgent(
257
+ agentPubkey: string,
258
+ conversationId?: string
259
+ ): McpSubscription[] {
260
+ const results: McpSubscription[] = [];
261
+ for (const sub of this.subscriptions.values()) {
262
+ if (sub.agentPubkey !== agentPubkey) continue;
263
+ if (conversationId && sub.conversationId !== conversationId) continue;
264
+ results.push(sub);
265
+ }
266
+ return results;
267
+ }
268
+
269
+ /**
270
+ * Get a subscription by ID.
271
+ */
272
+ public getSubscription(subscriptionId: string): McpSubscription | undefined {
273
+ return this.subscriptions.get(subscriptionId);
274
+ }
275
+
276
+ /**
277
+ * Check if an agent has any active subscriptions.
278
+ */
279
+ public hasActiveSubscriptions(agentPubkey: string): boolean {
280
+ for (const sub of this.subscriptions.values()) {
281
+ if (sub.agentPubkey === agentPubkey && sub.status === McpSubscriptionStatus.ACTIVE) {
282
+ return true;
283
+ }
284
+ }
285
+ return false;
286
+ }
287
+
288
+ /**
289
+ * Check if an agent has any subscriptions that can be stopped (ACTIVE or ERROR).
290
+ * Used for dynamic tool injection of mcp_subscription_stop.
291
+ */
292
+ public hasStoppableSubscriptions(agentPubkey: string): boolean {
293
+ for (const sub of this.subscriptions.values()) {
294
+ if (sub.agentPubkey !== agentPubkey) continue;
295
+ if (sub.status === McpSubscriptionStatus.ACTIVE || sub.status === McpSubscriptionStatus.ERROR) {
296
+ return true;
297
+ }
298
+ }
299
+ return false;
300
+ }
301
+
302
+ /**
303
+ * Setup MCP resource subscription and notification listener.
304
+ * Uses dispatcher pattern for handlers (no clobbering) and ref-counting for resources.
305
+ *
306
+ * IMPORTANT: Captures mcpManager reference at setup time so push notification
307
+ * handlers can read resources without depending on AsyncLocalStorage context.
308
+ * MCPManager.initialize() runs outside projectContextStore.run(), so push
309
+ * notification callbacks from the MCP SDK fire without project context.
310
+ */
311
+ private async setupMcpSubscription(subscription: McpSubscription): Promise<void> {
312
+ if (!isProjectContextInitialized()) {
313
+ throw new Error("Project context not available for MCP subscription setup");
314
+ }
315
+
316
+ const projectCtx = getProjectContext();
317
+ const mcpManager = projectCtx.mcpManager;
318
+
319
+ if (!mcpManager) {
320
+ throw new Error("MCPManager not available in project context");
321
+ }
322
+
323
+ if (!mcpManager.isServerRunning(subscription.serverName)) {
324
+ throw new Error(`MCP server '${subscription.serverName}' is not running`);
325
+ }
326
+
327
+ // Capture mcpManager reference for use in the notification callback.
328
+ // Push notifications fire from MCP SDK's transport read callbacks, which
329
+ // execute outside projectContextStore.run() scope. Without this capture,
330
+ // the handler would fail to get mcpManager via getProjectContext().
331
+ const capturedMcpManager: MCPManagerLike = mcpManager;
332
+
333
+ // Register per-subscription notification handler (dispatcher-safe, no clobbering)
334
+ const removeHandler = mcpManager.addResourceNotificationHandler(
335
+ subscription.serverName,
336
+ async (notification: { uri: string }) => {
337
+ if (notification.uri !== subscription.resourceUri) {
338
+ return;
339
+ }
340
+ await this.handleNotification(subscription, notification.uri, capturedMcpManager);
341
+ }
342
+ );
343
+
344
+ // Ref-count: only subscribe to the MCP server resource if this is the first subscription.
345
+ // IMPORTANT: handlerRemovers is set AFTER subscribeToResource succeeds so that
346
+ // teardownMcpSubscription can reliably infer setupSucceeded from its presence.
347
+ const refKey = this.makeResourceRefKey(subscription.serverName, subscription.resourceUri);
348
+ const currentCount = this.resourceRefCounts.get(refKey) ?? 0;
349
+ if (currentCount === 0) {
350
+ try {
351
+ await mcpManager.subscribeToResource(subscription.serverName, subscription.resourceUri);
352
+ } catch (error) {
353
+ // subscribeToResource failed — clean up the handler we already registered
354
+ removeHandler();
355
+ throw error;
356
+ }
357
+ }
358
+
359
+ // Only record state AFTER subscribe succeeds (or was already active via ref-count)
360
+ this.handlerRemovers.set(subscription.id, removeHandler);
361
+ this.resourceRefCounts.set(refKey, currentCount + 1);
362
+
363
+ logger.info(`MCP subscription '${subscription.id}' active`, {
364
+ server: subscription.serverName,
365
+ resource: subscription.resourceUri,
366
+ resourceRefCount: currentCount + 1,
367
+ });
368
+ }
369
+
370
+ /**
371
+ * Teardown MCP subscription.
372
+ * Removes the per-subscription handler and decrements the resource ref-count.
373
+ * Only unsubscribes from the MCP server when the last subscription for a resource is removed.
374
+ */
375
+ private async teardownMcpSubscription(subscription: McpSubscription): Promise<void> {
376
+ // Remove per-subscription notification handler
377
+ const removeHandler = this.handlerRemovers.get(subscription.id);
378
+ const setupSucceeded = removeHandler !== undefined;
379
+ if (removeHandler) {
380
+ removeHandler();
381
+ this.handlerRemovers.delete(subscription.id);
382
+ }
383
+
384
+ // Only decrement ref-count if setup actually succeeded (handler was registered).
385
+ // If setupMcpSubscription failed, ref-count was never incremented, so decrementing
386
+ // here would corrupt the count for other active subscriptions on the same resource.
387
+ if (!setupSucceeded) {
388
+ return;
389
+ }
390
+
391
+ // Decrement ref-count; only unsubscribe from MCP server when count reaches 0
392
+ const refKey = this.makeResourceRefKey(subscription.serverName, subscription.resourceUri);
393
+ const currentCount = this.resourceRefCounts.get(refKey) ?? 0;
394
+ const newCount = currentCount - 1;
395
+
396
+ if (newCount <= 0) {
397
+ this.resourceRefCounts.delete(refKey);
398
+
399
+ if (!isProjectContextInitialized()) {
400
+ return;
401
+ }
402
+
403
+ try {
404
+ const projectCtx = getProjectContext();
405
+ const mcpManager = projectCtx.mcpManager;
406
+
407
+ if (mcpManager && mcpManager.isServerRunning(subscription.serverName)) {
408
+ await mcpManager.unsubscribeFromResource(
409
+ subscription.serverName,
410
+ subscription.resourceUri
411
+ );
412
+ }
413
+ } catch (error) {
414
+ logger.warn("Failed to unsubscribe from MCP resource", {
415
+ subscription: subscription.id,
416
+ error: error instanceof Error ? error.message : String(error),
417
+ });
418
+ }
419
+ } else {
420
+ this.resourceRefCounts.set(refKey, newCount);
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Build a ref-count key for a server+resource pair.
426
+ */
427
+ private makeResourceRefKey(serverName: string, resourceUri: string): string {
428
+ return `${serverName}::${resourceUri}`;
429
+ }
430
+
431
+ /**
432
+ * Handle an MCP resource push notification.
433
+ * Reads the updated resource content and delivers it to the conversation.
434
+ *
435
+ * Uses the captured mcpManager reference from setup time rather than
436
+ * getProjectContext(), since push notifications fire from the MCP SDK's
437
+ * transport callbacks which execute outside AsyncLocalStorage scope.
438
+ */
439
+ private async handleNotification(
440
+ subscription: McpSubscription,
441
+ uri: string,
442
+ mcpManager: MCPManagerLike
443
+ ): Promise<void> {
444
+ // Guard: if the MCP server is no longer running (e.g. during shutdown),
445
+ // there's no point attempting to read the resource.
446
+ if (!mcpManager.isServerRunning(subscription.serverName)) {
447
+ logger.debug(`Ignoring notification for '${subscription.id}': server '${subscription.serverName}' no longer running`);
448
+ return;
449
+ }
450
+
451
+ try {
452
+ const content = await this.readResourceContent(mcpManager, subscription.serverName, uri);
453
+
454
+ if (!content) {
455
+ logger.debug(`Empty notification for subscription '${subscription.id}'`);
456
+ return;
457
+ }
458
+
459
+ // Extract only new items by comparing against previously-seen content
460
+ const newContent = this.extractNewItems(subscription.id, content);
461
+
462
+ if (!newContent) {
463
+ logger.debug(`No new items in notification for subscription '${subscription.id}'`);
464
+ return;
465
+ }
466
+
467
+ await this.deliverNotificationContent(subscription, newContent);
468
+ } catch (error) {
469
+ subscription.status = McpSubscriptionStatus.ERROR;
470
+ subscription.lastError = error instanceof Error ? error.message : String(error);
471
+ subscription.updatedAt = Date.now();
472
+ await this.saveSubscriptions();
473
+
474
+ logger.error(`Failed to handle notification for subscription '${subscription.id}'`, {
475
+ error: subscription.lastError,
476
+ });
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Read a resource and return its text content.
482
+ */
483
+ private async readResourceContent(
484
+ mcpManager: MCPManagerLike,
485
+ serverName: string,
486
+ uri: string
487
+ ): Promise<string> {
488
+ const result = await mcpManager.readResource(serverName, uri);
489
+
490
+ const textContents: string[] = [];
491
+ for (const content of result.contents) {
492
+ if ("text" in content && typeof content.text === "string") {
493
+ textContents.push(content.text);
494
+ } else if ("blob" in content && typeof content.blob === "string") {
495
+ textContents.push(`[Binary content: ${content.blob.length} bytes]`);
496
+ }
497
+ }
498
+
499
+ return textContents.join("\n\n");
500
+ }
501
+
502
+ /**
503
+ * Deliver notification content to the conversation.
504
+ * Updates subscription metrics and calls the registered notification handler.
505
+ */
506
+ private async deliverNotificationContent(
507
+ subscription: McpSubscription,
508
+ content: string
509
+ ): Promise<void> {
510
+ // Update metrics and recover from ERROR state on successful delivery
511
+ subscription.notificationsReceived++;
512
+ subscription.lastNotificationAt = Date.now();
513
+ subscription.updatedAt = Date.now();
514
+ if (subscription.status === McpSubscriptionStatus.ERROR) {
515
+ subscription.status = McpSubscriptionStatus.ACTIVE;
516
+ subscription.lastError = undefined;
517
+ logger.info(`Subscription '${subscription.id}' recovered from ERROR to ACTIVE`);
518
+ }
519
+ await this.saveSubscriptions();
520
+
521
+ // Deliver notification to conversation
522
+ if (this.notificationHandler) {
523
+ await this.notificationHandler(subscription, content);
524
+ } else {
525
+ logger.warn(`No notification handler registered for subscription '${subscription.id}'`);
526
+ }
527
+
528
+ logger.debug(`Delivered notification for subscription '${subscription.id}'`, {
529
+ notificationsTotal: subscription.notificationsReceived,
530
+ contentLength: content.length,
531
+ });
532
+
533
+ trace.getActiveSpan()?.addEvent("mcp_subscription.notification_delivered", {
534
+ "subscription.id": subscription.id,
535
+ "notification.content_length": content.length,
536
+ "notification.total": subscription.notificationsReceived,
537
+ });
538
+ }
539
+
540
+ // ========== Delta Tracking ==========
541
+
542
+ /**
543
+ * Extract only new items from resource content by comparing against previously-seen IDs.
544
+ *
545
+ * Content is expected to be a list of JSON objects (one per line).
546
+ * Each item is identified by its `id` field; if absent, the full line is used as the key.
547
+ *
548
+ * Returns only the lines that are new since the last notification, or null if no new items.
549
+ * Updates the snapshot for the next comparison.
550
+ */
551
+ private extractNewItems(subscriptionId: string, content: string): string | null {
552
+ const lines = content.split("\n").filter((line) => line.trim() !== "");
553
+
554
+ // Build a map of itemId -> line for the current content
555
+ const currentItems = new Map<string, string>();
556
+ for (const line of lines) {
557
+ const itemId = this.extractItemId(line);
558
+ currentItems.set(itemId, line);
559
+ }
560
+
561
+ const previousIds = this.contentSnapshots.get(subscriptionId);
562
+
563
+ // Update the snapshot with the current set of IDs
564
+ this.contentSnapshots.set(subscriptionId, new Set(currentItems.keys()));
565
+
566
+ // If no previous snapshot exists (first notification), deliver everything
567
+ if (!previousIds) {
568
+ return content;
569
+ }
570
+
571
+ // Find items present now but not in the previous snapshot
572
+ const newLines: string[] = [];
573
+ for (const [itemId, line] of currentItems) {
574
+ if (!previousIds.has(itemId)) {
575
+ newLines.push(line);
576
+ }
577
+ }
578
+
579
+ if (newLines.length === 0) {
580
+ return null;
581
+ }
582
+
583
+ logger.debug(`Delta tracking for subscription '${subscriptionId}'`, {
584
+ totalItems: currentItems.size,
585
+ previousItems: previousIds.size,
586
+ newItems: newLines.length,
587
+ });
588
+
589
+ return newLines.join("\n");
590
+ }
591
+
592
+ /**
593
+ * Extract a unique identifier from a content line.
594
+ * Attempts to parse the line as JSON and use the `id` field.
595
+ * Falls back to using the full line content as the identifier.
596
+ */
597
+ private extractItemId(line: string): string {
598
+ try {
599
+ const parsed = JSON.parse(line);
600
+ if (parsed && typeof parsed === "object" && "id" in parsed && parsed.id != null) {
601
+ return String(parsed.id);
602
+ }
603
+ } catch {
604
+ // Not valid JSON — fall through to use the full line
605
+ }
606
+ return line.trim();
607
+ }
608
+
609
+ // ========== Persistence ==========
610
+
611
+ private async loadSubscriptions(): Promise<void> {
612
+ try {
613
+ const data = await fs.readFile(this.persistencePath, "utf-8");
614
+ const subscriptions = JSON.parse(data) as McpSubscription[];
615
+
616
+ for (const sub of subscriptions) {
617
+ this.subscriptions.set(sub.id, sub);
618
+ }
619
+
620
+ logger.debug(`Loaded ${subscriptions.length} MCP subscriptions from disk`);
621
+ } catch (error) {
622
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
623
+ logger.error("Failed to load MCP subscriptions", { error });
624
+ }
625
+ }
626
+ }
627
+
628
+ private async saveSubscriptions(): Promise<void> {
629
+ try {
630
+ const subscriptions = Array.from(this.subscriptions.values());
631
+ await fs.writeFile(this.persistencePath, JSON.stringify(subscriptions, null, 2));
632
+ } catch (error) {
633
+ logger.error("Failed to save MCP subscriptions", { error });
634
+ }
635
+ }
636
+
637
+ private generateSubscriptionId(): string {
638
+ return `mcp-sub-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
639
+ }
640
+
641
+ /**
642
+ * Shutdown all active subscriptions.
643
+ */
644
+ public async shutdown(): Promise<void> {
645
+ for (const subscription of this.subscriptions.values()) {
646
+ if (subscription.status === McpSubscriptionStatus.ACTIVE) {
647
+ await this.teardownMcpSubscription(subscription);
648
+ }
649
+ }
650
+
651
+ logger.info("McpSubscriptionService shutdown complete");
652
+ }
653
+ }