@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,1057 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { ProjectAlreadyRunningError } from "./errors";
4
+ import { getNDK } from "@/nostr/ndkClient";
5
+ import { config } from "@/services/ConfigService";
6
+ import { getProjectContext } from "@/services/projects";
7
+ import { logger } from "@/utils/logger";
8
+ import type NDK from "@nostr-dev-kit/ndk";
9
+ import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
10
+ import { trace } from "@opentelemetry/api";
11
+ import { CronExpressionParser } from "cron-parser";
12
+ import * as cron from "node-cron";
13
+
14
+ /**
15
+ * Boot handler callback type for auto-booting projects.
16
+ * Called when a scheduled task needs to execute but the target project is not running.
17
+ *
18
+ * @param projectId - The project ID to boot
19
+ * @returns Promise that resolves when the project is ready to receive events
20
+ */
21
+ export type ProjectBootHandler = (projectId: string) => Promise<void>;
22
+
23
+ /**
24
+ * Project state resolver callback type for checking project status.
25
+ * Used to determine if a project is already running before attempting to boot.
26
+ *
27
+ * @param projectId - The project ID to check
28
+ * @returns True if the project is currently running, false otherwise
29
+ */
30
+ export type ProjectStateResolver = (projectId: string) => boolean;
31
+
32
+ /**
33
+ * Target pubkey resolver callback type for resolving the target agent.
34
+ * Used to determine the actual target pubkey for a task, potentially rerouting
35
+ * to the project manager if the original target agent is not in the project.
36
+ *
37
+ * @param projectId - The project ID
38
+ * @param originalTargetPubkey - The originally specified target pubkey
39
+ * @returns The resolved pubkey (original or PM)
40
+ */
41
+ export type TargetPubkeyResolver = (projectId: string, originalTargetPubkey: string) => string;
42
+
43
+ /** Truncate a pubkey for logging (first 8 characters) */
44
+ function truncatePubkey(pubkey: string): string {
45
+ return pubkey.substring(0, 8);
46
+ }
47
+
48
+ interface ScheduledTask {
49
+ id: string;
50
+ title?: string; // Human-readable title for the scheduled task
51
+ schedule: string; // Cron expression (for recurring) or ISO timestamp (for one-off)
52
+ prompt: string;
53
+ lastRun?: string;
54
+ nextRun?: string;
55
+ createdAt?: string; // When the task was created
56
+ fromPubkey: string; // Who scheduled this task (the scheduler)
57
+ toPubkey: string; // Target agent that should execute the task
58
+ projectId: string; // Project A-tag ID (format: "31933:authorPubkey:dTag")
59
+ type?: "cron" | "oneoff"; // Task type - defaults to "cron" for backward compatibility
60
+ executeAt?: string; // ISO timestamp for one-off tasks
61
+ }
62
+
63
+ // Export the type so it can be used by other modules
64
+ export type { ScheduledTask };
65
+
66
+ interface CatchUpConfig {
67
+ gracePeriodMs: number; // How far back to look for missed tasks (default: 24h)
68
+ delayBetweenTasksMs: number; // Delay between catch-up executions (default: 5s)
69
+ }
70
+
71
+ const DEFAULT_CATCHUP_CONFIG: CatchUpConfig = {
72
+ gracePeriodMs: 24 * 60 * 60 * 1000, // 24 hours
73
+ delayBetweenTasksMs: 5000, // 5 seconds
74
+ };
75
+
76
+ export class SchedulerService {
77
+ private static instance: SchedulerService;
78
+ private tasks: Map<string, cron.ScheduledTask> = new Map();
79
+ private oneoffTimers: Map<string, NodeJS.Timeout> = new Map(); // Timers for one-off tasks
80
+ private taskMetadata: Map<string, ScheduledTask> = new Map();
81
+ private taskFilePath: string;
82
+ private ndk: NDK | null = null;
83
+
84
+ // Callbacks injected by daemon layer for project management
85
+ // This maintains architectural separation (Layer 3 services don't import Layer 4 daemon)
86
+ private projectBootHandler: ProjectBootHandler | null = null;
87
+ private projectStateResolver: ProjectStateResolver | null = null;
88
+ private targetPubkeyResolver: TargetPubkeyResolver | null = null;
89
+
90
+ private constructor() {
91
+ // Use global location for scheduled tasks since it's a singleton
92
+ const tenexDir = config.getConfigPath();
93
+ this.taskFilePath = path.join(tenexDir, "scheduled_tasks.json");
94
+ }
95
+
96
+ /**
97
+ * Set callbacks for project management (injected by daemon layer).
98
+ * These callbacks enable the scheduler to boot projects and resolve targets
99
+ * without directly importing from the daemon layer.
100
+ *
101
+ * @param bootHandler - Called when a project needs to be booted for a scheduled task
102
+ * @param stateResolver - Called to check if a project is already running
103
+ * @param targetResolver - Called to resolve the target pubkey for a task
104
+ */
105
+ public setProjectCallbacks(
106
+ bootHandler: ProjectBootHandler,
107
+ stateResolver: ProjectStateResolver,
108
+ targetResolver: TargetPubkeyResolver
109
+ ): void {
110
+ this.projectBootHandler = bootHandler;
111
+ this.projectStateResolver = stateResolver;
112
+ this.targetPubkeyResolver = targetResolver;
113
+ logger.debug("Project callbacks registered with SchedulerService");
114
+ }
115
+
116
+ public static getInstance(): SchedulerService {
117
+ if (!SchedulerService.instance) {
118
+ SchedulerService.instance = new SchedulerService();
119
+ }
120
+ return SchedulerService.instance;
121
+ }
122
+
123
+ public async initialize(ndk: NDK, _projectPath?: string): Promise<void> {
124
+ this.ndk = ndk;
125
+
126
+ // Ensure .tenex directory exists
127
+ const tenexDir = path.dirname(this.taskFilePath);
128
+ await fs.mkdir(tenexDir, { recursive: true });
129
+
130
+ // Load existing tasks
131
+ await this.loadTasks();
132
+
133
+ // Check for missed tasks BEFORE starting regular scheduling
134
+ // This ensures catch-ups happen first and update lastRun
135
+ await this.checkMissedTasks();
136
+
137
+ // Start all loaded tasks (cron or one-off based on type)
138
+ for (const task of this.taskMetadata.values()) {
139
+ if (task.type === "oneoff") {
140
+ this.startOneoffTask(task);
141
+ } else {
142
+ this.startTask(task);
143
+ }
144
+ }
145
+
146
+ trace.getActiveSpan()?.addEvent("scheduler.initialized", {
147
+ "tasks.count": this.taskMetadata.size,
148
+ });
149
+ }
150
+
151
+ public async addTask(
152
+ schedule: string,
153
+ prompt: string,
154
+ fromPubkey: string,
155
+ toPubkey: string,
156
+ projectId?: string,
157
+ title?: string
158
+ ): Promise<string> {
159
+ // Validate cron expression
160
+ if (!cron.validate(schedule)) {
161
+ throw new Error(`Invalid cron expression: ${schedule}`);
162
+ }
163
+
164
+ // If projectId not provided, try to get it from current context
165
+ let resolvedProjectId = projectId;
166
+ if (!resolvedProjectId) {
167
+ try {
168
+ const projectCtx = getProjectContext();
169
+ resolvedProjectId = projectCtx.project.tagId();
170
+ } catch {
171
+ throw new Error(
172
+ "projectId is required when scheduling tasks outside of a project context"
173
+ );
174
+ }
175
+ }
176
+
177
+ const taskId = this.generateTaskId();
178
+
179
+ // Store locally for cron management
180
+ const task: ScheduledTask = {
181
+ id: taskId,
182
+ title,
183
+ schedule,
184
+ prompt,
185
+ fromPubkey,
186
+ toPubkey,
187
+ projectId: resolvedProjectId,
188
+ createdAt: new Date().toISOString(),
189
+ };
190
+
191
+ this.taskMetadata.set(taskId, task);
192
+
193
+ // Start the cron task
194
+ this.startTask(task);
195
+
196
+ await this.saveTasks();
197
+
198
+ logger.info(`Created scheduled task ${taskId} with cron schedule: ${schedule}`, {
199
+ projectId: resolvedProjectId,
200
+ fromPubkey: truncatePubkey(fromPubkey),
201
+ toPubkey: truncatePubkey(toPubkey),
202
+ });
203
+ return taskId;
204
+ }
205
+
206
+ /**
207
+ * Add a one-off task that executes once at a specific time.
208
+ * The task is automatically deleted after successful execution.
209
+ */
210
+ public async addOneoffTask(
211
+ executeAt: Date,
212
+ prompt: string,
213
+ fromPubkey: string,
214
+ toPubkey: string,
215
+ projectId?: string,
216
+ title?: string
217
+ ): Promise<string> {
218
+ // Validate execution time is in the future
219
+ const now = new Date();
220
+ if (executeAt <= now) {
221
+ throw new Error(
222
+ `Execution time must be in the future. Received: ${executeAt.toISOString()}`
223
+ );
224
+ }
225
+
226
+ // If projectId not provided, try to get it from current context
227
+ let resolvedProjectId = projectId;
228
+ if (!resolvedProjectId) {
229
+ try {
230
+ const projectCtx = getProjectContext();
231
+ resolvedProjectId = projectCtx.project.tagId();
232
+ } catch {
233
+ throw new Error(
234
+ "projectId is required when scheduling tasks outside of a project context"
235
+ );
236
+ }
237
+ }
238
+
239
+ const taskId = this.generateTaskId();
240
+
241
+ const task: ScheduledTask = {
242
+ id: taskId,
243
+ title,
244
+ schedule: executeAt.toISOString(), // Store ISO timestamp for display
245
+ prompt,
246
+ fromPubkey,
247
+ toPubkey,
248
+ projectId: resolvedProjectId,
249
+ createdAt: new Date().toISOString(),
250
+ type: "oneoff",
251
+ executeAt: executeAt.toISOString(),
252
+ };
253
+
254
+ this.taskMetadata.set(taskId, task);
255
+
256
+ // Start the timer for this one-off task
257
+ this.startOneoffTask(task);
258
+
259
+ await this.saveTasks();
260
+
261
+ logger.info(
262
+ `Created one-off scheduled task ${taskId} to execute at: ${executeAt.toISOString()}`,
263
+ {
264
+ projectId: resolvedProjectId,
265
+ fromPubkey: truncatePubkey(fromPubkey),
266
+ toPubkey: truncatePubkey(toPubkey),
267
+ executeAt: executeAt.toISOString(),
268
+ }
269
+ );
270
+
271
+ return taskId;
272
+ }
273
+
274
+ public async removeTask(taskId: string): Promise<boolean> {
275
+ // Stop cron task if exists
276
+ const cronTask = this.tasks.get(taskId);
277
+ if (cronTask) {
278
+ cronTask.stop();
279
+ this.tasks.delete(taskId);
280
+ }
281
+
282
+ // Stop one-off timer if exists
283
+ const timer = this.oneoffTimers.get(taskId);
284
+ if (timer) {
285
+ clearTimeout(timer);
286
+ this.oneoffTimers.delete(taskId);
287
+ }
288
+
289
+ // Remove from local storage
290
+ this.taskMetadata.delete(taskId);
291
+ await this.saveTasks();
292
+
293
+ logger.info(`Removed scheduled task ${taskId}`);
294
+ return true;
295
+ }
296
+
297
+ public async getTasks(projectId?: string): Promise<ScheduledTask[]> {
298
+ const allTasks = Array.from(this.taskMetadata.values());
299
+
300
+ // If projectId is provided, filter tasks by that project
301
+ if (projectId) {
302
+ return allTasks.filter((task) => task.projectId === projectId);
303
+ }
304
+
305
+ // Return all tasks if no filter specified
306
+ return allTasks;
307
+ }
308
+
309
+ private startTask(task: ScheduledTask): void {
310
+ const cronTask = cron.schedule(
311
+ task.schedule,
312
+ async () => {
313
+ try {
314
+ await this.executeTask(task);
315
+ } catch (error) {
316
+ logger.error(`Failed to execute scheduled task ${task.id}:`, error);
317
+ }
318
+ },
319
+ {
320
+ timezone: "UTC",
321
+ }
322
+ );
323
+
324
+ this.tasks.set(task.id, cronTask);
325
+ trace.getActiveSpan()?.addEvent("scheduler.task_started", {
326
+ "task.id": task.id,
327
+ "task.schedule": task.schedule,
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Start a one-off task timer.
333
+ * Uses setTimeout to schedule execution at the specified time.
334
+ */
335
+ private startOneoffTask(task: ScheduledTask): void {
336
+ if (!task.executeAt) {
337
+ logger.error(`One-off task ${task.id} missing executeAt timestamp, deleting task`);
338
+ this.purgeCorruptedOneoffTask(task.id);
339
+ return;
340
+ }
341
+
342
+ const executeAt = new Date(task.executeAt);
343
+
344
+ // Validate the timestamp - if invalid, delayMs will be NaN
345
+ if (Number.isNaN(executeAt.getTime())) {
346
+ logger.error(
347
+ `One-off task ${task.id} has invalid executeAt timestamp: ${task.executeAt}, deleting task`
348
+ );
349
+ this.purgeCorruptedOneoffTask(task.id);
350
+ return;
351
+ }
352
+
353
+ // Check if task already ran (has lastRun set) - prevents re-execution after crash
354
+ if (task.lastRun) {
355
+ logger.warn(
356
+ `One-off task ${task.id} already executed (lastRun: ${task.lastRun}), purging orphaned task`
357
+ );
358
+ this.purgeCorruptedOneoffTask(task.id);
359
+ return;
360
+ }
361
+
362
+ const now = new Date();
363
+ const delayMs = executeAt.getTime() - now.getTime();
364
+
365
+ if (delayMs <= 0) {
366
+ // Task time has already passed - this shouldn't happen for new tasks
367
+ // but might happen during startup if task was missed
368
+ logger.warn(`One-off task ${task.id} execution time has passed, executing immediately`);
369
+ this.executeOneoffTask(task);
370
+ return;
371
+ }
372
+
373
+ // Use setTimeout for the delay
374
+ // Note: setTimeout max delay is ~24.8 days (2^31 - 1 ms)
375
+ // For longer delays, we'll need to chain timeouts
376
+ const maxDelay = 2147483647; // Max 32-bit signed int
377
+ if (delayMs > maxDelay) {
378
+ // Schedule a re-check after max delay
379
+ const timer = setTimeout(() => {
380
+ this.oneoffTimers.delete(task.id);
381
+ this.startOneoffTask(task); // Re-evaluate
382
+ }, maxDelay);
383
+ this.oneoffTimers.set(task.id, timer);
384
+ } else {
385
+ const timer = setTimeout(async () => {
386
+ await this.executeOneoffTask(task);
387
+ }, delayMs);
388
+ this.oneoffTimers.set(task.id, timer);
389
+ }
390
+
391
+ trace.getActiveSpan()?.addEvent("scheduler.oneoff_task_started", {
392
+ "task.id": task.id,
393
+ "task.executeAt": task.executeAt,
394
+ "task.delayMs": delayMs,
395
+ });
396
+
397
+ logger.debug(
398
+ `One-off task ${task.id} scheduled to execute in ${Math.round(delayMs / 1000)}s`
399
+ );
400
+ }
401
+
402
+ /**
403
+ * Purge a corrupted or orphaned one-off task from storage.
404
+ * Used when a task has invalid data or has already been executed.
405
+ */
406
+ private purgeCorruptedOneoffTask(taskId: string): void {
407
+ this.oneoffTimers.delete(taskId);
408
+ this.taskMetadata.delete(taskId);
409
+ // Save asynchronously - don't block on corrupted task cleanup
410
+ this.saveTasks().catch((error) => {
411
+ logger.error(`Failed to save after purging corrupted task ${taskId}:`, error);
412
+ });
413
+
414
+ trace.getActiveSpan()?.addEvent("scheduler.oneoff_task_purged", {
415
+ "task.id": taskId,
416
+ "reason": "corrupted_or_orphaned",
417
+ });
418
+ }
419
+
420
+ /**
421
+ * Execute a one-off task and auto-delete it after successful execution.
422
+ */
423
+ private async executeOneoffTask(task: ScheduledTask): Promise<void> {
424
+ try {
425
+ await this.executeTask(task);
426
+
427
+ // Auto-delete after successful execution
428
+ logger.info(`One-off task ${task.id} executed successfully, auto-deleting`);
429
+ this.oneoffTimers.delete(task.id);
430
+ this.taskMetadata.delete(task.id);
431
+ await this.saveTasks();
432
+
433
+ trace.getActiveSpan()?.addEvent("scheduler.oneoff_task_completed", {
434
+ "task.id": task.id,
435
+ });
436
+ } catch (error) {
437
+ logger.error(`Failed to execute one-off task ${task.id}:`, error);
438
+ // Don't delete on failure - keep for potential retry or manual investigation
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Check for tasks that missed their execution window during downtime.
444
+ * Called once during initialize().
445
+ */
446
+ private async checkMissedTasks(
447
+ catchUpConfig: CatchUpConfig = DEFAULT_CATCHUP_CONFIG
448
+ ): Promise<void> {
449
+ const now = new Date();
450
+ const gracePeriodStart = new Date(now.getTime() - catchUpConfig.gracePeriodMs);
451
+
452
+ trace.getActiveSpan()?.addEvent("scheduler.catchup_check_started", {
453
+ "catchup.gracePeriodMs": catchUpConfig.gracePeriodMs,
454
+ "catchup.gracePeriodStart": gracePeriodStart.toISOString(),
455
+ "catchup.taskCount": this.taskMetadata.size,
456
+ });
457
+
458
+ logger.info("Checking for missed scheduled tasks", {
459
+ gracePeriodMs: catchUpConfig.gracePeriodMs,
460
+ gracePeriodStart: gracePeriodStart.toISOString(),
461
+ taskCount: this.taskMetadata.size,
462
+ });
463
+
464
+ // First, delete any expired one-off tasks that are past the grace period
465
+ await this.deleteExpiredOneoffTasks(gracePeriodStart);
466
+
467
+ const missedTasks = this.getMissedTasks(now, gracePeriodStart);
468
+ await this.executeCatchUpTasks(missedTasks, catchUpConfig.delayBetweenTasksMs);
469
+ }
470
+
471
+ /**
472
+ * Delete one-off tasks that have expired (executeAt < gracePeriodStart).
473
+ * These tasks missed their window and won't be caught up.
474
+ */
475
+ private async deleteExpiredOneoffTasks(gracePeriodStart: Date): Promise<void> {
476
+ const expiredTasks: string[] = [];
477
+
478
+ for (const task of this.taskMetadata.values()) {
479
+ if (task.type !== "oneoff" || !task.executeAt) continue;
480
+
481
+ const executeAt = new Date(task.executeAt);
482
+ if (Number.isNaN(executeAt.getTime())) {
483
+ // Invalid timestamp - mark for deletion
484
+ logger.warn(`One-off task ${task.id} has invalid executeAt: ${task.executeAt}, deleting`);
485
+ expiredTasks.push(task.id);
486
+ continue;
487
+ }
488
+
489
+ // Check if task is past the grace period (too old to catch up)
490
+ if (executeAt < gracePeriodStart) {
491
+ logger.info(
492
+ `One-off task ${task.id} expired (executeAt: ${executeAt.toISOString()} < gracePeriod: ${gracePeriodStart.toISOString()}), deleting`,
493
+ {
494
+ taskId: task.id,
495
+ executeAt: task.executeAt,
496
+ gracePeriodStart: gracePeriodStart.toISOString(),
497
+ }
498
+ );
499
+ expiredTasks.push(task.id);
500
+ }
501
+ }
502
+
503
+ // Delete all expired tasks
504
+ if (expiredTasks.length > 0) {
505
+ for (const taskId of expiredTasks) {
506
+ this.oneoffTimers.delete(taskId);
507
+ this.taskMetadata.delete(taskId);
508
+ }
509
+ await this.saveTasks();
510
+
511
+ trace.getActiveSpan()?.addEvent("scheduler.expired_oneoff_tasks_deleted", {
512
+ "catchup.expiredTasksDeleted": expiredTasks.length,
513
+ });
514
+
515
+ logger.info(`Deleted ${expiredTasks.length} expired one-off task(s)`);
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Scan all tasks and return those that missed execution within the grace period.
521
+ */
522
+ private getMissedTasks(now: Date, gracePeriodStart: Date): ScheduledTask[] {
523
+ const missedTasks: ScheduledTask[] = [];
524
+
525
+ for (const task of this.taskMetadata.values()) {
526
+ const missedExecution = this.getMissedExecutionTime(task, now, gracePeriodStart);
527
+ if (missedExecution) {
528
+ missedTasks.push(task);
529
+ }
530
+ }
531
+
532
+ return missedTasks;
533
+ }
534
+
535
+ /**
536
+ * Check if a task has a missed execution within the grace period.
537
+ * Returns the missed execution time if within grace period, null otherwise.
538
+ */
539
+ private getMissedExecutionTime(
540
+ task: ScheduledTask,
541
+ now: Date,
542
+ gracePeriodStart: Date
543
+ ): Date | null {
544
+ // Handle one-off tasks differently
545
+ if (task.type === "oneoff") {
546
+ return this.getMissedOneoffExecutionTime(task, now, gracePeriodStart);
547
+ }
548
+
549
+ // Skip tasks that have never run - let normal scheduling handle first run
550
+ if (!task.lastRun) {
551
+ logger.debug(`Task ${task.id} has no lastRun, skipping catch-up check`);
552
+ return null;
553
+ }
554
+
555
+ // Validate lastRun date
556
+ const lastRunDate = new Date(task.lastRun);
557
+ if (Number.isNaN(lastRunDate.getTime())) {
558
+ logger.warn(
559
+ `Task ${task.id} has invalid lastRun date: ${task.lastRun}, skipping catch-up check`
560
+ );
561
+ return null;
562
+ }
563
+
564
+ try {
565
+ // Parse cron expression starting from lastRun
566
+ const interval = CronExpressionParser.parse(task.schedule, {
567
+ currentDate: lastRunDate,
568
+ tz: "UTC",
569
+ });
570
+
571
+ // Get the next scheduled execution AFTER lastRun
572
+ const nextScheduledExecution = interval.next().toDate();
573
+
574
+ // Check if this execution was missed (it's in the past)
575
+ if (nextScheduledExecution < now) {
576
+ // Check if it's within the grace period
577
+ if (nextScheduledExecution >= gracePeriodStart) {
578
+ logger.info(
579
+ `Task ${task.id} missed execution at ${nextScheduledExecution.toISOString()}`,
580
+ {
581
+ taskId: task.id,
582
+ lastRun: task.lastRun,
583
+ missedAt: nextScheduledExecution.toISOString(),
584
+ schedule: task.schedule,
585
+ }
586
+ );
587
+ return nextScheduledExecution;
588
+ }
589
+ logger.info(`Task ${task.id} missed execution outside grace period, skipping`, {
590
+ taskId: task.id,
591
+ missedAt: nextScheduledExecution.toISOString(),
592
+ gracePeriodStart: gracePeriodStart.toISOString(),
593
+ });
594
+ }
595
+ } catch (error) {
596
+ logger.error(`Failed to parse cron expression for task ${task.id}:`, error);
597
+ }
598
+
599
+ return null;
600
+ }
601
+
602
+ /**
603
+ * Check if a one-off task was missed.
604
+ * One-off tasks are missed if their executeAt time has passed and they haven't run yet.
605
+ */
606
+ private getMissedOneoffExecutionTime(
607
+ task: ScheduledTask,
608
+ now: Date,
609
+ gracePeriodStart: Date
610
+ ): Date | null {
611
+ if (!task.executeAt) {
612
+ logger.warn(`One-off task ${task.id} missing executeAt timestamp`);
613
+ return null;
614
+ }
615
+
616
+ // If already run, don't catch up (shouldn't happen as one-off tasks are deleted after execution)
617
+ if (task.lastRun) {
618
+ logger.debug(`One-off task ${task.id} already executed, skipping catch-up`);
619
+ return null;
620
+ }
621
+
622
+ const executeAt = new Date(task.executeAt);
623
+ if (Number.isNaN(executeAt.getTime())) {
624
+ logger.warn(`One-off task ${task.id} has invalid executeAt: ${task.executeAt}`);
625
+ return null;
626
+ }
627
+
628
+ // Check if execution time has passed and is within grace period
629
+ if (executeAt < now && executeAt >= gracePeriodStart) {
630
+ logger.info(
631
+ `One-off task ${task.id} missed execution at ${executeAt.toISOString()}`,
632
+ {
633
+ taskId: task.id,
634
+ executeAt: task.executeAt,
635
+ }
636
+ );
637
+ return executeAt;
638
+ }
639
+
640
+ // Tasks past grace period are handled by deleteExpiredOneoffTasks
641
+ return null;
642
+ }
643
+
644
+ /**
645
+ * Execute catch-up tasks sequentially with delays between executions.
646
+ */
647
+ private async executeCatchUpTasks(
648
+ tasks: ScheduledTask[],
649
+ delayBetweenTasksMs: number
650
+ ): Promise<void> {
651
+ if (tasks.length === 0) {
652
+ trace.getActiveSpan()?.addEvent("scheduler.catchup_check_completed", {
653
+ "catchup.missedTasksFound": 0,
654
+ });
655
+ logger.info("No missed tasks to catch up");
656
+ return;
657
+ }
658
+
659
+ trace.getActiveSpan()?.addEvent("scheduler.catchup_execution_started", {
660
+ "catchup.tasksToExecute": tasks.length,
661
+ });
662
+
663
+ logger.info(`Executing ${tasks.length} catch-up task(s)`);
664
+
665
+ for (let i = 0; i < tasks.length; i++) {
666
+ const task = tasks[i];
667
+
668
+ try {
669
+ logger.info(`Executing catch-up for task ${task.id} (${i + 1}/${tasks.length})`);
670
+ await this.executeTask(task);
671
+
672
+ // Auto-delete one-off tasks after successful catch-up execution
673
+ if (task.type === "oneoff") {
674
+ logger.info(`One-off task ${task.id} catch-up completed, auto-deleting`);
675
+ this.oneoffTimers.delete(task.id);
676
+ this.taskMetadata.delete(task.id);
677
+ await this.saveTasks();
678
+ }
679
+
680
+ // Add delay between tasks (except after the last one)
681
+ if (i < tasks.length - 1) {
682
+ await this.delay(delayBetweenTasksMs);
683
+ }
684
+ } catch (error) {
685
+ logger.error(`Catch-up execution failed for task ${task.id}:`, error);
686
+ // Continue with next task even if one fails
687
+ }
688
+ }
689
+
690
+ trace.getActiveSpan()?.addEvent("scheduler.catchup_execution_completed", {
691
+ "catchup.tasksExecuted": tasks.length,
692
+ });
693
+
694
+ logger.info("Catch-up execution completed");
695
+ }
696
+
697
+ private delay(ms: number): Promise<void> {
698
+ return new Promise((resolve) => setTimeout(resolve, ms));
699
+ }
700
+
701
+ /**
702
+ * Ensure the project for a scheduled task is running before execution.
703
+ * If the project is not active, boot it up via the injected callback.
704
+ *
705
+ * Uses dependency injection to avoid Layer 3 → Layer 4 architectural violation.
706
+ * The boot handler is provided by the daemon layer during initialization.
707
+ *
708
+ * ProjectRuntime.start() completion IS the readiness signal - no additional
709
+ * delay is needed. When start() returns, isRunning=true and event handlers
710
+ * are fully initialized.
711
+ *
712
+ * @param projectId - The project ID to ensure is running
713
+ * @returns true if the project is confirmed running, false if it could not be started
714
+ */
715
+ private async ensureProjectRunning(projectId: string): Promise<boolean> {
716
+ // If no callbacks registered, we're running outside daemon mode
717
+ // (e.g., in tests or standalone CLI) - skip auto-boot, assume ok
718
+ if (!this.projectStateResolver || !this.projectBootHandler) {
719
+ logger.debug("Project callbacks not registered, skipping auto-boot", {
720
+ projectId,
721
+ });
722
+ return true;
723
+ }
724
+
725
+ try {
726
+ // Check if project is already running
727
+ if (this.projectStateResolver(projectId)) {
728
+ // Project already running, nothing to do
729
+ return true;
730
+ }
731
+
732
+ logger.info("Project not running, booting for scheduled task", {
733
+ projectId,
734
+ });
735
+
736
+ trace.getActiveSpan()?.addEvent("scheduler.project_auto_boot_start", {
737
+ "project.id": projectId,
738
+ });
739
+
740
+ await this.projectBootHandler(projectId);
741
+
742
+ // No delay needed - ProjectRuntime.start() completion IS the readiness signal.
743
+ // When start() returns, isRunning=true and event handlers are initialized.
744
+
745
+ logger.info("Project booted successfully for scheduled task", {
746
+ projectId,
747
+ });
748
+
749
+ trace.getActiveSpan()?.addEvent("scheduler.project_auto_boot_complete", {
750
+ "project.id": projectId,
751
+ });
752
+
753
+ return true;
754
+ } catch (error) {
755
+ // Check if this is a benign "already running" error (race condition)
756
+ // This can happen if the project started between our check and boot attempt
757
+ const isAlreadyRunning = error instanceof ProjectAlreadyRunningError;
758
+
759
+ if (isAlreadyRunning) {
760
+ // Re-check state - if running now, treat as success
761
+ if (this.projectStateResolver(projectId)) {
762
+ logger.debug("Project was already running (race condition, benign)", {
763
+ projectId,
764
+ });
765
+ trace.getActiveSpan()?.addEvent("scheduler.project_auto_boot_race_resolved", {
766
+ "project.id": projectId,
767
+ });
768
+ return true;
769
+ }
770
+ }
771
+
772
+ const errorMessage = error instanceof Error ? error.message : String(error);
773
+ logger.warn("Failed to auto-boot project for scheduled task", {
774
+ projectId,
775
+ error: errorMessage,
776
+ });
777
+
778
+ trace.getActiveSpan()?.addEvent("scheduler.project_auto_boot_failed", {
779
+ "project.id": projectId,
780
+ "error": errorMessage,
781
+ });
782
+
783
+ // Final check: is the project running despite the error?
784
+ if (this.projectStateResolver(projectId)) {
785
+ return true;
786
+ }
787
+
788
+ return false;
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Execute a scheduled task. Throws on failure to allow callers to handle errors.
794
+ * Updates lastRun only AFTER successful publish to ensure failed tasks can be retried.
795
+ *
796
+ * Skips execution if the task's project cannot be started, preventing events
797
+ * from being published into an ambiguous routing state where they might be
798
+ * routed to the wrong project via P-tag fallback.
799
+ */
800
+ private async executeTask(task: ScheduledTask): Promise<void> {
801
+ trace.getActiveSpan()?.addEvent("scheduler.task_executing", {
802
+ "task.id": task.id,
803
+ });
804
+
805
+ // Ensure project is running before executing task (auto-boot if needed)
806
+ // If the project can't be started, skip execution to prevent wrong-project routing
807
+ let projectRunning = false;
808
+ try {
809
+ projectRunning = await this.ensureProjectRunning(task.projectId);
810
+ } catch (error) {
811
+ const errorMessage = error instanceof Error ? error.message : String(error);
812
+ logger.warn("Unexpected error from ensureProjectRunning", {
813
+ taskId: task.id,
814
+ projectId: task.projectId,
815
+ error: errorMessage,
816
+ });
817
+ trace.getActiveSpan()?.addEvent("scheduler.ensure_project_running_unexpected_error", {
818
+ "task.id": task.id,
819
+ "project.id": task.projectId,
820
+ "error": errorMessage,
821
+ });
822
+ }
823
+
824
+ if (!projectRunning) {
825
+ logger.warn(
826
+ "Skipping scheduled task execution — project not running and could not be started. " +
827
+ "Task will be retried on next scheduled run or daemon restart.",
828
+ {
829
+ taskId: task.id,
830
+ projectId: task.projectId,
831
+ schedule: task.schedule,
832
+ title: task.title,
833
+ }
834
+ );
835
+ trace.getActiveSpan()?.addEvent("scheduler.task_skipped_no_project", {
836
+ "task.id": task.id,
837
+ "project.id": task.projectId,
838
+ });
839
+ return;
840
+ }
841
+
842
+ // Try to get NDK instance if not already set
843
+ if (!this.ndk) {
844
+ logger.warn("NDK not available in SchedulerService, attempting to get instance");
845
+ this.ndk = getNDK();
846
+ if (!this.ndk) {
847
+ throw new Error("SchedulerService not properly initialized - NDK unavailable");
848
+ }
849
+ }
850
+
851
+ // Publish kind:1 event to trigger the agent (unified conversation format)
852
+ await this.publishAgentTriggerEvent(task);
853
+
854
+ // Update last run time ONLY after successful publish
855
+ // This ensures failed tasks can be retried on next startup
856
+ task.lastRun = new Date().toISOString();
857
+ await this.saveTasks();
858
+
859
+ trace.getActiveSpan()?.addEvent("scheduler.task_triggered", {
860
+ "task.id": task.id,
861
+ });
862
+ }
863
+
864
+ /**
865
+ * Resolve the target pubkey for a scheduled task.
866
+ * If the original target agent is not in the task's project,
867
+ * route to the Project Manager (PM) of that project instead.
868
+ *
869
+ * Uses dependency injection to avoid Layer 3 → Layer 4 architectural violation.
870
+ * The resolver is provided by the daemon layer during initialization.
871
+ *
872
+ * @param task - The scheduled task to resolve target for
873
+ * @returns The pubkey to use as the target (either original or PM)
874
+ */
875
+ protected resolveTargetPubkey(task: ScheduledTask): string {
876
+ // If no resolver registered, fall back to original target
877
+ if (!this.targetPubkeyResolver) {
878
+ logger.debug("Target pubkey resolver not registered, using original target", {
879
+ taskId: task.id,
880
+ projectId: task.projectId,
881
+ });
882
+ return task.toPubkey;
883
+ }
884
+
885
+ try {
886
+ const resolvedPubkey = this.targetPubkeyResolver(task.projectId, task.toPubkey);
887
+
888
+ // Log if rerouted (pubkey changed)
889
+ if (resolvedPubkey !== task.toPubkey) {
890
+ logger.info("Scheduled task target resolved by daemon", {
891
+ taskId: task.id,
892
+ projectId: task.projectId,
893
+ originalTarget: truncatePubkey(task.toPubkey),
894
+ resolvedTarget: truncatePubkey(resolvedPubkey),
895
+ });
896
+
897
+ trace.getActiveSpan()?.addEvent("scheduler.task_rerouted", {
898
+ "task.id": task.id,
899
+ "task.original_target": truncatePubkey(task.toPubkey),
900
+ "task.resolved_target": truncatePubkey(resolvedPubkey),
901
+ "project.id": task.projectId,
902
+ });
903
+ }
904
+
905
+ return resolvedPubkey;
906
+ } catch (error) {
907
+ // If resolver fails, fall back to original target
908
+ logger.warn("Failed to resolve target pubkey, using original", {
909
+ taskId: task.id,
910
+ error: error instanceof Error ? error.message : String(error),
911
+ });
912
+ return task.toPubkey;
913
+ }
914
+ }
915
+
916
+ private async publishAgentTriggerEvent(task: ScheduledTask): Promise<void> {
917
+ if (!this.ndk) {
918
+ throw new Error("NDK not initialized");
919
+ }
920
+
921
+ // Validate that we have a project ID
922
+ if (!task.projectId) {
923
+ throw new Error(`Scheduled task ${task.id} is missing projectId - cannot route event`);
924
+ }
925
+
926
+ // Resolve the target pubkey - if the target agent is not in the project,
927
+ // route to the PM of that project instead
928
+ const targetPubkey = this.resolveTargetPubkey(task);
929
+
930
+ const event = new NDKEvent(this.ndk);
931
+ event.kind = 1; // Unified conversation format (kind:1)
932
+ event.content = task.prompt;
933
+
934
+ // Build tags - use stored projectId instead of getting from context
935
+ // The projectId is stored when the task is created (within project context)
936
+ const tags: string[][] = [
937
+ ["a", task.projectId], // Project reference (stored at task creation time)
938
+ ["p", targetPubkey], // Target agent that should handle this task (may be PM if original target not in project)
939
+ ];
940
+
941
+ // Add metadata about the scheduled task
942
+ tags.push(["scheduled-task-id", task.id]);
943
+
944
+ // Use appropriate tag based on task type
945
+ if (task.type === "oneoff" && task.executeAt) {
946
+ // One-off tasks use execute-at tag with ISO timestamp
947
+ tags.push(["scheduled-task-execute-at", task.executeAt]);
948
+ } else {
949
+ // Recurring tasks use cron tag with cron expression
950
+ tags.push(["scheduled-task-cron", task.schedule]);
951
+ }
952
+
953
+ event.tags = tags;
954
+
955
+ // Use backend signer for scheduled tasks
956
+ // The backend key is always available and whitelisted
957
+ // We store fromPubkey for tracking but sign with backend key
958
+ const privateKey = await config.ensureBackendPrivateKey();
959
+ const signer = new NDKPrivateKeySigner(privateKey);
960
+
961
+ // Sign and publish the event
962
+ await event.sign(signer);
963
+ await event.publish();
964
+
965
+ const wasRerouted = targetPubkey !== task.toPubkey;
966
+
967
+ logger.info("Published scheduled task event", {
968
+ taskId: task.id,
969
+ projectId: task.projectId,
970
+ eventId: event.id?.substring(0, 8),
971
+ from: truncatePubkey(signer.pubkey),
972
+ to: truncatePubkey(targetPubkey),
973
+ originalTarget: wasRerouted ? truncatePubkey(task.toPubkey) : undefined,
974
+ reroutedToPM: wasRerouted,
975
+ });
976
+
977
+ trace.getActiveSpan()?.addEvent("scheduler.event_published", {
978
+ "task.id": task.id,
979
+ "event.id": event.id || "unknown",
980
+ "event.from": truncatePubkey(signer.pubkey),
981
+ "event.to": truncatePubkey(targetPubkey),
982
+ "event.original_target": wasRerouted ? truncatePubkey(task.toPubkey) : undefined,
983
+ "event.rerouted_to_pm": wasRerouted,
984
+ "project.id": task.projectId,
985
+ });
986
+ }
987
+
988
+ private async loadTasks(): Promise<void> {
989
+ try {
990
+ const data = await fs.readFile(this.taskFilePath, "utf-8");
991
+ const tasks = JSON.parse(data) as ScheduledTask[];
992
+
993
+ for (const task of tasks) {
994
+ this.taskMetadata.set(task.id, task);
995
+ }
996
+
997
+ trace.getActiveSpan()?.addEvent("scheduler.tasks_loaded", {
998
+ "tasks.count": tasks.length,
999
+ });
1000
+ } catch (error: unknown) {
1001
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
1002
+ // No existing file, starting fresh - this is expected
1003
+ } else {
1004
+ logger.error("Failed to load scheduled tasks:", error);
1005
+ }
1006
+ }
1007
+ }
1008
+
1009
+ private async saveTasks(): Promise<void> {
1010
+ try {
1011
+ const tasks = Array.from(this.taskMetadata.values());
1012
+ await fs.writeFile(this.taskFilePath, JSON.stringify(tasks, null, 2));
1013
+ } catch (error) {
1014
+ logger.error("Failed to save scheduled tasks:", error);
1015
+ }
1016
+ }
1017
+
1018
+ private generateTaskId(): string {
1019
+ return `task-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
1020
+ }
1021
+
1022
+ public shutdown(): void {
1023
+ trace.getActiveSpan()?.addEvent("scheduler.shutting_down", {
1024
+ "tasks.count": this.tasks.size,
1025
+ "oneoff.count": this.oneoffTimers.size,
1026
+ });
1027
+
1028
+ // Stop all cron tasks
1029
+ for (const [, cronTask] of this.tasks.entries()) {
1030
+ cronTask.stop();
1031
+ }
1032
+
1033
+ // Clear all one-off timers
1034
+ for (const [, timer] of this.oneoffTimers.entries()) {
1035
+ clearTimeout(timer);
1036
+ }
1037
+
1038
+ this.tasks.clear();
1039
+ this.oneoffTimers.clear();
1040
+ this.taskMetadata.clear();
1041
+ trace.getActiveSpan()?.addEvent("scheduler.shutdown_complete");
1042
+ }
1043
+
1044
+ public async clearAllTasks(): Promise<void> {
1045
+ // Stop and remove all tasks
1046
+ for (const taskId of Array.from(this.tasks.keys())) {
1047
+ await this.removeTask(taskId);
1048
+ }
1049
+
1050
+ // Clear the tasks file
1051
+ try {
1052
+ await fs.writeFile(this.taskFilePath, JSON.stringify([], null, 2));
1053
+ } catch (error) {
1054
+ logger.error("Failed to clear tasks file:", error);
1055
+ }
1056
+ }
1057
+ }