@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.
- package/README.md +194 -0
- package/dist/backend-wrapper.cjs +3 -0
- package/dist/src/index.js +331928 -0
- package/package.json +103 -0
- package/src/agents/AgentRegistry.ts +418 -0
- package/src/agents/AgentStorage.ts +1133 -0
- package/src/agents/ConfigResolver.ts +229 -0
- package/src/agents/agent-installer.ts +236 -0
- package/src/agents/agent-loader.ts +241 -0
- package/src/agents/constants.ts +82 -0
- package/src/agents/errors.ts +48 -0
- package/src/agents/execution/AgentExecutor.ts +561 -0
- package/src/agents/execution/ExecutionContextFactory.ts +112 -0
- package/src/agents/execution/MessageCompiler.ts +597 -0
- package/src/agents/execution/MessageSyncer.ts +100 -0
- package/src/agents/execution/PostCompletionChecker.ts +278 -0
- package/src/agents/execution/ProgressMonitor.ts +50 -0
- package/src/agents/execution/RALResolver.ts +177 -0
- package/src/agents/execution/SessionManager.ts +181 -0
- package/src/agents/execution/StreamCallbacks.ts +312 -0
- package/src/agents/execution/StreamExecutionHandler.ts +579 -0
- package/src/agents/execution/StreamSetup.ts +313 -0
- package/src/agents/execution/ToolEventHandlers.ts +239 -0
- package/src/agents/execution/ToolExecutionTracker.ts +498 -0
- package/src/agents/execution/ToolResultUtils.ts +97 -0
- package/src/agents/execution/ToolSupervisionWrapper.ts +174 -0
- package/src/agents/execution/constants.ts +16 -0
- package/src/agents/execution/index.ts +3 -0
- package/src/agents/execution/types.ts +96 -0
- package/src/agents/execution/utils.ts +26 -0
- package/src/agents/index.ts +4 -0
- package/src/agents/script-installer.ts +266 -0
- package/src/agents/supervision/SupervisorLLMService.ts +253 -0
- package/src/agents/supervision/SupervisorOrchestrator.ts +471 -0
- package/src/agents/supervision/heuristics/ConsecutiveToolsWithoutTodoHeuristic.ts +73 -0
- package/src/agents/supervision/heuristics/DelegationClaimHeuristic.ts +80 -0
- package/src/agents/supervision/heuristics/HeuristicRegistry.ts +114 -0
- package/src/agents/supervision/heuristics/PendingTodosHeuristic.ts +93 -0
- package/src/agents/supervision/heuristics/SilentAgentHeuristic.ts +54 -0
- package/src/agents/supervision/heuristics/index.ts +5 -0
- package/src/agents/supervision/index.ts +28 -0
- package/src/agents/supervision/registerHeuristics.ts +110 -0
- package/src/agents/supervision/supervisionHealthCheck.ts +123 -0
- package/src/agents/supervision/types.ts +171 -0
- package/src/agents/tool-names.ts +46 -0
- package/src/agents/tool-normalization.ts +184 -0
- package/src/agents/types/index.ts +2 -0
- package/src/agents/types/runtime.ts +74 -0
- package/src/agents/types/storage.ts +145 -0
- package/src/commands/agent/import/index.ts +6 -0
- package/src/commands/agent/import/openclaw-distiller.ts +57 -0
- package/src/commands/agent/import/openclaw-reader.ts +141 -0
- package/src/commands/agent/import/openclaw.ts +154 -0
- package/src/commands/agent/index.ts +6 -0
- package/src/commands/agent.ts +215 -0
- package/src/commands/daemon.ts +198 -0
- package/src/commands/doctor.ts +134 -0
- package/src/commands/setup/embed.ts +228 -0
- package/src/commands/setup/global-system-prompt.ts +223 -0
- package/src/commands/setup/image.ts +179 -0
- package/src/commands/setup/index.ts +16 -0
- package/src/commands/setup/interactive.ts +95 -0
- package/src/commands/setup/llm.ts +38 -0
- package/src/commands/setup/onboarding.ts +294 -0
- package/src/commands/setup/providers.ts +27 -0
- package/src/constants.ts +34 -0
- package/src/conversations/ConversationDiskReader.ts +148 -0
- package/src/conversations/ConversationRegistry.ts +728 -0
- package/src/conversations/ConversationStore.ts +868 -0
- package/src/conversations/MessageBuilder.ts +866 -0
- package/src/conversations/executionTime.ts +62 -0
- package/src/conversations/formatters/DelegationXmlFormatter.ts +64 -0
- package/src/conversations/formatters/ThreadedConversationFormatter.ts +303 -0
- package/src/conversations/formatters/index.ts +9 -0
- package/src/conversations/formatters/utils/MessageFormatter.ts +46 -0
- package/src/conversations/formatters/utils/TimestampFormatter.ts +56 -0
- package/src/conversations/formatters/utils/TreeBuilder.ts +131 -0
- package/src/conversations/formatters/utils/TreeRenderer.ts +49 -0
- package/src/conversations/index.ts +2 -0
- package/src/conversations/persistence/ToolMessageStorage.ts +143 -0
- package/src/conversations/search/ConversationIndexManager.ts +393 -0
- package/src/conversations/search/QueryParser.ts +114 -0
- package/src/conversations/search/SearchEngine.ts +175 -0
- package/src/conversations/search/SnippetExtractor.ts +345 -0
- package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +484 -0
- package/src/conversations/search/embeddings/ConversationIndexingJob.ts +320 -0
- package/src/conversations/search/embeddings/IndexingStateManager.ts +338 -0
- package/src/conversations/search/embeddings/index.ts +18 -0
- package/src/conversations/search/index.ts +49 -0
- package/src/conversations/search/types.ts +124 -0
- package/src/conversations/services/CategoryManager.ts +160 -0
- package/src/conversations/services/ConversationResolver.ts +296 -0
- package/src/conversations/services/ConversationSummarizer.ts +234 -0
- package/src/conversations/services/MetadataDebounceManager.ts +188 -0
- package/src/conversations/services/index.ts +2 -0
- package/src/conversations/types.ts +148 -0
- package/src/conversations/utils/content-utils.ts +69 -0
- package/src/conversations/utils/image-placeholder.ts +281 -0
- package/src/conversations/utils/image-url-utils.ts +171 -0
- package/src/conversations/utils/multimodal-content.ts +90 -0
- package/src/conversations/utils/tool-result-truncator.ts +159 -0
- package/src/daemon/Daemon.ts +1883 -0
- package/src/daemon/ProjectRuntime.ts +657 -0
- package/src/daemon/RestartState.ts +152 -0
- package/src/daemon/RuntimeLifecycle.ts +268 -0
- package/src/daemon/SubscriptionManager.ts +305 -0
- package/src/daemon/UnixSocketTransport.ts +318 -0
- package/src/daemon/filters/SubscriptionFilterBuilder.ts +119 -0
- package/src/daemon/index.ts +9 -0
- package/src/daemon/routing/DaemonRouter.ts +491 -0
- package/src/daemon/types.ts +150 -0
- package/src/daemon/utils/routing-log.ts +76 -0
- package/src/daemon/utils/telemetry.ts +173 -0
- package/src/event-handler/agentDeletion.ts +383 -0
- package/src/event-handler/index.ts +749 -0
- package/src/event-handler/newConversation.ts +165 -0
- package/src/event-handler/project.ts +166 -0
- package/src/event-handler/reply.ts +18 -0
- package/src/events/NDKAgentDefinition.ts +292 -0
- package/src/events/NDKAgentLesson.ts +106 -0
- package/src/events/NDKEventMetadata.ts +34 -0
- package/src/events/NDKMCPTool.ts +60 -0
- package/src/events/NDKProjectStatus.ts +384 -0
- package/src/events/index.ts +4 -0
- package/src/index.ts +126 -0
- package/src/lib/agent-home.ts +334 -0
- package/src/lib/error-formatter.ts +200 -0
- package/src/lib/fs/filesystem.ts +128 -0
- package/src/lib/fs/index.ts +1 -0
- package/src/lib/json-parser.ts +30 -0
- package/src/lib/string.ts +15 -0
- package/src/lib/time.ts +74 -0
- package/src/llm/ChunkHandler.ts +277 -0
- package/src/llm/FinishHandler.ts +250 -0
- package/src/llm/LLMConfigEditor.ts +154 -0
- package/src/llm/LLMServiceFactory.ts +230 -0
- package/src/llm/MessageProcessor.ts +90 -0
- package/src/llm/RecordingState.ts +37 -0
- package/src/llm/StreamPublisher.ts +40 -0
- package/src/llm/TracingUtils.ts +77 -0
- package/src/llm/chunk-validators.ts +57 -0
- package/src/llm/constants.ts +6 -0
- package/src/llm/index.ts +12 -0
- package/src/llm/meta/MetaModelResolver.ts +352 -0
- package/src/llm/meta/index.ts +11 -0
- package/src/llm/middleware/flight-recorder.ts +188 -0
- package/src/llm/providers/MockProvider.ts +332 -0
- package/src/llm/providers/agent/ClaudeCodeProvider.ts +343 -0
- package/src/llm/providers/agent/ClaudeCodeToolsAdapter.ts +203 -0
- package/src/llm/providers/agent/CodexAppServerProvider.ts +214 -0
- package/src/llm/providers/agent/CodexAppServerToolsAdapter.ts +91 -0
- package/src/llm/providers/agent/index.ts +10 -0
- package/src/llm/providers/base/AgentProvider.ts +107 -0
- package/src/llm/providers/base/BaseProvider.ts +114 -0
- package/src/llm/providers/base/StandardProvider.ts +38 -0
- package/src/llm/providers/base/index.ts +9 -0
- package/src/llm/providers/index.ts +106 -0
- package/src/llm/providers/key-manager.ts +238 -0
- package/src/llm/providers/ollama-models.ts +105 -0
- package/src/llm/providers/openrouter-models.ts +102 -0
- package/src/llm/providers/provider-ids.ts +18 -0
- package/src/llm/providers/registry/ProviderRegistry.ts +414 -0
- package/src/llm/providers/registry/index.ts +7 -0
- package/src/llm/providers/standard/AnthropicProvider.ts +71 -0
- package/src/llm/providers/standard/OllamaProvider.ts +59 -0
- package/src/llm/providers/standard/OpenAIProvider.ts +44 -0
- package/src/llm/providers/standard/OpenRouterProvider.ts +103 -0
- package/src/llm/providers/standard/index.ts +10 -0
- package/src/llm/providers/types.ts +194 -0
- package/src/llm/providers/usage-metadata.ts +78 -0
- package/src/llm/service.ts +713 -0
- package/src/llm/types.ts +167 -0
- package/src/llm/utils/ConfigurationManager.ts +650 -0
- package/src/llm/utils/ConfigurationTester.ts +229 -0
- package/src/llm/utils/ModelSelector.ts +212 -0
- package/src/llm/utils/ProviderConfigUI.ts +177 -0
- package/src/llm/utils/claudeCodePromptCompiler.ts +141 -0
- package/src/llm/utils/codex-models.ts +53 -0
- package/src/llm/utils/context-window-cache.ts +30 -0
- package/src/llm/utils/models-dev-cache.ts +267 -0
- package/src/llm/utils/provider-setup.ts +50 -0
- package/src/llm/utils/tool-errors.ts +78 -0
- package/src/llm/utils/usage.ts +74 -0
- package/src/logging/EventRoutingLogger.ts +205 -0
- package/src/nostr/AgentEventDecoder.ts +357 -0
- package/src/nostr/AgentEventEncoder.ts +677 -0
- package/src/nostr/AgentProfilePublisher.ts +657 -0
- package/src/nostr/AgentPublisher.ts +437 -0
- package/src/nostr/BlossomService.ts +226 -0
- package/src/nostr/InterventionPublisher.ts +132 -0
- package/src/nostr/TagExtractor.ts +228 -0
- package/src/nostr/collectEvents.ts +83 -0
- package/src/nostr/constants.ts +38 -0
- package/src/nostr/encryption.ts +26 -0
- package/src/nostr/index.ts +31 -0
- package/src/nostr/keys.ts +17 -0
- package/src/nostr/kinds.ts +37 -0
- package/src/nostr/ndkClient.ts +72 -0
- package/src/nostr/relays.ts +43 -0
- package/src/nostr/trace-context.ts +39 -0
- package/src/nostr/types.ts +227 -0
- package/src/nostr/utils.ts +84 -0
- package/src/prompts/core/FragmentRegistry.ts +30 -0
- package/src/prompts/core/PromptBuilder.ts +98 -0
- package/src/prompts/core/index.ts +3 -0
- package/src/prompts/core/types.ts +13 -0
- package/src/prompts/fragments/00-global-system-prompt.ts +44 -0
- package/src/prompts/fragments/01-agent-identity.ts +69 -0
- package/src/prompts/fragments/02-agent-home-directory.ts +114 -0
- package/src/prompts/fragments/03-system-reminders-explanation.ts +14 -0
- package/src/prompts/fragments/04-relay-configuration.ts +38 -0
- package/src/prompts/fragments/05-delegation-chain.ts +45 -0
- package/src/prompts/fragments/06-agent-todos.ts +74 -0
- package/src/prompts/fragments/06-todo-usage-guidance.ts +34 -0
- package/src/prompts/fragments/07-meta-project-context.ts +234 -0
- package/src/prompts/fragments/08-active-conversations.ts +382 -0
- package/src/prompts/fragments/09-recent-conversations.ts +153 -0
- package/src/prompts/fragments/10-referenced-article.ts +21 -0
- package/src/prompts/fragments/11-nudges.ts +134 -0
- package/src/prompts/fragments/12-skills.ts +127 -0
- package/src/prompts/fragments/13-available-nudges.ts +122 -0
- package/src/prompts/fragments/15-available-agents.ts +53 -0
- package/src/prompts/fragments/16-stay-in-your-lane.ts +41 -0
- package/src/prompts/fragments/17-todo-before-delegation.ts +39 -0
- package/src/prompts/fragments/20-voice-mode.ts +62 -0
- package/src/prompts/fragments/22-scheduled-tasks.ts +175 -0
- package/src/prompts/fragments/24-retrieved-lessons.ts +26 -0
- package/src/prompts/fragments/25-rag-instructions.ts +333 -0
- package/src/prompts/fragments/26-mcp-resources.ts +237 -0
- package/src/prompts/fragments/27-memorized-reports.ts +77 -0
- package/src/prompts/fragments/28-agent-directed-monitoring.ts +32 -0
- package/src/prompts/fragments/29-rag-collections.ts +50 -0
- package/src/prompts/fragments/30-worktree-context.ts +98 -0
- package/src/prompts/fragments/31-agents-md-guidance.ts +96 -0
- package/src/prompts/fragments/32-process-metrics.ts +72 -0
- package/src/prompts/fragments/debug-mode.ts +48 -0
- package/src/prompts/fragments/delegation-completion.ts +44 -0
- package/src/prompts/fragments/index.ts +91 -0
- package/src/prompts/index.ts +21 -0
- package/src/prompts/utils/systemPromptBuilder.ts +777 -0
- package/src/scripts/migrate-prefix-index.ts +157 -0
- package/src/services/AgentDefinitionMonitor.ts +701 -0
- package/src/services/ConfigService.ts +723 -0
- package/src/services/CooldownRegistry.ts +199 -0
- package/src/services/LLMOperationsRegistry.ts +424 -0
- package/src/services/OwnerAgentListService.ts +354 -0
- package/src/services/PubkeyService.ts +308 -0
- package/src/services/agents/AgentMetadataStore.ts +72 -0
- package/src/services/agents/AgentResolution.ts +59 -0
- package/src/services/agents/EscalationService.ts +281 -0
- package/src/services/agents/NDKAgentDiscovery.ts +95 -0
- package/src/services/agents/index.ts +7 -0
- package/src/services/agents-md/AgentsMdService.ts +184 -0
- package/src/services/agents-md/SystemReminderInjector.ts +238 -0
- package/src/services/agents-md/index.ts +11 -0
- package/src/services/apns/APNsClient.ts +203 -0
- package/src/services/apns/APNsService.ts +358 -0
- package/src/services/apns/index.ts +11 -0
- package/src/services/apns/types.ts +80 -0
- package/src/services/compression/CompressionService.ts +445 -0
- package/src/services/compression/compression-schema.ts +28 -0
- package/src/services/compression/compression-types.ts +74 -0
- package/src/services/compression/compression-utils.ts +587 -0
- package/src/services/config/types.ts +394 -0
- package/src/services/dispatch/AgentDispatchService.ts +937 -0
- package/src/services/dispatch/AgentRouter.ts +181 -0
- package/src/services/dispatch/DelegationCompletionHandler.ts +232 -0
- package/src/services/embedding/EmbeddingProvider.ts +188 -0
- package/src/services/embedding/index.ts +5 -0
- package/src/services/event-context/EventContextService.ts +108 -0
- package/src/services/event-context/index.ts +2 -0
- package/src/services/heuristics/ContextBuilder.ts +106 -0
- package/src/services/heuristics/HeuristicEngine.ts +200 -0
- package/src/services/heuristics/formatters.ts +58 -0
- package/src/services/heuristics/index.ts +12 -0
- package/src/services/heuristics/rules/index.ts +25 -0
- package/src/services/heuristics/rules/todoBeforeDelegation.ts +69 -0
- package/src/services/heuristics/rules/todoReminderOnToolUse.ts +63 -0
- package/src/services/heuristics/types.ts +144 -0
- package/src/services/image/ImageGenerationService.ts +389 -0
- package/src/services/image/index.ts +12 -0
- package/src/services/intervention/InterventionService.ts +1352 -0
- package/src/services/intervention/index.ts +7 -0
- package/src/services/mcp/MCPManager.ts +683 -0
- package/src/services/mcp/McpNotificationDelivery.ts +139 -0
- package/src/services/mcp/McpSubscriptionService.ts +653 -0
- package/src/services/mcp/mcpInstaller.ts +130 -0
- package/src/services/nip46/Nip46SigningLog.ts +81 -0
- package/src/services/nip46/Nip46SigningService.ts +467 -0
- package/src/services/nip46/index.ts +4 -0
- package/src/services/nudge/NudgeService.ts +224 -0
- package/src/services/nudge/NudgeWhitelistService.ts +382 -0
- package/src/services/nudge/index.ts +5 -0
- package/src/services/nudge/types.ts +83 -0
- package/src/services/projects/ProjectContext.ts +672 -0
- package/src/services/projects/ProjectContextStore.ts +102 -0
- package/src/services/projects/index.ts +6 -0
- package/src/services/prompt-compiler/index.ts +15 -0
- package/src/services/prompt-compiler/prompt-compiler-service.ts +1143 -0
- package/src/services/pubkey-gate/PubkeyGateService.ts +93 -0
- package/src/services/pubkey-gate/index.ts +1 -0
- package/src/services/rag/EmbeddingProviderFactory.ts +292 -0
- package/src/services/rag/LanceDBMaintenanceService.ts +211 -0
- package/src/services/rag/RAGDatabaseService.ts +173 -0
- package/src/services/rag/RAGOperations.ts +682 -0
- package/src/services/rag/RAGService.ts +240 -0
- package/src/services/rag/RagSubscriptionService.ts +618 -0
- package/src/services/rag/rag-utils.ts +174 -0
- package/src/services/ral/PendingDelegationsRegistry.ts +168 -0
- package/src/services/ral/RALRegistry.ts +2782 -0
- package/src/services/ral/index.ts +4 -0
- package/src/services/ral/types.ts +292 -0
- package/src/services/reports/LocalReportStore.ts +380 -0
- package/src/services/reports/ReportEmbeddingService.ts +430 -0
- package/src/services/reports/ReportService.ts +440 -0
- package/src/services/reports/articleUtils.ts +52 -0
- package/src/services/reports/index.ts +7 -0
- package/src/services/scheduling/SchedulerService.ts +1057 -0
- package/src/services/scheduling/errors.ts +14 -0
- package/src/services/scheduling/index.ts +7 -0
- package/src/services/scheduling/utils.ts +77 -0
- package/src/services/search/SearchProviderRegistry.ts +78 -0
- package/src/services/search/UnifiedSearchService.ts +218 -0
- package/src/services/search/index.ts +47 -0
- package/src/services/search/projectFilter.ts +22 -0
- package/src/services/search/providers/ConversationSearchProvider.ts +48 -0
- package/src/services/search/providers/LessonSearchProvider.ts +75 -0
- package/src/services/search/providers/ReportSearchProvider.ts +49 -0
- package/src/services/search/types.ts +144 -0
- package/src/services/skill/SkillService.ts +482 -0
- package/src/services/skill/index.ts +2 -0
- package/src/services/skill/types.ts +70 -0
- package/src/services/status/OperationsStatusService.ts +276 -0
- package/src/services/status/ProjectStatusService.ts +522 -0
- package/src/services/status/index.ts +11 -0
- package/src/services/storage/PrefixKVStore.ts +242 -0
- package/src/services/storage/index.ts +1 -0
- package/src/services/system-reminder/SystemReminderUtils.ts +96 -0
- package/src/services/system-reminder/index.ts +7 -0
- package/src/services/trust-pubkeys/TrustPubkeyService.ts +325 -0
- package/src/services/trust-pubkeys/index.ts +2 -0
- package/src/telemetry/ConversationSpanManager.ts +111 -0
- package/src/telemetry/EventLoopMonitor.ts +206 -0
- package/src/telemetry/LLMSpanRegistry.ts +20 -0
- package/src/telemetry/NostrSpanProcessor.ts +89 -0
- package/src/telemetry/ToolCallSpanProcessor.ts +66 -0
- package/src/telemetry/diagnostics.ts +27 -0
- package/src/telemetry/setup.ts +120 -0
- package/src/tools/implementations/agents_discover.ts +121 -0
- package/src/tools/implementations/agents_hire.ts +127 -0
- package/src/tools/implementations/agents_list.ts +96 -0
- package/src/tools/implementations/agents_publish.ts +611 -0
- package/src/tools/implementations/agents_read.ts +173 -0
- package/src/tools/implementations/agents_write.ts +200 -0
- package/src/tools/implementations/ask.ts +411 -0
- package/src/tools/implementations/change_model.ts +141 -0
- package/src/tools/implementations/conversation_get.ts +661 -0
- package/src/tools/implementations/conversation_list.ts +377 -0
- package/src/tools/implementations/conversation_search.ts +370 -0
- package/src/tools/implementations/delegate.ts +327 -0
- package/src/tools/implementations/delegate_crossproject.ts +209 -0
- package/src/tools/implementations/delegate_followup.ts +300 -0
- package/src/tools/implementations/fs_edit.ts +162 -0
- package/src/tools/implementations/fs_glob.ts +182 -0
- package/src/tools/implementations/fs_grep.ts +513 -0
- package/src/tools/implementations/fs_read.ts +332 -0
- package/src/tools/implementations/fs_write.ts +113 -0
- package/src/tools/implementations/generate_image.ts +259 -0
- package/src/tools/implementations/home_fs.ts +515 -0
- package/src/tools/implementations/kill.ts +651 -0
- package/src/tools/implementations/learn.ts +166 -0
- package/src/tools/implementations/lesson-formatter.ts +38 -0
- package/src/tools/implementations/lesson_delete.ts +164 -0
- package/src/tools/implementations/lesson_get.ts +105 -0
- package/src/tools/implementations/lessons_list.ts +153 -0
- package/src/tools/implementations/mcp_resource_read.ts +161 -0
- package/src/tools/implementations/mcp_subscribe.ts +158 -0
- package/src/tools/implementations/mcp_subscription_stop.ts +85 -0
- package/src/tools/implementations/nostr_fetch.ts +149 -0
- package/src/tools/implementations/nostr_publish_as_user.ts +353 -0
- package/src/tools/implementations/project_list.ts +146 -0
- package/src/tools/implementations/rag_add_documents.ts +573 -0
- package/src/tools/implementations/rag_create_collection.ts +65 -0
- package/src/tools/implementations/rag_delete_collection.ts +68 -0
- package/src/tools/implementations/rag_list_collections.ts +77 -0
- package/src/tools/implementations/rag_query.ts +107 -0
- package/src/tools/implementations/rag_subscription_create.ts +105 -0
- package/src/tools/implementations/rag_subscription_delete.ts +80 -0
- package/src/tools/implementations/rag_subscription_get.ts +123 -0
- package/src/tools/implementations/rag_subscription_list.ts +128 -0
- package/src/tools/implementations/report_delete.ts +79 -0
- package/src/tools/implementations/report_read.ts +160 -0
- package/src/tools/implementations/report_write.ts +278 -0
- package/src/tools/implementations/reports_list.ts +77 -0
- package/src/tools/implementations/schedule_task.ts +104 -0
- package/src/tools/implementations/schedule_task_cancel.ts +62 -0
- package/src/tools/implementations/schedule_task_once.ts +128 -0
- package/src/tools/implementations/schedule_tasks_list.ts +79 -0
- package/src/tools/implementations/search.ts +160 -0
- package/src/tools/implementations/shell.ts +553 -0
- package/src/tools/implementations/todo.ts +260 -0
- package/src/tools/implementations/upload_blob.ts +381 -0
- package/src/tools/implementations/web_fetch.ts +153 -0
- package/src/tools/implementations/web_search.ts +250 -0
- package/src/tools/registry.ts +670 -0
- package/src/tools/types.ts +177 -0
- package/src/tools/utils.ts +256 -0
- package/src/types/event-ids.ts +320 -0
- package/src/types/index.ts +46 -0
- package/src/utils/agentFetcher.ts +107 -0
- package/src/utils/cli-error.ts +29 -0
- package/src/utils/conversation-id.ts +27 -0
- package/src/utils/conversation-utils.ts +1 -0
- package/src/utils/delegation-chain.ts +357 -0
- package/src/utils/error-handler.ts +42 -0
- package/src/utils/git/gitignore.ts +69 -0
- package/src/utils/git/index.ts +2 -0
- package/src/utils/git/initializeGitRepo.ts +204 -0
- package/src/utils/git/worktree.ts +260 -0
- package/src/utils/lessonFormatter.ts +70 -0
- package/src/utils/lessonTrust.ts +24 -0
- package/src/utils/lockfile.ts +123 -0
- package/src/utils/logger.ts +149 -0
- package/src/utils/nostr-entity-parser.ts +365 -0
- package/src/utils/process.ts +49 -0
- package/src/wrapper.ts +262 -0
- 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
|
+
}
|