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