specmem-hardwicksoftware 3.5.99
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/CHANGELOG.md +299 -0
- package/LICENSE.md +6406 -0
- package/README.md +539 -0
- package/bin/AegisTheme.cjs +1022 -0
- package/bin/AnsiRenderer.cjs +1055 -0
- package/bin/BoxRenderer.cjs +605 -0
- package/bin/ClaudeLiveScreen.cjs +1299 -0
- package/bin/DashboardModules.cjs +733 -0
- package/bin/LiveScreenCapture.cjs +1012 -0
- package/bin/MemoryBrowserScreen.cjs +1595 -0
- package/bin/TabManager.cjs +1414 -0
- package/bin/checkAgentStatus-fix.patch +30 -0
- package/bin/mcp-socket-client.cjs +462 -0
- package/bin/screen-utils.cjs +106 -0
- package/bin/specmem-autoclaude.cjs +663 -0
- package/bin/specmem-cleanup.cjs +421 -0
- package/bin/specmem-cli.cjs +794 -0
- package/bin/specmem-console-teamcomms-class.cjs +428 -0
- package/bin/specmem-console.cjs +8104 -0
- package/bin/specmem-statusbar.cjs +530 -0
- package/bootstrap.cjs +5065 -0
- package/claude-hooks/agent-chooser-hook.js +179 -0
- package/claude-hooks/agent-chooser-inject.js +121 -0
- package/claude-hooks/agent-loading-hook.js +990 -0
- package/claude-hooks/agent-output-fader.cjs +542 -0
- package/claude-hooks/agent-output-interceptor.js +193 -0
- package/claude-hooks/agent-type-matcher.js +419 -0
- package/claude-hooks/auto-bypass.py +74 -0
- package/claude-hooks/background-completion-silencer.js +134 -0
- package/claude-hooks/bash-auto-background.js +182 -0
- package/claude-hooks/build-cedict-dictionary.mjs +167 -0
- package/claude-hooks/bullshit-radar.cjs +323 -0
- package/claude-hooks/cedict-codes.json +270 -0
- package/claude-hooks/cedict-extracted.json +22632 -0
- package/claude-hooks/claude-watchdog.sh +401 -0
- package/claude-hooks/context-dedup.cjs +144 -0
- package/claude-hooks/context-yeeter.cjs +244 -0
- package/claude-hooks/debug-suffix.cjs +15 -0
- package/claude-hooks/debug2.cjs +7 -0
- package/claude-hooks/drilldown-enforcer.js +242 -0
- package/claude-hooks/english-morphology-standalone.cjs +149 -0
- package/claude-hooks/english-morphology.cjs +152 -0
- package/claude-hooks/extract-translations.mjs +193 -0
- package/claude-hooks/file-claim-enforcer.cjs +293 -0
- package/claude-hooks/file-claim-enforcer.js +293 -0
- package/claude-hooks/find-collisions.cjs +39 -0
- package/claude-hooks/fix-abbreviations.cjs +60 -0
- package/claude-hooks/fix-collisions.cjs +60 -0
- package/claude-hooks/fix-decompressor.cjs +79 -0
- package/claude-hooks/fix-suffixes.cjs +66 -0
- package/claude-hooks/grammar-engine.cjs +159 -0
- package/claude-hooks/input-aware-improver.js +231 -0
- package/claude-hooks/is-agent.cjs +64 -0
- package/claude-hooks/mega-test.cjs +213 -0
- package/claude-hooks/merge-dictionaries.mjs +207 -0
- package/claude-hooks/merged-codes.cjs +22675 -0
- package/claude-hooks/merged-codes.json +22676 -0
- package/claude-hooks/output-cleaner.cjs +388 -0
- package/claude-hooks/post-write-memory-hook.cjs +430 -0
- package/claude-hooks/quick-test.cjs +24 -0
- package/claude-hooks/quick-test2.cjs +24 -0
- package/claude-hooks/remove-bad-codes.cjs +23 -0
- package/claude-hooks/search-reminder-hook.js +90 -0
- package/claude-hooks/semantic-test.cjs +93 -0
- package/claude-hooks/settings.json +445 -0
- package/claude-hooks/smart-context-hook.cjs +547 -0
- package/claude-hooks/smart-context-hook.js +539 -0
- package/claude-hooks/smart-search-interceptor.js +364 -0
- package/claude-hooks/socket-connect-helper.cjs +235 -0
- package/claude-hooks/specmem/sockets/session-start.lock +1 -0
- package/claude-hooks/specmem-context-hook.cjs +357 -0
- package/claude-hooks/specmem-context-hook.js +357 -0
- package/claude-hooks/specmem-drilldown-hook.cjs +480 -0
- package/claude-hooks/specmem-drilldown-hook.js +480 -0
- package/claude-hooks/specmem-drilldown-setter.js +210 -0
- package/claude-hooks/specmem-paths.cjs +213 -0
- package/claude-hooks/specmem-precompact.js +183 -0
- package/claude-hooks/specmem-session-init.sh +33 -0
- package/claude-hooks/specmem-session-start.cjs +498 -0
- package/claude-hooks/specmem-stop-hook.cjs +73 -0
- package/claude-hooks/specmem-stop-hook.js +5 -0
- package/claude-hooks/specmem-team-comms.cjs +434 -0
- package/claude-hooks/specmem-team-member-inject.js +271 -0
- package/claude-hooks/specmem-unified-hook.py +670 -0
- package/claude-hooks/subagent-loading-hook.js +194 -0
- package/claude-hooks/sysprompt-squisher.cjs +167 -0
- package/claude-hooks/task-progress-hook.js +204 -0
- package/claude-hooks/team-comms-enforcer.cjs +585 -0
- package/claude-hooks/test-accuracy.cjs +27 -0
- package/claude-hooks/test-big.cjs +28 -0
- package/claude-hooks/test-inflectors.cjs +39 -0
- package/claude-hooks/test-pluralize.cjs +37 -0
- package/claude-hooks/test-quick.cjs +8 -0
- package/claude-hooks/test-wink.cjs +20 -0
- package/claude-hooks/token-compressor.cjs +940 -0
- package/claude-hooks/use-code-pointers.cjs +279 -0
- package/commands/COMMAND_TOOL_MAP.md +299 -0
- package/commands/specmem-agents.md +412 -0
- package/commands/specmem-autoclaude.md +295 -0
- package/commands/specmem-changes.md +247 -0
- package/commands/specmem-code.md +103 -0
- package/commands/specmem-configteammembercomms.md +322 -0
- package/commands/specmem-drilldown.md +208 -0
- package/commands/specmem-find.md +195 -0
- package/commands/specmem-getdashboard.md +243 -0
- package/commands/specmem-hooks.md +219 -0
- package/commands/specmem-pointers.md +149 -0
- package/commands/specmem-progress.md +287 -0
- package/commands/specmem-remember.md +123 -0
- package/commands/specmem-service.md +349 -0
- package/commands/specmem-stats.md +189 -0
- package/commands/specmem-team-member.md +409 -0
- package/commands/specmem-webdev.md +583 -0
- package/commands/specmem.md +363 -0
- package/dist/autoStart/index.d.ts +214 -0
- package/dist/autoStart/index.d.ts.map +1 -0
- package/dist/autoStart/index.js +883 -0
- package/dist/autoStart/index.js.map +1 -0
- package/dist/claude-sessions/contextRestorationParser.d.ts +74 -0
- package/dist/claude-sessions/contextRestorationParser.d.ts.map +1 -0
- package/dist/claude-sessions/contextRestorationParser.js +570 -0
- package/dist/claude-sessions/contextRestorationParser.js.map +1 -0
- package/dist/claude-sessions/index.d.ts +13 -0
- package/dist/claude-sessions/index.d.ts.map +1 -0
- package/dist/claude-sessions/index.js +11 -0
- package/dist/claude-sessions/index.js.map +1 -0
- package/dist/claude-sessions/sessionIntegration.d.ts +48 -0
- package/dist/claude-sessions/sessionIntegration.d.ts.map +1 -0
- package/dist/claude-sessions/sessionIntegration.js +146 -0
- package/dist/claude-sessions/sessionIntegration.js.map +1 -0
- package/dist/claude-sessions/sessionParser.d.ts +293 -0
- package/dist/claude-sessions/sessionParser.d.ts.map +1 -0
- package/dist/claude-sessions/sessionParser.js +1028 -0
- package/dist/claude-sessions/sessionParser.js.map +1 -0
- package/dist/claude-sessions/sessionWatcher.d.ts +139 -0
- package/dist/claude-sessions/sessionWatcher.d.ts.map +1 -0
- package/dist/claude-sessions/sessionWatcher.js +722 -0
- package/dist/claude-sessions/sessionWatcher.js.map +1 -0
- package/dist/cli/deploy-to-claude.d.ts +56 -0
- package/dist/cli/deploy-to-claude.d.ts.map +1 -0
- package/dist/cli/deploy-to-claude.js +576 -0
- package/dist/cli/deploy-to-claude.js.map +1 -0
- package/dist/code-explanations/explainCode.d.ts +86 -0
- package/dist/code-explanations/explainCode.d.ts.map +1 -0
- package/dist/code-explanations/explainCode.js +286 -0
- package/dist/code-explanations/explainCode.js.map +1 -0
- package/dist/code-explanations/feedback.d.ts +87 -0
- package/dist/code-explanations/feedback.d.ts.map +1 -0
- package/dist/code-explanations/feedback.js +212 -0
- package/dist/code-explanations/feedback.js.map +1 -0
- package/dist/code-explanations/getRelatedCode.d.ts +80 -0
- package/dist/code-explanations/getRelatedCode.d.ts.map +1 -0
- package/dist/code-explanations/getRelatedCode.js +262 -0
- package/dist/code-explanations/getRelatedCode.js.map +1 -0
- package/dist/code-explanations/index.d.ts +284 -0
- package/dist/code-explanations/index.d.ts.map +1 -0
- package/dist/code-explanations/index.js +249 -0
- package/dist/code-explanations/index.js.map +1 -0
- package/dist/code-explanations/linkCodeToPrompt.d.ts +79 -0
- package/dist/code-explanations/linkCodeToPrompt.d.ts.map +1 -0
- package/dist/code-explanations/linkCodeToPrompt.js +213 -0
- package/dist/code-explanations/linkCodeToPrompt.js.map +1 -0
- package/dist/code-explanations/recallExplanation.d.ts +88 -0
- package/dist/code-explanations/recallExplanation.d.ts.map +1 -0
- package/dist/code-explanations/recallExplanation.js +218 -0
- package/dist/code-explanations/recallExplanation.js.map +1 -0
- package/dist/code-explanations/schema.d.ts +32 -0
- package/dist/code-explanations/schema.d.ts.map +1 -0
- package/dist/code-explanations/schema.js +221 -0
- package/dist/code-explanations/schema.js.map +1 -0
- package/dist/code-explanations/semanticSearch.d.ts +75 -0
- package/dist/code-explanations/semanticSearch.d.ts.map +1 -0
- package/dist/code-explanations/semanticSearch.js +203 -0
- package/dist/code-explanations/semanticSearch.js.map +1 -0
- package/dist/code-explanations/types.d.ts +328 -0
- package/dist/code-explanations/types.d.ts.map +1 -0
- package/dist/code-explanations/types.js +122 -0
- package/dist/code-explanations/types.js.map +1 -0
- package/dist/codebase/codeAnalyzer.d.ts +272 -0
- package/dist/codebase/codeAnalyzer.d.ts.map +1 -0
- package/dist/codebase/codeAnalyzer.js +1353 -0
- package/dist/codebase/codeAnalyzer.js.map +1 -0
- package/dist/codebase/codebaseIndexer.d.ts +360 -0
- package/dist/codebase/codebaseIndexer.d.ts.map +1 -0
- package/dist/codebase/codebaseIndexer.js +1735 -0
- package/dist/codebase/codebaseIndexer.js.map +1 -0
- package/dist/codebase/codebaseTools.d.ts +853 -0
- package/dist/codebase/codebaseTools.d.ts.map +1 -0
- package/dist/codebase/codebaseTools.js +1279 -0
- package/dist/codebase/codebaseTools.js.map +1 -0
- package/dist/codebase/exclusions.d.ts +111 -0
- package/dist/codebase/exclusions.d.ts.map +1 -0
- package/dist/codebase/exclusions.js +771 -0
- package/dist/codebase/exclusions.js.map +1 -0
- package/dist/codebase/fileWatcher.d.ts +135 -0
- package/dist/codebase/fileWatcher.d.ts.map +1 -0
- package/dist/codebase/fileWatcher.js +309 -0
- package/dist/codebase/fileWatcher.js.map +1 -0
- package/dist/codebase/index.d.ts +33 -0
- package/dist/codebase/index.d.ts.map +1 -0
- package/dist/codebase/index.js +77 -0
- package/dist/codebase/index.js.map +1 -0
- package/dist/codebase/ingestion.d.ts +177 -0
- package/dist/codebase/ingestion.d.ts.map +1 -0
- package/dist/codebase/ingestion.js +690 -0
- package/dist/codebase/ingestion.js.map +1 -0
- package/dist/codebase/languageDetection.d.ts +75 -0
- package/dist/codebase/languageDetection.d.ts.map +1 -0
- package/dist/codebase/languageDetection.js +768 -0
- package/dist/codebase/languageDetection.js.map +1 -0
- package/dist/commands/codebaseCommands.d.ts +101 -0
- package/dist/commands/codebaseCommands.d.ts.map +1 -0
- package/dist/commands/codebaseCommands.js +911 -0
- package/dist/commands/codebaseCommands.js.map +1 -0
- package/dist/commands/commandHandler.d.ts +126 -0
- package/dist/commands/commandHandler.d.ts.map +1 -0
- package/dist/commands/commandHandler.js +296 -0
- package/dist/commands/commandHandler.js.map +1 -0
- package/dist/commands/commandLoader.d.ts +103 -0
- package/dist/commands/commandLoader.d.ts.map +1 -0
- package/dist/commands/commandLoader.js +223 -0
- package/dist/commands/commandLoader.js.map +1 -0
- package/dist/commands/contextCommands.d.ts +83 -0
- package/dist/commands/contextCommands.d.ts.map +1 -0
- package/dist/commands/contextCommands.js +512 -0
- package/dist/commands/contextCommands.js.map +1 -0
- package/dist/commands/index.d.ts +24 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +28 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/mcpResources.d.ts +50 -0
- package/dist/commands/mcpResources.d.ts.map +1 -0
- package/dist/commands/mcpResources.js +372 -0
- package/dist/commands/mcpResources.js.map +1 -0
- package/dist/commands/memoryCommands.d.ts +74 -0
- package/dist/commands/memoryCommands.d.ts.map +1 -0
- package/dist/commands/memoryCommands.js +609 -0
- package/dist/commands/memoryCommands.js.map +1 -0
- package/dist/commands/promptCommands.d.ts +91 -0
- package/dist/commands/promptCommands.d.ts.map +1 -0
- package/dist/commands/promptCommands.js +801 -0
- package/dist/commands/promptCommands.js.map +1 -0
- package/dist/commands/teamMemberCommands.d.ts +21 -0
- package/dist/commands/teamMemberCommands.d.ts.map +1 -0
- package/dist/commands/teamMemberCommands.js +137 -0
- package/dist/commands/teamMemberCommands.js.map +1 -0
- package/dist/comms/fileCommsTransport.d.ts +91 -0
- package/dist/comms/fileCommsTransport.d.ts.map +1 -0
- package/dist/comms/fileCommsTransport.js +244 -0
- package/dist/comms/fileCommsTransport.js.map +1 -0
- package/dist/comms/index.d.ts +7 -0
- package/dist/comms/index.d.ts.map +1 -0
- package/dist/comms/index.js +7 -0
- package/dist/comms/index.js.map +1 -0
- package/dist/config/apiKeyDetection.d.ts +41 -0
- package/dist/config/apiKeyDetection.d.ts.map +1 -0
- package/dist/config/apiKeyDetection.js +211 -0
- package/dist/config/apiKeyDetection.js.map +1 -0
- package/dist/config/autoConfig.d.ts +188 -0
- package/dist/config/autoConfig.d.ts.map +1 -0
- package/dist/config/autoConfig.js +850 -0
- package/dist/config/autoConfig.js.map +1 -0
- package/dist/config/configSync.d.ts +119 -0
- package/dist/config/configSync.d.ts.map +1 -0
- package/dist/config/configSync.js +878 -0
- package/dist/config/configSync.js.map +1 -0
- package/dist/config/embeddingTimeouts.d.ts +145 -0
- package/dist/config/embeddingTimeouts.d.ts.map +1 -0
- package/dist/config/embeddingTimeouts.js +255 -0
- package/dist/config/embeddingTimeouts.js.map +1 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/languageConfig.d.ts +68 -0
- package/dist/config/languageConfig.d.ts.map +1 -0
- package/dist/config/languageConfig.js +473 -0
- package/dist/config/languageConfig.js.map +1 -0
- package/dist/config/password.d.ts +145 -0
- package/dist/config/password.d.ts.map +1 -0
- package/dist/config/password.js +428 -0
- package/dist/config/password.js.map +1 -0
- package/dist/config.d.ts +338 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +1177 -0
- package/dist/config.js.map +1 -0
- package/dist/consolidation.d.ts +44 -0
- package/dist/consolidation.d.ts.map +1 -0
- package/dist/consolidation.js +447 -0
- package/dist/consolidation.js.map +1 -0
- package/dist/constants.d.ts +371 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +552 -0
- package/dist/constants.js.map +1 -0
- package/dist/coordination/TeamMemberRegistry.d.ts +192 -0
- package/dist/coordination/TeamMemberRegistry.d.ts.map +1 -0
- package/dist/coordination/TeamMemberRegistry.js +415 -0
- package/dist/coordination/TeamMemberRegistry.js.map +1 -0
- package/dist/coordination/events.d.ts +369 -0
- package/dist/coordination/events.d.ts.map +1 -0
- package/dist/coordination/events.js +232 -0
- package/dist/coordination/events.js.map +1 -0
- package/dist/coordination/handlers.d.ts +116 -0
- package/dist/coordination/handlers.d.ts.map +1 -0
- package/dist/coordination/handlers.js +400 -0
- package/dist/coordination/handlers.js.map +1 -0
- package/dist/coordination/index.d.ts +14 -0
- package/dist/coordination/index.d.ts.map +1 -0
- package/dist/coordination/index.js +31 -0
- package/dist/coordination/index.js.map +1 -0
- package/dist/coordination/integration.d.ts +260 -0
- package/dist/coordination/integration.d.ts.map +1 -0
- package/dist/coordination/integration.js +472 -0
- package/dist/coordination/integration.js.map +1 -0
- package/dist/coordination/server.d.ts +266 -0
- package/dist/coordination/server.d.ts.map +1 -0
- package/dist/coordination/server.js +995 -0
- package/dist/coordination/server.js.map +1 -0
- package/dist/coordination/serviceProvider.d.ts +70 -0
- package/dist/coordination/serviceProvider.d.ts.map +1 -0
- package/dist/coordination/serviceProvider.js +273 -0
- package/dist/coordination/serviceProvider.js.map +1 -0
- package/dist/dashboard/api/claudeControl.d.ts +44 -0
- package/dist/dashboard/api/claudeControl.d.ts.map +1 -0
- package/dist/dashboard/api/claudeControl.js +650 -0
- package/dist/dashboard/api/claudeControl.js.map +1 -0
- package/dist/dashboard/api/claudeHistory.d.ts +4 -0
- package/dist/dashboard/api/claudeHistory.d.ts.map +1 -0
- package/dist/dashboard/api/claudeHistory.js +319 -0
- package/dist/dashboard/api/claudeHistory.js.map +1 -0
- package/dist/dashboard/api/dataExport.d.ts +23 -0
- package/dist/dashboard/api/dataExport.d.ts.map +1 -0
- package/dist/dashboard/api/dataExport.js +509 -0
- package/dist/dashboard/api/dataExport.js.map +1 -0
- package/dist/dashboard/api/fileManager.d.ts +39 -0
- package/dist/dashboard/api/fileManager.d.ts.map +1 -0
- package/dist/dashboard/api/fileManager.js +814 -0
- package/dist/dashboard/api/fileManager.js.map +1 -0
- package/dist/dashboard/api/hooks.d.ts +16 -0
- package/dist/dashboard/api/hooks.d.ts.map +1 -0
- package/dist/dashboard/api/hooks.js +342 -0
- package/dist/dashboard/api/hooks.js.map +1 -0
- package/dist/dashboard/api/hotReload.d.ts +14 -0
- package/dist/dashboard/api/hotReload.d.ts.map +1 -0
- package/dist/dashboard/api/hotReload.js +219 -0
- package/dist/dashboard/api/hotReload.js.map +1 -0
- package/dist/dashboard/api/liveSessionStream.d.ts +19 -0
- package/dist/dashboard/api/liveSessionStream.d.ts.map +1 -0
- package/dist/dashboard/api/liveSessionStream.js +430 -0
- package/dist/dashboard/api/liveSessionStream.js.map +1 -0
- package/dist/dashboard/api/memoryRecall.d.ts +20 -0
- package/dist/dashboard/api/memoryRecall.d.ts.map +1 -0
- package/dist/dashboard/api/memoryRecall.js +524 -0
- package/dist/dashboard/api/memoryRecall.js.map +1 -0
- package/dist/dashboard/api/promptSend.d.ts +33 -0
- package/dist/dashboard/api/promptSend.d.ts.map +1 -0
- package/dist/dashboard/api/promptSend.js +544 -0
- package/dist/dashboard/api/promptSend.js.map +1 -0
- package/dist/dashboard/api/settings.d.ts +10 -0
- package/dist/dashboard/api/settings.d.ts.map +1 -0
- package/dist/dashboard/api/settings.js +656 -0
- package/dist/dashboard/api/settings.js.map +1 -0
- package/dist/dashboard/api/setup.d.ts +21 -0
- package/dist/dashboard/api/setup.d.ts.map +1 -0
- package/dist/dashboard/api/setup.js +663 -0
- package/dist/dashboard/api/setup.js.map +1 -0
- package/dist/dashboard/api/specmemTools.d.ts +14 -0
- package/dist/dashboard/api/specmemTools.d.ts.map +1 -0
- package/dist/dashboard/api/specmemTools.js +1059 -0
- package/dist/dashboard/api/specmemTools.js.map +1 -0
- package/dist/dashboard/api/taskTeamMembers.d.ts +8 -0
- package/dist/dashboard/api/taskTeamMembers.d.ts.map +1 -0
- package/dist/dashboard/api/taskTeamMembers.js +136 -0
- package/dist/dashboard/api/taskTeamMembers.js.map +1 -0
- package/dist/dashboard/api/teamMemberDeploy.d.ts +15 -0
- package/dist/dashboard/api/teamMemberDeploy.d.ts.map +1 -0
- package/dist/dashboard/api/teamMemberDeploy.js +421 -0
- package/dist/dashboard/api/teamMemberDeploy.js.map +1 -0
- package/dist/dashboard/api/teamMemberHistory.d.ts +38 -0
- package/dist/dashboard/api/teamMemberHistory.d.ts.map +1 -0
- package/dist/dashboard/api/teamMemberHistory.js +583 -0
- package/dist/dashboard/api/teamMemberHistory.js.map +1 -0
- package/dist/dashboard/api/terminal.d.ts +12 -0
- package/dist/dashboard/api/terminal.d.ts.map +1 -0
- package/dist/dashboard/api/terminal.js +344 -0
- package/dist/dashboard/api/terminal.js.map +1 -0
- package/dist/dashboard/api/terminalInject.d.ts +17 -0
- package/dist/dashboard/api/terminalInject.d.ts.map +1 -0
- package/dist/dashboard/api/terminalInject.js +322 -0
- package/dist/dashboard/api/terminalInject.js.map +1 -0
- package/dist/dashboard/api/terminalStream.d.ts +12 -0
- package/dist/dashboard/api/terminalStream.d.ts.map +1 -0
- package/dist/dashboard/api/terminalStream.js +482 -0
- package/dist/dashboard/api/terminalStream.js.map +1 -0
- package/dist/dashboard/index.d.ts +7 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +7 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/dashboard/ptyStreamer.d.ts +173 -0
- package/dist/dashboard/ptyStreamer.d.ts.map +1 -0
- package/dist/dashboard/ptyStreamer.js +661 -0
- package/dist/dashboard/ptyStreamer.js.map +1 -0
- package/dist/dashboard/public/DASHBOARD-README.md +378 -0
- package/dist/dashboard/public/INTEGRATION-GUIDE.md +395 -0
- package/dist/dashboard/public/codebase-config.html +1247 -0
- package/dist/dashboard/public/dashboard-v2.html +1942 -0
- package/dist/dashboard/public/data-export.html +819 -0
- package/dist/dashboard/public/example-page.html +164 -0
- package/dist/dashboard/public/file-explorer.html +1023 -0
- package/dist/dashboard/public/hooks.html +1103 -0
- package/dist/dashboard/public/index-improvements.css +499 -0
- package/dist/dashboard/public/index.html +5534 -0
- package/dist/dashboard/public/memory-controls.html +1959 -0
- package/dist/dashboard/public/memory-recall.html +1495 -0
- package/dist/dashboard/public/previews/skeleton-memory-graph.html +361 -0
- package/dist/dashboard/public/previews/skeleton-memory-list.html +366 -0
- package/dist/dashboard/public/previews/skeleton-search-results.html +609 -0
- package/dist/dashboard/public/previews/skeleton-stats-dashboard.html +556 -0
- package/dist/dashboard/public/prompt-console.html +2763 -0
- package/dist/dashboard/public/react-dist/assets/index-CkjobT5B.js +871 -0
- package/dist/dashboard/public/react-dist/assets/index-iRclxMst.css +1 -0
- package/dist/dashboard/public/react-dist/index.html +16 -0
- package/dist/dashboard/public/shared-header.js +325 -0
- package/dist/dashboard/public/shared-language-selector.js +626 -0
- package/dist/dashboard/public/shared-logger.js +66 -0
- package/dist/dashboard/public/shared-nav.js +325 -0
- package/dist/dashboard/public/shared-theme-blue.css +331 -0
- package/dist/dashboard/public/shared-theme.css +813 -0
- package/dist/dashboard/public/shared-toast.js +415 -0
- package/dist/dashboard/public/team-member-history.html +1291 -0
- package/dist/dashboard/public/team-member-spy.html +1199 -0
- package/dist/dashboard/public/team-members.html +3756 -0
- package/dist/dashboard/public/terminal-output.html +1013 -0
- package/dist/dashboard/public/terminal.html +372 -0
- package/dist/dashboard/sessionStore.d.ts +86 -0
- package/dist/dashboard/sessionStore.d.ts.map +1 -0
- package/dist/dashboard/sessionStore.js +262 -0
- package/dist/dashboard/sessionStore.js.map +1 -0
- package/dist/dashboard/standalone.d.ts +27 -0
- package/dist/dashboard/standalone.d.ts.map +1 -0
- package/dist/dashboard/standalone.js +380 -0
- package/dist/dashboard/standalone.js.map +1 -0
- package/dist/dashboard/webServer.d.ts +390 -0
- package/dist/dashboard/webServer.d.ts.map +1 -0
- package/dist/dashboard/webServer.js +4297 -0
- package/dist/dashboard/webServer.js.map +1 -0
- package/dist/dashboard/websocket/teamMemberStream.d.ts +87 -0
- package/dist/dashboard/websocket/teamMemberStream.d.ts.map +1 -0
- package/dist/dashboard/websocket/teamMemberStream.js +366 -0
- package/dist/dashboard/websocket/teamMemberStream.js.map +1 -0
- package/dist/dashboard/websocket/terminalStream.d.ts +130 -0
- package/dist/dashboard/websocket/terminalStream.d.ts.map +1 -0
- package/dist/dashboard/websocket/terminalStream.js +456 -0
- package/dist/dashboard/websocket/terminalStream.js.map +1 -0
- package/dist/database/embeddedPostgres.d.ts +187 -0
- package/dist/database/embeddedPostgres.d.ts.map +1 -0
- package/dist/database/embeddedPostgres.js +763 -0
- package/dist/database/embeddedPostgres.js.map +1 -0
- package/dist/database/index.d.ts +12 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +20 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/initEmbeddedPostgres.d.ts +124 -0
- package/dist/database/initEmbeddedPostgres.d.ts.map +1 -0
- package/dist/database/initEmbeddedPostgres.js +855 -0
- package/dist/database/initEmbeddedPostgres.js.map +1 -0
- package/dist/database.d.ts +256 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +1209 -0
- package/dist/database.js.map +1 -0
- package/dist/db/apiDataManager.d.ts +334 -0
- package/dist/db/apiDataManager.d.ts.map +1 -0
- package/dist/db/apiDataManager.js +631 -0
- package/dist/db/apiDataManager.js.map +1 -0
- package/dist/db/batchOperations.d.ts +154 -0
- package/dist/db/batchOperations.d.ts.map +1 -0
- package/dist/db/batchOperations.js +564 -0
- package/dist/db/batchOperations.js.map +1 -0
- package/dist/db/bigBrainMigrations.d.ts +48 -0
- package/dist/db/bigBrainMigrations.d.ts.map +1 -0
- package/dist/db/bigBrainMigrations.js +4266 -0
- package/dist/db/bigBrainMigrations.js.map +1 -0
- package/dist/db/connectionPoolGoBrrr.d.ts +94 -0
- package/dist/db/connectionPoolGoBrrr.d.ts.map +1 -0
- package/dist/db/connectionPoolGoBrrr.js +548 -0
- package/dist/db/connectionPoolGoBrrr.js.map +1 -0
- package/dist/db/dashboardQueries.d.ts +182 -0
- package/dist/db/dashboardQueries.d.ts.map +1 -0
- package/dist/db/dashboardQueries.js +821 -0
- package/dist/db/dashboardQueries.js.map +1 -0
- package/dist/db/deploymentBootstrap.d.ts +43 -0
- package/dist/db/deploymentBootstrap.d.ts.map +1 -0
- package/dist/db/deploymentBootstrap.js +329 -0
- package/dist/db/deploymentBootstrap.js.map +1 -0
- package/dist/db/dimensionService.d.ts +140 -0
- package/dist/db/dimensionService.d.ts.map +1 -0
- package/dist/db/dimensionService.js +261 -0
- package/dist/db/dimensionService.js.map +1 -0
- package/dist/db/embeddingOverflow.d.ts +69 -0
- package/dist/db/embeddingOverflow.d.ts.map +1 -0
- package/dist/db/embeddingOverflow.js +332 -0
- package/dist/db/embeddingOverflow.js.map +1 -0
- package/dist/db/embeddingOverflow.sql +221 -0
- package/dist/db/findThatShit.d.ts +145 -0
- package/dist/db/findThatShit.d.ts.map +1 -0
- package/dist/db/findThatShit.js +782 -0
- package/dist/db/findThatShit.js.map +1 -0
- package/dist/db/hotPathManager.d.ts +187 -0
- package/dist/db/hotPathManager.d.ts.map +1 -0
- package/dist/db/hotPathManager.js +504 -0
- package/dist/db/hotPathManager.js.map +1 -0
- package/dist/db/index.d.ts +85 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +219 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/memoryDrilldown.sql +99 -0
- package/dist/db/migrate.d.ts +3 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +97 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/migrateJsonToPostgres.d.ts +43 -0
- package/dist/db/migrateJsonToPostgres.d.ts.map +1 -0
- package/dist/db/migrateJsonToPostgres.js +465 -0
- package/dist/db/migrateJsonToPostgres.js.map +1 -0
- package/dist/db/nukeFromOrbit.d.ts +63 -0
- package/dist/db/nukeFromOrbit.d.ts.map +1 -0
- package/dist/db/nukeFromOrbit.js +499 -0
- package/dist/db/nukeFromOrbit.js.map +1 -0
- package/dist/db/processedTraining.sql +60 -0
- package/dist/db/projectNamespacing.d.ts +258 -0
- package/dist/db/projectNamespacing.d.ts.map +1 -0
- package/dist/db/projectNamespacing.js +920 -0
- package/dist/db/projectNamespacing.js.map +1 -0
- package/dist/db/projectNamespacing.sql +374 -0
- package/dist/db/projectSchemaInit.sql +271 -0
- package/dist/db/spatialMemory.d.ts +296 -0
- package/dist/db/spatialMemory.d.ts.map +1 -0
- package/dist/db/spatialMemory.js +818 -0
- package/dist/db/spatialMemory.js.map +1 -0
- package/dist/db/streamingQuery.d.ts +143 -0
- package/dist/db/streamingQuery.d.ts.map +1 -0
- package/dist/db/streamingQuery.js +350 -0
- package/dist/db/streamingQuery.js.map +1 -0
- package/dist/db/teamComms.sql +224 -0
- package/dist/db/yeetStuffInDb.d.ts +72 -0
- package/dist/db/yeetStuffInDb.d.ts.map +1 -0
- package/dist/db/yeetStuffInDb.js +473 -0
- package/dist/db/yeetStuffInDb.js.map +1 -0
- package/dist/embedding-providers/index.d.ts +10 -0
- package/dist/embedding-providers/index.d.ts.map +1 -0
- package/dist/embedding-providers/index.js +12 -0
- package/dist/embedding-providers/index.js.map +1 -0
- package/dist/embeddings/projectionLayer.d.ts +114 -0
- package/dist/embeddings/projectionLayer.d.ts.map +1 -0
- package/dist/embeddings/projectionLayer.js +345 -0
- package/dist/embeddings/projectionLayer.js.map +1 -0
- package/dist/events/Publisher.d.ts +193 -0
- package/dist/events/Publisher.d.ts.map +1 -0
- package/dist/events/Publisher.js +439 -0
- package/dist/events/Publisher.js.map +1 -0
- package/dist/events/config.d.ts +139 -0
- package/dist/events/config.d.ts.map +1 -0
- package/dist/events/config.js +266 -0
- package/dist/events/config.js.map +1 -0
- package/dist/events/index.d.ts +19 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +31 -0
- package/dist/events/index.js.map +1 -0
- package/dist/events/integration.d.ts +206 -0
- package/dist/events/integration.d.ts.map +1 -0
- package/dist/events/integration.js +348 -0
- package/dist/events/integration.js.map +1 -0
- package/dist/events/metrics.d.ts +147 -0
- package/dist/events/metrics.d.ts.map +1 -0
- package/dist/events/metrics.js +343 -0
- package/dist/events/metrics.js.map +1 -0
- package/dist/hooks/cli.d.ts +28 -0
- package/dist/hooks/cli.d.ts.map +1 -0
- package/dist/hooks/cli.js +118 -0
- package/dist/hooks/cli.js.map +1 -0
- package/dist/hooks/contextInjectionHook.d.ts +60 -0
- package/dist/hooks/contextInjectionHook.d.ts.map +1 -0
- package/dist/hooks/contextInjectionHook.js +294 -0
- package/dist/hooks/contextInjectionHook.js.map +1 -0
- package/dist/hooks/drilldownHook.d.ts +125 -0
- package/dist/hooks/drilldownHook.d.ts.map +1 -0
- package/dist/hooks/drilldownHook.js +181 -0
- package/dist/hooks/drilldownHook.js.map +1 -0
- package/dist/hooks/hookManager.d.ts +180 -0
- package/dist/hooks/hookManager.d.ts.map +1 -0
- package/dist/hooks/hookManager.js +782 -0
- package/dist/hooks/hookManager.js.map +1 -0
- package/dist/hooks/index.d.ts +62 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +66 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/lowContextHook.d.ts +71 -0
- package/dist/hooks/lowContextHook.d.ts.map +1 -0
- package/dist/hooks/lowContextHook.js +258 -0
- package/dist/hooks/lowContextHook.js.map +1 -0
- package/dist/hooks/simpleContextHook.d.ts +65 -0
- package/dist/hooks/simpleContextHook.d.ts.map +1 -0
- package/dist/hooks/simpleContextHook.js +194 -0
- package/dist/hooks/simpleContextHook.js.map +1 -0
- package/dist/hooks/teamFramingCli.d.ts +56 -0
- package/dist/hooks/teamFramingCli.d.ts.map +1 -0
- package/dist/hooks/teamFramingCli.js +264 -0
- package/dist/hooks/teamFramingCli.js.map +1 -0
- package/dist/hooks/teamMemberPrepromptHook.d.ts +150 -0
- package/dist/hooks/teamMemberPrepromptHook.d.ts.map +1 -0
- package/dist/hooks/teamMemberPrepromptHook.js +308 -0
- package/dist/hooks/teamMemberPrepromptHook.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4433 -0
- package/dist/index.js.map +1 -0
- package/dist/init/claudeConfigInjector.d.ts +116 -0
- package/dist/init/claudeConfigInjector.d.ts.map +1 -0
- package/dist/init/claudeConfigInjector.js +1154 -0
- package/dist/init/claudeConfigInjector.js.map +1 -0
- package/dist/installer/autoInstall.d.ts +72 -0
- package/dist/installer/autoInstall.d.ts.map +1 -0
- package/dist/installer/autoInstall.js +617 -0
- package/dist/installer/autoInstall.js.map +1 -0
- package/dist/installer/dbSetup.d.ts +84 -0
- package/dist/installer/dbSetup.d.ts.map +1 -0
- package/dist/installer/dbSetup.js +350 -0
- package/dist/installer/dbSetup.js.map +1 -0
- package/dist/installer/firstRun.d.ts +49 -0
- package/dist/installer/firstRun.d.ts.map +1 -0
- package/dist/installer/firstRun.js +207 -0
- package/dist/installer/firstRun.js.map +1 -0
- package/dist/installer/index.d.ts +10 -0
- package/dist/installer/index.d.ts.map +1 -0
- package/dist/installer/index.js +10 -0
- package/dist/installer/index.js.map +1 -0
- package/dist/installer/silentAutoInstall.d.ts +99 -0
- package/dist/installer/silentAutoInstall.d.ts.map +1 -0
- package/dist/installer/silentAutoInstall.js +491 -0
- package/dist/installer/silentAutoInstall.js.map +1 -0
- package/dist/installer/systemDeps.d.ts +54 -0
- package/dist/installer/systemDeps.d.ts.map +1 -0
- package/dist/installer/systemDeps.js +322 -0
- package/dist/installer/systemDeps.js.map +1 -0
- package/dist/mcp/cliNotifications.d.ts +133 -0
- package/dist/mcp/cliNotifications.d.ts.map +1 -0
- package/dist/mcp/cliNotifications.js +289 -0
- package/dist/mcp/cliNotifications.js.map +1 -0
- package/dist/mcp/embeddingServerManager.d.ts +307 -0
- package/dist/mcp/embeddingServerManager.d.ts.map +1 -0
- package/dist/mcp/embeddingServerManager.js +2081 -0
- package/dist/mcp/embeddingServerManager.js.map +1 -0
- package/dist/mcp/healthMonitor.d.ts +196 -0
- package/dist/mcp/healthMonitor.d.ts.map +1 -0
- package/dist/mcp/healthMonitor.js +685 -0
- package/dist/mcp/healthMonitor.js.map +1 -0
- package/dist/mcp/hotReloadManager.d.ts +101 -0
- package/dist/mcp/hotReloadManager.d.ts.map +1 -0
- package/dist/mcp/hotReloadManager.js +251 -0
- package/dist/mcp/hotReloadManager.js.map +1 -0
- package/dist/mcp/index.d.ts +16 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +22 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/mcpProtocolHandler.d.ts +64 -0
- package/dist/mcp/mcpProtocolHandler.d.ts.map +1 -0
- package/dist/mcp/mcpProtocolHandler.js +253 -0
- package/dist/mcp/mcpProtocolHandler.js.map +1 -0
- package/dist/mcp/miniCOTServerManager.d.ts +336 -0
- package/dist/mcp/miniCOTServerManager.d.ts.map +1 -0
- package/dist/mcp/miniCOTServerManager.js +1384 -0
- package/dist/mcp/miniCOTServerManager.js.map +1 -0
- package/dist/mcp/promptExecutor.d.ts +188 -0
- package/dist/mcp/promptExecutor.d.ts.map +1 -0
- package/dist/mcp/promptExecutor.js +469 -0
- package/dist/mcp/promptExecutor.js.map +1 -0
- package/dist/mcp/reloadBroadcast.d.ts +127 -0
- package/dist/mcp/reloadBroadcast.d.ts.map +1 -0
- package/dist/mcp/reloadBroadcast.js +275 -0
- package/dist/mcp/reloadBroadcast.js.map +1 -0
- package/dist/mcp/resilientTransport.d.ts +249 -0
- package/dist/mcp/resilientTransport.d.ts.map +1 -0
- package/dist/mcp/resilientTransport.js +931 -0
- package/dist/mcp/resilientTransport.js.map +1 -0
- package/dist/mcp/samplingHandler.d.ts +129 -0
- package/dist/mcp/samplingHandler.d.ts.map +1 -0
- package/dist/mcp/samplingHandler.js +276 -0
- package/dist/mcp/samplingHandler.js.map +1 -0
- package/dist/mcp/specMemServer.d.ts +305 -0
- package/dist/mcp/specMemServer.d.ts.map +1 -0
- package/dist/mcp/specMemServer.js +2048 -0
- package/dist/mcp/specMemServer.js.map +1 -0
- package/dist/mcp/toolRegistry.d.ts +122 -0
- package/dist/mcp/toolRegistry.d.ts.map +1 -0
- package/dist/mcp/toolRegistry.js +609 -0
- package/dist/mcp/toolRegistry.js.map +1 -0
- package/dist/mcp/tools/embeddingControl.d.ts +114 -0
- package/dist/mcp/tools/embeddingControl.d.ts.map +1 -0
- package/dist/mcp/tools/embeddingControl.js +222 -0
- package/dist/mcp/tools/embeddingControl.js.map +1 -0
- package/dist/mcp/tools/index.d.ts +10 -0
- package/dist/mcp/tools/index.d.ts.map +1 -0
- package/dist/mcp/tools/index.js +17 -0
- package/dist/mcp/tools/index.js.map +1 -0
- package/dist/mcp/tools/teamComms.d.ts +444 -0
- package/dist/mcp/tools/teamComms.d.ts.map +1 -0
- package/dist/mcp/tools/teamComms.js +1953 -0
- package/dist/mcp/tools/teamComms.js.map +1 -0
- package/dist/mcp/triggerSystem.d.ts +129 -0
- package/dist/mcp/triggerSystem.d.ts.map +1 -0
- package/dist/mcp/triggerSystem.js +363 -0
- package/dist/mcp/triggerSystem.js.map +1 -0
- package/dist/mcp/watcherIntegration.d.ts +77 -0
- package/dist/mcp/watcherIntegration.d.ts.map +1 -0
- package/dist/mcp/watcherIntegration.js +428 -0
- package/dist/mcp/watcherIntegration.js.map +1 -0
- package/dist/mcp/watcherToolWrappers.d.ts +89 -0
- package/dist/mcp/watcherToolWrappers.d.ts.map +1 -0
- package/dist/mcp/watcherToolWrappers.js +91 -0
- package/dist/mcp/watcherToolWrappers.js.map +1 -0
- package/dist/memorization/claudeCodeMigration.d.ts +34 -0
- package/dist/memorization/claudeCodeMigration.d.ts.map +1 -0
- package/dist/memorization/claudeCodeMigration.js +210 -0
- package/dist/memorization/claudeCodeMigration.js.map +1 -0
- package/dist/memorization/claudeCodeTracker.d.ts +147 -0
- package/dist/memorization/claudeCodeTracker.d.ts.map +1 -0
- package/dist/memorization/claudeCodeTracker.js +424 -0
- package/dist/memorization/claudeCodeTracker.js.map +1 -0
- package/dist/memorization/codeMemorizer.d.ts +158 -0
- package/dist/memorization/codeMemorizer.d.ts.map +1 -0
- package/dist/memorization/codeMemorizer.js +357 -0
- package/dist/memorization/codeMemorizer.js.map +1 -0
- package/dist/memorization/codeRecall.d.ts +156 -0
- package/dist/memorization/codeRecall.d.ts.map +1 -0
- package/dist/memorization/codeRecall.js +499 -0
- package/dist/memorization/codeRecall.js.map +1 -0
- package/dist/memorization/index.d.ts +55 -0
- package/dist/memorization/index.d.ts.map +1 -0
- package/dist/memorization/index.js +64 -0
- package/dist/memorization/index.js.map +1 -0
- package/dist/memorization/memorizationTools.d.ts +413 -0
- package/dist/memorization/memorizationTools.d.ts.map +1 -0
- package/dist/memorization/memorizationTools.js +513 -0
- package/dist/memorization/memorizationTools.js.map +1 -0
- package/dist/memorization/watcherIntegration.d.ts +100 -0
- package/dist/memorization/watcherIntegration.d.ts.map +1 -0
- package/dist/memorization/watcherIntegration.js +196 -0
- package/dist/memorization/watcherIntegration.js.map +1 -0
- package/dist/memory/humanLikeMemory.d.ts +206 -0
- package/dist/memory/humanLikeMemory.d.ts.map +1 -0
- package/dist/memory/humanLikeMemory.js +603 -0
- package/dist/memory/humanLikeMemory.js.map +1 -0
- package/dist/memory/index.d.ts +22 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +24 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/memoryEvolutionMigration.d.ts +36 -0
- package/dist/memory/memoryEvolutionMigration.d.ts.map +1 -0
- package/dist/memory/memoryEvolutionMigration.js +371 -0
- package/dist/memory/memoryEvolutionMigration.js.map +1 -0
- package/dist/memory/quadrantSearch.d.ts +221 -0
- package/dist/memory/quadrantSearch.d.ts.map +1 -0
- package/dist/memory/quadrantSearch.js +897 -0
- package/dist/memory/quadrantSearch.js.map +1 -0
- package/dist/middleware/apiVersioning.d.ts +83 -0
- package/dist/middleware/apiVersioning.d.ts.map +1 -0
- package/dist/middleware/apiVersioning.js +152 -0
- package/dist/middleware/apiVersioning.js.map +1 -0
- package/dist/middleware/compression.d.ts +48 -0
- package/dist/middleware/compression.d.ts.map +1 -0
- package/dist/middleware/compression.js +240 -0
- package/dist/middleware/compression.js.map +1 -0
- package/dist/middleware/csrf.d.ts +118 -0
- package/dist/middleware/csrf.d.ts.map +1 -0
- package/dist/middleware/csrf.js +300 -0
- package/dist/middleware/csrf.js.map +1 -0
- package/dist/middleware/index.d.ts +13 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +17 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/wsRateLimiter.d.ts +129 -0
- package/dist/middleware/wsRateLimiter.d.ts.map +1 -0
- package/dist/middleware/wsRateLimiter.js +279 -0
- package/dist/middleware/wsRateLimiter.js.map +1 -0
- package/dist/migrations/run.d.ts +2 -0
- package/dist/migrations/run.d.ts.map +1 -0
- package/dist/migrations/run.js +25 -0
- package/dist/migrations/run.js.map +1 -0
- package/dist/migrations/syncDimensions.d.ts +24 -0
- package/dist/migrations/syncDimensions.d.ts.map +1 -0
- package/dist/migrations/syncDimensions.js +140 -0
- package/dist/migrations/syncDimensions.js.map +1 -0
- package/dist/migrations/teamComms.d.ts +16 -0
- package/dist/migrations/teamComms.d.ts.map +1 -0
- package/dist/migrations/teamComms.js +210 -0
- package/dist/migrations/teamComms.js.map +1 -0
- package/dist/openapi/index.d.ts +10 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +10 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/openapi/spec.d.ts +902 -0
- package/dist/openapi/spec.d.ts.map +1 -0
- package/dist/openapi/spec.js +733 -0
- package/dist/openapi/spec.js.map +1 -0
- package/dist/packages/dependencyHistory.d.ts +113 -0
- package/dist/packages/dependencyHistory.d.ts.map +1 -0
- package/dist/packages/dependencyHistory.js +360 -0
- package/dist/packages/dependencyHistory.js.map +1 -0
- package/dist/packages/index.d.ts +30 -0
- package/dist/packages/index.d.ts.map +1 -0
- package/dist/packages/index.js +65 -0
- package/dist/packages/index.js.map +1 -0
- package/dist/packages/packageTools.d.ts +255 -0
- package/dist/packages/packageTools.d.ts.map +1 -0
- package/dist/packages/packageTools.js +242 -0
- package/dist/packages/packageTools.js.map +1 -0
- package/dist/packages/packageTracker.d.ts +98 -0
- package/dist/packages/packageTracker.d.ts.map +1 -0
- package/dist/packages/packageTracker.js +268 -0
- package/dist/packages/packageTracker.js.map +1 -0
- package/dist/packages/packageWatcher.d.ts +62 -0
- package/dist/packages/packageWatcher.d.ts.map +1 -0
- package/dist/packages/packageWatcher.js +146 -0
- package/dist/packages/packageWatcher.js.map +1 -0
- package/dist/providers/MiniCOTProvider.d.ts +48 -0
- package/dist/providers/MiniCOTProvider.d.ts.map +1 -0
- package/dist/providers/MiniCOTProvider.js +98 -0
- package/dist/providers/MiniCOTProvider.js.map +1 -0
- package/dist/reminders/index.d.ts +5 -0
- package/dist/reminders/index.d.ts.map +1 -0
- package/dist/reminders/index.js +5 -0
- package/dist/reminders/index.js.map +1 -0
- package/dist/reminders/skillReminder.d.ts +131 -0
- package/dist/reminders/skillReminder.d.ts.map +1 -0
- package/dist/reminders/skillReminder.js +386 -0
- package/dist/reminders/skillReminder.js.map +1 -0
- package/dist/search.d.ts +35 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +574 -0
- package/dist/search.js.map +1 -0
- package/dist/security/localhostOnly.d.ts +36 -0
- package/dist/security/localhostOnly.d.ts.map +1 -0
- package/dist/security/localhostOnly.js +101 -0
- package/dist/security/localhostOnly.js.map +1 -0
- package/dist/services/CameraZoomSearch.d.ts +206 -0
- package/dist/services/CameraZoomSearch.d.ts.map +1 -0
- package/dist/services/CameraZoomSearch.js +669 -0
- package/dist/services/CameraZoomSearch.js.map +1 -0
- package/dist/services/DataFlowPipeline.d.ts +111 -0
- package/dist/services/DataFlowPipeline.d.ts.map +1 -0
- package/dist/services/DataFlowPipeline.js +379 -0
- package/dist/services/DataFlowPipeline.js.map +1 -0
- package/dist/services/DimensionAdapter.d.ts +194 -0
- package/dist/services/DimensionAdapter.d.ts.map +1 -0
- package/dist/services/DimensionAdapter.js +566 -0
- package/dist/services/DimensionAdapter.js.map +1 -0
- package/dist/services/DimensionService.d.ts +252 -0
- package/dist/services/DimensionService.d.ts.map +1 -0
- package/dist/services/DimensionService.js +564 -0
- package/dist/services/DimensionService.js.map +1 -0
- package/dist/services/EmbeddingQueue.d.ts +71 -0
- package/dist/services/EmbeddingQueue.d.ts.map +1 -0
- package/dist/services/EmbeddingQueue.js +258 -0
- package/dist/services/EmbeddingQueue.js.map +1 -0
- package/dist/services/MemoryDrilldown.d.ts +226 -0
- package/dist/services/MemoryDrilldown.d.ts.map +1 -0
- package/dist/services/MemoryDrilldown.js +479 -0
- package/dist/services/MemoryDrilldown.js.map +1 -0
- package/dist/services/MiniCOTScorer.d.ts +140 -0
- package/dist/services/MiniCOTScorer.d.ts.map +1 -0
- package/dist/services/MiniCOTScorer.js +292 -0
- package/dist/services/MiniCOTScorer.js.map +1 -0
- package/dist/services/ProjectContext.d.ts +342 -0
- package/dist/services/ProjectContext.d.ts.map +1 -0
- package/dist/services/ProjectContext.js +667 -0
- package/dist/services/ProjectContext.js.map +1 -0
- package/dist/services/ResponseCompactor.d.ts +135 -0
- package/dist/services/ResponseCompactor.d.ts.map +1 -0
- package/dist/services/ResponseCompactor.js +501 -0
- package/dist/services/ResponseCompactor.js.map +1 -0
- package/dist/services/TeamCommsDbService.d.ts +202 -0
- package/dist/services/TeamCommsDbService.d.ts.map +1 -0
- package/dist/services/TeamCommsDbService.js +526 -0
- package/dist/services/TeamCommsDbService.js.map +1 -0
- package/dist/services/UnifiedPasswordService.d.ts +166 -0
- package/dist/services/UnifiedPasswordService.d.ts.map +1 -0
- package/dist/services/UnifiedPasswordService.js +587 -0
- package/dist/services/UnifiedPasswordService.js.map +1 -0
- package/dist/services/adaptiveSearchConfig.d.ts +64 -0
- package/dist/services/adaptiveSearchConfig.d.ts.map +1 -0
- package/dist/services/adaptiveSearchConfig.js +187 -0
- package/dist/services/adaptiveSearchConfig.js.map +1 -0
- package/dist/skills/index.d.ts +8 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +8 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/skillScanner.d.ts +203 -0
- package/dist/skills/skillScanner.d.ts.map +1 -0
- package/dist/skills/skillScanner.js +559 -0
- package/dist/skills/skillScanner.js.map +1 -0
- package/dist/skills/skillsResource.d.ts +69 -0
- package/dist/skills/skillsResource.d.ts.map +1 -0
- package/dist/skills/skillsResource.js +257 -0
- package/dist/skills/skillsResource.js.map +1 -0
- package/dist/startup/index.d.ts +9 -0
- package/dist/startup/index.d.ts.map +1 -0
- package/dist/startup/index.js +12 -0
- package/dist/startup/index.js.map +1 -0
- package/dist/startup/startupIndexing.d.ts +80 -0
- package/dist/startup/startupIndexing.d.ts.map +1 -0
- package/dist/startup/startupIndexing.js +463 -0
- package/dist/startup/startupIndexing.js.map +1 -0
- package/dist/startup/validation.d.ts +89 -0
- package/dist/startup/validation.d.ts.map +1 -0
- package/dist/startup/validation.js +590 -0
- package/dist/startup/validation.js.map +1 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +4 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/overflowManager.d.ts +80 -0
- package/dist/storage/overflowManager.d.ts.map +1 -0
- package/dist/storage/overflowManager.js +317 -0
- package/dist/storage/overflowManager.js.map +1 -0
- package/dist/storage/overflowStorage.d.ts +69 -0
- package/dist/storage/overflowStorage.d.ts.map +1 -0
- package/dist/storage/overflowStorage.js +379 -0
- package/dist/storage/overflowStorage.js.map +1 -0
- package/dist/storage/toonFormat.d.ts +50 -0
- package/dist/storage/toonFormat.d.ts.map +1 -0
- package/dist/storage/toonFormat.js +224 -0
- package/dist/storage/toonFormat.js.map +1 -0
- package/dist/team-members/communication.d.ts +237 -0
- package/dist/team-members/communication.d.ts.map +1 -0
- package/dist/team-members/communication.js +650 -0
- package/dist/team-members/communication.js.map +1 -0
- package/dist/team-members/index.d.ts +14 -0
- package/dist/team-members/index.d.ts.map +1 -0
- package/dist/team-members/index.js +22 -0
- package/dist/team-members/index.js.map +1 -0
- package/dist/team-members/taskOrchestrator.d.ts +224 -0
- package/dist/team-members/taskOrchestrator.d.ts.map +1 -0
- package/dist/team-members/taskOrchestrator.js +574 -0
- package/dist/team-members/taskOrchestrator.js.map +1 -0
- package/dist/team-members/taskTeamMemberLogger.d.ts +157 -0
- package/dist/team-members/taskTeamMemberLogger.d.ts.map +1 -0
- package/dist/team-members/taskTeamMemberLogger.js +478 -0
- package/dist/team-members/taskTeamMemberLogger.js.map +1 -0
- package/dist/team-members/teamCommsService.d.ts +221 -0
- package/dist/team-members/teamCommsService.d.ts.map +1 -0
- package/dist/team-members/teamCommsService.js +628 -0
- package/dist/team-members/teamCommsService.js.map +1 -0
- package/dist/team-members/teamMemberChannels.d.ts +217 -0
- package/dist/team-members/teamMemberChannels.d.ts.map +1 -0
- package/dist/team-members/teamMemberChannels.js +687 -0
- package/dist/team-members/teamMemberChannels.js.map +1 -0
- package/dist/team-members/teamMemberDashboard.d.ts +222 -0
- package/dist/team-members/teamMemberDashboard.d.ts.map +1 -0
- package/dist/team-members/teamMemberDashboard.js +610 -0
- package/dist/team-members/teamMemberDashboard.js.map +1 -0
- package/dist/team-members/teamMemberDeployment.d.ts +60 -0
- package/dist/team-members/teamMemberDeployment.d.ts.map +1 -0
- package/dist/team-members/teamMemberDeployment.js +429 -0
- package/dist/team-members/teamMemberDeployment.js.map +1 -0
- package/dist/team-members/teamMemberDiscovery.d.ts +178 -0
- package/dist/team-members/teamMemberDiscovery.d.ts.map +1 -0
- package/dist/team-members/teamMemberDiscovery.js +446 -0
- package/dist/team-members/teamMemberDiscovery.js.map +1 -0
- package/dist/team-members/teamMemberHistory.d.ts +80 -0
- package/dist/team-members/teamMemberHistory.d.ts.map +1 -0
- package/dist/team-members/teamMemberHistory.js +426 -0
- package/dist/team-members/teamMemberHistory.js.map +1 -0
- package/dist/team-members/teamMemberLimits.d.ts +66 -0
- package/dist/team-members/teamMemberLimits.d.ts.map +1 -0
- package/dist/team-members/teamMemberLimits.js +259 -0
- package/dist/team-members/teamMemberLimits.js.map +1 -0
- package/dist/team-members/teamMemberRegistry.d.ts +199 -0
- package/dist/team-members/teamMemberRegistry.d.ts.map +1 -0
- package/dist/team-members/teamMemberRegistry.js +572 -0
- package/dist/team-members/teamMemberRegistry.js.map +1 -0
- package/dist/team-members/teamMemberTracker.d.ts +148 -0
- package/dist/team-members/teamMemberTracker.d.ts.map +1 -0
- package/dist/team-members/teamMemberTracker.js +828 -0
- package/dist/team-members/teamMemberTracker.js.map +1 -0
- package/dist/team-members/workers/aiWorker.d.ts +53 -0
- package/dist/team-members/workers/aiWorker.d.ts.map +1 -0
- package/dist/team-members/workers/aiWorker.js +322 -0
- package/dist/team-members/workers/aiWorker.js.map +1 -0
- package/dist/team-members/workers/baseWorker.d.ts +101 -0
- package/dist/team-members/workers/baseWorker.d.ts.map +1 -0
- package/dist/team-members/workers/baseWorker.js +179 -0
- package/dist/team-members/workers/baseWorker.js.map +1 -0
- package/dist/team-members/workers/codeReviewWorker.d.ts +3 -0
- package/dist/team-members/workers/codeReviewWorker.d.ts.map +1 -0
- package/dist/team-members/workers/codeReviewWorker.js +144 -0
- package/dist/team-members/workers/codeReviewWorker.js.map +1 -0
- package/dist/team-members/workers/index.d.ts +7 -0
- package/dist/team-members/workers/index.d.ts.map +1 -0
- package/dist/team-members/workers/index.js +7 -0
- package/dist/team-members/workers/index.js.map +1 -0
- package/dist/team-members/workers/repairWorker.d.ts +9 -0
- package/dist/team-members/workers/repairWorker.d.ts.map +1 -0
- package/dist/team-members/workers/repairWorker.js +102 -0
- package/dist/team-members/workers/repairWorker.js.map +1 -0
- package/dist/team-members/workers/sendToTeamMemberB.d.ts +9 -0
- package/dist/team-members/workers/sendToTeamMemberB.d.ts.map +1 -0
- package/dist/team-members/workers/sendToTeamMemberB.js +105 -0
- package/dist/team-members/workers/sendToTeamMemberB.js.map +1 -0
- package/dist/team-members/workers/specmemClient.d.ts +179 -0
- package/dist/team-members/workers/specmemClient.d.ts.map +1 -0
- package/dist/team-members/workers/specmemClient.js +421 -0
- package/dist/team-members/workers/specmemClient.js.map +1 -0
- package/dist/team-members/workers/testCommunication.d.ts +8 -0
- package/dist/team-members/workers/testCommunication.d.ts.map +1 -0
- package/dist/team-members/workers/testCommunication.js +136 -0
- package/dist/team-members/workers/testCommunication.js.map +1 -0
- package/dist/team-members/workers/testCommunicationSuite.d.ts +26 -0
- package/dist/team-members/workers/testCommunicationSuite.d.ts.map +1 -0
- package/dist/team-members/workers/testCommunicationSuite.js +415 -0
- package/dist/team-members/workers/testCommunicationSuite.js.map +1 -0
- package/dist/team-members/workers/testWorker.d.ts +9 -0
- package/dist/team-members/workers/testWorker.d.ts.map +1 -0
- package/dist/team-members/workers/testWorker.js +107 -0
- package/dist/team-members/workers/testWorker.js.map +1 -0
- package/dist/tools/agentDefinitions.d.ts +30 -0
- package/dist/tools/agentDefinitions.d.ts.map +1 -0
- package/dist/tools/agentDefinitions.js +166 -0
- package/dist/tools/agentDefinitions.js.map +1 -0
- package/dist/tools/goofy/checkSyncStatus.d.ts +68 -0
- package/dist/tools/goofy/checkSyncStatus.d.ts.map +1 -0
- package/dist/tools/goofy/checkSyncStatus.js +112 -0
- package/dist/tools/goofy/checkSyncStatus.js.map +1 -0
- package/dist/tools/goofy/codeMemoryLink.d.ts +82 -0
- package/dist/tools/goofy/codeMemoryLink.d.ts.map +1 -0
- package/dist/tools/goofy/codeMemoryLink.js +212 -0
- package/dist/tools/goofy/codeMemoryLink.js.map +1 -0
- package/dist/tools/goofy/compareInstanceMemory.d.ts +97 -0
- package/dist/tools/goofy/compareInstanceMemory.d.ts.map +1 -0
- package/dist/tools/goofy/compareInstanceMemory.js +218 -0
- package/dist/tools/goofy/compareInstanceMemory.js.map +1 -0
- package/dist/tools/goofy/createReasoningChain.d.ts +135 -0
- package/dist/tools/goofy/createReasoningChain.d.ts.map +1 -0
- package/dist/tools/goofy/createReasoningChain.js +257 -0
- package/dist/tools/goofy/createReasoningChain.js.map +1 -0
- package/dist/tools/goofy/deployTeamMember.d.ts +63 -0
- package/dist/tools/goofy/deployTeamMember.d.ts.map +1 -0
- package/dist/tools/goofy/deployTeamMember.js +103 -0
- package/dist/tools/goofy/deployTeamMember.js.map +1 -0
- package/dist/tools/goofy/drillDown.d.ts +143 -0
- package/dist/tools/goofy/drillDown.d.ts.map +1 -0
- package/dist/tools/goofy/drillDown.js +288 -0
- package/dist/tools/goofy/drillDown.js.map +1 -0
- package/dist/tools/goofy/extractClaudeSessions.d.ts +90 -0
- package/dist/tools/goofy/extractClaudeSessions.d.ts.map +1 -0
- package/dist/tools/goofy/extractClaudeSessions.js +277 -0
- package/dist/tools/goofy/extractClaudeSessions.js.map +1 -0
- package/dist/tools/goofy/extractContextRestorations.d.ts +70 -0
- package/dist/tools/goofy/extractContextRestorations.d.ts.map +1 -0
- package/dist/tools/goofy/extractContextRestorations.js +100 -0
- package/dist/tools/goofy/extractContextRestorations.js.map +1 -0
- package/dist/tools/goofy/findCodePointers.d.ts +364 -0
- package/dist/tools/goofy/findCodePointers.d.ts.map +1 -0
- package/dist/tools/goofy/findCodePointers.js +1764 -0
- package/dist/tools/goofy/findCodePointers.js.map +1 -0
- package/dist/tools/goofy/findMemoryGallery.d.ts +40 -0
- package/dist/tools/goofy/findMemoryGallery.d.ts.map +1 -0
- package/dist/tools/goofy/findMemoryGallery.js +66 -0
- package/dist/tools/goofy/findMemoryGallery.js.map +1 -0
- package/dist/tools/goofy/findWhatISaid.d.ts +300 -0
- package/dist/tools/goofy/findWhatISaid.d.ts.map +1 -0
- package/dist/tools/goofy/findWhatISaid.js +2547 -0
- package/dist/tools/goofy/findWhatISaid.js.map +1 -0
- package/dist/tools/goofy/forceResync.d.ts +57 -0
- package/dist/tools/goofy/forceResync.d.ts.map +1 -0
- package/dist/tools/goofy/forceResync.js +100 -0
- package/dist/tools/goofy/forceResync.js.map +1 -0
- package/dist/tools/goofy/getActiveTeamMembers.d.ts +48 -0
- package/dist/tools/goofy/getActiveTeamMembers.d.ts.map +1 -0
- package/dist/tools/goofy/getActiveTeamMembers.js +136 -0
- package/dist/tools/goofy/getActiveTeamMembers.js.map +1 -0
- package/dist/tools/goofy/getMemoryFull.d.ts +34 -0
- package/dist/tools/goofy/getMemoryFull.d.ts.map +1 -0
- package/dist/tools/goofy/getMemoryFull.js +58 -0
- package/dist/tools/goofy/getMemoryFull.js.map +1 -0
- package/dist/tools/goofy/getSessionWatcherStatus.d.ts +43 -0
- package/dist/tools/goofy/getSessionWatcherStatus.d.ts.map +1 -0
- package/dist/tools/goofy/getSessionWatcherStatus.js +92 -0
- package/dist/tools/goofy/getSessionWatcherStatus.js.map +1 -0
- package/dist/tools/goofy/getTeamMemberOutput.d.ts +35 -0
- package/dist/tools/goofy/getTeamMemberOutput.d.ts.map +1 -0
- package/dist/tools/goofy/getTeamMemberOutput.js +62 -0
- package/dist/tools/goofy/getTeamMemberOutput.js.map +1 -0
- package/dist/tools/goofy/getTeamMemberScreen.d.ts +28 -0
- package/dist/tools/goofy/getTeamMemberScreen.d.ts.map +1 -0
- package/dist/tools/goofy/getTeamMemberScreen.js +59 -0
- package/dist/tools/goofy/getTeamMemberScreen.js.map +1 -0
- package/dist/tools/goofy/getTeamMemberStatus.d.ts +33 -0
- package/dist/tools/goofy/getTeamMemberStatus.d.ts.map +1 -0
- package/dist/tools/goofy/getTeamMemberStatus.js +56 -0
- package/dist/tools/goofy/getTeamMemberStatus.js.map +1 -0
- package/dist/tools/goofy/index.d.ts +39 -0
- package/dist/tools/goofy/index.d.ts.map +1 -0
- package/dist/tools/goofy/index.js +51 -0
- package/dist/tools/goofy/index.js.map +1 -0
- package/dist/tools/goofy/interveneTeamMember.d.ts +33 -0
- package/dist/tools/goofy/interveneTeamMember.d.ts.map +1 -0
- package/dist/tools/goofy/interveneTeamMember.js +69 -0
- package/dist/tools/goofy/interveneTeamMember.js.map +1 -0
- package/dist/tools/goofy/killDeployedTeamMember.d.ts +29 -0
- package/dist/tools/goofy/killDeployedTeamMember.d.ts.map +1 -0
- package/dist/tools/goofy/killDeployedTeamMember.js +56 -0
- package/dist/tools/goofy/killDeployedTeamMember.js.map +1 -0
- package/dist/tools/goofy/linkTheVibes.d.ts +125 -0
- package/dist/tools/goofy/linkTheVibes.d.ts.map +1 -0
- package/dist/tools/goofy/linkTheVibes.js +354 -0
- package/dist/tools/goofy/linkTheVibes.js.map +1 -0
- package/dist/tools/goofy/listDeployedTeamMembers.d.ts +26 -0
- package/dist/tools/goofy/listDeployedTeamMembers.d.ts.map +1 -0
- package/dist/tools/goofy/listDeployedTeamMembers.js +52 -0
- package/dist/tools/goofy/listDeployedTeamMembers.js.map +1 -0
- package/dist/tools/goofy/listenForMessages.d.ts +56 -0
- package/dist/tools/goofy/listenForMessages.d.ts.map +1 -0
- package/dist/tools/goofy/listenForMessages.js +122 -0
- package/dist/tools/goofy/listenForMessages.js.map +1 -0
- package/dist/tools/goofy/memoryHealthCheck.d.ts +159 -0
- package/dist/tools/goofy/memoryHealthCheck.d.ts.map +1 -0
- package/dist/tools/goofy/memoryHealthCheck.js +443 -0
- package/dist/tools/goofy/memoryHealthCheck.js.map +1 -0
- package/dist/tools/goofy/rememberThisShit.d.ts +103 -0
- package/dist/tools/goofy/rememberThisShit.d.ts.map +1 -0
- package/dist/tools/goofy/rememberThisShit.js +291 -0
- package/dist/tools/goofy/rememberThisShit.js.map +1 -0
- package/dist/tools/goofy/sayToTeamMember.d.ts +55 -0
- package/dist/tools/goofy/sayToTeamMember.d.ts.map +1 -0
- package/dist/tools/goofy/sayToTeamMember.js +116 -0
- package/dist/tools/goofy/sayToTeamMember.js.map +1 -0
- package/dist/tools/goofy/selfMessage.d.ts +54 -0
- package/dist/tools/goofy/selfMessage.d.ts.map +1 -0
- package/dist/tools/goofy/selfMessage.js +111 -0
- package/dist/tools/goofy/selfMessage.js.map +1 -0
- package/dist/tools/goofy/sendHeartbeat.d.ts +53 -0
- package/dist/tools/goofy/sendHeartbeat.d.ts.map +1 -0
- package/dist/tools/goofy/sendHeartbeat.js +119 -0
- package/dist/tools/goofy/sendHeartbeat.js.map +1 -0
- package/dist/tools/goofy/showMeTheStats.d.ts +216 -0
- package/dist/tools/goofy/showMeTheStats.d.ts.map +1 -0
- package/dist/tools/goofy/showMeTheStats.js +535 -0
- package/dist/tools/goofy/showMeTheStats.js.map +1 -0
- package/dist/tools/goofy/smartRecall.d.ts +136 -0
- package/dist/tools/goofy/smartRecall.d.ts.map +1 -0
- package/dist/tools/goofy/smartRecall.js +286 -0
- package/dist/tools/goofy/smartRecall.js.map +1 -0
- package/dist/tools/goofy/smartSearch.d.ts +64 -0
- package/dist/tools/goofy/smartSearch.d.ts.map +1 -0
- package/dist/tools/goofy/smartSearch.js +89 -0
- package/dist/tools/goofy/smartSearch.js.map +1 -0
- package/dist/tools/goofy/smushMemoriesTogether.d.ts +128 -0
- package/dist/tools/goofy/smushMemoriesTogether.d.ts.map +1 -0
- package/dist/tools/goofy/smushMemoriesTogether.js +536 -0
- package/dist/tools/goofy/smushMemoriesTogether.js.map +1 -0
- package/dist/tools/goofy/spatialSearch.d.ts +198 -0
- package/dist/tools/goofy/spatialSearch.d.ts.map +1 -0
- package/dist/tools/goofy/spatialSearch.js +551 -0
- package/dist/tools/goofy/spatialSearch.js.map +1 -0
- package/dist/tools/goofy/spawnResearchTeamMember.d.ts +104 -0
- package/dist/tools/goofy/spawnResearchTeamMember.d.ts.map +1 -0
- package/dist/tools/goofy/spawnResearchTeamMember.js +290 -0
- package/dist/tools/goofy/spawnResearchTeamMember.js.map +1 -0
- package/dist/tools/goofy/spawnResearchTeamMemberTool.d.ts +121 -0
- package/dist/tools/goofy/spawnResearchTeamMemberTool.d.ts.map +1 -0
- package/dist/tools/goofy/spawnResearchTeamMemberTool.js +215 -0
- package/dist/tools/goofy/spawnResearchTeamMemberTool.js.map +1 -0
- package/dist/tools/goofy/startWatchingTheFiles.d.ts +81 -0
- package/dist/tools/goofy/startWatchingTheFiles.d.ts.map +1 -0
- package/dist/tools/goofy/startWatchingTheFiles.js +161 -0
- package/dist/tools/goofy/startWatchingTheFiles.js.map +1 -0
- package/dist/tools/goofy/stopWatchingTheFiles.d.ts +50 -0
- package/dist/tools/goofy/stopWatchingTheFiles.d.ts.map +1 -0
- package/dist/tools/goofy/stopWatchingTheFiles.js +81 -0
- package/dist/tools/goofy/stopWatchingTheFiles.js.map +1 -0
- package/dist/tools/goofy/whatDidIMean.d.ts +113 -0
- package/dist/tools/goofy/whatDidIMean.d.ts.map +1 -0
- package/dist/tools/goofy/whatDidIMean.js +401 -0
- package/dist/tools/goofy/whatDidIMean.js.map +1 -0
- package/dist/tools/goofy/yeahNahDeleteThat.d.ts +109 -0
- package/dist/tools/goofy/yeahNahDeleteThat.d.ts.map +1 -0
- package/dist/tools/goofy/yeahNahDeleteThat.js +319 -0
- package/dist/tools/goofy/yeahNahDeleteThat.js.map +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/teamMemberDeployer.d.ts +117 -0
- package/dist/tools/teamMemberDeployer.d.ts.map +1 -0
- package/dist/tools/teamMemberDeployer.js +613 -0
- package/dist/tools/teamMemberDeployer.js.map +1 -0
- package/dist/trace/index.d.ts +14 -0
- package/dist/trace/index.d.ts.map +1 -0
- package/dist/trace/index.js +16 -0
- package/dist/trace/index.js.map +1 -0
- package/dist/trace/tools/analyzeImpact.d.ts +90 -0
- package/dist/trace/tools/analyzeImpact.d.ts.map +1 -0
- package/dist/trace/tools/analyzeImpact.js +240 -0
- package/dist/trace/tools/analyzeImpact.js.map +1 -0
- package/dist/trace/tools/exploreDependencies.d.ts +81 -0
- package/dist/trace/tools/exploreDependencies.d.ts.map +1 -0
- package/dist/trace/tools/exploreDependencies.js +161 -0
- package/dist/trace/tools/exploreDependencies.js.map +1 -0
- package/dist/trace/tools/findSimilarBugs.d.ts +112 -0
- package/dist/trace/tools/findSimilarBugs.d.ts.map +1 -0
- package/dist/trace/tools/findSimilarBugs.js +216 -0
- package/dist/trace/tools/findSimilarBugs.js.map +1 -0
- package/dist/trace/tools/index.d.ts +22 -0
- package/dist/trace/tools/index.d.ts.map +1 -0
- package/dist/trace/tools/index.js +39 -0
- package/dist/trace/tools/index.js.map +1 -0
- package/dist/trace/tools/smartExplore.d.ts +126 -0
- package/dist/trace/tools/smartExplore.d.ts.map +1 -0
- package/dist/trace/tools/smartExplore.js +303 -0
- package/dist/trace/tools/smartExplore.js.map +1 -0
- package/dist/trace/tools/traceError.d.ts +101 -0
- package/dist/trace/tools/traceError.d.ts.map +1 -0
- package/dist/trace/tools/traceError.js +175 -0
- package/dist/trace/tools/traceError.js.map +1 -0
- package/dist/trace/traceExploreSystem.d.ts +271 -0
- package/dist/trace/traceExploreSystem.d.ts.map +1 -0
- package/dist/trace/traceExploreSystem.js +789 -0
- package/dist/trace/traceExploreSystem.js.map +1 -0
- package/dist/types/index.d.ts +421 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +118 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/circuitBreaker.d.ts +195 -0
- package/dist/utils/circuitBreaker.d.ts.map +1 -0
- package/dist/utils/circuitBreaker.js +374 -0
- package/dist/utils/circuitBreaker.js.map +1 -0
- package/dist/utils/cleanupHandler.d.ts +108 -0
- package/dist/utils/cleanupHandler.d.ts.map +1 -0
- package/dist/utils/cleanupHandler.js +203 -0
- package/dist/utils/cleanupHandler.js.map +1 -0
- package/dist/utils/compactXmlResponse.d.ts +60 -0
- package/dist/utils/compactXmlResponse.d.ts.map +1 -0
- package/dist/utils/compactXmlResponse.js +209 -0
- package/dist/utils/compactXmlResponse.js.map +1 -0
- package/dist/utils/cotBroadcast.d.ts +56 -0
- package/dist/utils/cotBroadcast.d.ts.map +1 -0
- package/dist/utils/cotBroadcast.js +157 -0
- package/dist/utils/cotBroadcast.js.map +1 -0
- package/dist/utils/debugLogger.d.ts +95 -0
- package/dist/utils/debugLogger.d.ts.map +1 -0
- package/dist/utils/debugLogger.js +610 -0
- package/dist/utils/debugLogger.js.map +1 -0
- package/dist/utils/fileProcessingQueue.d.ts +259 -0
- package/dist/utils/fileProcessingQueue.d.ts.map +1 -0
- package/dist/utils/fileProcessingQueue.js +714 -0
- package/dist/utils/fileProcessingQueue.js.map +1 -0
- package/dist/utils/humanReadableOutput.d.ts +124 -0
- package/dist/utils/humanReadableOutput.d.ts.map +1 -0
- package/dist/utils/humanReadableOutput.js +340 -0
- package/dist/utils/humanReadableOutput.js.map +1 -0
- package/dist/utils/index.d.ts +32 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +71 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/instanceManager.d.ts +530 -0
- package/dist/utils/instanceManager.d.ts.map +1 -0
- package/dist/utils/instanceManager.js +1784 -0
- package/dist/utils/instanceManager.js.map +1 -0
- package/dist/utils/logger.d.ts +6 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +49 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/mapCleanup.d.ts +58 -0
- package/dist/utils/mapCleanup.d.ts.map +1 -0
- package/dist/utils/mapCleanup.js +150 -0
- package/dist/utils/mapCleanup.js.map +1 -0
- package/dist/utils/memoryManager.d.ts +349 -0
- package/dist/utils/memoryManager.d.ts.map +1 -0
- package/dist/utils/memoryManager.js +799 -0
- package/dist/utils/memoryManager.js.map +1 -0
- package/dist/utils/metrics.d.ts +160 -0
- package/dist/utils/metrics.d.ts.map +1 -0
- package/dist/utils/metrics.js +558 -0
- package/dist/utils/metrics.js.map +1 -0
- package/dist/utils/pathValidator.d.ts +96 -0
- package/dist/utils/pathValidator.d.ts.map +1 -0
- package/dist/utils/pathValidator.js +320 -0
- package/dist/utils/pathValidator.js.map +1 -0
- package/dist/utils/portAllocator.d.ts +296 -0
- package/dist/utils/portAllocator.d.ts.map +1 -0
- package/dist/utils/portAllocator.js +768 -0
- package/dist/utils/portAllocator.js.map +1 -0
- package/dist/utils/portUtils.d.ts +97 -0
- package/dist/utils/portUtils.d.ts.map +1 -0
- package/dist/utils/portUtils.js +285 -0
- package/dist/utils/portUtils.js.map +1 -0
- package/dist/utils/postgresAutoSetup.d.ts +55 -0
- package/dist/utils/postgresAutoSetup.d.ts.map +1 -0
- package/dist/utils/postgresAutoSetup.js +406 -0
- package/dist/utils/postgresAutoSetup.js.map +1 -0
- package/dist/utils/processHealthCheck.d.ts +61 -0
- package/dist/utils/processHealthCheck.d.ts.map +1 -0
- package/dist/utils/processHealthCheck.js +313 -0
- package/dist/utils/processHealthCheck.js.map +1 -0
- package/dist/utils/progressReporter.d.ts +151 -0
- package/dist/utils/progressReporter.d.ts.map +1 -0
- package/dist/utils/progressReporter.js +345 -0
- package/dist/utils/progressReporter.js.map +1 -0
- package/dist/utils/projectEnv.d.ts +73 -0
- package/dist/utils/projectEnv.d.ts.map +1 -0
- package/dist/utils/projectEnv.js +137 -0
- package/dist/utils/projectEnv.js.map +1 -0
- package/dist/utils/qoms.d.ts +122 -0
- package/dist/utils/qoms.d.ts.map +1 -0
- package/dist/utils/qoms.js +650 -0
- package/dist/utils/qoms.js.map +1 -0
- package/dist/utils/retryHelper.d.ts +122 -0
- package/dist/utils/retryHelper.d.ts.map +1 -0
- package/dist/utils/retryHelper.js +272 -0
- package/dist/utils/retryHelper.js.map +1 -0
- package/dist/utils/safeProcessTermination.d.ts +206 -0
- package/dist/utils/safeProcessTermination.d.ts.map +1 -0
- package/dist/utils/safeProcessTermination.js +552 -0
- package/dist/utils/safeProcessTermination.js.map +1 -0
- package/dist/utils/sessionInjector.d.ts +68 -0
- package/dist/utils/sessionInjector.d.ts.map +1 -0
- package/dist/utils/sessionInjector.js +189 -0
- package/dist/utils/sessionInjector.js.map +1 -0
- package/dist/utils/statsCache.d.ts +134 -0
- package/dist/utils/statsCache.d.ts.map +1 -0
- package/dist/utils/statsCache.js +285 -0
- package/dist/utils/statsCache.js.map +1 -0
- package/dist/utils/timeoutMiddleware.d.ts +81 -0
- package/dist/utils/timeoutMiddleware.d.ts.map +1 -0
- package/dist/utils/timeoutMiddleware.js +155 -0
- package/dist/utils/timeoutMiddleware.js.map +1 -0
- package/dist/utils/timerRegistry.d.ts +91 -0
- package/dist/utils/timerRegistry.d.ts.map +1 -0
- package/dist/utils/timerRegistry.js +187 -0
- package/dist/utils/timerRegistry.js.map +1 -0
- package/dist/utils/tokenCompressor.d.ts +332 -0
- package/dist/utils/tokenCompressor.d.ts.map +1 -0
- package/dist/utils/tokenCompressor.js +1306 -0
- package/dist/utils/tokenCompressor.js.map +1 -0
- package/dist/utils/tracing.d.ts +236 -0
- package/dist/utils/tracing.d.ts.map +1 -0
- package/dist/utils/tracing.js +378 -0
- package/dist/utils/tracing.js.map +1 -0
- package/dist/watcher/changeHandler.d.ts +123 -0
- package/dist/watcher/changeHandler.d.ts.map +1 -0
- package/dist/watcher/changeHandler.js +623 -0
- package/dist/watcher/changeHandler.js.map +1 -0
- package/dist/watcher/changeQueue.d.ts +133 -0
- package/dist/watcher/changeQueue.d.ts.map +1 -0
- package/dist/watcher/changeQueue.js +355 -0
- package/dist/watcher/changeQueue.js.map +1 -0
- package/dist/watcher/fileWatcher.d.ts +121 -0
- package/dist/watcher/fileWatcher.d.ts.map +1 -0
- package/dist/watcher/fileWatcher.js +531 -0
- package/dist/watcher/fileWatcher.js.map +1 -0
- package/dist/watcher/index.d.ts +94 -0
- package/dist/watcher/index.d.ts.map +1 -0
- package/dist/watcher/index.js +235 -0
- package/dist/watcher/index.js.map +1 -0
- package/dist/watcher/syncChecker.d.ts +93 -0
- package/dist/watcher/syncChecker.d.ts.map +1 -0
- package/dist/watcher/syncChecker.js +401 -0
- package/dist/watcher/syncChecker.js.map +1 -0
- package/dist/watcher/tsCompiler.d.ts +88 -0
- package/dist/watcher/tsCompiler.d.ts.map +1 -0
- package/dist/watcher/tsCompiler.js +212 -0
- package/dist/watcher/tsCompiler.js.map +1 -0
- package/embedding-sandbox/Dockerfile +77 -0
- package/embedding-sandbox/Dockerfile.frankenstein +91 -0
- package/embedding-sandbox/README.md +193 -0
- package/embedding-sandbox/__pycache__/frankenstein-embeddings.cpython-312.pyc +0 -0
- package/embedding-sandbox/__pycache__/frankenstein-embeddings.cpython-313.pyc +0 -0
- package/embedding-sandbox/__pycache__/qqms_v2.cpython-312.pyc +0 -0
- package/embedding-sandbox/__pycache__/qqms_v2.cpython-313.pyc +0 -0
- package/embedding-sandbox/add_js_docs.py +684 -0
- package/embedding-sandbox/build_docs_db.py +239 -0
- package/embedding-sandbox/client.cjs +376 -0
- package/embedding-sandbox/client.ts +913 -0
- package/embedding-sandbox/deploy-frankenstein.sh +240 -0
- package/embedding-sandbox/docker-compose.yml +60 -0
- package/embedding-sandbox/docker-manager.py +325 -0
- package/embedding-sandbox/docs/python_docs.db +0 -0
- package/embedding-sandbox/download-model.mjs +79 -0
- package/embedding-sandbox/download-model.py +28 -0
- package/embedding-sandbox/embedding-supervisor.sh +164 -0
- package/embedding-sandbox/frankenstein-embeddings.py +3940 -0
- package/embedding-sandbox/manage-services.sh +354 -0
- package/embedding-sandbox/overflow_queue.py +345 -0
- package/embedding-sandbox/package.json +17 -0
- package/embedding-sandbox/project_isolation.py +292 -0
- package/embedding-sandbox/qqms_v2.py +967 -0
- package/embedding-sandbox/ram-manager.sh +311 -0
- package/embedding-sandbox/requirements-frankenstein.txt +7 -0
- package/embedding-sandbox/run_js_docs.py +59 -0
- package/embedding-sandbox/seed_docs.py +885 -0
- package/embedding-sandbox/server-batch.mjs +228 -0
- package/embedding-sandbox/server.mjs +389 -0
- package/embedding-sandbox/specmem/sockets/claude-input-state.json +1 -0
- package/embedding-sandbox/specmem/sockets/embedding-death-reason.txt +3 -0
- package/embedding-sandbox/specmem/sockets/seen-sessions.json +1 -0
- package/embedding-sandbox/specmem/sockets/session-start.lock +1 -0
- package/embedding-sandbox/specmem/sockets/session-stops.log +7 -0
- package/embedding-sandbox/start-frankenstein-throttled.sh +98 -0
- package/embedding-sandbox/start-on-demand.sh +116 -0
- package/embedding-sandbox/start-sandbox.sh +237 -0
- package/embedding-sandbox/start-supervised.sh +11 -0
- package/embedding-sandbox/stop-sandbox.sh +51 -0
- package/embedding-sandbox/test-socket.mjs +61 -0
- package/embedding-sandbox/warm-start.sh +353 -0
- package/embedding-sandbox/warm_start_feeder.py +660 -0
- package/legal/README.md +31 -0
- package/legal/anthropic-privacy-center-screenshot-2026-01-30.png +0 -0
- package/legal/anthropic-tos-screenshot-2026-01-30.png +0 -0
- package/lib/codebase-bridge.cjs +308 -0
- package/package.json +136 -0
- package/plugins/specmem-agents/agents/bug-hunter.md +79 -0
- package/plugins/specmem-agents/agents/memory-explorer.md +57 -0
- package/plugins/specmem-agents/agents/team-coordinator.md +82 -0
- package/scripts/auto-updater.cjs +399 -0
- package/scripts/backfill-code-definition-embeddings.ts +440 -0
- package/scripts/backfill-code-embeddings.ts +206 -0
- package/scripts/capture-tos-screenshots.cjs +94 -0
- package/scripts/check-global-install.cjs +67 -0
- package/scripts/cleanup-embedding-servers.sh +25 -0
- package/scripts/dashboard-standalone.sh +369 -0
- package/scripts/deploy-hooks.cjs +1451 -0
- package/scripts/deploy.sh +106 -0
- package/scripts/docker-project-down.sh +83 -0
- package/scripts/docker-project-list.sh +40 -0
- package/scripts/docker-project-up.sh +79 -0
- package/scripts/fast-backfill-embeddings.ts +173 -0
- package/scripts/fast-batch-embedder.cjs +334 -0
- package/scripts/first-run-model-setup.cjs +849 -0
- package/scripts/global-postinstall.cjs +1957 -0
- package/scripts/index-codebase.js +72 -0
- package/scripts/migrate-fix-embeddings.py +110 -0
- package/scripts/migrate-to-project-schemas.ts +525 -0
- package/scripts/optimize-embedding-model.py +324 -0
- package/scripts/optimize-instructions.cjs +530 -0
- package/scripts/pack-docker-images.sh +68 -0
- package/scripts/pack-for-testing.sh +130 -0
- package/scripts/postinstall.cjs +54 -0
- package/scripts/project-env.sh +51 -0
- package/scripts/reset-db.sh +30 -0
- package/scripts/run-indexer.ts +69 -0
- package/scripts/run-migrations.js +47 -0
- package/scripts/setup-db.sh +34 -0
- package/scripts/setup-minimal-schema.sql +143 -0
- package/scripts/skills/code-review.md +44 -0
- package/scripts/skills/debugging.md +56 -0
- package/scripts/skills/specmem-deployteam.md +239 -0
- package/scripts/skills/teammemberskills/EFFICIENT_GREP.md +171 -0
- package/scripts/skills/teammemberskills/task-planning.md +67 -0
- package/scripts/specmem/sockets/session-start.lock +1 -0
- package/scripts/specmem/sockets/session-stops.log +1 -0
- package/scripts/specmem-health.sh +382 -0
- package/scripts/specmem-init.cjs +6935 -0
- package/scripts/strip-debug-logs.cjs +43 -0
- package/scripts/test-mcp-standalone.sh +365 -0
- package/scripts/test-optimized-models.py +166 -0
- package/scripts/verify-embedding-fix.sh +148 -0
- package/skills/code-review.md +44 -0
- package/skills/debugging.md +56 -0
- package/skills/specmem-deployteam.md +239 -0
- package/skills/teammemberskills/EFFICIENT_GREP.md +171 -0
- package/skills/teammemberskills/task-planning.md +67 -0
- package/specmem-health.cjs +522 -0
- package/specmem.env +216 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4433 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SpecMem - Speculative Memory MCP Server
|
|
4
|
+
*
|
|
5
|
+
* yo shoutout to doobidoo/mcp-memory-service for the inspo
|
|
6
|
+
* we took their SQLite version and made it POSTGRESQL BEAST MODE
|
|
7
|
+
* - hardwicksoftwareservices
|
|
8
|
+
*
|
|
9
|
+
* A high-performance memory management system with:
|
|
10
|
+
* - Semantic search using pgvector (cosine similarity)
|
|
11
|
+
* - Dream-inspired consolidation (DBSCAN clustering)
|
|
12
|
+
* - Auto-splitting for unlimited content length
|
|
13
|
+
* - Natural language time queries ("yesterday", "last week")
|
|
14
|
+
* - Embedding caching (90% hit rate target)
|
|
15
|
+
* - Image storage (base64 in BYTEA)
|
|
16
|
+
* - Memory relationships (graph traversal)
|
|
17
|
+
* - SKILLS SYSTEM - drag & drop .md files for instant capabilities
|
|
18
|
+
* - CODEBASE INDEXING - knows your entire project
|
|
19
|
+
*
|
|
20
|
+
* Scale Requirements:
|
|
21
|
+
* - Millions of lines of code
|
|
22
|
+
* - Thousands of prompts
|
|
23
|
+
* - Hundreds of images
|
|
24
|
+
* - <100ms semantic search
|
|
25
|
+
*/
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// STARTUP LOGGING - Debug MCP connection issues
|
|
28
|
+
// Write to same log file as bootstrap.cjs for unified timeline
|
|
29
|
+
// Uses project-isolated path: {PROJECT_DIR}/specmem/run/mcp-startup.log
|
|
30
|
+
// ============================================================================
|
|
31
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, statSync, openSync } from 'fs';
|
|
32
|
+
import { access, constants } from 'fs/promises';
|
|
33
|
+
import * as path from 'path';
|
|
34
|
+
import { fileURLToPath } from 'url';
|
|
35
|
+
// ESM __dirname equivalent - replaces hardcoded paths
|
|
36
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
37
|
+
const __dirname = path.dirname(__filename);
|
|
38
|
+
// DEBUG LOGGING - only enabled when SPECMEM_DEBUG=1
|
|
39
|
+
const __debugLog = process.env['SPECMEM_DEBUG'] === '1'
|
|
40
|
+
? (...args) => console.error('[DEBUG]', ...args) // stderr, not stdout!
|
|
41
|
+
: () => { };
|
|
42
|
+
// MULTI-PROJECT ISOLATION: SPECMEM_PROJECT_PATH is set at startup and NEVER changes
|
|
43
|
+
// REMOVED: Marker file - caused race condition with simultaneous projects!
|
|
44
|
+
function _getStartupProjectPath() {
|
|
45
|
+
return process.env['SPECMEM_PROJECT_PATH'] || process.cwd();
|
|
46
|
+
}
|
|
47
|
+
// Compute project identifiers early for log isolation
|
|
48
|
+
// NO MORE HASHES - use readable project directory name for EVERYTHING
|
|
49
|
+
const _projectPath = _getStartupProjectPath();
|
|
50
|
+
// Readable project directory name - used for containers, sockets, databases, etc.
|
|
51
|
+
const _projectDirName = process.env['SPECMEM_PROJECT_DIR_NAME'] ||
|
|
52
|
+
path.basename(_projectPath)
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
.replace(/[^a-z0-9_.-]/g, '-')
|
|
55
|
+
.replace(/-+/g, '-')
|
|
56
|
+
.replace(/^-|-$/g, '') || 'default';
|
|
57
|
+
// DEPRECATED: _projectHash now equals _projectDirName for backwards compat
|
|
58
|
+
const _projectHash = _projectDirName;
|
|
59
|
+
// Use PROJECT DIRECTORY for all specmem data - NOT ~/.specmem, NOT /tmp
|
|
60
|
+
// User requirement: "EVERYTHING LOCALIZED WITHIN THE PROJECT"
|
|
61
|
+
// Pattern: {PROJECT_DIR}/specmem/
|
|
62
|
+
const _projectInstanceDir = path.join(_projectPath, 'specmem');
|
|
63
|
+
const _projectTmpDir = path.join(_projectInstanceDir, 'run');
|
|
64
|
+
// Ensure project instance directory exists
|
|
65
|
+
try {
|
|
66
|
+
if (!existsSync(_projectTmpDir)) {
|
|
67
|
+
mkdirSync(_projectTmpDir, { recursive: true, mode: 0o755 });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Ignore - will be created on first write
|
|
72
|
+
}
|
|
73
|
+
const STARTUP_LOG_PATH = `${_projectTmpDir}/mcp-startup.log`;
|
|
74
|
+
function startupLog(msg, error) {
|
|
75
|
+
const timestamp = new Date().toISOString();
|
|
76
|
+
const pid = process.pid;
|
|
77
|
+
let logLine = `${timestamp} [PID:${pid}] [index.ts] ${msg}\n`;
|
|
78
|
+
if (error) {
|
|
79
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
80
|
+
const errStack = error instanceof Error ? error.stack : undefined;
|
|
81
|
+
logLine += `${timestamp} [PID:${pid}] [index.ts] ERROR: ${errMsg}\n`;
|
|
82
|
+
if (errStack) {
|
|
83
|
+
logLine += `${timestamp} [PID:${pid}] [index.ts] STACK: ${errStack}\n`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
appendFileSync(STARTUP_LOG_PATH, logLine);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Ignore write errors - logging should never break the app
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
startupLog('index.ts ENTRY POINT - ES module loading');
|
|
94
|
+
import { SpecMemServer } from './mcp/specMemServer.js';
|
|
95
|
+
import { CachingEmbeddingProvider } from './mcp/toolRegistry.js';
|
|
96
|
+
import { EmbeddingServerManager, getEmbeddingServerManager } from './mcp/embeddingServerManager.js';
|
|
97
|
+
import { config, loadSkillsConfig, loadCodebaseConfig, getEmbeddingSocketPath, getRunDir, getProjectInfo, getProjectPath } from './config.js';
|
|
98
|
+
import { logger } from './utils/logger.js';
|
|
99
|
+
import { ensureProjectEnv, getSpawnEnv, getPythonPath } from './utils/projectEnv.js';
|
|
100
|
+
import { reportRetry, reportError } from './utils/progressReporter.js';
|
|
101
|
+
import { ensureSocketDirAtomicSync } from './utils/fileProcessingQueue.js';
|
|
102
|
+
import { initializeWatcher, shutdownWatcher, registerCleanupHandlers } from './mcp/watcherIntegration.js';
|
|
103
|
+
import { initializeSessionWatcher, shutdownSessionWatcher } from './claude-sessions/sessionIntegration.js';
|
|
104
|
+
// Skills & Codebase system imports
|
|
105
|
+
import { getSkillScanner, resetSkillScanner } from './skills/skillScanner.js';
|
|
106
|
+
import { getSkillResourceProvider } from './skills/skillsResource.js';
|
|
107
|
+
import { getCodebaseIndexer, resetCodebaseIndexer } from './codebase/codebaseIndexer.js';
|
|
108
|
+
import { getSkillReminder, resetSkillReminder } from './reminders/skillReminder.js';
|
|
109
|
+
import { getDatabase } from './database.js';
|
|
110
|
+
// yooo we need the big brain db layer for watchers and team features
|
|
111
|
+
import { initializeTheBigBrainDb } from './db/index.js';
|
|
112
|
+
// fs imports consolidated above (line 32)
|
|
113
|
+
import { join } from 'path';
|
|
114
|
+
import { createConnection } from 'net';
|
|
115
|
+
import { execSync, spawn } from 'child_process';
|
|
116
|
+
import { setTargetDimension } from './embeddings/projectionLayer.js';
|
|
117
|
+
import { getDimensionAdapter, initializeDimensionAdapter } from './services/DimensionAdapter.js';
|
|
118
|
+
import { getEmbeddingQueue } from './services/EmbeddingQueue.js';
|
|
119
|
+
import { qoms } from './utils/qoms.js';
|
|
120
|
+
// Coordination server import - now uses lazy initialization
|
|
121
|
+
import { configureLazyCoordinationServer, disableLazyCoordinationServer, executeLazyShutdownHandlers, getLazyCoordinationServerStatus, resetLazyCoordinationServer, } from './coordination/index.js';
|
|
122
|
+
// Startup validation - pre-flight checks to catch issues early
|
|
123
|
+
import { quickValidation, fullValidation, formatValidationErrors, EXIT_CODES, } from './startup/index.js';
|
|
124
|
+
// Startup indexing - auto-index codebase and extract sessions on MCP startup
|
|
125
|
+
import { runStartupIndexing } from './startup/startupIndexing.js';
|
|
126
|
+
// Config Injector - auto-injects hooks and hot-patches running instances
|
|
127
|
+
import { injectConfig } from './init/claudeConfigInjector.js';
|
|
128
|
+
// Auto-config system - syncs config to ~/.specmem/config.json for hooks
|
|
129
|
+
import { syncConfigToUserFile } from './config/autoConfig.js';
|
|
130
|
+
// Auto-deploy hooks and commands to 's directories
|
|
131
|
+
import { deployTo } from './cli/deploy-to-claude.js';
|
|
132
|
+
// Centralized password management
|
|
133
|
+
import { getPassword, isUsingDefaultPassword } from './config/password.js';
|
|
134
|
+
// Unified embedding timeout configuration
|
|
135
|
+
import { getEmbeddingTimeout, getAllEmbeddingTimeouts, hasMasterTimeout } from './config/embeddingTimeouts.js';
|
|
136
|
+
// Port allocation for unique per-instance ports
|
|
137
|
+
import { allocatePorts, setAllocatedPorts } from './utils/portAllocator.js';
|
|
138
|
+
// Instance manager for per-project instance tracking
|
|
139
|
+
import { getInstanceManager, cleanupSameProjectInstances, migrateFromOldStructure, } from './utils/instanceManager.js';
|
|
140
|
+
// CLI Notifications - centralized notification system for Code CLI
|
|
141
|
+
import { getDashboardUrl } from './mcp/cliNotifications.js';
|
|
142
|
+
/**
|
|
143
|
+
* Display SpecMem LOADED banner in Code CLI
|
|
144
|
+
*
|
|
145
|
+
* Uses the centralized CLINotifier system which:
|
|
146
|
+
* 1. Writes a visual banner to stderr (appears in terminal)
|
|
147
|
+
* 2. Can optionally send MCP logging message (appears in 's logs)
|
|
148
|
+
*
|
|
149
|
+
* The banner includes:
|
|
150
|
+
* - "SpecMem Loaded" status message with emoji
|
|
151
|
+
* - Dashboard URL with emoji indicator
|
|
152
|
+
* - Hooks and commands deployment status
|
|
153
|
+
* - Quick reference for available commands
|
|
154
|
+
*/
|
|
155
|
+
function displayLoadedBanner(deployResult, dashboardUrl) {
|
|
156
|
+
// Use centralized notification system
|
|
157
|
+
// Note: We can't use the full CLINotifier here because we don't have the MCP server instance
|
|
158
|
+
// The MCP-level notifications are handled by specMemServer.ts announceToOnStartup()
|
|
159
|
+
// This function focuses on the stderr banner display
|
|
160
|
+
const c = {
|
|
161
|
+
reset: '\x1b[0m',
|
|
162
|
+
bright: '\x1b[1m',
|
|
163
|
+
yellow: '\x1b[33m',
|
|
164
|
+
green: '\x1b[32m',
|
|
165
|
+
cyan: '\x1b[36m',
|
|
166
|
+
magenta: '\x1b[35m',
|
|
167
|
+
dim: '\x1b[2m',
|
|
168
|
+
};
|
|
169
|
+
const hooksCount = deployResult.hooksDeployed.length;
|
|
170
|
+
const commandsCount = deployResult.commandsDeployed.length;
|
|
171
|
+
// Format status with proper padding for alignment
|
|
172
|
+
const hooksStatus = hooksCount > 0
|
|
173
|
+
? `${c.green}${hooksCount} registered${c.reset}`
|
|
174
|
+
: `${c.dim}already up-to-date${c.reset}`;
|
|
175
|
+
const commandsStatus = commandsCount > 0
|
|
176
|
+
? `${c.green}${commandsCount} registered${c.reset}`
|
|
177
|
+
: `${c.dim}already up-to-date${c.reset}`;
|
|
178
|
+
// Dashboard URL with emoji - use appropriate host based on mode
|
|
179
|
+
const dashboardDisplay = dashboardUrl
|
|
180
|
+
? `${c.magenta}${dashboardUrl}${c.reset}`
|
|
181
|
+
: `${c.dim}disabled${c.reset}`;
|
|
182
|
+
const banner = `
|
|
183
|
+
${c.yellow}+==================================================================+${c.reset}
|
|
184
|
+
${c.yellow}|${c.reset} ${c.bright}${c.green}SpecMem Loaded${c.reset} ${c.yellow}|${c.reset}
|
|
185
|
+
${c.yellow}+==================================================================+${c.reset}
|
|
186
|
+
${c.yellow}|${c.reset} ${c.cyan}Hooks:${c.reset} ${hooksStatus} ${c.yellow}|${c.reset}
|
|
187
|
+
${c.yellow}|${c.reset} ${c.cyan}Commands:${c.reset} ${commandsStatus} ${c.yellow}|${c.reset}
|
|
188
|
+
${c.yellow}|${c.reset} ${c.cyan}Dashboard:${c.reset} ${dashboardDisplay} ${c.yellow}|${c.reset}
|
|
189
|
+
${c.yellow}+==================================================================+${c.reset}
|
|
190
|
+
${c.yellow}|${c.reset} ${c.dim}Type /specmem for commands | /specmem-find to search memories${c.reset} ${c.yellow}|${c.reset}
|
|
191
|
+
${c.yellow}+==================================================================+${c.reset}
|
|
192
|
+
`;
|
|
193
|
+
// Write to stderr so it appears in Code CLI terminal
|
|
194
|
+
// (stdout is reserved for MCP JSON-RPC protocol)
|
|
195
|
+
process.stderr.write(banner);
|
|
196
|
+
}
|
|
197
|
+
// Dashboard server import
|
|
198
|
+
import { getDashboardServer, resetDashboardServer } from './dashboard/index.js';
|
|
199
|
+
// Memory management import
|
|
200
|
+
import { getMemoryManager, resetMemoryManager } from './utils/memoryManager.js';
|
|
201
|
+
import { createEmbeddingOverflowHandler } from './db/embeddingOverflow.js';
|
|
202
|
+
// re-export for external use
|
|
203
|
+
export { SpecMemServer } from './mcp/specMemServer.js';
|
|
204
|
+
export { ToolRegistry, createToolRegistry, CachingEmbeddingProvider } from './mcp/toolRegistry.js';
|
|
205
|
+
export { MCPProtocolHandler, parseTimeExpression, splitContent } from './mcp/mcpProtocolHandler.js';
|
|
206
|
+
export { DatabaseManager, getDatabase, resetDatabase } from './database.js';
|
|
207
|
+
// export all the goofy tools
|
|
208
|
+
export { RememberThisShit, FindWhatISaid, WhatDidIMean, YeahNahDeleteThat, SmushMemoriesTogether, LinkTheVibes, ShowMeTheStats, FindCodePointers } from './tools/goofy/index.js';
|
|
209
|
+
// export the command system - doobidoo style slash commands
|
|
210
|
+
export { CommandHandler, createCommandHandler, MemoryCommands, CodebaseCommands, ContextCommands, PromptCommands, getCommandsResource, getCommandHelpResource } from './commands/index.js';
|
|
211
|
+
// export skills system
|
|
212
|
+
export { SkillScanner, getSkillScanner, resetSkillScanner } from './skills/skillScanner.js';
|
|
213
|
+
export { SkillResourceProvider, getSkillResourceProvider } from './skills/skillsResource.js';
|
|
214
|
+
// export codebase indexer
|
|
215
|
+
export { CodebaseIndexer, getCodebaseIndexer, resetCodebaseIndexer } from './codebase/codebaseIndexer.js';
|
|
216
|
+
// export skill reminder
|
|
217
|
+
export { SkillReminder, getSkillReminder, resetSkillReminder } from './reminders/skillReminder.js';
|
|
218
|
+
// export dashboard
|
|
219
|
+
export { DashboardWebServer, getDashboardServer, resetDashboardServer } from './dashboard/index.js';
|
|
220
|
+
// export memory manager with instance tracking
|
|
221
|
+
export { MemoryManager, LRUCache, getMemoryManager, resetMemoryManager, getInstanceRegistry } from './utils/memoryManager.js';
|
|
222
|
+
// export embedding overflow handler
|
|
223
|
+
export { EmbeddingOverflowHandler, createEmbeddingOverflowHandler } from './db/embeddingOverflow.js';
|
|
224
|
+
// export instance manager for per-project tracking
|
|
225
|
+
export { InstanceManager, getInstanceManager, resetInstanceManager, hasInstanceManager, listInstances, killInstance, killAllInstances, cleanupSameProjectInstances, hashProjectPath, migrateFromOldStructure, } from './utils/instanceManager.js';
|
|
226
|
+
// export startup validation for external use
|
|
227
|
+
export { runStartupValidation, quickValidation, fullValidation, formatValidationErrors, validateOrExit, EXIT_CODES, } from './startup/index.js';
|
|
228
|
+
/**
|
|
229
|
+
* Local Embedding Provider with Sandboxed ML Support
|
|
230
|
+
*
|
|
231
|
+
* FULLY DYNAMIC - all dimensions come from database!
|
|
232
|
+
*
|
|
233
|
+
* Priority:
|
|
234
|
+
* 1. Air-gapped sandbox (real ML embeddings) - if running
|
|
235
|
+
* 2. Hash-based fallback (deterministic pseudo-embeddings)
|
|
236
|
+
*
|
|
237
|
+
* The sandboxed embedding service:
|
|
238
|
+
* - Runs in Docker with --network none (air-gapped)
|
|
239
|
+
* - Uses all-MiniLM-L6-v2 model (or any model - dimension auto-detected!)
|
|
240
|
+
* - Communicates via Unix socket only
|
|
241
|
+
* - Cannot phone home or access the internet
|
|
242
|
+
*
|
|
243
|
+
* Fallback hash-based embeddings:
|
|
244
|
+
* - Deterministic (same text = same embedding)
|
|
245
|
+
* - Normalized to unit vectors (for cosine similarity)
|
|
246
|
+
* - Dimension fetched from database (pgvector table metadata)
|
|
247
|
+
*
|
|
248
|
+
* DATABASE IS THE SINGLE SOURCE OF TRUTH FOR DIMENSIONS!
|
|
249
|
+
*/
|
|
250
|
+
class LocalEmbeddingProvider {
|
|
251
|
+
targetDimension = null; // Target dimension - fetched from DB dynamically!
|
|
252
|
+
detectedDimension = null; // Auto-detected from Frankenstein
|
|
253
|
+
sandboxSocketPath;
|
|
254
|
+
sandboxAvailable = false;
|
|
255
|
+
lastSandboxCheck = 0;
|
|
256
|
+
sandboxCheckInterval = 5000; // Re-check every 5 seconds
|
|
257
|
+
autoStartAttempted = false;
|
|
258
|
+
// FIX: Version counter to prevent thundering herd restart attempts
|
|
259
|
+
// Only first request to detect failure triggers restart
|
|
260
|
+
sandboxFailureVersion = 0;
|
|
261
|
+
restartInProgress = false;
|
|
262
|
+
dimensionFetched = false;
|
|
263
|
+
// CRITICAL: Container name must be PROJECT-ISOLATED to prevent multi-instance conflicts!
|
|
264
|
+
// Without this, two sessions on different projects fight over the same container
|
|
265
|
+
// Uses readable dir name for easier debugging (matches start-sandbox.sh)
|
|
266
|
+
static CONTAINER_NAME = `specmem-embedding-${_projectDirName}`;
|
|
267
|
+
static IMAGE_NAME = 'specmem-embedding:latest';
|
|
268
|
+
// Adaptive timeout tracking - timeout adjusts based on actual response times
|
|
269
|
+
// CONFIGURABLE via environment variables to prevent AbortError timeouts
|
|
270
|
+
responseTimes = []; // Rolling window of last N response times
|
|
271
|
+
static RESPONSE_TIME_WINDOW = 20; // Track last 20 responses
|
|
272
|
+
// WARM SOCKET - keeps ONE socket ready for fast embeddings
|
|
273
|
+
// Unlike broken persistent socket, this has simple health checks and immediate fallback
|
|
274
|
+
warmSocket = null;
|
|
275
|
+
warmSocketReady = false;
|
|
276
|
+
warmSocketPath = null;
|
|
277
|
+
warmSocketLastUsed = 0;
|
|
278
|
+
static WARM_SOCKET_IDLE_TIMEOUT_MS = 30000; // Close idle sockets after 30s
|
|
279
|
+
static WARM_SOCKET_HEALTH_CHECK_MS = 5000; // Health check every 5s
|
|
280
|
+
warmSocketHealthInterval = null;
|
|
281
|
+
// FIX: Mutex lock to prevent warm socket race condition
|
|
282
|
+
// Ensures only one request uses warm socket at a time
|
|
283
|
+
warmSocketLock = Promise.resolve();
|
|
284
|
+
// LEGACY - kept for backwards compat but not used by new warm socket approach
|
|
285
|
+
persistentSocket = null;
|
|
286
|
+
socketConnected = false;
|
|
287
|
+
socketReconnecting = false;
|
|
288
|
+
pendingRequests = new Map();
|
|
289
|
+
// Timeout values - now centralized in config/embeddingTimeouts.ts
|
|
290
|
+
// Set SPECMEM_EMBEDDING_TIMEOUT (in seconds) to control ALL timeouts at once
|
|
291
|
+
// See config/embeddingTimeouts.ts for full documentation
|
|
292
|
+
static MIN_TIMEOUT_MS = getEmbeddingTimeout('min');
|
|
293
|
+
static MAX_TIMEOUT_MS = getEmbeddingTimeout('max');
|
|
294
|
+
static INITIAL_TIMEOUT_MS = getEmbeddingTimeout('initial');
|
|
295
|
+
static TIMEOUT_MULTIPLIER = 3; // timeout = avg + 3x stddev
|
|
296
|
+
// Retry configuration for transient failures
|
|
297
|
+
static SOCKET_MAX_RETRIES = parseInt(process.env['SPECMEM_EMBEDDING_MAX_RETRIES'] || '3', 10);
|
|
298
|
+
static SOCKET_INITIAL_RETRY_DELAY_MS = 1000; // Start with 1 second delay
|
|
299
|
+
static SOCKET_MAX_RETRY_DELAY_MS = 10000; // Cap at 10 seconds
|
|
300
|
+
// PostgreSQL-backed embedding queue for overflow when socket is unavailable
|
|
301
|
+
embeddingQueue;
|
|
302
|
+
constructor(initialTargetDimension) {
|
|
303
|
+
// If dimension provided, use it; otherwise will query DB on first use
|
|
304
|
+
this.targetDimension = initialTargetDimension ?? null;
|
|
305
|
+
this.dimensionFetched = initialTargetDimension !== undefined;
|
|
306
|
+
// Initialize embedding queue for overflow handling when socket is unavailable
|
|
307
|
+
this.embeddingQueue = getEmbeddingQueue(getDatabase().getPool());
|
|
308
|
+
// Use centralized config for socket path
|
|
309
|
+
this.sandboxSocketPath = getEmbeddingSocketPath();
|
|
310
|
+
// Log timeout configuration for debugging using centralized config
|
|
311
|
+
const timeoutConfig = getAllEmbeddingTimeouts();
|
|
312
|
+
logger.info({
|
|
313
|
+
socketPath: this.sandboxSocketPath,
|
|
314
|
+
masterTimeout: hasMasterTimeout() ? `${timeoutConfig.master}ms` : 'not set',
|
|
315
|
+
minTimeoutMs: LocalEmbeddingProvider.MIN_TIMEOUT_MS,
|
|
316
|
+
maxTimeoutMs: LocalEmbeddingProvider.MAX_TIMEOUT_MS,
|
|
317
|
+
initialTimeoutMs: LocalEmbeddingProvider.INITIAL_TIMEOUT_MS,
|
|
318
|
+
maxRetries: LocalEmbeddingProvider.SOCKET_MAX_RETRIES,
|
|
319
|
+
targetDimension: this.targetDimension,
|
|
320
|
+
configSource: hasMasterTimeout() ? 'SPECMEM_EMBEDDING_TIMEOUT (master)' : 'individual env vars'
|
|
321
|
+
}, 'LocalEmbeddingProvider: initialized with timeout configuration');
|
|
322
|
+
// Sync check in constructor only - one time startup check is acceptable
|
|
323
|
+
this.checkSandboxAvailabilitySync();
|
|
324
|
+
// If sandbox not available, try to auto-start the container
|
|
325
|
+
if (!this.sandboxAvailable && !this.autoStartAttempted) {
|
|
326
|
+
this.tryAutoStartContainer();
|
|
327
|
+
}
|
|
328
|
+
// Initialize persistent socket connection
|
|
329
|
+
this.initPersistentSocket();
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Initialize the persistent socket connection
|
|
333
|
+
* This keeps the socket OPEN and reuses it for all embedding requests
|
|
334
|
+
*/
|
|
335
|
+
initPersistentSocket() {
|
|
336
|
+
const methodStart = Date.now();
|
|
337
|
+
// CRITICAL: Re-detect socket path on EVERY reconnect attempt
|
|
338
|
+
// The socket might not exist at MCP startup but appear later
|
|
339
|
+
const freshSocketPath = getEmbeddingSocketPath();
|
|
340
|
+
if (freshSocketPath !== this.sandboxSocketPath) {
|
|
341
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_PATH_CHANGED', {
|
|
342
|
+
oldPath: this.sandboxSocketPath,
|
|
343
|
+
newPath: freshSocketPath
|
|
344
|
+
});
|
|
345
|
+
// PATH CHANGED: Destroy old socket and reset state to force new connection
|
|
346
|
+
if (this.persistentSocket) {
|
|
347
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_DESTROYING_OLD', {
|
|
348
|
+
reason: 'path changed, need new socket'
|
|
349
|
+
});
|
|
350
|
+
try {
|
|
351
|
+
this.persistentSocket.destroy();
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
// Ignore destroy errors
|
|
355
|
+
}
|
|
356
|
+
this.persistentSocket = null;
|
|
357
|
+
this.socketConnected = false;
|
|
358
|
+
this.socketReconnecting = false;
|
|
359
|
+
}
|
|
360
|
+
this.sandboxSocketPath = freshSocketPath;
|
|
361
|
+
}
|
|
362
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_START', {
|
|
363
|
+
alreadyHasPersistentSocket: !!this.persistentSocket,
|
|
364
|
+
socketReconnecting: this.socketReconnecting,
|
|
365
|
+
socketPath: this.sandboxSocketPath
|
|
366
|
+
});
|
|
367
|
+
// FORCE NEW CONNECTION: If socket exists but not connected, destroy and retry
|
|
368
|
+
if (this.persistentSocket && !this.socketConnected) {
|
|
369
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_FORCE_RECONNECT', {
|
|
370
|
+
reason: 'socket exists but not connected'
|
|
371
|
+
});
|
|
372
|
+
try {
|
|
373
|
+
this.persistentSocket.destroy();
|
|
374
|
+
}
|
|
375
|
+
catch (e) {
|
|
376
|
+
// Ignore
|
|
377
|
+
}
|
|
378
|
+
this.persistentSocket = null;
|
|
379
|
+
this.socketReconnecting = false;
|
|
380
|
+
}
|
|
381
|
+
if (this.persistentSocket || this.socketReconnecting) {
|
|
382
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_SKIPPED', {
|
|
383
|
+
reason: this.persistentSocket ? 'already has socket' : 'already reconnecting',
|
|
384
|
+
elapsedMs: Date.now() - methodStart
|
|
385
|
+
});
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
this.socketReconnecting = true;
|
|
389
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_CREATING_CONNECTION', {
|
|
390
|
+
socketPath: this.sandboxSocketPath
|
|
391
|
+
});
|
|
392
|
+
logger.debug({ socketPath: this.sandboxSocketPath }, 'LocalEmbeddingProvider: initializing persistent socket');
|
|
393
|
+
try {
|
|
394
|
+
this.persistentSocket = createConnection(this.sandboxSocketPath);
|
|
395
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_CONNECTION_CREATED', {
|
|
396
|
+
elapsedMs: Date.now() - methodStart
|
|
397
|
+
});
|
|
398
|
+
this.persistentSocket.on('connect', () => {
|
|
399
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_ON_CONNECT', {
|
|
400
|
+
timeSinceInitMs: Date.now() - methodStart,
|
|
401
|
+
socketPath: this.sandboxSocketPath
|
|
402
|
+
});
|
|
403
|
+
this.socketConnected = true;
|
|
404
|
+
this.socketReconnecting = false;
|
|
405
|
+
logger.info('LocalEmbeddingProvider: persistent socket connected');
|
|
406
|
+
});
|
|
407
|
+
let buffer = '';
|
|
408
|
+
this.persistentSocket.on('data', (data) => {
|
|
409
|
+
const dataLength = data.length;
|
|
410
|
+
buffer += data.toString();
|
|
411
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_ON_DATA', {
|
|
412
|
+
dataLength,
|
|
413
|
+
bufferLength: buffer.length,
|
|
414
|
+
hasNewline: buffer.includes('\n')
|
|
415
|
+
});
|
|
416
|
+
// IDLE-BASED TIMEOUT: Reset ALL pending timeouts on any data received
|
|
417
|
+
// This means server is alive and working - keep waiting
|
|
418
|
+
const timeoutMs = this.getAdaptiveTimeout();
|
|
419
|
+
for (const [reqId, pending] of this.pendingRequests) {
|
|
420
|
+
clearTimeout(pending.timeout);
|
|
421
|
+
pending.timeout = setTimeout(() => {
|
|
422
|
+
if (this.pendingRequests.has(reqId)) {
|
|
423
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_IDLE_TIMEOUT', {
|
|
424
|
+
requestId: reqId,
|
|
425
|
+
timeoutMs
|
|
426
|
+
});
|
|
427
|
+
this.pendingRequests.delete(reqId);
|
|
428
|
+
pending.reject(new Error(`Embedding idle timeout after ${Math.round(timeoutMs / 1000)}s of no activity. ` +
|
|
429
|
+
`Socket: ${this.sandboxSocketPath}. ` +
|
|
430
|
+
`If model is slow, increase SPECMEM_EMBEDDING_TIMEOUT.`));
|
|
431
|
+
}
|
|
432
|
+
}, timeoutMs);
|
|
433
|
+
}
|
|
434
|
+
// Process all complete JSON messages in buffer
|
|
435
|
+
let newlineIndex;
|
|
436
|
+
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
437
|
+
const message = buffer.slice(0, newlineIndex);
|
|
438
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
439
|
+
try {
|
|
440
|
+
const response = JSON.parse(message);
|
|
441
|
+
const requestId = response.requestId;
|
|
442
|
+
// Handle heartbeat/processing status - just reset timeout and continue
|
|
443
|
+
if (response.status === 'processing') {
|
|
444
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_HEARTBEAT', {
|
|
445
|
+
requestId,
|
|
446
|
+
textLength: response.text_length
|
|
447
|
+
});
|
|
448
|
+
continue; // Keep waiting for actual embedding
|
|
449
|
+
}
|
|
450
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_MESSAGE_PARSED', {
|
|
451
|
+
requestId,
|
|
452
|
+
hasEmbedding: !!response.embedding,
|
|
453
|
+
hasError: !!response.error,
|
|
454
|
+
pendingRequestsCount: this.pendingRequests.size
|
|
455
|
+
});
|
|
456
|
+
if (requestId && this.pendingRequests.has(requestId)) {
|
|
457
|
+
const pending = this.pendingRequests.get(requestId);
|
|
458
|
+
clearTimeout(pending.timeout);
|
|
459
|
+
this.pendingRequests.delete(requestId);
|
|
460
|
+
if (response.error) {
|
|
461
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_RESOLVING_ERROR', {
|
|
462
|
+
requestId,
|
|
463
|
+
error: response.error
|
|
464
|
+
});
|
|
465
|
+
pending.reject(new Error(response.error));
|
|
466
|
+
}
|
|
467
|
+
else if (response.embedding) {
|
|
468
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_RESOLVING_SUCCESS', {
|
|
469
|
+
requestId,
|
|
470
|
+
embeddingDim: response.embedding.length
|
|
471
|
+
});
|
|
472
|
+
pending.resolve(response.embedding);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_RESOLVING_INVALID', {
|
|
476
|
+
requestId,
|
|
477
|
+
responseKeys: Object.keys(response)
|
|
478
|
+
});
|
|
479
|
+
pending.reject(new Error('Invalid response from sandbox'));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_ORPHAN_RESPONSE', {
|
|
484
|
+
requestId,
|
|
485
|
+
pendingRequestIds: Array.from(this.pendingRequests.keys())
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_PARSE_ERROR', {
|
|
491
|
+
error: err instanceof Error ? err.message : String(err),
|
|
492
|
+
messageLength: message.length,
|
|
493
|
+
messagePreview: message.substring(0, 100)
|
|
494
|
+
});
|
|
495
|
+
logger.debug({ err, message }, 'LocalEmbeddingProvider: failed to parse socket message');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
this.persistentSocket.on('error', (err) => {
|
|
500
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_ON_ERROR', {
|
|
501
|
+
error: err.message,
|
|
502
|
+
code: err.code,
|
|
503
|
+
pendingRequestsCount: this.pendingRequests.size,
|
|
504
|
+
socketPath: this.sandboxSocketPath
|
|
505
|
+
});
|
|
506
|
+
logger.warn({ err }, 'LocalEmbeddingProvider: persistent socket error');
|
|
507
|
+
this.socketConnected = false;
|
|
508
|
+
this.persistentSocket = null;
|
|
509
|
+
// Reject all pending requests
|
|
510
|
+
for (const [requestId, pending] of this.pendingRequests) {
|
|
511
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_REJECTING_PENDING', {
|
|
512
|
+
requestId,
|
|
513
|
+
error: err.message
|
|
514
|
+
});
|
|
515
|
+
clearTimeout(pending.timeout);
|
|
516
|
+
pending.reject(new Error(`Socket error: ${err.message}`));
|
|
517
|
+
}
|
|
518
|
+
this.pendingRequests.clear();
|
|
519
|
+
// Try to reconnect after delay
|
|
520
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_SCHEDULING_RECONNECT', {
|
|
521
|
+
delayMs: 1000
|
|
522
|
+
});
|
|
523
|
+
this.socketReconnecting = false;
|
|
524
|
+
setTimeout(() => this.initPersistentSocket(), 1000);
|
|
525
|
+
});
|
|
526
|
+
this.persistentSocket.on('close', () => {
|
|
527
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_ON_CLOSE', {
|
|
528
|
+
pendingRequestsCount: this.pendingRequests.size,
|
|
529
|
+
socketPath: this.sandboxSocketPath
|
|
530
|
+
});
|
|
531
|
+
logger.debug('LocalEmbeddingProvider: persistent socket closed');
|
|
532
|
+
this.socketConnected = false;
|
|
533
|
+
this.persistentSocket = null;
|
|
534
|
+
this.socketReconnecting = false;
|
|
535
|
+
// Reject all pending requests
|
|
536
|
+
for (const [requestId, pending] of this.pendingRequests) {
|
|
537
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_CLOSE_REJECTING', {
|
|
538
|
+
requestId
|
|
539
|
+
});
|
|
540
|
+
clearTimeout(pending.timeout);
|
|
541
|
+
pending.reject(new Error('Socket closed'));
|
|
542
|
+
}
|
|
543
|
+
this.pendingRequests.clear();
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_CATCH_ERROR', {
|
|
548
|
+
error: err instanceof Error ? err.message : String(err),
|
|
549
|
+
elapsedMs: Date.now() - methodStart,
|
|
550
|
+
socketPath: this.sandboxSocketPath
|
|
551
|
+
});
|
|
552
|
+
logger.debug({ err }, 'LocalEmbeddingProvider: failed to create persistent socket');
|
|
553
|
+
this.socketReconnecting = false;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* PUBLIC: Force reset the persistent socket connection
|
|
558
|
+
* Call this after restarting the embedding server to pick up new socket
|
|
559
|
+
*/
|
|
560
|
+
resetSocket() {
|
|
561
|
+
logger.info('[LocalEmbeddingProvider] Resetting socket connection...');
|
|
562
|
+
// CRITICAL: Close warm socket too - it caches connections that become stale after server restart
|
|
563
|
+
this.closeWarmSocket();
|
|
564
|
+
// Destroy existing socket if any
|
|
565
|
+
if (this.persistentSocket) {
|
|
566
|
+
try {
|
|
567
|
+
this.persistentSocket.destroy();
|
|
568
|
+
}
|
|
569
|
+
catch (e) {
|
|
570
|
+
// Ignore destroy errors
|
|
571
|
+
}
|
|
572
|
+
this.persistentSocket = null;
|
|
573
|
+
}
|
|
574
|
+
// Reset state
|
|
575
|
+
this.socketConnected = false;
|
|
576
|
+
this.socketReconnecting = false;
|
|
577
|
+
// Clear pending requests
|
|
578
|
+
for (const pending of this.pendingRequests.values()) {
|
|
579
|
+
clearTimeout(pending.timeout);
|
|
580
|
+
pending.reject(new Error('Socket reset by user'));
|
|
581
|
+
}
|
|
582
|
+
this.pendingRequests.clear();
|
|
583
|
+
// Re-detect socket path and reinitialize
|
|
584
|
+
this.sandboxSocketPath = getEmbeddingSocketPath();
|
|
585
|
+
this.initPersistentSocket();
|
|
586
|
+
logger.info({ socketPath: this.sandboxSocketPath }, '[LocalEmbeddingProvider] Socket reset complete');
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Ensure persistent socket is connected, reconnect if needed
|
|
590
|
+
*
|
|
591
|
+
* CRITICAL: Previous 5-second timeout was TOO SHORT - caused fallback to
|
|
592
|
+
* slow per-request socket creation (120s timeout, 3 retries = 360s+ total).
|
|
593
|
+
* Now uses 30s default (configurable via SPECMEM_SOCKET_CONNECT_TIMEOUT_MS).
|
|
594
|
+
*
|
|
595
|
+
* FIX: Increased from 5000ms to 30000ms to give socket time to connect
|
|
596
|
+
* instead of immediately falling back to the slow path.
|
|
597
|
+
*/
|
|
598
|
+
async ensurePersistentSocket() {
|
|
599
|
+
const methodStart = Date.now();
|
|
600
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'ENSURE_PERSISTENT_SOCKET_START', {
|
|
601
|
+
socketConnected: this.socketConnected,
|
|
602
|
+
hasPersistentSocket: !!this.persistentSocket,
|
|
603
|
+
socketReconnecting: this.socketReconnecting,
|
|
604
|
+
socketPath: this.sandboxSocketPath
|
|
605
|
+
});
|
|
606
|
+
if (this.socketConnected && this.persistentSocket) {
|
|
607
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'ENSURE_PERSISTENT_SOCKET_ALREADY_CONNECTED', {
|
|
608
|
+
elapsedMs: Date.now() - methodStart
|
|
609
|
+
});
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
// AUTO-FIX STALE STATE: If socketReconnecting is stuck true but no socket exists,
|
|
613
|
+
// reset the state and force a fresh connection attempt
|
|
614
|
+
if (this.socketReconnecting && !this.persistentSocket) {
|
|
615
|
+
const staleTime = 5000; // 5 seconds is too long to be "reconnecting" without a socket
|
|
616
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'ENSURE_PERSISTENT_SOCKET_STALE_STATE_DETECTED', {
|
|
617
|
+
reason: 'socketReconnecting=true but no socket exists - forcing reset'
|
|
618
|
+
});
|
|
619
|
+
this.socketReconnecting = false;
|
|
620
|
+
this.socketConnected = false;
|
|
621
|
+
}
|
|
622
|
+
if (!this.socketReconnecting) {
|
|
623
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'ENSURE_PERSISTENT_SOCKET_CALLING_INIT', {
|
|
624
|
+
reason: 'not reconnecting, initiating socket'
|
|
625
|
+
});
|
|
626
|
+
this.initPersistentSocket();
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'ENSURE_PERSISTENT_SOCKET_ALREADY_RECONNECTING', {
|
|
630
|
+
reason: 'socketReconnecting=true, skipping init'
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
// Wait for connection with timeout - DYNAMIC via env var
|
|
634
|
+
// Balance: not too short (causes fallback) but not too long (makes MCP unresponsive)
|
|
635
|
+
// 10s is a good middle ground - enough for most connections, fast enough to fail gracefully
|
|
636
|
+
const maxWait = parseInt(process.env['SPECMEM_SOCKET_CONNECT_TIMEOUT_MS'] || '10000', 10);
|
|
637
|
+
const startTime = Date.now();
|
|
638
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'ENSURE_PERSISTENT_SOCKET_WAIT_LOOP_START', {
|
|
639
|
+
maxWaitMs: maxWait,
|
|
640
|
+
pollIntervalMs: 100
|
|
641
|
+
});
|
|
642
|
+
let pollCount = 0;
|
|
643
|
+
while (Date.now() - startTime < maxWait) {
|
|
644
|
+
pollCount++;
|
|
645
|
+
if (this.socketConnected && this.persistentSocket) {
|
|
646
|
+
const waitTimeMs = Date.now() - startTime;
|
|
647
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'ENSURE_PERSISTENT_SOCKET_CONNECTED_IN_LOOP', {
|
|
648
|
+
waitTimeMs,
|
|
649
|
+
pollCount,
|
|
650
|
+
totalMethodElapsedMs: Date.now() - methodStart
|
|
651
|
+
});
|
|
652
|
+
logger.debug({ waitTimeMs: Date.now() - startTime }, 'Persistent socket connected');
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
656
|
+
}
|
|
657
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'ENSURE_PERSISTENT_SOCKET_TIMEOUT', {
|
|
658
|
+
maxWait,
|
|
659
|
+
socketPath: this.sandboxSocketPath,
|
|
660
|
+
pollCount,
|
|
661
|
+
totalMethodElapsedMs: Date.now() - methodStart,
|
|
662
|
+
finalSocketConnected: this.socketConnected,
|
|
663
|
+
finalHasPersistentSocket: !!this.persistentSocket
|
|
664
|
+
});
|
|
665
|
+
logger.warn({ maxWait, socketPath: this.sandboxSocketPath }, 'Persistent socket connection timeout - falling back to per-request socket');
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Fetch target dimension from database if not already known
|
|
670
|
+
* Database is the single source of truth for dimensions!
|
|
671
|
+
*/
|
|
672
|
+
async ensureTargetDimension() {
|
|
673
|
+
if (this.targetDimension !== null) {
|
|
674
|
+
return this.targetDimension;
|
|
675
|
+
}
|
|
676
|
+
// Query database for dimension
|
|
677
|
+
try {
|
|
678
|
+
const db = getDatabase();
|
|
679
|
+
const result = await db.query(`
|
|
680
|
+
SELECT atttypmod FROM pg_attribute
|
|
681
|
+
WHERE attrelid = 'memories'::regclass AND attname = 'embedding'
|
|
682
|
+
`);
|
|
683
|
+
if (result.rows.length > 0) {
|
|
684
|
+
this.targetDimension = result.rows[0].atttypmod;
|
|
685
|
+
this.dimensionFetched = true;
|
|
686
|
+
logger.info({ targetDimension: this.targetDimension }, 'LocalEmbeddingProvider: fetched target dimension from database');
|
|
687
|
+
return this.targetDimension;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
logger.debug({ error: err }, 'LocalEmbeddingProvider: could not fetch dimension from DB (may not be initialized)');
|
|
692
|
+
}
|
|
693
|
+
// If DB query fails, detect from first embedding and use that
|
|
694
|
+
// This handles the cold-start case where DB schema isn't ready yet
|
|
695
|
+
logger.warn('LocalEmbeddingProvider: could not get dimension from DB, will use embedding dimension');
|
|
696
|
+
return 0; // Signal to use embedding dimension as-is
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Auto-detect embedding dimension from Frankenstein
|
|
700
|
+
* Called once on first use, cached thereafter
|
|
701
|
+
*/
|
|
702
|
+
async getEmbeddingDimension() {
|
|
703
|
+
if (this.detectedDimension) {
|
|
704
|
+
return this.detectedDimension;
|
|
705
|
+
}
|
|
706
|
+
// Try to get a test embedding from Frankenstein
|
|
707
|
+
if (await this.isSandboxAvailableAsync()) {
|
|
708
|
+
try {
|
|
709
|
+
const testEmbedding = await this.generateSandboxedEmbedding('test dimension detection');
|
|
710
|
+
this.detectedDimension = testEmbedding.length;
|
|
711
|
+
logger.info({ dimension: this.detectedDimension }, 'Auto-detected embedding dimension from Frankenstein');
|
|
712
|
+
return this.detectedDimension;
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
logger.warn({ error: err }, 'Failed to detect dimension from Frankenstein, using target dimension');
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// Fallback to target dimension if detection fails
|
|
719
|
+
this.detectedDimension = this.targetDimension;
|
|
720
|
+
return this.detectedDimension;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Try to auto-start the embedding Docker container
|
|
724
|
+
* This runs asynchronously and updates sandboxAvailable when ready
|
|
725
|
+
*/
|
|
726
|
+
tryAutoStartContainer() {
|
|
727
|
+
this.autoStartAttempted = true;
|
|
728
|
+
// Run auto-start in background to not block constructor
|
|
729
|
+
this.autoStartContainer().catch(err => {
|
|
730
|
+
logger.warn({ error: err }, 'failed to auto-start embedding container');
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Fix permissions on all parent directories of a path
|
|
735
|
+
* Ensures each parent has at least 755 (world-readable/executable)
|
|
736
|
+
* This is critical for Docker to access the socket directory
|
|
737
|
+
*/
|
|
738
|
+
fixParentDirectoryPermissions(targetPath) {
|
|
739
|
+
const { chmodSync } = require('fs');
|
|
740
|
+
const pathModule = require('path');
|
|
741
|
+
// Get all parent directories from target up to root
|
|
742
|
+
const parents = [];
|
|
743
|
+
let current = pathModule.dirname(targetPath);
|
|
744
|
+
// Collect all parents (stop at /tmp or / to avoid modifying system dirs)
|
|
745
|
+
while (current && current !== '/' && current !== '/tmp' && !parents.includes(current)) {
|
|
746
|
+
parents.push(current);
|
|
747
|
+
current = pathModule.dirname(current);
|
|
748
|
+
}
|
|
749
|
+
// Fix permissions from root towards target (top-down)
|
|
750
|
+
parents.reverse();
|
|
751
|
+
for (const dir of parents) {
|
|
752
|
+
try {
|
|
753
|
+
if (existsSync(dir)) {
|
|
754
|
+
const stats = statSync(dir);
|
|
755
|
+
const currentMode = stats.mode & 0o777;
|
|
756
|
+
// Check if directory has at least 755 (rwxr-xr-x)
|
|
757
|
+
// This means: owner has rwx, group and others have rx
|
|
758
|
+
const minMode = 0o755;
|
|
759
|
+
if ((currentMode & minMode) !== minMode) {
|
|
760
|
+
// Add the missing permissions (don't remove existing ones)
|
|
761
|
+
const newMode = currentMode | minMode;
|
|
762
|
+
chmodSync(dir, newMode);
|
|
763
|
+
logger.info({
|
|
764
|
+
dir,
|
|
765
|
+
oldMode: currentMode.toString(8),
|
|
766
|
+
newMode: newMode.toString(8)
|
|
767
|
+
}, 'fixed parent directory permissions');
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
catch (err) {
|
|
772
|
+
// Log but don't fail - we may not have permission to modify some dirs
|
|
773
|
+
logger.debug({ error: err, dir }, 'could not fix permissions on parent directory');
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Auto-start the embedding Docker container if not running
|
|
779
|
+
*
|
|
780
|
+
* CRITICAL: The SOCKET_PATH env var MUST be within the mounted volume!
|
|
781
|
+
* Container mount: -v ${socketDir}:${socketDir}
|
|
782
|
+
* Socket path: ${socketDir}/embeddings.sock
|
|
783
|
+
*
|
|
784
|
+
* If these don't match, the container will fail with EACCES trying to
|
|
785
|
+
* create a socket in a non-existent directory.
|
|
786
|
+
*/
|
|
787
|
+
async autoStartContainer() {
|
|
788
|
+
// Step 1: Ensure socket directory exists (use centralized config)
|
|
789
|
+
const socketDir = getRunDir();
|
|
790
|
+
// CRITICAL: Fix permissions on ALL parent directories BEFORE creating socket dir
|
|
791
|
+
// This ensures Docker can traverse the path to access the socket
|
|
792
|
+
this.fixParentDirectoryPermissions(socketDir);
|
|
793
|
+
// Task #17 FIX: Use atomic mkdir to prevent race condition when multiple
|
|
794
|
+
// MCP servers try to create the socket directory simultaneously
|
|
795
|
+
try {
|
|
796
|
+
const created = ensureSocketDirAtomicSync(socketDir);
|
|
797
|
+
if (created) {
|
|
798
|
+
logger.info({ dir: socketDir }, 'created socket directory atomically');
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
catch (err) {
|
|
802
|
+
logger.error({ error: err, dir: socketDir }, 'failed to create socket directory');
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
// Ensure socket directory has 777 permissions for Docker access
|
|
806
|
+
try {
|
|
807
|
+
const { chmodSync } = require('fs');
|
|
808
|
+
chmodSync(socketDir, 0o777);
|
|
809
|
+
logger.info({ dir: socketDir, mode: '777' }, 'set socket directory permissions');
|
|
810
|
+
}
|
|
811
|
+
catch (err) {
|
|
812
|
+
logger.warn({ error: err, dir: socketDir }, 'could not set socket directory to 777');
|
|
813
|
+
}
|
|
814
|
+
// CRITICAL FIX: Container socket path MUST be inside the mounted volume
|
|
815
|
+
// NOT this.sandboxSocketPath which might point elsewhere
|
|
816
|
+
const containerSocketPath = `${socketDir}/embeddings.sock`;
|
|
817
|
+
// Step 1.5: Cleanup any stale socket files before starting container
|
|
818
|
+
// A stale socket can prevent the container from binding properly
|
|
819
|
+
await this.cleanupStaleSocket(containerSocketPath);
|
|
820
|
+
// Step 2: ALWAYS prefer native Python over Docker
|
|
821
|
+
// Docker containers crash-loop when native Python owns the socket
|
|
822
|
+
// Native Python is faster, more reliable, and doesn't have permission issues
|
|
823
|
+
const pythonAvailable = await this.isPythonEmbeddingAvailable();
|
|
824
|
+
if (pythonAvailable) {
|
|
825
|
+
logger.info('Native Python embedding available - using Python (preferred over Docker)');
|
|
826
|
+
await this.autoStartPythonEmbedding();
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
// Step 2b: Check if Docker is available - only use Docker if Python isn't available
|
|
830
|
+
if (!this.isDockerAvailable()) {
|
|
831
|
+
logger.info('Docker not available - falling back to Python embedding server');
|
|
832
|
+
await this.autoStartPythonEmbedding();
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
// Step 3: Check if container is already running
|
|
836
|
+
if (this.isContainerRunning()) {
|
|
837
|
+
logger.debug({ container: LocalEmbeddingProvider.CONTAINER_NAME }, 'embedding container already running');
|
|
838
|
+
// Update socket path to match what the running container uses
|
|
839
|
+
this.sandboxSocketPath = containerSocketPath;
|
|
840
|
+
// Wait a bit for socket to appear if container just started
|
|
841
|
+
await this.waitForSocket(10000);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
// Step 4: Check if image exists
|
|
845
|
+
if (!this.isImageAvailable()) {
|
|
846
|
+
logger.warn({ image: LocalEmbeddingProvider.IMAGE_NAME }, 'embedding image not found - cannot auto-start');
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
// Step 5: Remove any stopped container with the same name
|
|
850
|
+
this.removeStoppedContainer();
|
|
851
|
+
// Step 6: Start the container with security flags
|
|
852
|
+
logger.info({
|
|
853
|
+
container: LocalEmbeddingProvider.CONTAINER_NAME,
|
|
854
|
+
socketDir,
|
|
855
|
+
containerSocketPath
|
|
856
|
+
}, 'auto-starting embedding container...');
|
|
857
|
+
try {
|
|
858
|
+
execSync(`docker run -d ` +
|
|
859
|
+
`--name ${LocalEmbeddingProvider.CONTAINER_NAME} ` +
|
|
860
|
+
`--restart=on-failure:5 ` + // Auto-restart up to 5 times on crash/OOM
|
|
861
|
+
`--network none ` +
|
|
862
|
+
`--read-only ` +
|
|
863
|
+
`--cap-drop ALL ` +
|
|
864
|
+
`--security-opt no-new-privileges:true ` +
|
|
865
|
+
`--memory=2g ` +
|
|
866
|
+
`--cpus=2 ` +
|
|
867
|
+
`-v ${socketDir}:${socketDir} ` +
|
|
868
|
+
`-v specmem-model-cache:/app/models ` +
|
|
869
|
+
`-e SOCKET_PATH=${containerSocketPath} ` + // MUST match the mounted volume!
|
|
870
|
+
`-e TARGET_DIMENSION=384 ` + // Fixed dimension for air-gapped container
|
|
871
|
+
`-e SKIP_DB_DIMENSION_QUERY=true ` + // Container is air-gapped, can't query DB
|
|
872
|
+
`-l specmem.project=${_projectDirName} ` + // Label for cleanup
|
|
873
|
+
`${LocalEmbeddingProvider.IMAGE_NAME}`, { stdio: 'pipe', timeout: parseInt(process.env['SPECMEM_DOCKER_EXEC_TIMEOUT_MS'] || '30000', 10) });
|
|
874
|
+
// Update our socket path to match what we told the container
|
|
875
|
+
this.sandboxSocketPath = containerSocketPath;
|
|
876
|
+
logger.info({
|
|
877
|
+
container: LocalEmbeddingProvider.CONTAINER_NAME,
|
|
878
|
+
socketPath: containerSocketPath
|
|
879
|
+
}, 'embedding container started');
|
|
880
|
+
// Wait for socket to become available - configurable via SPECMEM_SOCKET_WAIT_TIMEOUT_MS
|
|
881
|
+
const socketWaitTimeout = parseInt(process.env['SPECMEM_SOCKET_WAIT_TIMEOUT_MS'] || '30000', 10);
|
|
882
|
+
await this.waitForSocket(socketWaitTimeout);
|
|
883
|
+
}
|
|
884
|
+
catch (err) {
|
|
885
|
+
// Task #16 FIX: Proper error handling for Docker spawn failures
|
|
886
|
+
// Extract detailed error info for debugging
|
|
887
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
888
|
+
const errorCode = err?.code || 'UNKNOWN';
|
|
889
|
+
const errorSignal = err?.signal;
|
|
890
|
+
const errorStatus = err?.status;
|
|
891
|
+
// Log detailed error with all available context
|
|
892
|
+
logger.error({
|
|
893
|
+
error: errorMessage,
|
|
894
|
+
code: errorCode,
|
|
895
|
+
signal: errorSignal,
|
|
896
|
+
status: errorStatus,
|
|
897
|
+
container: LocalEmbeddingProvider.CONTAINER_NAME,
|
|
898
|
+
socketDir,
|
|
899
|
+
containerSocketPath,
|
|
900
|
+
image: LocalEmbeddingProvider.IMAGE_NAME
|
|
901
|
+
}, 'Docker container spawn failed - falling back to Python embedding server');
|
|
902
|
+
// Common failure modes with helpful messages:
|
|
903
|
+
// - ETIMEDOUT: Docker command took too long (increase SPECMEM_DOCKER_EXEC_TIMEOUT_MS)
|
|
904
|
+
// - exit code 125: Docker daemon error (permissions, daemon not running)
|
|
905
|
+
// - exit code 126: Container command cannot be invoked
|
|
906
|
+
// - exit code 127: Container command not found
|
|
907
|
+
// - exit code 137: OOM killed (increase --memory limit)
|
|
908
|
+
if (errorStatus === 125) {
|
|
909
|
+
logger.warn('Docker daemon error - check if Docker is running and user has permissions');
|
|
910
|
+
}
|
|
911
|
+
else if (errorCode === 'ETIMEDOUT') {
|
|
912
|
+
logger.warn({ timeout: process.env['SPECMEM_DOCKER_EXEC_TIMEOUT_MS'] || '30000' }, 'Docker command timed out - increase SPECMEM_DOCKER_EXEC_TIMEOUT_MS if needed');
|
|
913
|
+
}
|
|
914
|
+
else if (errorStatus === 137) {
|
|
915
|
+
logger.warn('Container was OOM killed - may need more memory');
|
|
916
|
+
}
|
|
917
|
+
// CRITICAL: Fall back to Python embedding server instead of silent failure
|
|
918
|
+
logger.info('Attempting Python embedding server fallback after Docker failure...');
|
|
919
|
+
try {
|
|
920
|
+
await this.autoStartPythonEmbedding();
|
|
921
|
+
}
|
|
922
|
+
catch (fallbackErr) {
|
|
923
|
+
logger.error({
|
|
924
|
+
error: fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)
|
|
925
|
+
}, 'Python embedding fallback also failed - embedding service unavailable');
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Check if native Python embedding server can be started
|
|
931
|
+
* Always prefer Python over Docker - it's faster and doesn't have permission issues
|
|
932
|
+
*/
|
|
933
|
+
async isPythonEmbeddingAvailable() {
|
|
934
|
+
try {
|
|
935
|
+
// Check if frankenstein-embeddings.py exists in the expected location
|
|
936
|
+
const projectPath = getProjectPath();
|
|
937
|
+
const embeddingScript = join(projectPath, 'embedding-sandbox', 'frankenstein-embeddings.py');
|
|
938
|
+
if (existsSync(embeddingScript)) {
|
|
939
|
+
logger.debug({ embeddingScript }, 'Python embedding script found');
|
|
940
|
+
return true;
|
|
941
|
+
}
|
|
942
|
+
// Also check relative to this file (for npm installed packages)
|
|
943
|
+
const altScript = join(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url)))), 'embedding-sandbox', 'frankenstein-embeddings.py');
|
|
944
|
+
if (existsSync(altScript)) {
|
|
945
|
+
logger.debug({ altScript }, 'Python embedding script found (alt location)');
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Check if Docker daemon is available
|
|
956
|
+
*/
|
|
957
|
+
isDockerAvailable() {
|
|
958
|
+
try {
|
|
959
|
+
execSync('docker info', { stdio: 'pipe', timeout: 5000 });
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Check if the embedding container is currently running
|
|
968
|
+
*/
|
|
969
|
+
isContainerRunning() {
|
|
970
|
+
try {
|
|
971
|
+
const result = execSync(`docker ps --filter "name=^${LocalEmbeddingProvider.CONTAINER_NAME}$" --format "{{.Names}}"`, { stdio: 'pipe', timeout: 5000 });
|
|
972
|
+
return result.toString().trim() === LocalEmbeddingProvider.CONTAINER_NAME;
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Check if the embedding image is available locally
|
|
980
|
+
*/
|
|
981
|
+
isImageAvailable() {
|
|
982
|
+
try {
|
|
983
|
+
execSync(`docker image inspect ${LocalEmbeddingProvider.IMAGE_NAME}`, { stdio: 'pipe', timeout: 5000 });
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
catch {
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Remove any stopped container with our name
|
|
992
|
+
*/
|
|
993
|
+
removeStoppedContainer() {
|
|
994
|
+
try {
|
|
995
|
+
execSync(`docker rm ${LocalEmbeddingProvider.CONTAINER_NAME}`, { stdio: 'pipe', timeout: 5000 });
|
|
996
|
+
logger.debug({ container: LocalEmbeddingProvider.CONTAINER_NAME }, 'removed stopped container');
|
|
997
|
+
}
|
|
998
|
+
catch {
|
|
999
|
+
// Container doesn't exist, which is fine
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Cleanup stale socket file on startup
|
|
1004
|
+
*
|
|
1005
|
+
* A stale socket can occur when:
|
|
1006
|
+
* - Container crashed without cleanup
|
|
1007
|
+
* - Host rebooted while container was running
|
|
1008
|
+
* - Manual docker kill without socket cleanup
|
|
1009
|
+
*
|
|
1010
|
+
* This tries to connect to the socket. If connection fails, the socket is stale
|
|
1011
|
+
* and should be removed before starting a new container.
|
|
1012
|
+
*/
|
|
1013
|
+
async cleanupStaleSocket(socketPath) {
|
|
1014
|
+
if (!existsSync(socketPath)) {
|
|
1015
|
+
logger.debug({ socketPath }, 'LocalEmbeddingProvider: no socket file to cleanup');
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
logger.info({ socketPath }, 'LocalEmbeddingProvider: checking if socket is stale...');
|
|
1019
|
+
// Try to connect to the socket to see if it's alive
|
|
1020
|
+
return new Promise((resolve) => {
|
|
1021
|
+
const testSocket = createConnection(socketPath);
|
|
1022
|
+
const timeout = setTimeout(() => {
|
|
1023
|
+
testSocket.destroy();
|
|
1024
|
+
// Connection timed out - socket is stale
|
|
1025
|
+
this.removeStaleSocketFile(socketPath);
|
|
1026
|
+
resolve();
|
|
1027
|
+
}, 2000); // 2 second timeout for connection test
|
|
1028
|
+
testSocket.on('connect', () => {
|
|
1029
|
+
// Socket is alive - don't remove it
|
|
1030
|
+
clearTimeout(timeout);
|
|
1031
|
+
testSocket.destroy();
|
|
1032
|
+
logger.info({ socketPath }, 'LocalEmbeddingProvider: socket is alive, not removing');
|
|
1033
|
+
resolve();
|
|
1034
|
+
});
|
|
1035
|
+
testSocket.on('error', (err) => {
|
|
1036
|
+
// Connection failed - socket is stale
|
|
1037
|
+
clearTimeout(timeout);
|
|
1038
|
+
testSocket.destroy();
|
|
1039
|
+
logger.info({ socketPath, error: err.message }, 'LocalEmbeddingProvider: socket connection failed, removing stale socket');
|
|
1040
|
+
this.removeStaleSocketFile(socketPath);
|
|
1041
|
+
resolve();
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Remove a stale socket file from the filesystem
|
|
1047
|
+
*/
|
|
1048
|
+
removeStaleSocketFile(socketPath) {
|
|
1049
|
+
try {
|
|
1050
|
+
unlinkSync(socketPath);
|
|
1051
|
+
logger.info({ socketPath }, 'LocalEmbeddingProvider: removed stale socket file');
|
|
1052
|
+
}
|
|
1053
|
+
catch (err) {
|
|
1054
|
+
logger.warn({ socketPath, error: err }, 'LocalEmbeddingProvider: failed to remove stale socket file');
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
// Track Python embedding server process with overload protection
|
|
1058
|
+
pythonEmbeddingPid = null;
|
|
1059
|
+
pythonAutoStartAttempted = false;
|
|
1060
|
+
lastPythonStartAttempt = 0;
|
|
1061
|
+
pythonRestartCount = 0;
|
|
1062
|
+
pythonRestartWindowStart = 0;
|
|
1063
|
+
pythonConsecutiveFailures = 0;
|
|
1064
|
+
static PYTHON_RESTART_COOLDOWN_MS = 5000; // 5 seconds base cooldown
|
|
1065
|
+
static PYTHON_MAX_RESTARTS_PER_MINUTE = 4; // Max 4 restarts per minute
|
|
1066
|
+
static PYTHON_MAX_CONSECUTIVE_FAILURES = 3; // Give up after 3 consecutive failures
|
|
1067
|
+
static PYTHON_FAILURE_BACKOFF_MS = 60000; // 1 minute backoff after max failures
|
|
1068
|
+
/**
|
|
1069
|
+
* Auto-start the embedding server as a Python script (not Docker)
|
|
1070
|
+
*
|
|
1071
|
+
* CRITICAL: This enables transparent restart when embedding server dies!
|
|
1072
|
+
* User runs command → socket dead → auto-restart → command succeeds
|
|
1073
|
+
*
|
|
1074
|
+
* OVERLOAD PROTECTION:
|
|
1075
|
+
* - Max 4 restarts per minute
|
|
1076
|
+
* - After 3 consecutive failures, 1 minute backoff
|
|
1077
|
+
* - Exponential backoff on repeated failures
|
|
1078
|
+
*
|
|
1079
|
+
* Triggered when:
|
|
1080
|
+
* - Session starts and Docker not available
|
|
1081
|
+
* - Socket connection fails mid-session
|
|
1082
|
+
* - Server process gets OOM killed
|
|
1083
|
+
* - Server idle-shutdown and user needs embedding
|
|
1084
|
+
*
|
|
1085
|
+
* @returns true if started or already running, false if failed
|
|
1086
|
+
*/
|
|
1087
|
+
async autoStartPythonEmbedding() {
|
|
1088
|
+
const now = Date.now();
|
|
1089
|
+
// OVERLOAD PROTECTION: Check consecutive failures
|
|
1090
|
+
if (this.pythonConsecutiveFailures >= LocalEmbeddingProvider.PYTHON_MAX_CONSECUTIVE_FAILURES) {
|
|
1091
|
+
const timeSinceLastAttempt = now - this.lastPythonStartAttempt;
|
|
1092
|
+
const backoffTime = LocalEmbeddingProvider.PYTHON_FAILURE_BACKOFF_MS *
|
|
1093
|
+
Math.pow(2, Math.min(this.pythonConsecutiveFailures - LocalEmbeddingProvider.PYTHON_MAX_CONSECUTIVE_FAILURES, 3));
|
|
1094
|
+
if (timeSinceLastAttempt < backoffTime) {
|
|
1095
|
+
const waitTime = Math.round((backoffTime - timeSinceLastAttempt) / 1000);
|
|
1096
|
+
logger.warn({
|
|
1097
|
+
consecutiveFailures: this.pythonConsecutiveFailures,
|
|
1098
|
+
backoffSeconds: Math.round(backoffTime / 1000),
|
|
1099
|
+
waitSeconds: waitTime
|
|
1100
|
+
}, `Embedding server restart in backoff mode (${this.pythonConsecutiveFailures} failures). ` +
|
|
1101
|
+
`Wait ${waitTime}s or check logs at {PROJECT}/specmem/sockets/embedding-autostart.log`);
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
// Backoff expired, reset failure count and try again
|
|
1105
|
+
logger.info('Backoff expired, retrying embedding server...');
|
|
1106
|
+
this.pythonConsecutiveFailures = 0;
|
|
1107
|
+
}
|
|
1108
|
+
// RATE LIMIT: Check restarts per minute
|
|
1109
|
+
if (now - this.pythonRestartWindowStart > 60000) {
|
|
1110
|
+
// Reset window
|
|
1111
|
+
this.pythonRestartWindowStart = now;
|
|
1112
|
+
this.pythonRestartCount = 0;
|
|
1113
|
+
}
|
|
1114
|
+
if (this.pythonRestartCount >= LocalEmbeddingProvider.PYTHON_MAX_RESTARTS_PER_MINUTE) {
|
|
1115
|
+
logger.warn({
|
|
1116
|
+
restartCount: this.pythonRestartCount,
|
|
1117
|
+
maxPerMinute: LocalEmbeddingProvider.PYTHON_MAX_RESTARTS_PER_MINUTE
|
|
1118
|
+
}, 'Embedding server restart rate limit hit. Will retry in ~1 minute.');
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
// COOLDOWN: Short delay between attempts
|
|
1122
|
+
if (now - this.lastPythonStartAttempt < LocalEmbeddingProvider.PYTHON_RESTART_COOLDOWN_MS) {
|
|
1123
|
+
const waitMs = LocalEmbeddingProvider.PYTHON_RESTART_COOLDOWN_MS - (now - this.lastPythonStartAttempt);
|
|
1124
|
+
await new Promise(r => setTimeout(r, waitMs));
|
|
1125
|
+
}
|
|
1126
|
+
this.lastPythonStartAttempt = Date.now();
|
|
1127
|
+
this.pythonRestartCount++;
|
|
1128
|
+
// Get project path
|
|
1129
|
+
const projectPath = getProjectPath();
|
|
1130
|
+
const socketDir = join(projectPath, 'specmem', 'sockets');
|
|
1131
|
+
const socketPath = join(socketDir, 'embeddings.sock');
|
|
1132
|
+
const lockPath = join(socketDir, 'embedding.lock');
|
|
1133
|
+
// Ensure socket directory exists
|
|
1134
|
+
// Task #17 FIX: Use atomic mkdir to prevent race condition when multiple
|
|
1135
|
+
// MCP servers try to create the socket directory simultaneously
|
|
1136
|
+
try {
|
|
1137
|
+
ensureSocketDirAtomicSync(socketDir);
|
|
1138
|
+
}
|
|
1139
|
+
catch (err) {
|
|
1140
|
+
logger.warn({ error: err, socketDir }, 'Failed to create socket directory for Python embedding');
|
|
1141
|
+
}
|
|
1142
|
+
// Check if socket exists and is responsive
|
|
1143
|
+
if (existsSync(socketPath)) {
|
|
1144
|
+
try {
|
|
1145
|
+
const isAlive = await this.testSocketConnection(socketPath);
|
|
1146
|
+
if (isAlive) {
|
|
1147
|
+
logger.debug('Python embedding server already running');
|
|
1148
|
+
this.sandboxAvailable = true;
|
|
1149
|
+
this.sandboxSocketPath = socketPath;
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
// Socket exists but not responsive - clean it up
|
|
1153
|
+
await this.cleanupStaleSocket(socketPath);
|
|
1154
|
+
}
|
|
1155
|
+
catch (e) {
|
|
1156
|
+
logger.debug({ error: e }, 'Socket test failed, will restart');
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1160
|
+
// LOCK FILE: Prevent race condition when multiple MCP instances start
|
|
1161
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1162
|
+
const acquireLock = () => {
|
|
1163
|
+
try {
|
|
1164
|
+
// Check if lock exists
|
|
1165
|
+
if (existsSync(lockPath)) {
|
|
1166
|
+
const lockContent = readFileSync(lockPath, 'utf8').trim();
|
|
1167
|
+
const [lockPid, lockTime] = lockContent.split(':').map(Number);
|
|
1168
|
+
// Check if lock is stale (PID not running or lock too old - 5 min max)
|
|
1169
|
+
const lockAge = Date.now() - lockTime;
|
|
1170
|
+
const isStale = lockAge > 300000; // 5 minutes
|
|
1171
|
+
let pidRunning = false;
|
|
1172
|
+
try {
|
|
1173
|
+
process.kill(lockPid, 0); // Signal 0 = check if process exists
|
|
1174
|
+
pidRunning = true;
|
|
1175
|
+
}
|
|
1176
|
+
catch {
|
|
1177
|
+
pidRunning = false;
|
|
1178
|
+
}
|
|
1179
|
+
if (!pidRunning || isStale) {
|
|
1180
|
+
// Lock is stale - remove it
|
|
1181
|
+
logger.info({ lockPid, lockAge, pidRunning, isStale }, 'Removing stale embedding lock');
|
|
1182
|
+
unlinkSync(lockPath);
|
|
1183
|
+
}
|
|
1184
|
+
else {
|
|
1185
|
+
// Lock is valid - another process is spawning
|
|
1186
|
+
logger.debug({ lockPid, lockAge }, 'Embedding lock held by another process');
|
|
1187
|
+
return { acquired: false, existingPid: lockPid };
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
// Try to acquire lock atomically
|
|
1191
|
+
writeFileSync(lockPath, `${process.pid}:${Date.now()}`, { flag: 'wx' });
|
|
1192
|
+
logger.debug({ pid: process.pid }, 'Acquired embedding lock');
|
|
1193
|
+
return { acquired: true };
|
|
1194
|
+
}
|
|
1195
|
+
catch (e) {
|
|
1196
|
+
// Lock file creation failed (likely another process beat us)
|
|
1197
|
+
if (e && typeof e === 'object' && 'code' in e && e.code === 'EEXIST') {
|
|
1198
|
+
logger.debug('Embedding lock already exists (race)');
|
|
1199
|
+
return { acquired: false };
|
|
1200
|
+
}
|
|
1201
|
+
logger.warn({ error: e }, 'Failed to acquire embedding lock');
|
|
1202
|
+
return { acquired: false };
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
const releaseLock = () => {
|
|
1206
|
+
try {
|
|
1207
|
+
if (existsSync(lockPath)) {
|
|
1208
|
+
const lockContent = readFileSync(lockPath, 'utf8').trim();
|
|
1209
|
+
const [lockPid] = lockContent.split(':').map(Number);
|
|
1210
|
+
// Only release if we own it
|
|
1211
|
+
if (lockPid === process.pid) {
|
|
1212
|
+
unlinkSync(lockPath);
|
|
1213
|
+
logger.debug({ pid: process.pid }, 'Released embedding lock');
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
catch { /* ignore release errors */ }
|
|
1218
|
+
};
|
|
1219
|
+
// Try to acquire lock
|
|
1220
|
+
const lockResult = acquireLock();
|
|
1221
|
+
if (!lockResult.acquired) {
|
|
1222
|
+
// Another process is spawning - wait for socket to appear
|
|
1223
|
+
logger.info({ existingPid: lockResult.existingPid }, 'Another process spawning embedding, waiting...');
|
|
1224
|
+
this.sandboxSocketPath = socketPath;
|
|
1225
|
+
await this.waitForSocket(20000); // Wait up to 20s for other process
|
|
1226
|
+
if (this.sandboxAvailable) {
|
|
1227
|
+
return true;
|
|
1228
|
+
}
|
|
1229
|
+
// Still not available - try again next time
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
// We have the lock - verify socket one more time (other process may have finished)
|
|
1233
|
+
if (existsSync(socketPath)) {
|
|
1234
|
+
try {
|
|
1235
|
+
const isAlive = await this.testSocketConnection(socketPath);
|
|
1236
|
+
if (isAlive) {
|
|
1237
|
+
logger.debug('Socket became available while acquiring lock');
|
|
1238
|
+
this.sandboxAvailable = true;
|
|
1239
|
+
this.sandboxSocketPath = socketPath;
|
|
1240
|
+
releaseLock();
|
|
1241
|
+
return true;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
catch { /* proceed to spawn */ }
|
|
1245
|
+
}
|
|
1246
|
+
// Find the embedding server script
|
|
1247
|
+
// Try multiple locations: SPECMEM_PKG env, relative to this file, project specmem dir
|
|
1248
|
+
// Docker location via env var (defaults to /opt/specmem if SPECMEM_DOCKER_PATH set)
|
|
1249
|
+
const dockerPath = process.env.SPECMEM_DOCKER_PATH || (process.env.SPECMEM_IN_DOCKER ? '/opt/specmem' : null);
|
|
1250
|
+
const candidatePaths = [
|
|
1251
|
+
process.env.SPECMEM_PKG ? join(process.env.SPECMEM_PKG, 'embedding-sandbox', 'frankenstein-embeddings.py') : null,
|
|
1252
|
+
join(__dirname, '..', 'embedding-sandbox', 'frankenstein-embeddings.py'),
|
|
1253
|
+
join(projectPath, 'specmem', 'embedding-sandbox', 'frankenstein-embeddings.py'),
|
|
1254
|
+
dockerPath ? join(dockerPath, 'embedding-sandbox', 'frankenstein-embeddings.py') : null,
|
|
1255
|
+
// Global npm install fallback (platform-agnostic)
|
|
1256
|
+
join(path.dirname(path.dirname(process.execPath)), 'lib', 'node_modules', 'specmem-hardwicksoftware', 'embedding-sandbox', 'frankenstein-embeddings.py'),
|
|
1257
|
+
].filter(Boolean);
|
|
1258
|
+
let embeddingScript = null;
|
|
1259
|
+
for (const candidate of candidatePaths) {
|
|
1260
|
+
if (existsSync(candidate)) {
|
|
1261
|
+
embeddingScript = candidate;
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (!embeddingScript) {
|
|
1266
|
+
logger.warn({ candidatePaths }, 'Embedding server script not found - cannot auto-start');
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
// Start the embedding server in background
|
|
1270
|
+
try {
|
|
1271
|
+
const logFile = join(socketDir, 'embedding-autostart.log');
|
|
1272
|
+
logger.info({
|
|
1273
|
+
script: embeddingScript,
|
|
1274
|
+
projectPath,
|
|
1275
|
+
socketPath,
|
|
1276
|
+
logFile
|
|
1277
|
+
}, 'Auto-starting Python embedding server...');
|
|
1278
|
+
// bruh ALWAYS use getSpawnEnv for project isolation
|
|
1279
|
+
// Task #22 fix: Use getPythonPath() instead of hardcoded 'python3'
|
|
1280
|
+
const pythonPath = getPythonPath();
|
|
1281
|
+
logger.debug({ pythonPath }, 'Using Python executable for embedding server');
|
|
1282
|
+
const child = spawn(pythonPath, [embeddingScript], {
|
|
1283
|
+
cwd: path.dirname(embeddingScript),
|
|
1284
|
+
env: getSpawnEnv(), // includes SPECMEM_PROJECT_PATH and all other project env vars
|
|
1285
|
+
detached: true,
|
|
1286
|
+
stdio: ['ignore', openSync(logFile, 'a'), openSync(logFile, 'a')]
|
|
1287
|
+
});
|
|
1288
|
+
child.unref();
|
|
1289
|
+
this.pythonEmbeddingPid = child.pid || null;
|
|
1290
|
+
// CRITICAL: Persist PID to file for orphan cleanup on next startup
|
|
1291
|
+
// Without this, orphaned embedding processes accumulate forever!
|
|
1292
|
+
if (this.pythonEmbeddingPid) {
|
|
1293
|
+
const pidFile = join(socketDir, 'embedding.pid');
|
|
1294
|
+
try {
|
|
1295
|
+
writeFileSync(pidFile, `${this.pythonEmbeddingPid}:${Date.now()}`);
|
|
1296
|
+
logger.debug({ pidFile, pid: this.pythonEmbeddingPid }, 'Embedding PID file written');
|
|
1297
|
+
}
|
|
1298
|
+
catch (pidErr) {
|
|
1299
|
+
logger.warn({ pidErr, pidFile }, 'Failed to write embedding PID file (orphan cleanup may fail)');
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
logger.info({
|
|
1303
|
+
pid: this.pythonEmbeddingPid,
|
|
1304
|
+
socketPath
|
|
1305
|
+
}, 'Python embedding server spawned');
|
|
1306
|
+
// Wait for socket to appear
|
|
1307
|
+
this.sandboxSocketPath = socketPath;
|
|
1308
|
+
await this.waitForSocket(15000); // 15 second timeout for Python startup
|
|
1309
|
+
if (this.sandboxAvailable) {
|
|
1310
|
+
logger.info({ socketPath, restartCount: this.pythonRestartCount }, 'Python embedding server ready');
|
|
1311
|
+
this.pythonConsecutiveFailures = 0; // SUCCESS: reset failure counter
|
|
1312
|
+
releaseLock(); // Release lock on success
|
|
1313
|
+
return true;
|
|
1314
|
+
}
|
|
1315
|
+
else {
|
|
1316
|
+
logger.warn({ socketPath }, 'Python embedding server started but socket not ready');
|
|
1317
|
+
this.pythonConsecutiveFailures++; // FAILURE: increment counter
|
|
1318
|
+
releaseLock(); // Release lock on failure
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
catch (err) {
|
|
1323
|
+
logger.error({ error: err, script: embeddingScript }, 'Failed to spawn Python embedding server');
|
|
1324
|
+
this.pythonConsecutiveFailures++; // FAILURE: increment counter
|
|
1325
|
+
releaseLock(); // Release lock on error
|
|
1326
|
+
return false;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Test if a socket is responsive
|
|
1331
|
+
* @returns true if socket responds to a test request
|
|
1332
|
+
*/
|
|
1333
|
+
testSocketConnection(socketPath) {
|
|
1334
|
+
return new Promise((resolve) => {
|
|
1335
|
+
const testSocket = createConnection(socketPath);
|
|
1336
|
+
const timeout = setTimeout(() => {
|
|
1337
|
+
testSocket.destroy();
|
|
1338
|
+
resolve(false);
|
|
1339
|
+
}, 2000);
|
|
1340
|
+
testSocket.on('connect', () => {
|
|
1341
|
+
clearTimeout(timeout);
|
|
1342
|
+
// Try sending a minimal request
|
|
1343
|
+
testSocket.write('{"text":"test"}\n');
|
|
1344
|
+
});
|
|
1345
|
+
testSocket.on('data', (data) => {
|
|
1346
|
+
clearTimeout(timeout);
|
|
1347
|
+
testSocket.destroy();
|
|
1348
|
+
// Check if we got a valid embedding response
|
|
1349
|
+
const response = data.toString();
|
|
1350
|
+
resolve(response.includes('embedding') || response.includes('['));
|
|
1351
|
+
});
|
|
1352
|
+
testSocket.on('error', () => {
|
|
1353
|
+
clearTimeout(timeout);
|
|
1354
|
+
testSocket.destroy();
|
|
1355
|
+
resolve(false);
|
|
1356
|
+
});
|
|
1357
|
+
testSocket.on('timeout', () => {
|
|
1358
|
+
clearTimeout(timeout);
|
|
1359
|
+
testSocket.destroy();
|
|
1360
|
+
resolve(false);
|
|
1361
|
+
});
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Wait for the socket to become available
|
|
1366
|
+
*/
|
|
1367
|
+
async waitForSocket(timeoutMs) {
|
|
1368
|
+
const startTime = Date.now();
|
|
1369
|
+
const checkInterval = 500;
|
|
1370
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
1371
|
+
if (existsSync(this.sandboxSocketPath)) {
|
|
1372
|
+
this.sandboxAvailable = true;
|
|
1373
|
+
this.lastSandboxCheck = Date.now();
|
|
1374
|
+
logger.info({ socketPath: this.sandboxSocketPath }, 'embedding socket now available');
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
1378
|
+
}
|
|
1379
|
+
logger.warn({ socketPath: this.sandboxSocketPath, timeoutMs }, 'timeout waiting for embedding socket');
|
|
1380
|
+
}
|
|
1381
|
+
// Track restart attempts to avoid infinite restart loops
|
|
1382
|
+
restartAttempts = 0;
|
|
1383
|
+
lastRestartAttempt = 0;
|
|
1384
|
+
static MAX_RESTART_ATTEMPTS = 3;
|
|
1385
|
+
static RESTART_COOLDOWN_MS = 60000; // 1 minute between restart attempts
|
|
1386
|
+
/**
|
|
1387
|
+
* Try to restart the embedding container when it becomes unhealthy
|
|
1388
|
+
* Runs in background to not block embedding requests
|
|
1389
|
+
*/
|
|
1390
|
+
tryRestartContainer() {
|
|
1391
|
+
const now = Date.now();
|
|
1392
|
+
// Check cooldown
|
|
1393
|
+
if (now - this.lastRestartAttempt < LocalEmbeddingProvider.RESTART_COOLDOWN_MS) {
|
|
1394
|
+
logger.debug('restart cooldown active, skipping');
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
// Check max attempts
|
|
1398
|
+
if (this.restartAttempts >= LocalEmbeddingProvider.MAX_RESTART_ATTEMPTS) {
|
|
1399
|
+
logger.warn({ attempts: this.restartAttempts }, 'max restart attempts reached - giving up');
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
this.lastRestartAttempt = now;
|
|
1403
|
+
this.restartAttempts++;
|
|
1404
|
+
// Run restart in background
|
|
1405
|
+
this.restartContainer().catch(err => {
|
|
1406
|
+
logger.error({ error: err }, 'failed to restart embedding container');
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Restart the embedding Docker container
|
|
1411
|
+
* Task #16 FIX: Added proper error handling with detailed diagnostics and Python fallback
|
|
1412
|
+
*/
|
|
1413
|
+
async restartContainer() {
|
|
1414
|
+
logger.info({ attempt: this.restartAttempts }, 'attempting to restart embedding container');
|
|
1415
|
+
// Check if Docker is available - fall back to Python if not
|
|
1416
|
+
if (!this.isDockerAvailable()) {
|
|
1417
|
+
logger.warn('Docker not available - falling back to Python embedding server');
|
|
1418
|
+
try {
|
|
1419
|
+
await this.autoStartPythonEmbedding();
|
|
1420
|
+
}
|
|
1421
|
+
catch (fallbackErr) {
|
|
1422
|
+
logger.error({
|
|
1423
|
+
error: fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)
|
|
1424
|
+
}, 'Python embedding fallback failed after Docker unavailable');
|
|
1425
|
+
}
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
try {
|
|
1429
|
+
// First try to just restart the existing container
|
|
1430
|
+
// Docker command timeout - configurable via SPECMEM_DOCKER_EXEC_TIMEOUT_MS
|
|
1431
|
+
const dockerTimeout = parseInt(process.env['SPECMEM_DOCKER_EXEC_TIMEOUT_MS'] || '30000', 10);
|
|
1432
|
+
if (this.isContainerRunning()) {
|
|
1433
|
+
logger.info('restarting running container');
|
|
1434
|
+
execSync(`docker restart ${LocalEmbeddingProvider.CONTAINER_NAME}`, {
|
|
1435
|
+
stdio: 'pipe',
|
|
1436
|
+
timeout: dockerTimeout
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
else {
|
|
1440
|
+
// Container stopped - start it
|
|
1441
|
+
logger.info('starting stopped container');
|
|
1442
|
+
execSync(`docker start ${LocalEmbeddingProvider.CONTAINER_NAME}`, {
|
|
1443
|
+
stdio: 'pipe',
|
|
1444
|
+
timeout: dockerTimeout
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
// Wait for socket to become available - configurable via SPECMEM_SOCKET_WAIT_TIMEOUT_MS
|
|
1448
|
+
const socketWaitTimeout = parseInt(process.env['SPECMEM_SOCKET_WAIT_TIMEOUT_MS'] || '15000', 10);
|
|
1449
|
+
await this.waitForSocket(socketWaitTimeout);
|
|
1450
|
+
if (this.sandboxAvailable) {
|
|
1451
|
+
logger.info('embedding container successfully restarted');
|
|
1452
|
+
// Reset restart attempts on success
|
|
1453
|
+
this.restartAttempts = 0;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
catch (err) {
|
|
1457
|
+
// Task #16 FIX: Extract detailed error info for debugging
|
|
1458
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1459
|
+
const errorCode = err?.code || 'UNKNOWN';
|
|
1460
|
+
const errorSignal = err?.signal;
|
|
1461
|
+
const errorStatus = err?.status;
|
|
1462
|
+
logger.error({
|
|
1463
|
+
error: errorMessage,
|
|
1464
|
+
code: errorCode,
|
|
1465
|
+
signal: errorSignal,
|
|
1466
|
+
status: errorStatus,
|
|
1467
|
+
container: LocalEmbeddingProvider.CONTAINER_NAME,
|
|
1468
|
+
attempt: this.restartAttempts
|
|
1469
|
+
}, 'Docker restart command failed');
|
|
1470
|
+
// If restart failed, try full recreation
|
|
1471
|
+
try {
|
|
1472
|
+
logger.info('attempting full container recreation');
|
|
1473
|
+
this.removeStoppedContainer();
|
|
1474
|
+
await this.autoStartContainer();
|
|
1475
|
+
}
|
|
1476
|
+
catch (recreateErr) {
|
|
1477
|
+
const recreateMsg = recreateErr instanceof Error ? recreateErr.message : String(recreateErr);
|
|
1478
|
+
logger.error({ error: recreateMsg }, 'full container recreation also failed - trying Python fallback');
|
|
1479
|
+
// CRITICAL: Fall back to Python as last resort
|
|
1480
|
+
try {
|
|
1481
|
+
await this.autoStartPythonEmbedding();
|
|
1482
|
+
}
|
|
1483
|
+
catch (pythonErr) {
|
|
1484
|
+
logger.error({
|
|
1485
|
+
error: pythonErr instanceof Error ? pythonErr.message : String(pythonErr)
|
|
1486
|
+
}, 'All embedding service options exhausted - service unavailable');
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
// Sync version - ONLY for constructor initial check (one-time)
|
|
1492
|
+
checkSandboxAvailabilitySync() {
|
|
1493
|
+
this.sandboxAvailable = existsSync(this.sandboxSocketPath);
|
|
1494
|
+
if (this.sandboxAvailable) {
|
|
1495
|
+
logger.info({ socketPath: this.sandboxSocketPath }, 'sandboxed embedding service available - using real ML embeddings');
|
|
1496
|
+
}
|
|
1497
|
+
else {
|
|
1498
|
+
logger.debug({ socketPath: this.sandboxSocketPath }, 'sandbox not available - using hash fallback');
|
|
1499
|
+
}
|
|
1500
|
+
this.lastSandboxCheck = Date.now();
|
|
1501
|
+
}
|
|
1502
|
+
// Async version - use this for all runtime checks (non-blocking)
|
|
1503
|
+
// SELF-HEALING: If socket missing, attempt warm start immediately
|
|
1504
|
+
async checkSandboxAvailabilityAsync() {
|
|
1505
|
+
try {
|
|
1506
|
+
await access(this.sandboxSocketPath, constants.F_OK);
|
|
1507
|
+
// Socket file exists - assume it's available (model may be lazy-loading)
|
|
1508
|
+
this.sandboxAvailable = true;
|
|
1509
|
+
}
|
|
1510
|
+
catch {
|
|
1511
|
+
// Socket missing - MAKE IT!
|
|
1512
|
+
logger.info({ socketPath: this.sandboxSocketPath }, '[SELF-HEAL] Socket missing - starting embedding server');
|
|
1513
|
+
this.sandboxAvailable = false;
|
|
1514
|
+
// Try to start embedding server
|
|
1515
|
+
const started = await this.autoStartPythonEmbedding();
|
|
1516
|
+
if (started) {
|
|
1517
|
+
this.sandboxAvailable = true;
|
|
1518
|
+
logger.info({ socketPath: this.sandboxSocketPath }, '[SELF-HEAL] Embedding server started successfully');
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
if (this.sandboxAvailable) {
|
|
1522
|
+
logger.debug({ socketPath: this.sandboxSocketPath }, 'sandboxed embedding service available - using real ML embeddings');
|
|
1523
|
+
}
|
|
1524
|
+
else {
|
|
1525
|
+
logger.warn({ socketPath: this.sandboxSocketPath }, 'sandbox not available after self-heal attempt');
|
|
1526
|
+
}
|
|
1527
|
+
this.lastSandboxCheck = Date.now();
|
|
1528
|
+
}
|
|
1529
|
+
// Async availability check - use this at runtime (non-blocking)
|
|
1530
|
+
async isSandboxAvailableAsync() {
|
|
1531
|
+
if (Date.now() - this.lastSandboxCheck > this.sandboxCheckInterval) {
|
|
1532
|
+
await this.checkSandboxAvailabilityAsync();
|
|
1533
|
+
}
|
|
1534
|
+
return this.sandboxAvailable;
|
|
1535
|
+
}
|
|
1536
|
+
async generateEmbedding(text) {
|
|
1537
|
+
const methodStart = Date.now();
|
|
1538
|
+
const textPreview = text.length > 50 ? text.substring(0, 50) + '...' : text;
|
|
1539
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_START', {
|
|
1540
|
+
textLength: text.length,
|
|
1541
|
+
textPreview,
|
|
1542
|
+
sandboxAvailable: this.sandboxAvailable,
|
|
1543
|
+
socketConnected: this.socketConnected
|
|
1544
|
+
});
|
|
1545
|
+
// Use QOMS to ensure we never exceed 20% CPU/RAM
|
|
1546
|
+
return qoms.medium(async () => {
|
|
1547
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_QOMS_ACQUIRED', {
|
|
1548
|
+
elapsedMs: Date.now() - methodStart
|
|
1549
|
+
});
|
|
1550
|
+
// Ensure we have a target dimension from database
|
|
1551
|
+
const dimStart = Date.now();
|
|
1552
|
+
const targetDim = await this.ensureTargetDimension();
|
|
1553
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_TARGET_DIM_FETCHED', {
|
|
1554
|
+
targetDim,
|
|
1555
|
+
dimFetchMs: Date.now() - dimStart,
|
|
1556
|
+
totalElapsedMs: Date.now() - methodStart
|
|
1557
|
+
});
|
|
1558
|
+
// CRITICAL: ML model MUST be available - no fallback to hash embeddings!
|
|
1559
|
+
// Hash embeddings are in a completely different vector space and break semantic search.
|
|
1560
|
+
// SELF-HEALING: checkSandboxAvailabilityAsync now auto-starts server if socket missing/dead
|
|
1561
|
+
const MAX_RETRIES = 5;
|
|
1562
|
+
const RETRY_DELAY_MS = 1000; // Faster retries since self-healing is aggressive
|
|
1563
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
1564
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_ATTEMPT_START', {
|
|
1565
|
+
attempt,
|
|
1566
|
+
maxRetries: MAX_RETRIES,
|
|
1567
|
+
totalElapsedMs: Date.now() - methodStart
|
|
1568
|
+
});
|
|
1569
|
+
// SELF-HEAL: This now auto-starts server if socket missing or dead!
|
|
1570
|
+
if (!(await this.isSandboxAvailableAsync())) {
|
|
1571
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_SANDBOX_UNAVAILABLE', {
|
|
1572
|
+
attempt,
|
|
1573
|
+
socketPath: this.sandboxSocketPath,
|
|
1574
|
+
totalElapsedMs: Date.now() - methodStart
|
|
1575
|
+
});
|
|
1576
|
+
logger.warn({ attempt, maxRetries: MAX_RETRIES, socketPath: this.sandboxSocketPath }, '[SELF-HEAL] Embedding service unavailable - auto-healing in progress');
|
|
1577
|
+
// Report retry progress
|
|
1578
|
+
reportRetry('embedding', attempt, MAX_RETRIES);
|
|
1579
|
+
// SELF-HEAL: Try multiple restart strategies
|
|
1580
|
+
if (attempt === 1) {
|
|
1581
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_RESTART_CONTAINER', {
|
|
1582
|
+
attempt
|
|
1583
|
+
});
|
|
1584
|
+
this.tryRestartContainer();
|
|
1585
|
+
} else if (attempt === 2) {
|
|
1586
|
+
// Second attempt: try Python directly (in case Docker is the problem)
|
|
1587
|
+
logger.info('[SELF-HEAL] Attempt 2: Trying direct Python startup');
|
|
1588
|
+
await this.autoStartPythonEmbedding();
|
|
1589
|
+
}
|
|
1590
|
+
// Wait before retry - faster since self-healing is aggressive
|
|
1591
|
+
const waitMs = RETRY_DELAY_MS * attempt;
|
|
1592
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_WAITING_FOR_SANDBOX', {
|
|
1593
|
+
waitMs,
|
|
1594
|
+
attempt
|
|
1595
|
+
});
|
|
1596
|
+
await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
1597
|
+
await this.checkSandboxAvailabilityAsync();
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
try {
|
|
1601
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_CALLING_SANDBOXED', {
|
|
1602
|
+
attempt,
|
|
1603
|
+
totalElapsedMs: Date.now() - methodStart
|
|
1604
|
+
});
|
|
1605
|
+
const sandboxStart = Date.now();
|
|
1606
|
+
const embedding = await this.generateSandboxedEmbedding(text);
|
|
1607
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_SANDBOXED_COMPLETE', {
|
|
1608
|
+
attempt,
|
|
1609
|
+
embeddingDim: embedding.length,
|
|
1610
|
+
sandboxedMs: Date.now() - sandboxStart,
|
|
1611
|
+
totalElapsedMs: Date.now() - methodStart
|
|
1612
|
+
});
|
|
1613
|
+
// Auto-detect model dimension on first call
|
|
1614
|
+
if (!this.detectedDimension) {
|
|
1615
|
+
this.detectedDimension = embedding.length;
|
|
1616
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_DIMENSION_DETECTED', {
|
|
1617
|
+
detectedDimension: this.detectedDimension,
|
|
1618
|
+
targetDim
|
|
1619
|
+
});
|
|
1620
|
+
logger.info({ modelDim: this.detectedDimension, dbDim: targetDim }, 'Auto-detected embedding dimension from AI model');
|
|
1621
|
+
}
|
|
1622
|
+
// DYNAMIC SCALING - match DB dimension, whatever it is!
|
|
1623
|
+
// If targetDim is 0 (DB not ready), return embedding as-is
|
|
1624
|
+
if (targetDim > 0 && embedding.length !== targetDim) {
|
|
1625
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_SCALING', {
|
|
1626
|
+
from: embedding.length,
|
|
1627
|
+
to: targetDim
|
|
1628
|
+
});
|
|
1629
|
+
const scaled = this.scaleEmbedding(embedding, targetDim);
|
|
1630
|
+
logger.debug({ from: embedding.length, to: targetDim }, 'Scaled embedding to match DB');
|
|
1631
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_SUCCESS', {
|
|
1632
|
+
finalDim: scaled.length,
|
|
1633
|
+
scaled: true,
|
|
1634
|
+
totalElapsedMs: Date.now() - methodStart
|
|
1635
|
+
});
|
|
1636
|
+
return scaled;
|
|
1637
|
+
}
|
|
1638
|
+
// Return as-is if dimensions already match or DB not ready
|
|
1639
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_SUCCESS', {
|
|
1640
|
+
finalDim: embedding.length,
|
|
1641
|
+
scaled: false,
|
|
1642
|
+
totalElapsedMs: Date.now() - methodStart
|
|
1643
|
+
});
|
|
1644
|
+
return embedding;
|
|
1645
|
+
}
|
|
1646
|
+
catch (error) {
|
|
1647
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_SANDBOXED_ERROR', {
|
|
1648
|
+
attempt,
|
|
1649
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1650
|
+
totalElapsedMs: Date.now() - methodStart
|
|
1651
|
+
});
|
|
1652
|
+
logger.warn({ error, attempt }, 'sandbox embedding failed - will retry');
|
|
1653
|
+
// FIX: Use version tracking to prevent thundering herd restarts
|
|
1654
|
+
// Only first request to detect failure triggers restart
|
|
1655
|
+
const failureVersion = ++this.sandboxFailureVersion;
|
|
1656
|
+
this.sandboxAvailable = false;
|
|
1657
|
+
// Report retry progress
|
|
1658
|
+
reportRetry('embedding', attempt, MAX_RETRIES);
|
|
1659
|
+
if (attempt < MAX_RETRIES) {
|
|
1660
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_RETRY_WAIT', {
|
|
1661
|
+
attempt,
|
|
1662
|
+
nextAttempt: attempt + 1,
|
|
1663
|
+
waitMs: RETRY_DELAY_MS * attempt,
|
|
1664
|
+
failureVersion
|
|
1665
|
+
});
|
|
1666
|
+
// Only restart if we're the first to detect failure (no other restart in progress)
|
|
1667
|
+
if (!this.restartInProgress && failureVersion === this.sandboxFailureVersion) {
|
|
1668
|
+
this.restartInProgress = true;
|
|
1669
|
+
this.tryRestartContainer();
|
|
1670
|
+
this.restartInProgress = false;
|
|
1671
|
+
}
|
|
1672
|
+
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS * attempt));
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
// All retries exhausted - FAIL LOUDLY instead of silent hash fallback
|
|
1677
|
+
// Hash embeddings are in different vector space and poison the search index!
|
|
1678
|
+
const errorMsg = `ML embedding service unavailable after ${MAX_RETRIES} attempts. ` +
|
|
1679
|
+
`Socket: ${this.sandboxSocketPath}. ` +
|
|
1680
|
+
`REFUSING to use hash fallback - would corrupt semantic search. ` +
|
|
1681
|
+
`Start the Frankenstein embedding service!`;
|
|
1682
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDING_ALL_RETRIES_EXHAUSTED', {
|
|
1683
|
+
maxRetries: MAX_RETRIES,
|
|
1684
|
+
socketPath: this.sandboxSocketPath,
|
|
1685
|
+
totalElapsedMs: Date.now() - methodStart
|
|
1686
|
+
});
|
|
1687
|
+
logger.error({ socketPath: this.sandboxSocketPath }, errorMsg);
|
|
1688
|
+
reportError('embedding', 'ML embedding service unavailable');
|
|
1689
|
+
throw new Error(errorMsg);
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* BATCH EMBEDDING - generates multiple embeddings in a single socket request!
|
|
1694
|
+
* Uses the Python server's batch protocol: {"texts": [...]} -> {"embeddings": [[...], ...]}
|
|
1695
|
+
* This is 5-10x faster than individual calls for large batches.
|
|
1696
|
+
*/
|
|
1697
|
+
async generateEmbeddingsBatch(texts) {
|
|
1698
|
+
if (texts.length === 0)
|
|
1699
|
+
return [];
|
|
1700
|
+
if (texts.length === 1) {
|
|
1701
|
+
// Single text - use regular method
|
|
1702
|
+
const embedding = await this.generateEmbedding(texts[0]);
|
|
1703
|
+
return [embedding];
|
|
1704
|
+
}
|
|
1705
|
+
const methodStart = Date.now();
|
|
1706
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDINGS_BATCH_START', {
|
|
1707
|
+
batchSize: texts.length,
|
|
1708
|
+
totalChars: texts.reduce((sum, t) => sum + t.length, 0)
|
|
1709
|
+
});
|
|
1710
|
+
// Use QOMS to ensure we never exceed resource limits
|
|
1711
|
+
return qoms.medium(async () => {
|
|
1712
|
+
// Ensure we have target dimension from database
|
|
1713
|
+
const targetDim = await this.ensureTargetDimension();
|
|
1714
|
+
// Wait for sandbox to be available
|
|
1715
|
+
const MAX_RETRIES = 5;
|
|
1716
|
+
const RETRY_DELAY_MS = 2000;
|
|
1717
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
1718
|
+
if (!(await this.isSandboxAvailableAsync())) {
|
|
1719
|
+
logger.warn({ attempt, maxRetries: MAX_RETRIES, socketPath: this.sandboxSocketPath }, 'ML embedding service unavailable for batch - waiting');
|
|
1720
|
+
if (attempt === 1)
|
|
1721
|
+
this.tryRestartContainer();
|
|
1722
|
+
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS * attempt));
|
|
1723
|
+
await this.checkSandboxAvailabilityAsync();
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
try {
|
|
1727
|
+
const embeddings = await this.generateBatchWithDirectSocket(texts);
|
|
1728
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'GENERATE_EMBEDDINGS_BATCH_SUCCESS', {
|
|
1729
|
+
batchSize: texts.length,
|
|
1730
|
+
embeddingsDim: embeddings[0]?.length,
|
|
1731
|
+
totalMs: Date.now() - methodStart
|
|
1732
|
+
});
|
|
1733
|
+
// Scale all embeddings to target dimension if needed
|
|
1734
|
+
if (targetDim > 0 && embeddings.length > 0 && embeddings[0].length !== targetDim) {
|
|
1735
|
+
return embeddings.map(emb => this.scaleEmbedding(emb, targetDim));
|
|
1736
|
+
}
|
|
1737
|
+
return embeddings;
|
|
1738
|
+
}
|
|
1739
|
+
catch (error) {
|
|
1740
|
+
logger.warn({ error, attempt }, 'Batch embedding failed - will retry');
|
|
1741
|
+
// FIX: Use version tracking to prevent thundering herd restarts
|
|
1742
|
+
const failureVersion = ++this.sandboxFailureVersion;
|
|
1743
|
+
this.sandboxAvailable = false;
|
|
1744
|
+
if (attempt < MAX_RETRIES) {
|
|
1745
|
+
// Only restart if we're first to detect failure
|
|
1746
|
+
if (!this.restartInProgress && failureVersion === this.sandboxFailureVersion) {
|
|
1747
|
+
this.restartInProgress = true;
|
|
1748
|
+
this.tryRestartContainer();
|
|
1749
|
+
this.restartInProgress = false;
|
|
1750
|
+
}
|
|
1751
|
+
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS * attempt));
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
// Fallback: sequential calls if batch fails
|
|
1756
|
+
logger.warn({ batchSize: texts.length }, 'Batch embedding failed, falling back to sequential');
|
|
1757
|
+
const results = [];
|
|
1758
|
+
for (const text of texts) {
|
|
1759
|
+
results.push(await this.generateEmbedding(text));
|
|
1760
|
+
}
|
|
1761
|
+
return results;
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Generate batch embeddings using direct socket connection
|
|
1766
|
+
* Uses {"texts": [...]} protocol for single round-trip
|
|
1767
|
+
*/
|
|
1768
|
+
generateBatchWithDirectSocket(texts) {
|
|
1769
|
+
return new Promise((resolve, reject) => {
|
|
1770
|
+
const socketPath = getEmbeddingSocketPath();
|
|
1771
|
+
const socket = createConnection(socketPath);
|
|
1772
|
+
let buffer = '';
|
|
1773
|
+
let resolved = false;
|
|
1774
|
+
const startTime = Date.now();
|
|
1775
|
+
// Longer timeout for batches - scale with batch size
|
|
1776
|
+
const baseTimeout = this.getAdaptiveTimeout();
|
|
1777
|
+
const timeoutMs = Math.min(baseTimeout * Math.ceil(texts.length / 10), 300000); // Max 5 min
|
|
1778
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'BATCH_SOCKET_CONNECTING', {
|
|
1779
|
+
socketPath,
|
|
1780
|
+
batchSize: texts.length,
|
|
1781
|
+
timeoutMs
|
|
1782
|
+
});
|
|
1783
|
+
let timeout = setTimeout(() => {
|
|
1784
|
+
if (!resolved) {
|
|
1785
|
+
resolved = true;
|
|
1786
|
+
socket.destroy();
|
|
1787
|
+
reject(new Error(`Batch embedding timeout after ${Math.round(timeoutMs / 1000)}s for ${texts.length} texts`));
|
|
1788
|
+
}
|
|
1789
|
+
}, timeoutMs);
|
|
1790
|
+
socket.on('connect', () => {
|
|
1791
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'BATCH_SOCKET_CONNECTED', {
|
|
1792
|
+
batchSize: texts.length,
|
|
1793
|
+
connectTimeMs: Date.now() - startTime
|
|
1794
|
+
});
|
|
1795
|
+
// Use batch protocol: {"texts": [...]}
|
|
1796
|
+
const request = JSON.stringify({ texts }) + '\n';
|
|
1797
|
+
socket.write(request);
|
|
1798
|
+
});
|
|
1799
|
+
socket.on('data', (data) => {
|
|
1800
|
+
buffer += data.toString();
|
|
1801
|
+
// Reset timeout on data received
|
|
1802
|
+
clearTimeout(timeout);
|
|
1803
|
+
timeout = setTimeout(() => {
|
|
1804
|
+
if (!resolved) {
|
|
1805
|
+
resolved = true;
|
|
1806
|
+
socket.destroy();
|
|
1807
|
+
reject(new Error(`Batch embedding idle timeout for ${texts.length} texts`));
|
|
1808
|
+
}
|
|
1809
|
+
}, timeoutMs);
|
|
1810
|
+
// Process all complete JSON messages (newline-delimited)
|
|
1811
|
+
// Server sends heartbeat first: {"status":"processing",...}
|
|
1812
|
+
// Then actual response: {"embeddings":[...],...}
|
|
1813
|
+
let newlineIndex;
|
|
1814
|
+
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
1815
|
+
if (resolved)
|
|
1816
|
+
return;
|
|
1817
|
+
const responseJson = buffer.slice(0, newlineIndex);
|
|
1818
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
1819
|
+
try {
|
|
1820
|
+
const response = JSON.parse(responseJson);
|
|
1821
|
+
// Skip heartbeat/processing status - keep waiting
|
|
1822
|
+
if (response.status === 'processing') {
|
|
1823
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'BATCH_SOCKET_HEARTBEAT', {
|
|
1824
|
+
batchSize: texts.length,
|
|
1825
|
+
count: response.count,
|
|
1826
|
+
elapsedMs: Date.now() - startTime
|
|
1827
|
+
});
|
|
1828
|
+
continue; // Keep waiting for actual embeddings
|
|
1829
|
+
}
|
|
1830
|
+
// Got actual response - resolve or reject
|
|
1831
|
+
clearTimeout(timeout);
|
|
1832
|
+
resolved = true;
|
|
1833
|
+
socket.destroy();
|
|
1834
|
+
const responseTime = Date.now() - startTime;
|
|
1835
|
+
this.recordResponseTime(responseTime / texts.length); // Per-text average
|
|
1836
|
+
if (response.error) {
|
|
1837
|
+
reject(new Error(`Batch embedding error: ${response.error}`));
|
|
1838
|
+
}
|
|
1839
|
+
else if (response.embeddings && Array.isArray(response.embeddings)) {
|
|
1840
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'BATCH_SOCKET_SUCCESS', {
|
|
1841
|
+
batchSize: texts.length,
|
|
1842
|
+
embeddingsCount: response.embeddings.length,
|
|
1843
|
+
totalMs: responseTime,
|
|
1844
|
+
msPerText: Math.round(responseTime / texts.length)
|
|
1845
|
+
});
|
|
1846
|
+
resolve(response.embeddings);
|
|
1847
|
+
}
|
|
1848
|
+
else {
|
|
1849
|
+
reject(new Error('Invalid batch response from embedding service'));
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
catch (err) {
|
|
1853
|
+
clearTimeout(timeout);
|
|
1854
|
+
resolved = true;
|
|
1855
|
+
socket.destroy();
|
|
1856
|
+
reject(new Error(`Failed to parse batch embedding response: ${err}`));
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
socket.on('error', (err) => {
|
|
1861
|
+
if (!resolved) {
|
|
1862
|
+
resolved = true;
|
|
1863
|
+
clearTimeout(timeout);
|
|
1864
|
+
reject(err);
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
socket.on('close', () => {
|
|
1868
|
+
if (!resolved) {
|
|
1869
|
+
resolved = true;
|
|
1870
|
+
clearTimeout(timeout);
|
|
1871
|
+
reject(new Error('Batch socket closed unexpectedly'));
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Scale embedding to target dimension (up or down)
|
|
1878
|
+
* Uses interpolation for downscaling, pattern repetition for upscaling
|
|
1879
|
+
*/
|
|
1880
|
+
scaleEmbedding(embedding, targetDim) {
|
|
1881
|
+
const srcDim = embedding.length;
|
|
1882
|
+
if (srcDim === targetDim)
|
|
1883
|
+
return embedding;
|
|
1884
|
+
const result = new Array(targetDim);
|
|
1885
|
+
if (targetDim < srcDim) {
|
|
1886
|
+
// DOWNSCALE: Average neighboring values
|
|
1887
|
+
const ratio = srcDim / targetDim;
|
|
1888
|
+
for (let i = 0; i < targetDim; i++) {
|
|
1889
|
+
const start = Math.floor(i * ratio);
|
|
1890
|
+
const end = Math.floor((i + 1) * ratio);
|
|
1891
|
+
let sum = 0;
|
|
1892
|
+
for (let j = start; j < end; j++) {
|
|
1893
|
+
sum += embedding[j];
|
|
1894
|
+
}
|
|
1895
|
+
result[i] = sum / (end - start);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
else {
|
|
1899
|
+
// UPSCALE: Linear interpolation
|
|
1900
|
+
const ratio = (srcDim - 1) / (targetDim - 1);
|
|
1901
|
+
for (let i = 0; i < targetDim; i++) {
|
|
1902
|
+
const srcIdx = i * ratio;
|
|
1903
|
+
const low = Math.floor(srcIdx);
|
|
1904
|
+
const high = Math.min(low + 1, srcDim - 1);
|
|
1905
|
+
const frac = srcIdx - low;
|
|
1906
|
+
result[i] = embedding[low] * (1 - frac) + embedding[high] * frac;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
// Normalize to unit length (important for cosine similarity!)
|
|
1910
|
+
const norm = Math.sqrt(result.reduce((sum, v) => sum + v * v, 0));
|
|
1911
|
+
if (norm > 0) {
|
|
1912
|
+
for (let i = 0; i < targetDim; i++) {
|
|
1913
|
+
result[i] /= norm;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
return result;
|
|
1917
|
+
}
|
|
1918
|
+
async generateSandboxedEmbedding(text) {
|
|
1919
|
+
const methodStart = Date.now();
|
|
1920
|
+
// Get fresh socket path
|
|
1921
|
+
const freshSocketPath = getEmbeddingSocketPath();
|
|
1922
|
+
this.sandboxSocketPath = freshSocketPath;
|
|
1923
|
+
// WARM SOCKET APPROACH: Try warm socket first, fall back to direct if fails
|
|
1924
|
+
// This gives us fast starts (~50ms) when warm, with reliable fallback (~500ms) when cold
|
|
1925
|
+
// FIX: Use lock to prevent race condition where two requests use warm socket,
|
|
1926
|
+
// one fails and destroys it while the other is mid-request
|
|
1927
|
+
// Try warm socket if available and path matches
|
|
1928
|
+
if (this.warmSocketReady && this.warmSocket && this.warmSocketPath === freshSocketPath) {
|
|
1929
|
+
// Acquire lock - wait for previous warm socket operation to complete
|
|
1930
|
+
let releaseLock = () => { };
|
|
1931
|
+
const lockPromise = new Promise(resolve => { releaseLock = resolve; });
|
|
1932
|
+
const previousLock = this.warmSocketLock;
|
|
1933
|
+
this.warmSocketLock = lockPromise;
|
|
1934
|
+
try {
|
|
1935
|
+
await previousLock; // Wait for previous operation
|
|
1936
|
+
// Re-check conditions after acquiring lock (may have changed)
|
|
1937
|
+
if (!this.warmSocketReady || !this.warmSocket || this.warmSocketPath !== freshSocketPath) {
|
|
1938
|
+
releaseLock();
|
|
1939
|
+
// Fall through to direct socket
|
|
1940
|
+
}
|
|
1941
|
+
else {
|
|
1942
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'WARM_SOCKET_ATTEMPT', {
|
|
1943
|
+
textLength: text.length,
|
|
1944
|
+
socketPath: freshSocketPath,
|
|
1945
|
+
idleMs: Date.now() - this.warmSocketLastUsed
|
|
1946
|
+
});
|
|
1947
|
+
const result = await this.generateWithWarmSocket(text);
|
|
1948
|
+
this.warmSocketLastUsed = Date.now();
|
|
1949
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'WARM_SOCKET_SUCCESS', {
|
|
1950
|
+
totalMs: Date.now() - methodStart,
|
|
1951
|
+
resultDim: result.length
|
|
1952
|
+
});
|
|
1953
|
+
releaseLock();
|
|
1954
|
+
return result;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
catch (err) {
|
|
1958
|
+
// Warm socket failed - close it and fall back to direct
|
|
1959
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'WARM_SOCKET_FAILED', {
|
|
1960
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1961
|
+
fallingBackToDirect: true
|
|
1962
|
+
});
|
|
1963
|
+
this.closeWarmSocket();
|
|
1964
|
+
releaseLock();
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
// Direct connection (cold start or fallback)
|
|
1968
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_START', {
|
|
1969
|
+
textLength: text.length,
|
|
1970
|
+
socketPath: freshSocketPath,
|
|
1971
|
+
reason: !this.warmSocketReady ? 'no warm socket' : 'path mismatch or failed'
|
|
1972
|
+
});
|
|
1973
|
+
try {
|
|
1974
|
+
const result = await this.generateWithDirectSocket(text, freshSocketPath);
|
|
1975
|
+
// Warm up socket in background for next call
|
|
1976
|
+
this.warmUpSocketInBackground(freshSocketPath);
|
|
1977
|
+
return result;
|
|
1978
|
+
}
|
|
1979
|
+
catch (directSocketError) {
|
|
1980
|
+
// Step 3: Direct socket failed - TRY ON-DEMAND DOCKER UNPAUSE
|
|
1981
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_FAILED', {
|
|
1982
|
+
error: directSocketError instanceof Error ? directSocketError.message : String(directSocketError),
|
|
1983
|
+
textLength: text.length,
|
|
1984
|
+
tryingOnDemandUnpause: true
|
|
1985
|
+
});
|
|
1986
|
+
// ON-DEMAND UNPAUSE: Docker stays paused until we need it
|
|
1987
|
+
const wasUnpaused = await this.unpauseDockerIfNeeded();
|
|
1988
|
+
if (wasUnpaused) {
|
|
1989
|
+
// Docker was paused and is now unpaused - retry the direct socket
|
|
1990
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'RETRY_AFTER_UNPAUSE', {
|
|
1991
|
+
textLength: text.length
|
|
1992
|
+
});
|
|
1993
|
+
try {
|
|
1994
|
+
const result = await this.generateWithDirectSocket(text, freshSocketPath);
|
|
1995
|
+
// Warm up socket in background for next call
|
|
1996
|
+
this.warmUpSocketInBackground(freshSocketPath);
|
|
1997
|
+
return result;
|
|
1998
|
+
}
|
|
1999
|
+
catch (retryError) {
|
|
2000
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'RETRY_AFTER_UNPAUSE_FAILED', {
|
|
2001
|
+
error: retryError instanceof Error ? retryError.message : String(retryError)
|
|
2002
|
+
});
|
|
2003
|
+
// Fall through to Python restart
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
// Step 4: Try Python auto-start (for non-Docker environments or dead sockets)
|
|
2007
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'TRYING_PYTHON_AUTOSTART', {
|
|
2008
|
+
textLength: text.length,
|
|
2009
|
+
wasUnpaused
|
|
2010
|
+
});
|
|
2011
|
+
const pythonStarted = await this.autoStartPythonEmbedding();
|
|
2012
|
+
if (pythonStarted) {
|
|
2013
|
+
// Python server started - retry with new socket
|
|
2014
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'RETRY_AFTER_PYTHON_START', {
|
|
2015
|
+
textLength: text.length,
|
|
2016
|
+
newSocketPath: this.sandboxSocketPath
|
|
2017
|
+
});
|
|
2018
|
+
try {
|
|
2019
|
+
const result = await this.generateWithDirectSocket(text, this.sandboxSocketPath);
|
|
2020
|
+
this.warmUpSocketInBackground(this.sandboxSocketPath);
|
|
2021
|
+
return result;
|
|
2022
|
+
}
|
|
2023
|
+
catch (retryError) {
|
|
2024
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'RETRY_AFTER_PYTHON_START_FAILED', {
|
|
2025
|
+
error: retryError instanceof Error ? retryError.message : String(retryError)
|
|
2026
|
+
});
|
|
2027
|
+
// Fall through to queue
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
// Step 5: Still failed - queue the request for later processing
|
|
2031
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'QUEUEING_REQUEST', {
|
|
2032
|
+
textLength: text.length,
|
|
2033
|
+
wasUnpaused
|
|
2034
|
+
});
|
|
2035
|
+
// Queue the request and return the promise that will resolve when socket warms up
|
|
2036
|
+
if (this.embeddingQueue) {
|
|
2037
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'QUEUING_TO_POSTGRES', {
|
|
2038
|
+
textLength: text.length
|
|
2039
|
+
});
|
|
2040
|
+
// Also trigger warm-up in background (which will drain queue when ready)
|
|
2041
|
+
this.warmUpSocketInBackground(freshSocketPath);
|
|
2042
|
+
return this.embeddingQueue.queueForEmbedding(text);
|
|
2043
|
+
}
|
|
2044
|
+
else {
|
|
2045
|
+
// No queue available - re-throw the original error
|
|
2046
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'NO_QUEUE_AVAILABLE', {
|
|
2047
|
+
error: 'EmbeddingQueue not initialized, cannot queue request'
|
|
2048
|
+
});
|
|
2049
|
+
throw directSocketError;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Generate embedding using the WARM socket (fast path ~50ms)
|
|
2055
|
+
* Simple timeout-based approach - no complex state machine
|
|
2056
|
+
*/
|
|
2057
|
+
generateWithWarmSocket(text) {
|
|
2058
|
+
return new Promise((resolve, reject) => {
|
|
2059
|
+
if (!this.warmSocket || !this.warmSocketReady) {
|
|
2060
|
+
reject(new Error('Warm socket not ready'));
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
const startTime = Date.now();
|
|
2064
|
+
const timeoutMs = this.getAdaptiveTimeout();
|
|
2065
|
+
let buffer = '';
|
|
2066
|
+
let resolved = false;
|
|
2067
|
+
const timeout = setTimeout(() => {
|
|
2068
|
+
if (!resolved) {
|
|
2069
|
+
resolved = true;
|
|
2070
|
+
reject(new Error(`Warm socket timeout after ${timeoutMs}ms`));
|
|
2071
|
+
}
|
|
2072
|
+
}, timeoutMs);
|
|
2073
|
+
const dataHandler = (data) => {
|
|
2074
|
+
buffer += data.toString();
|
|
2075
|
+
// Process all complete JSON lines in buffer
|
|
2076
|
+
let newlineIndex;
|
|
2077
|
+
while ((newlineIndex = buffer.indexOf('\n')) !== -1 && !resolved) {
|
|
2078
|
+
const line = buffer.slice(0, newlineIndex);
|
|
2079
|
+
buffer = buffer.slice(newlineIndex + 1); // Keep remainder for next line
|
|
2080
|
+
try {
|
|
2081
|
+
const response = JSON.parse(line);
|
|
2082
|
+
// HEARTBEAT: "processing" status means server is working - reset timeout and keep waiting
|
|
2083
|
+
if (response.status === 'processing') {
|
|
2084
|
+
clearTimeout(timeout);
|
|
2085
|
+
timeout = setTimeout(() => {
|
|
2086
|
+
if (!resolved) {
|
|
2087
|
+
resolved = true;
|
|
2088
|
+
reject(new Error(`Warm socket timeout after ${timeoutMs}ms`));
|
|
2089
|
+
}
|
|
2090
|
+
}, timeoutMs);
|
|
2091
|
+
continue; // Keep reading for actual embedding
|
|
2092
|
+
}
|
|
2093
|
+
// Got actual response - resolve
|
|
2094
|
+
clearTimeout(timeout);
|
|
2095
|
+
resolved = true;
|
|
2096
|
+
this.warmSocket?.removeListener('data', dataHandler);
|
|
2097
|
+
const responseTime = Date.now() - startTime;
|
|
2098
|
+
this.recordResponseTime(responseTime);
|
|
2099
|
+
if (response.error) {
|
|
2100
|
+
reject(new Error(`Embedding service error: ${response.error}`));
|
|
2101
|
+
}
|
|
2102
|
+
else if (response.embedding) {
|
|
2103
|
+
resolve(response.embedding);
|
|
2104
|
+
}
|
|
2105
|
+
else {
|
|
2106
|
+
reject(new Error('Invalid response from embedding service'));
|
|
2107
|
+
}
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
catch (err) {
|
|
2111
|
+
clearTimeout(timeout);
|
|
2112
|
+
resolved = true;
|
|
2113
|
+
this.warmSocket?.removeListener('data', dataHandler);
|
|
2114
|
+
reject(new Error(`Failed to parse embedding response: ${err}`));
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
this.warmSocket.on('data', dataHandler);
|
|
2120
|
+
// Send request
|
|
2121
|
+
const request = JSON.stringify({ type: 'embed', text }) + '\n';
|
|
2122
|
+
this.warmSocket.write(request);
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Close the warm socket cleanly
|
|
2127
|
+
*/
|
|
2128
|
+
closeWarmSocket() {
|
|
2129
|
+
if (this.warmSocket) {
|
|
2130
|
+
try {
|
|
2131
|
+
this.warmSocket.removeAllListeners();
|
|
2132
|
+
this.warmSocket.destroy();
|
|
2133
|
+
}
|
|
2134
|
+
catch (e) {
|
|
2135
|
+
// Ignore errors during cleanup
|
|
2136
|
+
}
|
|
2137
|
+
this.warmSocket = null;
|
|
2138
|
+
}
|
|
2139
|
+
this.warmSocketReady = false;
|
|
2140
|
+
this.warmSocketPath = null;
|
|
2141
|
+
if (this.warmSocketHealthInterval) {
|
|
2142
|
+
clearInterval(this.warmSocketHealthInterval);
|
|
2143
|
+
this.warmSocketHealthInterval = null;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Warm up a socket in the background for faster subsequent calls
|
|
2148
|
+
* Non-blocking - doesn't affect current request
|
|
2149
|
+
*/
|
|
2150
|
+
warmUpSocketInBackground(socketPath) {
|
|
2151
|
+
// Don't warm up if already warming or path matches
|
|
2152
|
+
if (this.warmSocketReady && this.warmSocketPath === socketPath) {
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
// Close existing warm socket if path changed
|
|
2156
|
+
if (this.warmSocketPath && this.warmSocketPath !== socketPath) {
|
|
2157
|
+
this.closeWarmSocket();
|
|
2158
|
+
}
|
|
2159
|
+
// Create new warm socket in background
|
|
2160
|
+
setImmediate(() => {
|
|
2161
|
+
try {
|
|
2162
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'WARMING_SOCKET', { socketPath });
|
|
2163
|
+
this.warmSocket = createConnection(socketPath);
|
|
2164
|
+
this.warmSocketPath = socketPath;
|
|
2165
|
+
this.warmSocket.on('connect', () => {
|
|
2166
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'WARM_SOCKET_CONNECTED', { socketPath });
|
|
2167
|
+
// QUEUE DRAIN: Process pending embeddings BEFORE marking socket ready
|
|
2168
|
+
// This ensures queued requests get resolved before new requests come in
|
|
2169
|
+
this.drainEmbeddingQueue(socketPath).then((drained) => {
|
|
2170
|
+
if (drained > 0) {
|
|
2171
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'QUEUE_DRAINED', {
|
|
2172
|
+
socketPath,
|
|
2173
|
+
itemsProcessed: drained
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
// NOW mark socket ready for new requests
|
|
2177
|
+
this.warmSocketReady = true;
|
|
2178
|
+
this.warmSocketLastUsed = Date.now();
|
|
2179
|
+
// Start health check interval
|
|
2180
|
+
this.startWarmSocketHealthCheck();
|
|
2181
|
+
}).catch((err) => {
|
|
2182
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'QUEUE_DRAIN_ERROR', {
|
|
2183
|
+
socketPath,
|
|
2184
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2185
|
+
});
|
|
2186
|
+
// Still mark socket ready even if drain fails - new requests should work
|
|
2187
|
+
this.warmSocketReady = true;
|
|
2188
|
+
this.warmSocketLastUsed = Date.now();
|
|
2189
|
+
this.startWarmSocketHealthCheck();
|
|
2190
|
+
});
|
|
2191
|
+
});
|
|
2192
|
+
this.warmSocket.on('error', (err) => {
|
|
2193
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'WARM_SOCKET_ERROR', {
|
|
2194
|
+
socketPath,
|
|
2195
|
+
error: err.message
|
|
2196
|
+
});
|
|
2197
|
+
this.closeWarmSocket();
|
|
2198
|
+
});
|
|
2199
|
+
this.warmSocket.on('close', () => {
|
|
2200
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'WARM_SOCKET_CLOSED', { socketPath });
|
|
2201
|
+
this.warmSocketReady = false;
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
catch (err) {
|
|
2205
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'WARM_SOCKET_CREATE_ERROR', {
|
|
2206
|
+
socketPath,
|
|
2207
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
// On-demand Docker unpause tracking
|
|
2213
|
+
lastDockerUnpauseAttempt = 0;
|
|
2214
|
+
static DOCKER_UNPAUSE_COOLDOWN_MS = 5000; // Don't spam unpause attempts
|
|
2215
|
+
/**
|
|
2216
|
+
* Check if the Docker container is paused
|
|
2217
|
+
* Uses docker inspect to check container state
|
|
2218
|
+
*/
|
|
2219
|
+
async isDockerPaused() {
|
|
2220
|
+
try {
|
|
2221
|
+
const containerName = LocalEmbeddingProvider.CONTAINER_NAME;
|
|
2222
|
+
const { execSync } = await import('child_process');
|
|
2223
|
+
const result = execSync(`docker inspect -f "{{.State.Paused}}" ${containerName} 2>/dev/null`, {
|
|
2224
|
+
encoding: 'utf-8',
|
|
2225
|
+
timeout: 5000
|
|
2226
|
+
}).trim();
|
|
2227
|
+
return result === 'true';
|
|
2228
|
+
}
|
|
2229
|
+
catch (err) {
|
|
2230
|
+
// Container doesn't exist or docker not available - not paused (different issue)
|
|
2231
|
+
return false;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
/**
|
|
2235
|
+
* Unpause the Docker container if it's paused
|
|
2236
|
+
* ON-DEMAND activation - Docker stays paused until we need embeddings
|
|
2237
|
+
*/
|
|
2238
|
+
async unpauseDockerIfNeeded() {
|
|
2239
|
+
// Cooldown to prevent spamming unpause
|
|
2240
|
+
const now = Date.now();
|
|
2241
|
+
if (now - this.lastDockerUnpauseAttempt < LocalEmbeddingProvider.DOCKER_UNPAUSE_COOLDOWN_MS) {
|
|
2242
|
+
return false;
|
|
2243
|
+
}
|
|
2244
|
+
this.lastDockerUnpauseAttempt = now;
|
|
2245
|
+
try {
|
|
2246
|
+
const isPaused = await this.isDockerPaused();
|
|
2247
|
+
if (!isPaused) {
|
|
2248
|
+
return false; // Already running
|
|
2249
|
+
}
|
|
2250
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DOCKER_ON_DEMAND_UNPAUSE', {
|
|
2251
|
+
containerName: LocalEmbeddingProvider.CONTAINER_NAME,
|
|
2252
|
+
reason: 'embedding request received'
|
|
2253
|
+
});
|
|
2254
|
+
const containerName = LocalEmbeddingProvider.CONTAINER_NAME;
|
|
2255
|
+
const { execSync } = await import('child_process');
|
|
2256
|
+
execSync(`docker unpause ${containerName}`, { timeout: 10000 });
|
|
2257
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DOCKER_UNPAUSED_SUCCESS', {
|
|
2258
|
+
containerName
|
|
2259
|
+
});
|
|
2260
|
+
// Wait briefly for socket to be ready
|
|
2261
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
2262
|
+
return true;
|
|
2263
|
+
}
|
|
2264
|
+
catch (err) {
|
|
2265
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DOCKER_UNPAUSE_FAILED', {
|
|
2266
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2267
|
+
});
|
|
2268
|
+
return false;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
/**
|
|
2272
|
+
* Drain the embedding queue when socket becomes available
|
|
2273
|
+
* Uses generateWithDirectSocket for the actual embedding generation
|
|
2274
|
+
*
|
|
2275
|
+
* @param socketPath - The socket path to use for embedding generation
|
|
2276
|
+
* @returns Number of items drained from queue
|
|
2277
|
+
*/
|
|
2278
|
+
async drainEmbeddingQueue(socketPath) {
|
|
2279
|
+
if (!this.embeddingQueue) {
|
|
2280
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DRAIN_SKIP_NO_QUEUE', {
|
|
2281
|
+
reason: 'embeddingQueue not initialized'
|
|
2282
|
+
});
|
|
2283
|
+
return 0;
|
|
2284
|
+
}
|
|
2285
|
+
try {
|
|
2286
|
+
// Check if there are pending items first
|
|
2287
|
+
const pendingCount = await this.embeddingQueue.getPendingCount();
|
|
2288
|
+
if (pendingCount === 0) {
|
|
2289
|
+
return 0;
|
|
2290
|
+
}
|
|
2291
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DRAINING_QUEUE', {
|
|
2292
|
+
socketPath,
|
|
2293
|
+
pendingCount
|
|
2294
|
+
});
|
|
2295
|
+
// Drain the queue using generateWithDirectSocket for actual embedding
|
|
2296
|
+
const drained = await this.embeddingQueue.drainQueue((text) => this.generateWithDirectSocket(text, socketPath));
|
|
2297
|
+
return drained;
|
|
2298
|
+
}
|
|
2299
|
+
catch (err) {
|
|
2300
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DRAIN_ERROR', {
|
|
2301
|
+
socketPath,
|
|
2302
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2303
|
+
});
|
|
2304
|
+
throw err;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Start periodic health check for warm socket
|
|
2309
|
+
* Closes socket if idle too long or unhealthy
|
|
2310
|
+
*/
|
|
2311
|
+
startWarmSocketHealthCheck() {
|
|
2312
|
+
if (this.warmSocketHealthInterval) {
|
|
2313
|
+
clearInterval(this.warmSocketHealthInterval);
|
|
2314
|
+
}
|
|
2315
|
+
this.warmSocketHealthInterval = setInterval(() => {
|
|
2316
|
+
// Close if idle too long
|
|
2317
|
+
const idleTime = Date.now() - this.warmSocketLastUsed;
|
|
2318
|
+
if (idleTime > LocalEmbeddingProvider.WARM_SOCKET_IDLE_TIMEOUT_MS) {
|
|
2319
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'WARM_SOCKET_IDLE_CLOSE', {
|
|
2320
|
+
idleMs: idleTime,
|
|
2321
|
+
maxIdleMs: LocalEmbeddingProvider.WARM_SOCKET_IDLE_TIMEOUT_MS
|
|
2322
|
+
});
|
|
2323
|
+
this.closeWarmSocket();
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
// Check if socket is still alive
|
|
2327
|
+
if (this.warmSocket && !this.warmSocket.destroyed) {
|
|
2328
|
+
// Socket looks healthy
|
|
2329
|
+
}
|
|
2330
|
+
else {
|
|
2331
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'WARM_SOCKET_UNHEALTHY', {
|
|
2332
|
+
destroyed: this.warmSocket?.destroyed
|
|
2333
|
+
});
|
|
2334
|
+
this.closeWarmSocket();
|
|
2335
|
+
}
|
|
2336
|
+
}, LocalEmbeddingProvider.WARM_SOCKET_HEALTH_CHECK_MS);
|
|
2337
|
+
}
|
|
2338
|
+
/**
|
|
2339
|
+
* NUCLEAR FIX: Generate embedding using a DIRECT socket connection
|
|
2340
|
+
* Bypasses the broken persistent socket state machine entirely.
|
|
2341
|
+
* Creates a fresh connection for each call - no state, no caching, no bugs.
|
|
2342
|
+
* Takes socket path as parameter to ensure we ALWAYS use the fresh path.
|
|
2343
|
+
*/
|
|
2344
|
+
async generateWithDirectSocket(text, socketPath) {
|
|
2345
|
+
let lastError = null;
|
|
2346
|
+
for (let attempt = 1; attempt <= LocalEmbeddingProvider.SOCKET_MAX_RETRIES; attempt++) {
|
|
2347
|
+
try {
|
|
2348
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_ATTEMPT', {
|
|
2349
|
+
attempt,
|
|
2350
|
+
socketPath,
|
|
2351
|
+
maxRetries: LocalEmbeddingProvider.SOCKET_MAX_RETRIES
|
|
2352
|
+
});
|
|
2353
|
+
return await this.generateWithDirectSocketAttempt(text, socketPath, attempt);
|
|
2354
|
+
}
|
|
2355
|
+
catch (err) {
|
|
2356
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2357
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_ATTEMPT_ERROR', {
|
|
2358
|
+
attempt,
|
|
2359
|
+
error: lastError.message,
|
|
2360
|
+
socketPath
|
|
2361
|
+
});
|
|
2362
|
+
// Check if error is retryable (timeout or transient socket error)
|
|
2363
|
+
const isRetryable = lastError.message.includes('timeout') ||
|
|
2364
|
+
lastError.message.includes('ECONNRESET') ||
|
|
2365
|
+
lastError.message.includes('ECONNREFUSED') ||
|
|
2366
|
+
lastError.message.includes('EPIPE') ||
|
|
2367
|
+
lastError.message.includes('ENOENT');
|
|
2368
|
+
if (!isRetryable || attempt >= LocalEmbeddingProvider.SOCKET_MAX_RETRIES) {
|
|
2369
|
+
break;
|
|
2370
|
+
}
|
|
2371
|
+
// Exponential backoff with jitter
|
|
2372
|
+
const baseDelay = LocalEmbeddingProvider.SOCKET_INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
|
|
2373
|
+
const jitter = Math.random() * 0.3 * baseDelay; // 0-30% jitter
|
|
2374
|
+
const delay = Math.min(baseDelay + jitter, LocalEmbeddingProvider.SOCKET_MAX_RETRY_DELAY_MS);
|
|
2375
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_BACKOFF', {
|
|
2376
|
+
attempt,
|
|
2377
|
+
delayMs: Math.round(delay),
|
|
2378
|
+
nextAttempt: attempt + 1
|
|
2379
|
+
});
|
|
2380
|
+
logger.warn({
|
|
2381
|
+
attempt,
|
|
2382
|
+
maxRetries: LocalEmbeddingProvider.SOCKET_MAX_RETRIES,
|
|
2383
|
+
error: lastError.message,
|
|
2384
|
+
retryDelayMs: Math.round(delay),
|
|
2385
|
+
socketPath
|
|
2386
|
+
}, 'Direct socket embedding failed, retrying with exponential backoff');
|
|
2387
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
// All retries exhausted
|
|
2391
|
+
throw new Error(`Embedding generation failed after ${LocalEmbeddingProvider.SOCKET_MAX_RETRIES} attempts. ` +
|
|
2392
|
+
`Socket: ${socketPath}. ` +
|
|
2393
|
+
`Last error: ${lastError?.message || 'unknown'}. ` +
|
|
2394
|
+
`Check if Frankenstein embedding service is running.`);
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* Single attempt to generate embedding via DIRECT socket connection
|
|
2398
|
+
* Uses the passed socketPath, not cached state
|
|
2399
|
+
*/
|
|
2400
|
+
generateWithDirectSocketAttempt(text, socketPath, attempt) {
|
|
2401
|
+
return new Promise((resolve, reject) => {
|
|
2402
|
+
const socket = createConnection(socketPath);
|
|
2403
|
+
let buffer = '';
|
|
2404
|
+
let resolved = false;
|
|
2405
|
+
const startTime = Date.now();
|
|
2406
|
+
const timeoutMs = this.getAdaptiveTimeout();
|
|
2407
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_CONNECTING', {
|
|
2408
|
+
socketPath,
|
|
2409
|
+
attempt,
|
|
2410
|
+
timeoutMs
|
|
2411
|
+
});
|
|
2412
|
+
// IDLE-BASED TIMEOUT: Timer resets on any data received from socket
|
|
2413
|
+
// This allows long-running embeddings as long as server sends heartbeats
|
|
2414
|
+
let timeout = setTimeout(() => {
|
|
2415
|
+
if (!resolved) {
|
|
2416
|
+
resolved = true;
|
|
2417
|
+
socket.destroy();
|
|
2418
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_INITIAL_TIMEOUT', {
|
|
2419
|
+
socketPath,
|
|
2420
|
+
attempt,
|
|
2421
|
+
timeoutMs
|
|
2422
|
+
});
|
|
2423
|
+
reject(new Error(`Embedding idle timeout after ${Math.round(timeoutMs / 1000)}s of no activity ` +
|
|
2424
|
+
`(attempt ${attempt}/${LocalEmbeddingProvider.SOCKET_MAX_RETRIES}). ` +
|
|
2425
|
+
`Socket: ${socketPath}. ` +
|
|
2426
|
+
`If model is slow, increase SPECMEM_EMBEDDING_TIMEOUT.`));
|
|
2427
|
+
}
|
|
2428
|
+
}, timeoutMs);
|
|
2429
|
+
socket.on('connect', () => {
|
|
2430
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_CONNECTED', {
|
|
2431
|
+
socketPath,
|
|
2432
|
+
attempt,
|
|
2433
|
+
connectTimeMs: Date.now() - startTime
|
|
2434
|
+
});
|
|
2435
|
+
const request = JSON.stringify({ type: 'embed', text }) + '\n';
|
|
2436
|
+
socket.write(request);
|
|
2437
|
+
});
|
|
2438
|
+
socket.on('data', (data) => {
|
|
2439
|
+
buffer += data.toString();
|
|
2440
|
+
// IDLE-BASED TIMEOUT: Reset timer on ANY data received
|
|
2441
|
+
// This means if the server is actively sending, we keep waiting
|
|
2442
|
+
clearTimeout(timeout);
|
|
2443
|
+
timeout = setTimeout(() => {
|
|
2444
|
+
if (!resolved) {
|
|
2445
|
+
resolved = true;
|
|
2446
|
+
socket.destroy();
|
|
2447
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_IDLE_TIMEOUT', {
|
|
2448
|
+
socketPath,
|
|
2449
|
+
attempt,
|
|
2450
|
+
timeoutMs
|
|
2451
|
+
});
|
|
2452
|
+
reject(new Error(`Embedding idle timeout after ${Math.round(timeoutMs / 1000)}s of no activity ` +
|
|
2453
|
+
`(attempt ${attempt}/${LocalEmbeddingProvider.SOCKET_MAX_RETRIES}). ` +
|
|
2454
|
+
`Socket: ${socketPath}. ` +
|
|
2455
|
+
`If model is slow, increase SPECMEM_EMBEDDING_TIMEOUT.`));
|
|
2456
|
+
}
|
|
2457
|
+
}, timeoutMs);
|
|
2458
|
+
// Process complete JSON messages (newline-delimited)
|
|
2459
|
+
let newlineIndex;
|
|
2460
|
+
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
2461
|
+
if (resolved)
|
|
2462
|
+
return;
|
|
2463
|
+
const responseJson = buffer.slice(0, newlineIndex);
|
|
2464
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
2465
|
+
try {
|
|
2466
|
+
const response = JSON.parse(responseJson);
|
|
2467
|
+
// Handle heartbeat/processing status - just keep waiting
|
|
2468
|
+
if (response.status === 'processing') {
|
|
2469
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_HEARTBEAT', {
|
|
2470
|
+
socketPath,
|
|
2471
|
+
attempt,
|
|
2472
|
+
textLength: response.text_length,
|
|
2473
|
+
elapsedMs: Date.now() - startTime
|
|
2474
|
+
});
|
|
2475
|
+
continue; // Keep waiting for actual embedding
|
|
2476
|
+
}
|
|
2477
|
+
// Got actual response - resolve or reject
|
|
2478
|
+
clearTimeout(timeout);
|
|
2479
|
+
resolved = true;
|
|
2480
|
+
const responseTime = Date.now() - startTime;
|
|
2481
|
+
this.recordResponseTime(responseTime);
|
|
2482
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_RESPONSE', {
|
|
2483
|
+
socketPath,
|
|
2484
|
+
attempt,
|
|
2485
|
+
responseTimeMs: responseTime,
|
|
2486
|
+
bufferLength: buffer.length
|
|
2487
|
+
});
|
|
2488
|
+
if (response.error) {
|
|
2489
|
+
reject(new Error(`Embedding service error: ${response.error} (socket: ${socketPath})`));
|
|
2490
|
+
}
|
|
2491
|
+
else if (response.embedding) {
|
|
2492
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_SUCCESS', {
|
|
2493
|
+
socketPath,
|
|
2494
|
+
attempt,
|
|
2495
|
+
embeddingDim: response.embedding.length,
|
|
2496
|
+
totalTimeMs: Date.now() - startTime
|
|
2497
|
+
});
|
|
2498
|
+
resolve(response.embedding);
|
|
2499
|
+
}
|
|
2500
|
+
else {
|
|
2501
|
+
reject(new Error(`Invalid response from embedding service (socket: ${socketPath})`));
|
|
2502
|
+
}
|
|
2503
|
+
socket.end();
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
catch (err) {
|
|
2507
|
+
clearTimeout(timeout);
|
|
2508
|
+
resolved = true;
|
|
2509
|
+
reject(new Error(`Failed to parse embedding response: ${err instanceof Error ? err.message : err} (socket: ${socketPath})`));
|
|
2510
|
+
socket.end();
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
});
|
|
2515
|
+
socket.on('error', (err) => {
|
|
2516
|
+
clearTimeout(timeout);
|
|
2517
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_ERROR', {
|
|
2518
|
+
socketPath,
|
|
2519
|
+
attempt,
|
|
2520
|
+
error: err.message
|
|
2521
|
+
});
|
|
2522
|
+
if (!resolved) {
|
|
2523
|
+
resolved = true;
|
|
2524
|
+
reject(new Error(`Socket error: ${err.message} (socket: ${socketPath})`));
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
/**
|
|
2530
|
+
* Generate embedding using the PERSISTENT socket (fast path)
|
|
2531
|
+
* No connection overhead - socket stays open!
|
|
2532
|
+
* Includes retry logic with exponential backoff for transient failures.
|
|
2533
|
+
*/
|
|
2534
|
+
async generateWithPersistentSocket(text) {
|
|
2535
|
+
const methodStart = Date.now();
|
|
2536
|
+
const textPreview = text.length > 30 ? text.substring(0, 30) + '...' : text;
|
|
2537
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_START', {
|
|
2538
|
+
textLength: text.length,
|
|
2539
|
+
textPreview,
|
|
2540
|
+
maxRetries: LocalEmbeddingProvider.SOCKET_MAX_RETRIES,
|
|
2541
|
+
socketConnected: this.socketConnected,
|
|
2542
|
+
hasPersistentSocket: !!this.persistentSocket,
|
|
2543
|
+
socketPath: this.sandboxSocketPath
|
|
2544
|
+
});
|
|
2545
|
+
let lastError = null;
|
|
2546
|
+
for (let attempt = 1; attempt <= LocalEmbeddingProvider.SOCKET_MAX_RETRIES; attempt++) {
|
|
2547
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_ATTEMPT_START', {
|
|
2548
|
+
attempt,
|
|
2549
|
+
maxRetries: LocalEmbeddingProvider.SOCKET_MAX_RETRIES,
|
|
2550
|
+
totalElapsedMs: Date.now() - methodStart,
|
|
2551
|
+
socketConnected: this.socketConnected
|
|
2552
|
+
});
|
|
2553
|
+
try {
|
|
2554
|
+
const attemptStart = Date.now();
|
|
2555
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_CALLING_ATTEMPT', {
|
|
2556
|
+
attempt
|
|
2557
|
+
});
|
|
2558
|
+
const result = await this.generateWithPersistentSocketAttempt(text, attempt);
|
|
2559
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_ATTEMPT_SUCCESS', {
|
|
2560
|
+
attempt,
|
|
2561
|
+
attemptMs: Date.now() - attemptStart,
|
|
2562
|
+
totalElapsedMs: Date.now() - methodStart,
|
|
2563
|
+
resultDim: result.length
|
|
2564
|
+
});
|
|
2565
|
+
return result;
|
|
2566
|
+
}
|
|
2567
|
+
catch (err) {
|
|
2568
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2569
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_ATTEMPT_ERROR', {
|
|
2570
|
+
attempt,
|
|
2571
|
+
error: lastError.message,
|
|
2572
|
+
totalElapsedMs: Date.now() - methodStart
|
|
2573
|
+
});
|
|
2574
|
+
// Check if error is retryable (timeout or transient socket error)
|
|
2575
|
+
const isRetryable = lastError.message.includes('timeout') ||
|
|
2576
|
+
lastError.message.includes('ECONNRESET') ||
|
|
2577
|
+
lastError.message.includes('EPIPE') ||
|
|
2578
|
+
lastError.message.includes('socket');
|
|
2579
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_RETRY_CHECK', {
|
|
2580
|
+
attempt,
|
|
2581
|
+
isRetryable,
|
|
2582
|
+
isLastAttempt: attempt >= LocalEmbeddingProvider.SOCKET_MAX_RETRIES,
|
|
2583
|
+
errorMessage: lastError.message
|
|
2584
|
+
});
|
|
2585
|
+
if (!isRetryable || attempt >= LocalEmbeddingProvider.SOCKET_MAX_RETRIES) {
|
|
2586
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_BREAKING_LOOP', {
|
|
2587
|
+
attempt,
|
|
2588
|
+
reason: !isRetryable ? 'not retryable' : 'max retries reached'
|
|
2589
|
+
});
|
|
2590
|
+
break;
|
|
2591
|
+
}
|
|
2592
|
+
// Exponential backoff with jitter
|
|
2593
|
+
const baseDelay = LocalEmbeddingProvider.SOCKET_INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
|
|
2594
|
+
const jitter = Math.random() * 0.3 * baseDelay; // 0-30% jitter
|
|
2595
|
+
const delay = Math.min(baseDelay + jitter, LocalEmbeddingProvider.SOCKET_MAX_RETRY_DELAY_MS);
|
|
2596
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_BACKOFF_WAIT', {
|
|
2597
|
+
attempt,
|
|
2598
|
+
baseDelay,
|
|
2599
|
+
jitter: Math.round(jitter),
|
|
2600
|
+
delay: Math.round(delay)
|
|
2601
|
+
});
|
|
2602
|
+
logger.warn({
|
|
2603
|
+
attempt,
|
|
2604
|
+
maxRetries: LocalEmbeddingProvider.SOCKET_MAX_RETRIES,
|
|
2605
|
+
error: lastError.message,
|
|
2606
|
+
retryDelayMs: Math.round(delay),
|
|
2607
|
+
socketPath: this.sandboxSocketPath
|
|
2608
|
+
}, 'Persistent socket embedding failed, retrying with exponential backoff');
|
|
2609
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
2610
|
+
// Try to reconnect socket before retry
|
|
2611
|
+
if (!this.socketConnected) {
|
|
2612
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_RECONNECTING', {
|
|
2613
|
+
attempt,
|
|
2614
|
+
nextAttempt: attempt + 1
|
|
2615
|
+
});
|
|
2616
|
+
this.initPersistentSocket();
|
|
2617
|
+
await new Promise(resolve => setTimeout(resolve, 500)); // Wait for reconnection
|
|
2618
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_RECONNECT_WAIT_DONE', {
|
|
2619
|
+
socketConnected: this.socketConnected,
|
|
2620
|
+
hasPersistentSocket: !!this.persistentSocket
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
// All retries exhausted
|
|
2626
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_ALL_RETRIES_EXHAUSTED', {
|
|
2627
|
+
maxRetries: LocalEmbeddingProvider.SOCKET_MAX_RETRIES,
|
|
2628
|
+
lastError: lastError?.message,
|
|
2629
|
+
totalElapsedMs: Date.now() - methodStart,
|
|
2630
|
+
socketPath: this.sandboxSocketPath
|
|
2631
|
+
});
|
|
2632
|
+
throw new Error(`Embedding generation failed after ${LocalEmbeddingProvider.SOCKET_MAX_RETRIES} attempts. ` +
|
|
2633
|
+
`Socket: ${this.sandboxSocketPath}. ` +
|
|
2634
|
+
`Last error: ${lastError?.message || 'unknown'}. ` +
|
|
2635
|
+
`Check if Frankenstein embedding service is running.`);
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* Single attempt to generate embedding via persistent socket
|
|
2639
|
+
*/
|
|
2640
|
+
generateWithPersistentSocketAttempt(text, attempt) {
|
|
2641
|
+
const methodStart = Date.now();
|
|
2642
|
+
const textPreview = text.length > 30 ? text.substring(0, 30) + '...' : text;
|
|
2643
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_ATTEMPT_INIT', {
|
|
2644
|
+
attempt,
|
|
2645
|
+
textLength: text.length,
|
|
2646
|
+
textPreview,
|
|
2647
|
+
hasPersistentSocket: !!this.persistentSocket,
|
|
2648
|
+
socketConnected: this.socketConnected,
|
|
2649
|
+
socketPath: this.sandboxSocketPath
|
|
2650
|
+
});
|
|
2651
|
+
return new Promise((resolve, reject) => {
|
|
2652
|
+
if (!this.persistentSocket || !this.socketConnected) {
|
|
2653
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_ATTEMPT_NOT_CONNECTED', {
|
|
2654
|
+
attempt,
|
|
2655
|
+
hasPersistentSocket: !!this.persistentSocket,
|
|
2656
|
+
socketConnected: this.socketConnected,
|
|
2657
|
+
elapsedMs: Date.now() - methodStart
|
|
2658
|
+
});
|
|
2659
|
+
return reject(new Error(`Persistent socket not connected (socket: ${this.sandboxSocketPath})`));
|
|
2660
|
+
}
|
|
2661
|
+
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2662
|
+
const startTime = Date.now();
|
|
2663
|
+
const timeoutMs = this.getAdaptiveTimeout();
|
|
2664
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_ATTEMPT_SETUP', {
|
|
2665
|
+
attempt,
|
|
2666
|
+
requestId,
|
|
2667
|
+
timeoutMs,
|
|
2668
|
+
pendingRequestsCount: this.pendingRequests.size
|
|
2669
|
+
});
|
|
2670
|
+
const timeout = setTimeout(() => {
|
|
2671
|
+
if (this.pendingRequests.has(requestId)) {
|
|
2672
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_ATTEMPT_TIMEOUT_FIRED', {
|
|
2673
|
+
attempt,
|
|
2674
|
+
requestId,
|
|
2675
|
+
timeoutMs,
|
|
2676
|
+
totalElapsedMs: Date.now() - methodStart,
|
|
2677
|
+
pendingRequestsCount: this.pendingRequests.size
|
|
2678
|
+
});
|
|
2679
|
+
this.pendingRequests.delete(requestId);
|
|
2680
|
+
reject(new Error(`Embedding generation timeout after ${Math.round(timeoutMs / 1000)}s ` +
|
|
2681
|
+
`(attempt ${attempt}/${LocalEmbeddingProvider.SOCKET_MAX_RETRIES}). ` +
|
|
2682
|
+
`Socket: ${this.sandboxSocketPath}. ` +
|
|
2683
|
+
`Cold starts may need longer timeout - set SPECMEM_EMBEDDING_TIMEOUT=60 for 60 seconds.`));
|
|
2684
|
+
}
|
|
2685
|
+
}, timeoutMs);
|
|
2686
|
+
// Store pending request
|
|
2687
|
+
this.pendingRequests.set(requestId, {
|
|
2688
|
+
resolve: (embedding) => {
|
|
2689
|
+
const responseTime = Date.now() - startTime;
|
|
2690
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_ATTEMPT_RESPONSE_RECEIVED', {
|
|
2691
|
+
attempt,
|
|
2692
|
+
requestId,
|
|
2693
|
+
responseTimeMs: responseTime,
|
|
2694
|
+
embeddingDim: embedding.length,
|
|
2695
|
+
totalElapsedMs: Date.now() - methodStart
|
|
2696
|
+
});
|
|
2697
|
+
this.recordResponseTime(responseTime);
|
|
2698
|
+
resolve(embedding);
|
|
2699
|
+
},
|
|
2700
|
+
reject,
|
|
2701
|
+
timeout
|
|
2702
|
+
});
|
|
2703
|
+
// Send request with ID so we can match responses
|
|
2704
|
+
const request = JSON.stringify({ type: 'embed', text, requestId }) + '\n';
|
|
2705
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_ATTEMPT_WRITING_REQUEST', {
|
|
2706
|
+
attempt,
|
|
2707
|
+
requestId,
|
|
2708
|
+
requestLength: request.length,
|
|
2709
|
+
elapsedMs: Date.now() - methodStart
|
|
2710
|
+
});
|
|
2711
|
+
this.persistentSocket.write(request);
|
|
2712
|
+
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'PERSISTENT_SOCKET_ATTEMPT_REQUEST_WRITTEN', {
|
|
2713
|
+
attempt,
|
|
2714
|
+
requestId,
|
|
2715
|
+
waitingForResponseWithTimeoutMs: timeoutMs
|
|
2716
|
+
});
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
/**
|
|
2720
|
+
* Generate embedding with a NEW socket (slow fallback)
|
|
2721
|
+
* Used when persistent socket is unavailable.
|
|
2722
|
+
* Includes retry logic with exponential backoff for transient failures.
|
|
2723
|
+
*/
|
|
2724
|
+
async generateWithNewSocket(text) {
|
|
2725
|
+
let lastError = null;
|
|
2726
|
+
for (let attempt = 1; attempt <= LocalEmbeddingProvider.SOCKET_MAX_RETRIES; attempt++) {
|
|
2727
|
+
try {
|
|
2728
|
+
return await this.generateWithNewSocketAttempt(text, attempt);
|
|
2729
|
+
}
|
|
2730
|
+
catch (err) {
|
|
2731
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2732
|
+
// Check if error is retryable (timeout or transient socket error)
|
|
2733
|
+
const isRetryable = lastError.message.includes('timeout') ||
|
|
2734
|
+
lastError.message.includes('ECONNRESET') ||
|
|
2735
|
+
lastError.message.includes('ECONNREFUSED') ||
|
|
2736
|
+
lastError.message.includes('EPIPE') ||
|
|
2737
|
+
lastError.message.includes('ENOENT');
|
|
2738
|
+
if (!isRetryable || attempt >= LocalEmbeddingProvider.SOCKET_MAX_RETRIES) {
|
|
2739
|
+
break;
|
|
2740
|
+
}
|
|
2741
|
+
// Exponential backoff with jitter
|
|
2742
|
+
const baseDelay = LocalEmbeddingProvider.SOCKET_INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
|
|
2743
|
+
const jitter = Math.random() * 0.3 * baseDelay; // 0-30% jitter
|
|
2744
|
+
const delay = Math.min(baseDelay + jitter, LocalEmbeddingProvider.SOCKET_MAX_RETRY_DELAY_MS);
|
|
2745
|
+
logger.warn({
|
|
2746
|
+
attempt,
|
|
2747
|
+
maxRetries: LocalEmbeddingProvider.SOCKET_MAX_RETRIES,
|
|
2748
|
+
error: lastError.message,
|
|
2749
|
+
retryDelayMs: Math.round(delay),
|
|
2750
|
+
socketPath: this.sandboxSocketPath
|
|
2751
|
+
}, 'New socket embedding failed, retrying with exponential backoff');
|
|
2752
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
// All retries exhausted
|
|
2756
|
+
throw new Error(`Embedding generation failed after ${LocalEmbeddingProvider.SOCKET_MAX_RETRIES} attempts. ` +
|
|
2757
|
+
`Socket: ${this.sandboxSocketPath}. ` +
|
|
2758
|
+
`Last error: ${lastError?.message || 'unknown'}. ` +
|
|
2759
|
+
`Check if Frankenstein embedding service is running.`);
|
|
2760
|
+
}
|
|
2761
|
+
/**
|
|
2762
|
+
* Single attempt to generate embedding via new socket
|
|
2763
|
+
* Uses IDLE-BASED timeout - resets on any data received
|
|
2764
|
+
*/
|
|
2765
|
+
generateWithNewSocketAttempt(text, attempt) {
|
|
2766
|
+
return new Promise((resolve, reject) => {
|
|
2767
|
+
const socket = createConnection(this.sandboxSocketPath);
|
|
2768
|
+
let buffer = '';
|
|
2769
|
+
let resolved = false;
|
|
2770
|
+
const startTime = Date.now();
|
|
2771
|
+
const timeoutMs = this.getAdaptiveTimeout();
|
|
2772
|
+
// IDLE-BASED TIMEOUT: Resets on any data received
|
|
2773
|
+
let timeout = setTimeout(() => {
|
|
2774
|
+
if (!resolved) {
|
|
2775
|
+
resolved = true;
|
|
2776
|
+
socket.destroy();
|
|
2777
|
+
reject(new Error(`Embedding idle timeout after ${Math.round(timeoutMs / 1000)}s of no activity ` +
|
|
2778
|
+
`(attempt ${attempt}/${LocalEmbeddingProvider.SOCKET_MAX_RETRIES}). ` +
|
|
2779
|
+
`Socket: ${this.sandboxSocketPath}. ` +
|
|
2780
|
+
`If model is slow, increase SPECMEM_EMBEDDING_TIMEOUT.`));
|
|
2781
|
+
}
|
|
2782
|
+
}, timeoutMs);
|
|
2783
|
+
socket.on('connect', () => {
|
|
2784
|
+
const request = JSON.stringify({ type: 'embed', text }) + '\n';
|
|
2785
|
+
socket.write(request);
|
|
2786
|
+
});
|
|
2787
|
+
socket.on('data', (data) => {
|
|
2788
|
+
buffer += data.toString();
|
|
2789
|
+
// IDLE-BASED TIMEOUT: Reset timer on ANY data received
|
|
2790
|
+
clearTimeout(timeout);
|
|
2791
|
+
timeout = setTimeout(() => {
|
|
2792
|
+
if (!resolved) {
|
|
2793
|
+
resolved = true;
|
|
2794
|
+
socket.destroy();
|
|
2795
|
+
reject(new Error(`Embedding idle timeout after ${Math.round(timeoutMs / 1000)}s of no activity ` +
|
|
2796
|
+
`(attempt ${attempt}/${LocalEmbeddingProvider.SOCKET_MAX_RETRIES}). ` +
|
|
2797
|
+
`Socket: ${this.sandboxSocketPath}. ` +
|
|
2798
|
+
`If model is slow, increase SPECMEM_EMBEDDING_TIMEOUT.`));
|
|
2799
|
+
}
|
|
2800
|
+
}, timeoutMs);
|
|
2801
|
+
// Process complete JSON messages (newline-delimited)
|
|
2802
|
+
let newlineIndex;
|
|
2803
|
+
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
2804
|
+
if (resolved)
|
|
2805
|
+
return;
|
|
2806
|
+
const responseJson = buffer.slice(0, newlineIndex);
|
|
2807
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
2808
|
+
try {
|
|
2809
|
+
const response = JSON.parse(responseJson);
|
|
2810
|
+
// Handle heartbeat/processing status - just keep waiting
|
|
2811
|
+
if (response.status === 'processing') {
|
|
2812
|
+
continue;
|
|
2813
|
+
}
|
|
2814
|
+
// Got actual response - resolve or reject
|
|
2815
|
+
clearTimeout(timeout);
|
|
2816
|
+
resolved = true;
|
|
2817
|
+
const responseTime = Date.now() - startTime;
|
|
2818
|
+
this.recordResponseTime(responseTime);
|
|
2819
|
+
if (response.error) {
|
|
2820
|
+
reject(new Error(`Embedding service error: ${response.error} (socket: ${this.sandboxSocketPath})`));
|
|
2821
|
+
}
|
|
2822
|
+
else if (response.embedding) {
|
|
2823
|
+
resolve(response.embedding);
|
|
2824
|
+
}
|
|
2825
|
+
else {
|
|
2826
|
+
reject(new Error(`Invalid response from embedding service (socket: ${this.sandboxSocketPath})`));
|
|
2827
|
+
}
|
|
2828
|
+
socket.end();
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
catch (err) {
|
|
2832
|
+
clearTimeout(timeout);
|
|
2833
|
+
resolved = true;
|
|
2834
|
+
reject(new Error(`Failed to parse embedding response: ${err instanceof Error ? err.message : err} (socket: ${this.sandboxSocketPath})`));
|
|
2835
|
+
socket.end();
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
});
|
|
2840
|
+
socket.on('error', (err) => {
|
|
2841
|
+
clearTimeout(timeout);
|
|
2842
|
+
if (!resolved) {
|
|
2843
|
+
resolved = true;
|
|
2844
|
+
reject(new Error(`Socket error: ${err.message} (socket: ${this.sandboxSocketPath})`));
|
|
2845
|
+
}
|
|
2846
|
+
});
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
/**
|
|
2850
|
+
* Calculate adaptive timeout based on recent response times
|
|
2851
|
+
* Uses rolling average + 3x standard deviation for safety margin
|
|
2852
|
+
*/
|
|
2853
|
+
getAdaptiveTimeout() {
|
|
2854
|
+
if (this.responseTimes.length < 3) {
|
|
2855
|
+
// Not enough data yet, use initial timeout
|
|
2856
|
+
return LocalEmbeddingProvider.INITIAL_TIMEOUT_MS;
|
|
2857
|
+
}
|
|
2858
|
+
// Calculate mean
|
|
2859
|
+
const sum = this.responseTimes.reduce((a, b) => a + b, 0);
|
|
2860
|
+
const mean = sum / this.responseTimes.length;
|
|
2861
|
+
// Calculate standard deviation
|
|
2862
|
+
const squaredDiffs = this.responseTimes.map(t => Math.pow(t - mean, 2));
|
|
2863
|
+
const avgSquaredDiff = squaredDiffs.reduce((a, b) => a + b, 0) / squaredDiffs.length;
|
|
2864
|
+
const stdDev = Math.sqrt(avgSquaredDiff);
|
|
2865
|
+
// Timeout = mean + MULTIPLIER * stdDev (covers 99.7% of cases with 3x)
|
|
2866
|
+
const adaptiveTimeout = mean + (LocalEmbeddingProvider.TIMEOUT_MULTIPLIER * stdDev);
|
|
2867
|
+
// Clamp to min/max bounds
|
|
2868
|
+
const clampedTimeout = Math.max(LocalEmbeddingProvider.MIN_TIMEOUT_MS, Math.min(LocalEmbeddingProvider.MAX_TIMEOUT_MS, adaptiveTimeout));
|
|
2869
|
+
logger.debug({
|
|
2870
|
+
mean: Math.round(mean),
|
|
2871
|
+
stdDev: Math.round(stdDev),
|
|
2872
|
+
adaptiveTimeout: Math.round(adaptiveTimeout),
|
|
2873
|
+
clampedTimeout: Math.round(clampedTimeout),
|
|
2874
|
+
sampleCount: this.responseTimes.length
|
|
2875
|
+
}, 'Calculated adaptive embedding timeout');
|
|
2876
|
+
return clampedTimeout;
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Record a response time for adaptive timeout calculation
|
|
2880
|
+
*/
|
|
2881
|
+
recordResponseTime(responseTimeMs) {
|
|
2882
|
+
this.responseTimes.push(responseTimeMs);
|
|
2883
|
+
// Keep only the last N response times (rolling window)
|
|
2884
|
+
if (this.responseTimes.length > LocalEmbeddingProvider.RESPONSE_TIME_WINDOW) {
|
|
2885
|
+
this.responseTimes.shift();
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
padEmbedding(embedding, targetDims) {
|
|
2889
|
+
// Pad smaller embedding to target dimensions by repeating pattern
|
|
2890
|
+
const padded = new Array(targetDims);
|
|
2891
|
+
for (let i = 0; i < targetDims; i++) {
|
|
2892
|
+
padded[i] = embedding[i % embedding.length];
|
|
2893
|
+
}
|
|
2894
|
+
// Re-normalize
|
|
2895
|
+
const magnitude = Math.sqrt(padded.reduce((sum, val) => sum + val * val, 0));
|
|
2896
|
+
if (magnitude > 0) {
|
|
2897
|
+
for (let i = 0; i < padded.length; i++) {
|
|
2898
|
+
padded[i] = padded[i] / magnitude;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
return padded;
|
|
2902
|
+
}
|
|
2903
|
+
generateHashEmbedding(text, overrideDimension) {
|
|
2904
|
+
// Normalize text for more consistent embeddings
|
|
2905
|
+
const normalizedText = text.toLowerCase().trim();
|
|
2906
|
+
// Use override dimension, or fall back to instance target dimension
|
|
2907
|
+
// If no dimension is known (DB not ready), we CANNOT generate hash embedding
|
|
2908
|
+
// because the dimension must come from the database. Throw to force retry.
|
|
2909
|
+
const dimension = overrideDimension ?? this.targetDimension;
|
|
2910
|
+
if (dimension === null || dimension === 0) {
|
|
2911
|
+
throw new Error('Cannot generate hash embedding: database dimension not yet known. Ensure database is initialized first.');
|
|
2912
|
+
}
|
|
2913
|
+
// Generate base hash
|
|
2914
|
+
const hash = this.hashString(normalizedText);
|
|
2915
|
+
const embedding = new Array(dimension);
|
|
2916
|
+
// Create embedding using multiple hash seeds for distribution
|
|
2917
|
+
for (let i = 0; i < dimension; i++) {
|
|
2918
|
+
// Use combination of position and text hash for variety
|
|
2919
|
+
const seed1 = hash + i * 31;
|
|
2920
|
+
const seed2 = this.hashString(normalizedText.slice(0, Math.min(i + 10, normalizedText.length)));
|
|
2921
|
+
const combined = seed1 ^ seed2;
|
|
2922
|
+
// Generate value between -1 and 1
|
|
2923
|
+
embedding[i] = Math.sin(combined) * Math.cos(combined * 0.7);
|
|
2924
|
+
}
|
|
2925
|
+
// Add n-gram influence for better semantic grouping
|
|
2926
|
+
this.addNgramInfluence(normalizedText, embedding, dimension);
|
|
2927
|
+
// Normalize to unit vector for cosine similarity
|
|
2928
|
+
const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
|
|
2929
|
+
if (magnitude > 0) {
|
|
2930
|
+
for (let i = 0; i < embedding.length; i++) {
|
|
2931
|
+
embedding[i] = embedding[i] / magnitude;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
return embedding;
|
|
2935
|
+
}
|
|
2936
|
+
hashString(str) {
|
|
2937
|
+
let hash = 5381;
|
|
2938
|
+
for (let i = 0; i < str.length; i++) {
|
|
2939
|
+
const char = str.charCodeAt(i);
|
|
2940
|
+
hash = ((hash << 5) + hash) ^ char;
|
|
2941
|
+
}
|
|
2942
|
+
return Math.abs(hash);
|
|
2943
|
+
}
|
|
2944
|
+
addNgramInfluence(text, embedding, dimension) {
|
|
2945
|
+
// Add 3-gram influence for better semantic grouping
|
|
2946
|
+
const ngramSize = 3;
|
|
2947
|
+
for (let i = 0; i <= text.length - ngramSize; i++) {
|
|
2948
|
+
const ngram = text.slice(i, i + ngramSize);
|
|
2949
|
+
const ngramHash = this.hashString(ngram);
|
|
2950
|
+
const position = ngramHash % dimension;
|
|
2951
|
+
embedding[position] = (embedding[position] ?? 0) + 0.1;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
/**
|
|
2955
|
+
* Shutdown the embedding provider - kills Python embedding process if running
|
|
2956
|
+
* Called during graceful shutdown to prevent orphaned processes
|
|
2957
|
+
*
|
|
2958
|
+
* Handles two cases:
|
|
2959
|
+
* 1. MCP server started the embedding (pythonEmbeddingPid is set)
|
|
2960
|
+
* 2. Session hook started the embedding (read from PID file)
|
|
2961
|
+
*/
|
|
2962
|
+
async shutdown() {
|
|
2963
|
+
let pidToKill = this.pythonEmbeddingPid;
|
|
2964
|
+
// If we didn't start the embedding server, check the PID file
|
|
2965
|
+
// (session start hook may have started it)
|
|
2966
|
+
if (!pidToKill) {
|
|
2967
|
+
const projectPath = getProjectPath();
|
|
2968
|
+
const pidFile = join(projectPath, 'specmem', 'sockets', 'embedding.pid');
|
|
2969
|
+
try {
|
|
2970
|
+
if (existsSync(pidFile)) {
|
|
2971
|
+
const pidData = readFileSync(pidFile, 'utf8').trim();
|
|
2972
|
+
const [pidStr] = pidData.split(':');
|
|
2973
|
+
const pid = parseInt(pidStr, 10);
|
|
2974
|
+
if (!isNaN(pid) && pid > 0) {
|
|
2975
|
+
pidToKill = pid;
|
|
2976
|
+
logger.info({ pid, pidFile }, 'Found embedding PID from file (started by session hook)');
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
catch (err) {
|
|
2981
|
+
logger.debug({ pidFile, error: err }, 'Could not read embedding PID file');
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
if (pidToKill) {
|
|
2985
|
+
logger.info({ pid: pidToKill }, 'Killing Python embedding server process');
|
|
2986
|
+
try {
|
|
2987
|
+
process.kill(pidToKill, 'SIGTERM');
|
|
2988
|
+
// Give it a moment to exit gracefully
|
|
2989
|
+
await new Promise(r => setTimeout(r, 500));
|
|
2990
|
+
// If still running, force kill
|
|
2991
|
+
try {
|
|
2992
|
+
process.kill(pidToKill, 0); // Check if process exists
|
|
2993
|
+
process.kill(pidToKill, 'SIGKILL');
|
|
2994
|
+
logger.warn({ pid: pidToKill }, 'Had to SIGKILL embedding process');
|
|
2995
|
+
}
|
|
2996
|
+
catch (e) {
|
|
2997
|
+
// Process already exited - good
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
catch (err) {
|
|
3001
|
+
// Process may already be dead
|
|
3002
|
+
logger.debug({ pid: pidToKill, error: err }, 'Embedding process kill failed (may already be dead)');
|
|
3003
|
+
}
|
|
3004
|
+
this.pythonEmbeddingPid = null;
|
|
3005
|
+
// Clean up PID file
|
|
3006
|
+
const projectPath = getProjectPath();
|
|
3007
|
+
const pidFile = join(projectPath, 'specmem', 'sockets', 'embedding.pid');
|
|
3008
|
+
try {
|
|
3009
|
+
if (existsSync(pidFile)) {
|
|
3010
|
+
unlinkSync(pidFile);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
catch (err) {
|
|
3014
|
+
// Ignore - file may not exist
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
/**
|
|
3020
|
+
* Create the embedding provider
|
|
3021
|
+
* Always uses local embeddings - no external API needed
|
|
3022
|
+
* DYNAMICALLY detects dimension from database (no hardcoding!)
|
|
3023
|
+
*
|
|
3024
|
+
* NOTE: Embedding dimensions are AUTO-DETECTED from the database pgvector column.
|
|
3025
|
+
* The SPECMEM_EMBEDDING_DIMENSIONS config setting is DEPRECATED and ignored.
|
|
3026
|
+
* The database pg_attribute table is the single source of truth for dimensions.
|
|
3027
|
+
*
|
|
3028
|
+
* Uses the centralized DimensionAdapter for comprehensive dimension detection.
|
|
3029
|
+
*/
|
|
3030
|
+
async function createEmbeddingProvider() {
|
|
3031
|
+
// FIX: Wait for embedding server to be ready before creating provider
|
|
3032
|
+
// This prevents 21+ embedding errors during startup when the server is still loading
|
|
3033
|
+
const embeddingSocketPath = getEmbeddingSocketPath();
|
|
3034
|
+
const maxWaitMs = parseInt(process.env['SPECMEM_EMBEDDING_WAIT_MS'] || '30000', 10);
|
|
3035
|
+
const checkIntervalMs = 1000;
|
|
3036
|
+
const startWait = Date.now();
|
|
3037
|
+
logger.info({ socketPath: embeddingSocketPath, maxWaitMs }, '[createEmbeddingProvider] Waiting for embedding server to be ready...');
|
|
3038
|
+
while (Date.now() - startWait < maxWaitMs) {
|
|
3039
|
+
if (existsSync(embeddingSocketPath)) {
|
|
3040
|
+
// Socket file exists - try a quick health check
|
|
3041
|
+
try {
|
|
3042
|
+
const testResult = await new Promise((resolve) => {
|
|
3043
|
+
const testSocket = createConnection(embeddingSocketPath);
|
|
3044
|
+
const timeout = setTimeout(() => {
|
|
3045
|
+
testSocket.destroy();
|
|
3046
|
+
resolve(false);
|
|
3047
|
+
}, 5000);
|
|
3048
|
+
testSocket.on('connect', () => {
|
|
3049
|
+
testSocket.write('{"type":"health"}\n');
|
|
3050
|
+
});
|
|
3051
|
+
testSocket.on('data', () => {
|
|
3052
|
+
clearTimeout(timeout);
|
|
3053
|
+
testSocket.destroy();
|
|
3054
|
+
resolve(true);
|
|
3055
|
+
});
|
|
3056
|
+
testSocket.on('error', () => {
|
|
3057
|
+
clearTimeout(timeout);
|
|
3058
|
+
testSocket.destroy();
|
|
3059
|
+
resolve(false);
|
|
3060
|
+
});
|
|
3061
|
+
});
|
|
3062
|
+
if (testResult) {
|
|
3063
|
+
logger.info({ elapsed: Date.now() - startWait }, '[createEmbeddingProvider] Embedding server is ready');
|
|
3064
|
+
break;
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
catch {
|
|
3068
|
+
// Socket test failed - keep waiting
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
// Still waiting - log progress every 5 seconds
|
|
3072
|
+
if ((Date.now() - startWait) % 5000 < checkIntervalMs) {
|
|
3073
|
+
logger.debug({ elapsed: Date.now() - startWait, maxWaitMs }, '[createEmbeddingProvider] Still waiting for embedding server...');
|
|
3074
|
+
}
|
|
3075
|
+
await new Promise(resolve => setTimeout(resolve, checkIntervalMs));
|
|
3076
|
+
}
|
|
3077
|
+
// Log if we timed out (non-fatal - provider will handle server startup)
|
|
3078
|
+
if (!existsSync(embeddingSocketPath)) {
|
|
3079
|
+
logger.warn({ elapsed: Date.now() - startWait, maxWaitMs }, '[createEmbeddingProvider] Embedding server not ready after wait - will use fallback mechanisms');
|
|
3080
|
+
}
|
|
3081
|
+
// Initialize the DimensionAdapter for centralized dimension management
|
|
3082
|
+
// This detects dimensions from ALL tables with vector columns
|
|
3083
|
+
let dimensionResult = null;
|
|
3084
|
+
let dbDimension = null;
|
|
3085
|
+
try {
|
|
3086
|
+
const db = getDatabase();
|
|
3087
|
+
// Initialize DimensionAdapter - detects ALL vector columns in database
|
|
3088
|
+
dimensionResult = await initializeDimensionAdapter(db);
|
|
3089
|
+
if (dimensionResult.success && dimensionResult.canonicalDimension !== null) {
|
|
3090
|
+
dbDimension = dimensionResult.canonicalDimension;
|
|
3091
|
+
// Log comprehensive dimension detection results
|
|
3092
|
+
logger.info('='.repeat(60));
|
|
3093
|
+
logger.info('DIMENSION DETECTION COMPLETE - Database is Source of Truth');
|
|
3094
|
+
logger.info('='.repeat(60));
|
|
3095
|
+
logger.info({ canonicalDimension: dbDimension }, 'Canonical dimension (from memories table)');
|
|
3096
|
+
logger.info({ tablesWithVectors: dimensionResult.tables.length }, 'Tables with vector columns detected');
|
|
3097
|
+
// Log each table's dimension
|
|
3098
|
+
for (const table of dimensionResult.tables) {
|
|
3099
|
+
const indexInfo = table.hasIndex ? ` (${table.indexType || 'indexed'})` : ' (no index)';
|
|
3100
|
+
logger.info({
|
|
3101
|
+
table: table.tableName,
|
|
3102
|
+
column: table.columnName,
|
|
3103
|
+
dimension: table.dimension ?? 'unbounded',
|
|
3104
|
+
indexed: table.hasIndex
|
|
3105
|
+
}, ` ${table.tableName}.${table.columnName}: ${table.dimension ?? 'unbounded'}${indexInfo}`);
|
|
3106
|
+
}
|
|
3107
|
+
// Warn about inconsistencies
|
|
3108
|
+
if (dimensionResult.inconsistencies.length > 0) {
|
|
3109
|
+
logger.warn({ count: dimensionResult.inconsistencies.length }, 'DIMENSION INCONSISTENCIES DETECTED');
|
|
3110
|
+
for (const inc of dimensionResult.inconsistencies) {
|
|
3111
|
+
logger.warn({
|
|
3112
|
+
table: inc.table,
|
|
3113
|
+
column: inc.column,
|
|
3114
|
+
dimension: inc.dimension,
|
|
3115
|
+
expected: inc.expected
|
|
3116
|
+
}, ` ${inc.table}.${inc.column}: has ${inc.dimension}, expected ${inc.expected}`);
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
else {
|
|
3120
|
+
logger.info('All vector columns have consistent dimensions');
|
|
3121
|
+
}
|
|
3122
|
+
logger.info('='.repeat(60));
|
|
3123
|
+
// Warm the projection layer cache with the database dimension
|
|
3124
|
+
setTargetDimension(dbDimension);
|
|
3125
|
+
}
|
|
3126
|
+
else {
|
|
3127
|
+
logger.warn('DimensionAdapter: Could not detect canonical dimension - will use dimension from first embedding');
|
|
3128
|
+
// Fallback to direct query for memories table
|
|
3129
|
+
try {
|
|
3130
|
+
const result = await db.query(`
|
|
3131
|
+
SELECT atttypmod FROM pg_attribute
|
|
3132
|
+
WHERE attrelid = 'memories'::regclass AND attname = 'embedding'
|
|
3133
|
+
`);
|
|
3134
|
+
if (result.rows.length > 0 && result.rows[0].atttypmod > 0) {
|
|
3135
|
+
dbDimension = result.rows[0].atttypmod;
|
|
3136
|
+
logger.info({ dbDimension }, 'Fallback: detected dimension from memories table');
|
|
3137
|
+
setTargetDimension(dbDimension);
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
catch (fallbackErr) {
|
|
3141
|
+
logger.debug({ error: fallbackErr }, 'Fallback dimension query failed (table may not exist yet)');
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
catch (err) {
|
|
3146
|
+
logger.warn({ error: err }, 'Failed to initialize DimensionAdapter - will use dimension from first embedding');
|
|
3147
|
+
}
|
|
3148
|
+
// If we got a dimension from DB, use it; otherwise LocalEmbeddingProvider will auto-detect from first embedding
|
|
3149
|
+
if (dbDimension) {
|
|
3150
|
+
logger.info({ targetDimension: dbDimension }, 'Using local standalone embeddings with DB-detected dimension');
|
|
3151
|
+
return new LocalEmbeddingProvider(dbDimension);
|
|
3152
|
+
}
|
|
3153
|
+
else {
|
|
3154
|
+
// No DB dimension yet - let LocalEmbeddingProvider query DB on first embedding
|
|
3155
|
+
logger.info('Using local standalone embeddings - dimension will be auto-detected from database on first use');
|
|
3156
|
+
return new LocalEmbeddingProvider(); // Constructor will auto-detect from DB
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
// Global state for skill/codebase systems
|
|
3160
|
+
let skillScanner = null;
|
|
3161
|
+
let skillResourceProvider = null;
|
|
3162
|
+
let codebaseIndexer = null;
|
|
3163
|
+
let skillReminder = null;
|
|
3164
|
+
// Global state for memory management
|
|
3165
|
+
let memoryManager = null;
|
|
3166
|
+
let embeddingOverflowHandler = null;
|
|
3167
|
+
/**
|
|
3168
|
+
* Initialize Skills System
|
|
3169
|
+
*/
|
|
3170
|
+
async function initializeSkillsSystem(embeddingProvider) {
|
|
3171
|
+
const skillsConfig = loadSkillsConfig();
|
|
3172
|
+
if (!skillsConfig.enabled) {
|
|
3173
|
+
logger.info('skills system disabled');
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
logger.info({ skillsPath: skillsConfig.skillsPath }, 'initializing skills system...');
|
|
3177
|
+
try {
|
|
3178
|
+
// create and initialize skill scanner
|
|
3179
|
+
skillScanner = getSkillScanner({
|
|
3180
|
+
skillsPath: skillsConfig.skillsPath,
|
|
3181
|
+
autoReload: skillsConfig.autoReload,
|
|
3182
|
+
debounceMs: 500
|
|
3183
|
+
});
|
|
3184
|
+
const scanResult = await skillScanner.initialize();
|
|
3185
|
+
logger.info({
|
|
3186
|
+
totalSkills: scanResult.totalCount,
|
|
3187
|
+
categories: Array.from(scanResult.categories.keys()),
|
|
3188
|
+
autoReload: skillsConfig.autoReload
|
|
3189
|
+
}, 'skills scanner initialized');
|
|
3190
|
+
// create resource provider
|
|
3191
|
+
skillResourceProvider = getSkillResourceProvider(skillScanner);
|
|
3192
|
+
// log skill reminder
|
|
3193
|
+
logger.info(`\n${'-'.repeat(50)}`);
|
|
3194
|
+
logger.info('SKILLS LOADED:');
|
|
3195
|
+
for (const category of skillScanner.getCategories()) {
|
|
3196
|
+
const skills = skillScanner.getSkillsByCategory(category);
|
|
3197
|
+
logger.info(` ${category}: ${skills.map(s => s.name).join(', ')}`);
|
|
3198
|
+
}
|
|
3199
|
+
logger.info(`${'-'.repeat(50)}\n`);
|
|
3200
|
+
}
|
|
3201
|
+
catch (error) {
|
|
3202
|
+
logger.error({ error }, 'failed to initialize skills system');
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
/**
|
|
3206
|
+
* Initialize Codebase Indexer
|
|
3207
|
+
*/
|
|
3208
|
+
async function initializeCodebaseSystem(embeddingProvider) {
|
|
3209
|
+
const codebaseConfig = loadCodebaseConfig();
|
|
3210
|
+
if (!codebaseConfig.enabled) {
|
|
3211
|
+
logger.info('codebase indexer disabled');
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
logger.info({ codebasePath: codebaseConfig.codebasePath }, 'initializing codebase indexer...');
|
|
3215
|
+
try {
|
|
3216
|
+
// Get database for persisting files
|
|
3217
|
+
const db = getDatabase();
|
|
3218
|
+
// Wrap embedding provider with caching layer (SHA-256 hash keys for correct relevancy)
|
|
3219
|
+
const cachingProvider = new CachingEmbeddingProvider(embeddingProvider);
|
|
3220
|
+
codebaseIndexer = getCodebaseIndexer({
|
|
3221
|
+
codebasePath: codebaseConfig.codebasePath,
|
|
3222
|
+
excludePatterns: codebaseConfig.excludePatterns,
|
|
3223
|
+
watchForChanges: codebaseConfig.watchForChanges,
|
|
3224
|
+
generateEmbeddings: true
|
|
3225
|
+
}, cachingProvider, db);
|
|
3226
|
+
const stats = await codebaseIndexer.initialize();
|
|
3227
|
+
logger.info({
|
|
3228
|
+
totalFiles: stats.totalFiles,
|
|
3229
|
+
totalLines: stats.totalLines,
|
|
3230
|
+
languages: Object.keys(stats.languageBreakdown).length,
|
|
3231
|
+
watching: stats.isWatching
|
|
3232
|
+
}, 'codebase indexer initialized');
|
|
3233
|
+
}
|
|
3234
|
+
catch (error) {
|
|
3235
|
+
logger.error({ error }, 'failed to initialize codebase indexer');
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
/**
|
|
3239
|
+
* Initialize Skill Reminder System
|
|
3240
|
+
*/
|
|
3241
|
+
async function initializeReminderSystem() {
|
|
3242
|
+
if (!skillScanner) {
|
|
3243
|
+
logger.debug('skill scanner not initialized - skipping reminder system');
|
|
3244
|
+
return;
|
|
3245
|
+
}
|
|
3246
|
+
logger.info('initializing skill reminder system...');
|
|
3247
|
+
try {
|
|
3248
|
+
skillReminder = getSkillReminder({
|
|
3249
|
+
enabled: true,
|
|
3250
|
+
includeFullSkillContent: true,
|
|
3251
|
+
includeCodebaseOverview: codebaseIndexer !== null,
|
|
3252
|
+
refreshIntervalMinutes: 30
|
|
3253
|
+
}, skillScanner, codebaseIndexer || undefined);
|
|
3254
|
+
await skillReminder.initialize();
|
|
3255
|
+
// log startup reminder
|
|
3256
|
+
const reminder = skillReminder.getStartupReminder();
|
|
3257
|
+
logger.info(reminder);
|
|
3258
|
+
}
|
|
3259
|
+
catch (error) {
|
|
3260
|
+
logger.error({ error }, 'failed to initialize reminder system');
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
/**
|
|
3264
|
+
* Initialize Memory Manager with PostgreSQL Overflow
|
|
3265
|
+
*
|
|
3266
|
+
* Implements 100MB RAM limit with:
|
|
3267
|
+
* - 70% warning threshold
|
|
3268
|
+
* - 80% critical threshold (triggers PostgreSQL overflow)
|
|
3269
|
+
* - 90% emergency threshold (aggressive eviction)
|
|
3270
|
+
*/
|
|
3271
|
+
async function initializeMemoryManager() {
|
|
3272
|
+
// Support both SPECMEM_MEMORY_LIMIT (dashboard config) and SPECMEM_MAX_HEAP_MB (legacy)
|
|
3273
|
+
const maxHeapMB = parseInt(process.env['SPECMEM_MEMORY_LIMIT'] || process.env['SPECMEM_MAX_HEAP_MB'] || '100', 10);
|
|
3274
|
+
const warningThreshold = parseFloat(process.env['SPECMEM_MEMORY_WARNING'] || '0.7');
|
|
3275
|
+
const criticalThreshold = parseFloat(process.env['SPECMEM_MEMORY_CRITICAL'] || '0.8');
|
|
3276
|
+
const emergencyThreshold = parseFloat(process.env['SPECMEM_MEMORY_EMERGENCY'] || '0.9');
|
|
3277
|
+
logger.info({
|
|
3278
|
+
maxHeapMB,
|
|
3279
|
+
warningThreshold: `${warningThreshold * 100}%`,
|
|
3280
|
+
criticalThreshold: `${criticalThreshold * 100}%`,
|
|
3281
|
+
emergencyThreshold: `${emergencyThreshold * 100}%`
|
|
3282
|
+
}, 'initializing memory manager with RAM limits...');
|
|
3283
|
+
try {
|
|
3284
|
+
// Create memory manager with configured limits
|
|
3285
|
+
memoryManager = getMemoryManager({
|
|
3286
|
+
maxHeapBytes: maxHeapMB * 1024 * 1024,
|
|
3287
|
+
warningThreshold,
|
|
3288
|
+
criticalThreshold,
|
|
3289
|
+
emergencyThreshold,
|
|
3290
|
+
checkIntervalMs: 5000,
|
|
3291
|
+
maxCacheEntries: 1000
|
|
3292
|
+
});
|
|
3293
|
+
// Connect PostgreSQL overflow handler
|
|
3294
|
+
try {
|
|
3295
|
+
const db = getDatabase();
|
|
3296
|
+
embeddingOverflowHandler = createEmbeddingOverflowHandler(db);
|
|
3297
|
+
await embeddingOverflowHandler.initialize();
|
|
3298
|
+
memoryManager.setOverflowHandler(embeddingOverflowHandler);
|
|
3299
|
+
logger.info('memory manager connected to PostgreSQL overflow');
|
|
3300
|
+
}
|
|
3301
|
+
catch (error) {
|
|
3302
|
+
logger.warn({ error }, 'PostgreSQL overflow not available - running without persistence');
|
|
3303
|
+
}
|
|
3304
|
+
// Initialize and start monitoring
|
|
3305
|
+
memoryManager.initialize();
|
|
3306
|
+
// Log initial stats
|
|
3307
|
+
const stats = memoryManager.getStats();
|
|
3308
|
+
logger.info({
|
|
3309
|
+
heapUsedMB: Math.round(stats.heapUsed / 1024 / 1024),
|
|
3310
|
+
maxHeapMB: Math.round(stats.maxHeap / 1024 / 1024),
|
|
3311
|
+
usagePercent: `${(stats.usagePercent * 100).toFixed(1)}%`,
|
|
3312
|
+
pressureLevel: stats.pressureLevel
|
|
3313
|
+
}, 'memory manager initialized');
|
|
3314
|
+
}
|
|
3315
|
+
catch (error) {
|
|
3316
|
+
logger.error({ error }, 'failed to initialize memory manager');
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
/**
|
|
3320
|
+
* Shutdown Memory Manager
|
|
3321
|
+
*/
|
|
3322
|
+
async function shutdownMemoryManager() {
|
|
3323
|
+
if (memoryManager) {
|
|
3324
|
+
logger.info('shutting down memory manager...');
|
|
3325
|
+
await memoryManager.shutdown();
|
|
3326
|
+
await resetMemoryManager();
|
|
3327
|
+
memoryManager = null;
|
|
3328
|
+
embeddingOverflowHandler = null;
|
|
3329
|
+
logger.info('memory manager shut down');
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
/**
|
|
3333
|
+
* Shutdown Skills & Codebase Systems
|
|
3334
|
+
*/
|
|
3335
|
+
async function shutdownBrainSystems() {
|
|
3336
|
+
logger.info('shutting down brain systems...');
|
|
3337
|
+
if (skillReminder) {
|
|
3338
|
+
await skillReminder.shutdown();
|
|
3339
|
+
resetSkillReminder();
|
|
3340
|
+
}
|
|
3341
|
+
if (codebaseIndexer) {
|
|
3342
|
+
await codebaseIndexer.shutdown();
|
|
3343
|
+
resetCodebaseIndexer();
|
|
3344
|
+
}
|
|
3345
|
+
if (skillScanner) {
|
|
3346
|
+
await skillScanner.shutdown();
|
|
3347
|
+
resetSkillScanner();
|
|
3348
|
+
}
|
|
3349
|
+
logger.info('brain systems shut down');
|
|
3350
|
+
}
|
|
3351
|
+
/**
|
|
3352
|
+
* Main entry point
|
|
3353
|
+
*/
|
|
3354
|
+
// Global instance manager reference
|
|
3355
|
+
let instanceManager = null;
|
|
3356
|
+
/**
|
|
3357
|
+
* Initialize the instance manager for per-project tracking
|
|
3358
|
+
* This replaces the old cleanupStaleLocks() approach with proper per-project isolation
|
|
3359
|
+
*/
|
|
3360
|
+
async function initializeInstanceManager() {
|
|
3361
|
+
const projectPath = process.env.SPECMEM_PROJECT_PATH || process.cwd();
|
|
3362
|
+
// Migrate from old structure if needed
|
|
3363
|
+
try {
|
|
3364
|
+
await migrateFromOldStructure(projectPath);
|
|
3365
|
+
}
|
|
3366
|
+
catch (err) {
|
|
3367
|
+
logger.debug({ err }, 'Migration check failed (non-fatal)');
|
|
3368
|
+
}
|
|
3369
|
+
// Clean up any zombie instances for this project BEFORE we initialize
|
|
3370
|
+
try {
|
|
3371
|
+
const cleanup = cleanupSameProjectInstances(projectPath);
|
|
3372
|
+
if (cleanup.killed.length > 0) {
|
|
3373
|
+
logger.info({ killedPIDs: cleanup.killed, projectPath }, 'Cleaned up previous zombie instances for this project');
|
|
3374
|
+
}
|
|
3375
|
+
if (cleanup.skipped.length > 0) {
|
|
3376
|
+
logger.debug({ skippedPIDs: cleanup.skipped }, 'Skipped PIDs during cleanup');
|
|
3377
|
+
}
|
|
3378
|
+
if (cleanup.errors.length > 0) {
|
|
3379
|
+
logger.warn({ errors: cleanup.errors }, 'Some zombie instances could not be cleaned up');
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
catch (err) {
|
|
3383
|
+
logger.warn({ err }, 'Failed to cleanup zombie instances (non-fatal)');
|
|
3384
|
+
}
|
|
3385
|
+
// Initialize instance manager
|
|
3386
|
+
instanceManager = getInstanceManager({
|
|
3387
|
+
projectPath,
|
|
3388
|
+
autoCleanup: true,
|
|
3389
|
+
lockStrategy: 'both',
|
|
3390
|
+
healthCheckIntervalMs: 30000,
|
|
3391
|
+
});
|
|
3392
|
+
const result = await instanceManager.initialize();
|
|
3393
|
+
if (!result.success) {
|
|
3394
|
+
if (result.alreadyRunning) {
|
|
3395
|
+
logger.warn('Another SpecMem instance is already running for this project');
|
|
3396
|
+
// Don't throw - allow this instance to continue but log the conflict
|
|
3397
|
+
}
|
|
3398
|
+
else if (result.error) {
|
|
3399
|
+
logger.warn({ error: result.error }, 'Instance manager initialization issue (non-fatal)');
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
else {
|
|
3403
|
+
logger.info({
|
|
3404
|
+
projectPath,
|
|
3405
|
+
instanceDir: instanceManager.getInstanceDir(),
|
|
3406
|
+
pid: process.pid,
|
|
3407
|
+
}, 'Instance manager initialized - per-project tracking enabled');
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
/**
|
|
3411
|
+
* Clean up stale lock files from previous runs
|
|
3412
|
+
* This now uses the InstanceManager for per-project instance tracking
|
|
3413
|
+
* IMPORTANT: Checks file age to avoid race conditions with new processes
|
|
3414
|
+
*
|
|
3415
|
+
* @deprecated Use InstanceManager.cleanupStaleLocks() instead
|
|
3416
|
+
*/
|
|
3417
|
+
function cleanupStaleLocks() {
|
|
3418
|
+
// Use the new instance manager if available
|
|
3419
|
+
if (instanceManager) {
|
|
3420
|
+
instanceManager.cleanupStaleLocks();
|
|
3421
|
+
return;
|
|
3422
|
+
}
|
|
3423
|
+
// Fallback to legacy behavior for backward compatibility
|
|
3424
|
+
const specmemDir = join(process.cwd(), '.specmem');
|
|
3425
|
+
if (!existsSync(specmemDir)) {
|
|
3426
|
+
return; // No lock dir, nothing to clean
|
|
3427
|
+
}
|
|
3428
|
+
const pidFile = join(specmemDir, 'specmem.pid');
|
|
3429
|
+
const sockFile = join(specmemDir, 'specmem.sock');
|
|
3430
|
+
const instanceFile = join(specmemDir, 'instance.json');
|
|
3431
|
+
// Check PID file age - don't clean if too recent (might be a new process starting)
|
|
3432
|
+
const MIN_AGE_MS = 10000; // 10 seconds
|
|
3433
|
+
if (existsSync(pidFile)) {
|
|
3434
|
+
try {
|
|
3435
|
+
const stats = statSync(pidFile);
|
|
3436
|
+
const pidFileAge = Date.now() - stats.mtimeMs;
|
|
3437
|
+
// If the PID file is very recent, skip cleanup to avoid race condition
|
|
3438
|
+
if (pidFileAge < MIN_AGE_MS) {
|
|
3439
|
+
logger.debug({ ageMs: pidFileAge }, 'PID file is too recent, skipping stale check');
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
catch (e) {
|
|
3444
|
+
// Can't stat, continue with cleanup check
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
// Check if there's a stale PID file
|
|
3448
|
+
if (existsSync(pidFile)) {
|
|
3449
|
+
try {
|
|
3450
|
+
const pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
|
|
3451
|
+
// Never clean our own PID!
|
|
3452
|
+
if (pid === process.pid) {
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
// Check if the process is still running
|
|
3456
|
+
try {
|
|
3457
|
+
process.kill(pid, 0); // Signal 0 = check if process exists
|
|
3458
|
+
// Process exists - check if it's actually a specmem/node process
|
|
3459
|
+
const cmdline = execSync(`ps -p ${pid} -o comm= 2>/dev/null || echo ""`, { encoding: 'utf8' }).trim();
|
|
3460
|
+
if (cmdline.includes('node') || cmdline.includes('bootstrap')) {
|
|
3461
|
+
// It's a node process, but is it US starting up? Check if it's very old
|
|
3462
|
+
if (existsSync(instanceFile)) {
|
|
3463
|
+
const instance = JSON.parse(readFileSync(instanceFile, 'utf8'));
|
|
3464
|
+
const startTime = new Date(instance.startTime).getTime();
|
|
3465
|
+
const ageMs = Date.now() - startTime;
|
|
3466
|
+
// If it's been running for more than 1 hour and we're starting fresh, kill it
|
|
3467
|
+
if (ageMs > 3600000) {
|
|
3468
|
+
logger.warn({ pid, ageMs }, 'Killing very old specmem process to allow fresh start');
|
|
3469
|
+
try {
|
|
3470
|
+
process.kill(pid, 'SIGTERM');
|
|
3471
|
+
}
|
|
3472
|
+
catch (e) { /* ignore */ }
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
catch (e) {
|
|
3478
|
+
// Process doesn't exist - stale lock!
|
|
3479
|
+
logger.info({ pid }, 'Cleaning up stale lock files from dead process');
|
|
3480
|
+
try {
|
|
3481
|
+
unlinkSync(pidFile);
|
|
3482
|
+
}
|
|
3483
|
+
catch (e) { /* ignore */ }
|
|
3484
|
+
try {
|
|
3485
|
+
unlinkSync(sockFile);
|
|
3486
|
+
}
|
|
3487
|
+
catch (e) { /* ignore */ }
|
|
3488
|
+
try {
|
|
3489
|
+
unlinkSync(instanceFile);
|
|
3490
|
+
}
|
|
3491
|
+
catch (e) { /* ignore */ }
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
catch (e) {
|
|
3495
|
+
// Can't read PID file - just clean up
|
|
3496
|
+
logger.debug('Cleaning up unreadable lock files');
|
|
3497
|
+
try {
|
|
3498
|
+
unlinkSync(pidFile);
|
|
3499
|
+
}
|
|
3500
|
+
catch (e) { /* ignore */ }
|
|
3501
|
+
try {
|
|
3502
|
+
unlinkSync(sockFile);
|
|
3503
|
+
}
|
|
3504
|
+
catch (e) { /* ignore */ }
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
// Also clean stale socket if it exists without a PID file
|
|
3508
|
+
if (existsSync(sockFile) && !existsSync(pidFile)) {
|
|
3509
|
+
logger.info('Cleaning up orphaned socket file');
|
|
3510
|
+
try {
|
|
3511
|
+
unlinkSync(sockFile);
|
|
3512
|
+
}
|
|
3513
|
+
catch (e) { /* ignore */ }
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
async function main() {
|
|
3517
|
+
startupLog('main() CALLED - entering main async function');
|
|
3518
|
+
// =========================================================================
|
|
3519
|
+
// ENSURE PROJECT ENVIRONMENT IS SET
|
|
3520
|
+
// This must happen early before any project-scoped operations
|
|
3521
|
+
// =========================================================================
|
|
3522
|
+
ensureProjectEnv();
|
|
3523
|
+
const projectInfo = getProjectInfo();
|
|
3524
|
+
startupLog(`Project path: ${projectInfo.path}`);
|
|
3525
|
+
startupLog(`Project hash: ${projectInfo.hashFull}`);
|
|
3526
|
+
startupLog(`Instance dir: ${projectInfo.instanceDir}`);
|
|
3527
|
+
// NOTE: cleanupStaleLocks() moved to after MCP transport connection
|
|
3528
|
+
// to ensure fastest possible startup time
|
|
3529
|
+
logger.info('Starting SpecMem MCP Server - THE BRAIN OF CLAUDE...');
|
|
3530
|
+
logger.info({ ...projectInfo }, 'Project environment initialized');
|
|
3531
|
+
startupLog('Starting SpecMem MCP Server...');
|
|
3532
|
+
// =========================================================================
|
|
3533
|
+
// PRE-FLIGHT VALIDATION (FAST - must complete in < 100ms)
|
|
3534
|
+
// Validates socket directories and environment variables BEFORE MCP connects.
|
|
3535
|
+
// Database validation is deferred to Phase 2 to not block MCP connection.
|
|
3536
|
+
// =========================================================================
|
|
3537
|
+
startupLog('Running pre-flight validation (fast checks only)...');
|
|
3538
|
+
let preflightResult;
|
|
3539
|
+
try {
|
|
3540
|
+
preflightResult = await quickValidation();
|
|
3541
|
+
if (!preflightResult.valid) {
|
|
3542
|
+
// Critical errors found - log and exit before MCP connects
|
|
3543
|
+
const errorOutput = formatValidationErrors(preflightResult);
|
|
3544
|
+
process.stderr.write(errorOutput);
|
|
3545
|
+
startupLog(`Pre-flight validation FAILED with ${preflightResult.errors.length} errors`);
|
|
3546
|
+
// Log each error for debugging
|
|
3547
|
+
for (const error of preflightResult.errors) {
|
|
3548
|
+
startupLog(` ERROR [${error.code}]: ${error.message}`);
|
|
3549
|
+
logger.error({
|
|
3550
|
+
code: error.code,
|
|
3551
|
+
message: error.message,
|
|
3552
|
+
details: error.details,
|
|
3553
|
+
suggestion: error.suggestion,
|
|
3554
|
+
}, 'Startup validation error');
|
|
3555
|
+
}
|
|
3556
|
+
// Exit with the first error's code
|
|
3557
|
+
const exitCode = preflightResult.errors[0]?.code || EXIT_CODES.GENERAL_ERROR;
|
|
3558
|
+
process.exit(exitCode);
|
|
3559
|
+
}
|
|
3560
|
+
// Log warnings but continue
|
|
3561
|
+
if (preflightResult.warnings.length > 0) {
|
|
3562
|
+
startupLog(`Pre-flight validation passed with ${preflightResult.warnings.length} warnings`);
|
|
3563
|
+
for (const warning of preflightResult.warnings) {
|
|
3564
|
+
logger.warn({ warning }, 'Startup validation warning');
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
else {
|
|
3568
|
+
startupLog(`Pre-flight validation passed (${preflightResult.duration}ms)`);
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
catch (validationError) {
|
|
3572
|
+
// Validation itself failed - log but continue (don't block MCP connection)
|
|
3573
|
+
startupLog('Pre-flight validation threw an exception - continuing anyway', validationError);
|
|
3574
|
+
logger.error({ error: validationError }, 'Pre-flight validation failed unexpectedly');
|
|
3575
|
+
preflightResult = { valid: true, errors: [], warnings: [], duration: 0 };
|
|
3576
|
+
}
|
|
3577
|
+
// ==========================================================================
|
|
3578
|
+
// CRITICAL TIMING FIX: MCP CONNECTION MUST BE ESTABLISHED FIRST!
|
|
3579
|
+
// ==========================================================================
|
|
3580
|
+
//
|
|
3581
|
+
// Code has a short timeout (~5-10s) for MCP server connections.
|
|
3582
|
+
// If we don't establish the stdio transport quickly, shows:
|
|
3583
|
+
// "Failed to connect to MCP server"
|
|
3584
|
+
//
|
|
3585
|
+
// PREVIOUS BUG: We did heavy initialization BEFORE starting the server:
|
|
3586
|
+
// - Config sync (file I/O)
|
|
3587
|
+
// - Deploy to (file I/O)
|
|
3588
|
+
// - Database init (can be 1-3s if cold)
|
|
3589
|
+
// - Config injection (file I/O)
|
|
3590
|
+
// - Embedding provider creation (DB queries)
|
|
3591
|
+
// All of this could take 5-10+ seconds, causing to timeout!
|
|
3592
|
+
//
|
|
3593
|
+
// NEW APPROACH: Start MCP server IMMEDIATELY, defer everything else.
|
|
3594
|
+
// The server connects transport first, then initializes DB in background.
|
|
3595
|
+
// Tools will wait for DB if they need it.
|
|
3596
|
+
// ==========================================================================
|
|
3597
|
+
// === PHASE 1: FAST MCP CONNECTION (< 500ms target) ===
|
|
3598
|
+
startupLog('PHASE 1: Creating deferred embedding provider...');
|
|
3599
|
+
// Create minimal embedding provider stub - will be upgraded after DB init
|
|
3600
|
+
// Using null-safe pattern that defers to real provider once ready
|
|
3601
|
+
let embeddingProvider = null;
|
|
3602
|
+
let embeddingProviderReady = false;
|
|
3603
|
+
// HIGH-23 FIX: Queue for pending embedding requests instead of returning garbage
|
|
3604
|
+
// Requests wait for provider to be ready, with timeout to prevent indefinite hangs
|
|
3605
|
+
// REDUCED from 30s to 5s to fail fast and not make MCP appear unresponsive
|
|
3606
|
+
const EMBEDDING_PROVIDER_TIMEOUT_MS = 5000; // 5 second timeout - fail fast!
|
|
3607
|
+
const pendingEmbeddingQueue = [];
|
|
3608
|
+
// Process queued requests once provider is ready
|
|
3609
|
+
const processEmbeddingQueue = async () => {
|
|
3610
|
+
if (!embeddingProviderReady || !embeddingProvider)
|
|
3611
|
+
return;
|
|
3612
|
+
while (pendingEmbeddingQueue.length > 0) {
|
|
3613
|
+
const request = pendingEmbeddingQueue.shift();
|
|
3614
|
+
if (!request)
|
|
3615
|
+
continue;
|
|
3616
|
+
// Check if request has timed out
|
|
3617
|
+
const elapsed = Date.now() - request.timestamp;
|
|
3618
|
+
if (elapsed > EMBEDDING_PROVIDER_TIMEOUT_MS) {
|
|
3619
|
+
request.reject(new Error(`Embedding request timed out after ${EMBEDDING_PROVIDER_TIMEOUT_MS}ms`));
|
|
3620
|
+
continue;
|
|
3621
|
+
}
|
|
3622
|
+
try {
|
|
3623
|
+
const embedding = await embeddingProvider.generateEmbedding(request.text);
|
|
3624
|
+
request.resolve(embedding);
|
|
3625
|
+
}
|
|
3626
|
+
catch (err) {
|
|
3627
|
+
request.reject(err instanceof Error ? err : new Error(String(err)));
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
};
|
|
3631
|
+
// Stub provider that queues requests until real provider is ready
|
|
3632
|
+
const deferredEmbeddingProvider = {
|
|
3633
|
+
async generateEmbedding(text) {
|
|
3634
|
+
// If provider is ready, use it directly
|
|
3635
|
+
if (embeddingProviderReady && embeddingProvider) {
|
|
3636
|
+
return embeddingProvider.generateEmbedding(text);
|
|
3637
|
+
}
|
|
3638
|
+
// HIGH-23 FIX: Queue the request and wait for provider to be ready
|
|
3639
|
+
// Instead of returning garbage placeholder data, we properly queue
|
|
3640
|
+
logger.debug('Embedding provider not ready yet, queueing request');
|
|
3641
|
+
return new Promise((resolve, reject) => {
|
|
3642
|
+
const timestamp = Date.now();
|
|
3643
|
+
pendingEmbeddingQueue.push({ text, resolve, reject, timestamp });
|
|
3644
|
+
// Set timeout to reject if provider doesn't become ready
|
|
3645
|
+
setTimeout(() => {
|
|
3646
|
+
const idx = pendingEmbeddingQueue.findIndex(r => r.resolve === resolve && r.timestamp === timestamp);
|
|
3647
|
+
if (idx !== -1) {
|
|
3648
|
+
pendingEmbeddingQueue.splice(idx, 1);
|
|
3649
|
+
reject(new Error(`Embedding service starting up - retry in a few seconds (waited ${EMBEDDING_PROVIDER_TIMEOUT_MS}ms)`));
|
|
3650
|
+
}
|
|
3651
|
+
}, EMBEDDING_PROVIDER_TIMEOUT_MS);
|
|
3652
|
+
});
|
|
3653
|
+
}
|
|
3654
|
+
};
|
|
3655
|
+
// Create server with deferred embedding provider
|
|
3656
|
+
startupLog('Creating SpecMemServer instance...');
|
|
3657
|
+
const server = new SpecMemServer(deferredEmbeddingProvider);
|
|
3658
|
+
startupLog('SpecMemServer instance created');
|
|
3659
|
+
// START MCP SERVER IMMEDIATELY - establishes transport connection
|
|
3660
|
+
// This is the CRITICAL path - must complete in < 1 second
|
|
3661
|
+
startupLog('CRITICAL: About to call server.start() - this establishes MCP transport connection');
|
|
3662
|
+
const startTime = Date.now();
|
|
3663
|
+
await server.start();
|
|
3664
|
+
const startDuration = Date.now() - startTime;
|
|
3665
|
+
startupLog(`MCP SERVER STARTED in ${startDuration}ms - transport connection established!`);
|
|
3666
|
+
logger.info('MCP server started - connection established!');
|
|
3667
|
+
// ==========================================================================
|
|
3668
|
+
// EARLY EMBEDDING CHECK: If socket exists and responds, mark ready NOW!
|
|
3669
|
+
// This allows find_memory to work IMMEDIATELY if server is already running
|
|
3670
|
+
// ==========================================================================
|
|
3671
|
+
const earlySocketPath = getEmbeddingSocketPath();
|
|
3672
|
+
if (existsSync(earlySocketPath)) {
|
|
3673
|
+
startupLog('Embedding socket exists - testing if server is already running...');
|
|
3674
|
+
try {
|
|
3675
|
+
const earlyTestResult = await new Promise((resolve) => {
|
|
3676
|
+
const testSocket = createConnection(earlySocketPath);
|
|
3677
|
+
const timeout = setTimeout(() => { testSocket.destroy(); resolve(false); }, 2000);
|
|
3678
|
+
testSocket.on('connect', () => { testSocket.write('{"type":"health"}\n'); });
|
|
3679
|
+
testSocket.on('data', () => { clearTimeout(timeout); testSocket.end(); resolve(true); });
|
|
3680
|
+
testSocket.on('error', () => { clearTimeout(timeout); resolve(false); });
|
|
3681
|
+
});
|
|
3682
|
+
if (earlyTestResult) {
|
|
3683
|
+
startupLog('FAST PATH: Embedding server already running! Creating early provider...');
|
|
3684
|
+
// Create minimal provider that talks directly to socket
|
|
3685
|
+
const socketPath = earlySocketPath;
|
|
3686
|
+
embeddingProvider = {
|
|
3687
|
+
generateEmbedding: async (text) => {
|
|
3688
|
+
return new Promise((resolve, reject) => {
|
|
3689
|
+
const socket = createConnection(socketPath);
|
|
3690
|
+
let buffer = '';
|
|
3691
|
+
let resolved = false;
|
|
3692
|
+
const timeout = setTimeout(() => {
|
|
3693
|
+
if (!resolved) { resolved = true; socket.destroy(); reject(new Error('Embedding timeout')); }
|
|
3694
|
+
}, 60000);
|
|
3695
|
+
socket.on('connect', () => { socket.write(JSON.stringify({ text }) + '\n'); });
|
|
3696
|
+
socket.on('data', (data) => {
|
|
3697
|
+
buffer += data.toString();
|
|
3698
|
+
let idx;
|
|
3699
|
+
while ((idx = buffer.indexOf('\n')) !== -1) {
|
|
3700
|
+
if (resolved) return;
|
|
3701
|
+
const line = buffer.slice(0, idx);
|
|
3702
|
+
buffer = buffer.slice(idx + 1);
|
|
3703
|
+
try {
|
|
3704
|
+
const resp = JSON.parse(line);
|
|
3705
|
+
if (resp.error) { clearTimeout(timeout); resolved = true; socket.end(); reject(new Error(resp.error)); return; }
|
|
3706
|
+
if (resp.status === 'processing') continue;
|
|
3707
|
+
if (resp.embedding && Array.isArray(resp.embedding)) {
|
|
3708
|
+
clearTimeout(timeout); resolved = true; socket.end(); resolve(resp.embedding); return;
|
|
3709
|
+
}
|
|
3710
|
+
} catch (e) { /* ignore parse errors */ }
|
|
3711
|
+
}
|
|
3712
|
+
});
|
|
3713
|
+
socket.on('error', (e) => { clearTimeout(timeout); if (!resolved) { resolved = true; reject(e); } });
|
|
3714
|
+
});
|
|
3715
|
+
},
|
|
3716
|
+
generateEmbeddingsBatch: async (texts) => {
|
|
3717
|
+
return Promise.all(texts.map(t => embeddingProvider.generateEmbedding(t)));
|
|
3718
|
+
}
|
|
3719
|
+
};
|
|
3720
|
+
embeddingProviderReady = true;
|
|
3721
|
+
logger.info('EARLY EMBEDDING PROVIDER READY - find_memory will work immediately!');
|
|
3722
|
+
// CRITICAL: Start KYS heartbeat to keep embedding server alive!
|
|
3723
|
+
// Without this, the server will suicide after 90s of no heartbeat
|
|
3724
|
+
const { EmbeddingServerManager } = await import('./mcp/embeddingServerManager.js');
|
|
3725
|
+
const earlyManager = EmbeddingServerManager.getInstance();
|
|
3726
|
+
earlyManager.startKysHeartbeat();
|
|
3727
|
+
logger.info('KYS heartbeat started for early provider');
|
|
3728
|
+
// Process any already-queued requests
|
|
3729
|
+
if (pendingEmbeddingQueue.length > 0) {
|
|
3730
|
+
startupLog(`Processing ${pendingEmbeddingQueue.length} early-queued requests`);
|
|
3731
|
+
processEmbeddingQueue().catch(e => logger.warn({ e }, 'Early queue processing failed'));
|
|
3732
|
+
}
|
|
3733
|
+
} else {
|
|
3734
|
+
startupLog('Embedding socket exists but server not responding - will initialize normally');
|
|
3735
|
+
}
|
|
3736
|
+
} catch (e) {
|
|
3737
|
+
startupLog('Early embedding check failed (non-fatal): ' + e.message);
|
|
3738
|
+
}
|
|
3739
|
+
} else {
|
|
3740
|
+
startupLog('No existing embedding socket - will initialize normally');
|
|
3741
|
+
}
|
|
3742
|
+
// ==========================================================================
|
|
3743
|
+
// PHASE 2: DEFERRED INITIALIZATION (runs after MCP connection established)
|
|
3744
|
+
// ==========================================================================
|
|
3745
|
+
// These can take time but is already connected and won't timeout
|
|
3746
|
+
startupLog('PHASE 2: Beginning deferred initialization (MCP already connected)');
|
|
3747
|
+
// Initialize instance manager for per-project tracking
|
|
3748
|
+
// This replaces the old cleanupStaleLocks() with proper project isolation
|
|
3749
|
+
try {
|
|
3750
|
+
await initializeInstanceManager();
|
|
3751
|
+
startupLog('Instance manager initialized - per-project tracking enabled');
|
|
3752
|
+
}
|
|
3753
|
+
catch (err) {
|
|
3754
|
+
logger.warn({ err }, 'Instance manager initialization failed (non-fatal), falling back to legacy cleanup');
|
|
3755
|
+
cleanupStaleLocks();
|
|
3756
|
+
}
|
|
3757
|
+
startupLog('Stale locks cleaned (deferred)');
|
|
3758
|
+
// ==========================================================================
|
|
3759
|
+
// EMBEDDING SERVER MANAGER - ENSURE FRESH START
|
|
3760
|
+
// ==========================================================================
|
|
3761
|
+
// CRITICAL: Initialize embedding server manager EARLY to ensure fresh start
|
|
3762
|
+
// This kills any old embedding servers and removes stale sockets BEFORE
|
|
3763
|
+
// LocalEmbeddingProvider tries to connect
|
|
3764
|
+
startupLog('Initializing EmbeddingServerManager for fresh start...');
|
|
3765
|
+
let embeddingManager = null;
|
|
3766
|
+
try {
|
|
3767
|
+
// CRITICAL FIX: Use the singleton getter so MCP tools share the same instance!
|
|
3768
|
+
// Previously this used `new EmbeddingServerManager()` directly, causing two separate
|
|
3769
|
+
// manager instances - startup had heartbeat running, tools didn't share it
|
|
3770
|
+
embeddingManager = getEmbeddingServerManager({
|
|
3771
|
+
healthCheckIntervalMs: 30000,
|
|
3772
|
+
// FIX: Increased from 5s to 15s - health checks during startup can take longer
|
|
3773
|
+
// while the model is still loading into memory. 5s was causing false negatives.
|
|
3774
|
+
healthCheckTimeoutMs: 15000,
|
|
3775
|
+
maxFailuresBeforeRestart: 3,
|
|
3776
|
+
restartCooldownMs: 10000,
|
|
3777
|
+
// FIX: Reduced from 60s to 45s to match DEFAULT_CONFIG and avoid unnecessary waiting
|
|
3778
|
+
// The server should be ready well within 45s; 60s just delays error detection
|
|
3779
|
+
startupTimeoutMs: 45000,
|
|
3780
|
+
maxRestartAttempts: 5,
|
|
3781
|
+
autoStart: true, // Auto-start the embedding server
|
|
3782
|
+
killStaleOnStart: true, // CRITICAL: Kill any stale processes
|
|
3783
|
+
maxProcessAgeHours: 1
|
|
3784
|
+
});
|
|
3785
|
+
await embeddingManager.initialize();
|
|
3786
|
+
logger.info('EmbeddingServerManager: Fresh embedding server started');
|
|
3787
|
+
startupLog('EmbeddingServerManager initialized - fresh server ready');
|
|
3788
|
+
}
|
|
3789
|
+
catch (err) {
|
|
3790
|
+
logger.warn({ error: err }, 'EmbeddingServerManager initialization failed (non-fatal)');
|
|
3791
|
+
startupLog('EmbeddingServerManager failed (non-fatal)', err);
|
|
3792
|
+
// LOW-32 FIX: Set explicit fallback state to indicate manager failed initialization
|
|
3793
|
+
// This prevents ambiguity between "not initialized yet" and "failed to initialize"
|
|
3794
|
+
embeddingManager = null;
|
|
3795
|
+
// Set an env var so downstream code knows embedding manager failed
|
|
3796
|
+
process.env['SPECMEM_EMBEDDING_MANAGER_FAILED'] = 'true';
|
|
3797
|
+
}
|
|
3798
|
+
// Cleanup orphaned embedding processes for THIS project
|
|
3799
|
+
// CRITICAL FIX: SKIP if EmbeddingServerManager is active - it already handles this!
|
|
3800
|
+
// The legacy cleanup was killing servers that the manager JUST started (race condition)
|
|
3801
|
+
if (embeddingManager && embeddingManager.isRunning) {
|
|
3802
|
+
startupLog('Skipping legacy orphan cleanup - EmbeddingServerManager is active');
|
|
3803
|
+
}
|
|
3804
|
+
const cleanupOrphanedEmbeddings = async () => {
|
|
3805
|
+
// CRITICAL: Skip cleanup if embedding manager started successfully
|
|
3806
|
+
if (embeddingManager && embeddingManager.isRunning) {
|
|
3807
|
+
return; // Manager owns the embedding server, don't kill it!
|
|
3808
|
+
}
|
|
3809
|
+
const projectPath = getProjectPath();
|
|
3810
|
+
const socketDir = path.join(projectPath, 'specmem', 'sockets');
|
|
3811
|
+
const socketPath = path.join(socketDir, 'embeddings.sock');
|
|
3812
|
+
const lockPath = path.join(socketDir, 'embedding.lock');
|
|
3813
|
+
const pidFile = path.join(socketDir, 'embedding.pid');
|
|
3814
|
+
try {
|
|
3815
|
+
// STEP 1: Kill orphaned embedding process using PID file
|
|
3816
|
+
if (existsSync(pidFile)) {
|
|
3817
|
+
try {
|
|
3818
|
+
const pidContent = readFileSync(pidFile, 'utf8').trim();
|
|
3819
|
+
const [oldPid, spawnTime] = pidContent.split(':').map(Number);
|
|
3820
|
+
if (oldPid && !isNaN(oldPid)) {
|
|
3821
|
+
// Check if process is still running
|
|
3822
|
+
let isRunning = false;
|
|
3823
|
+
try {
|
|
3824
|
+
process.kill(oldPid, 0);
|
|
3825
|
+
isRunning = true;
|
|
3826
|
+
}
|
|
3827
|
+
catch {
|
|
3828
|
+
isRunning = false;
|
|
3829
|
+
}
|
|
3830
|
+
if (isRunning) {
|
|
3831
|
+
startupLog(`Killing orphaned embedding server: PID ${oldPid} (spawned ${Date.now() - spawnTime}ms ago)`);
|
|
3832
|
+
try {
|
|
3833
|
+
process.kill(oldPid, 'SIGTERM');
|
|
3834
|
+
// Wait a bit for graceful shutdown
|
|
3835
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
3836
|
+
// Force kill if still running
|
|
3837
|
+
try {
|
|
3838
|
+
process.kill(oldPid, 0);
|
|
3839
|
+
process.kill(oldPid, 'SIGKILL');
|
|
3840
|
+
}
|
|
3841
|
+
catch { /* dead */ }
|
|
3842
|
+
}
|
|
3843
|
+
catch (killErr) {
|
|
3844
|
+
logger.warn({ killErr, pid: oldPid }, 'Failed to kill orphaned embedding (may be already dead)');
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
// Clean up PID file regardless
|
|
3848
|
+
try {
|
|
3849
|
+
unlinkSync(pidFile);
|
|
3850
|
+
}
|
|
3851
|
+
catch { /* ignore */ }
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
catch (pidErr) {
|
|
3855
|
+
logger.warn({ pidErr, pidFile }, 'Failed to read/process embedding PID file');
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
// STEP 2: Clean up stale socket if not responsive
|
|
3859
|
+
if (existsSync(socketPath)) {
|
|
3860
|
+
const testAlive = () => new Promise((resolve) => {
|
|
3861
|
+
const testSocket = createConnection(socketPath);
|
|
3862
|
+
const timeout = setTimeout(() => { testSocket.destroy(); resolve(false); }, 2000);
|
|
3863
|
+
testSocket.on('connect', () => {
|
|
3864
|
+
clearTimeout(timeout);
|
|
3865
|
+
testSocket.write('{"text":"test"}\n');
|
|
3866
|
+
});
|
|
3867
|
+
testSocket.on('data', () => { clearTimeout(timeout); testSocket.destroy(); resolve(true); });
|
|
3868
|
+
testSocket.on('error', () => { clearTimeout(timeout); testSocket.destroy(); resolve(false); });
|
|
3869
|
+
});
|
|
3870
|
+
const isAlive = await testAlive();
|
|
3871
|
+
if (!isAlive) {
|
|
3872
|
+
startupLog(`Cleaning stale embedding socket: ${socketPath}`);
|
|
3873
|
+
try {
|
|
3874
|
+
unlinkSync(socketPath);
|
|
3875
|
+
}
|
|
3876
|
+
catch { /* ignore */ }
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
// STEP 3: Clean up stale lock files
|
|
3880
|
+
if (existsSync(lockPath)) {
|
|
3881
|
+
try {
|
|
3882
|
+
const lockContent = readFileSync(lockPath, 'utf8').trim();
|
|
3883
|
+
const [lockPid, lockTime] = lockContent.split(':').map(Number);
|
|
3884
|
+
const lockAge = Date.now() - lockTime;
|
|
3885
|
+
let pidRunning = false;
|
|
3886
|
+
try {
|
|
3887
|
+
process.kill(lockPid, 0);
|
|
3888
|
+
pidRunning = true;
|
|
3889
|
+
}
|
|
3890
|
+
catch {
|
|
3891
|
+
pidRunning = false;
|
|
3892
|
+
}
|
|
3893
|
+
if (!pidRunning || lockAge > 300000) {
|
|
3894
|
+
startupLog(`Cleaning stale embedding lock: pid=${lockPid}, age=${lockAge}ms`);
|
|
3895
|
+
unlinkSync(lockPath);
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
catch { /* ignore */ }
|
|
3899
|
+
}
|
|
3900
|
+
logger.debug({ projectPath }, 'Embedding orphan cleanup completed for project');
|
|
3901
|
+
}
|
|
3902
|
+
catch (err) {
|
|
3903
|
+
logger.warn({ err, projectPath }, 'Embedding cleanup failed (non-fatal)');
|
|
3904
|
+
}
|
|
3905
|
+
};
|
|
3906
|
+
try {
|
|
3907
|
+
await cleanupOrphanedEmbeddings();
|
|
3908
|
+
startupLog('Orphaned embeddings cleaned');
|
|
3909
|
+
}
|
|
3910
|
+
catch (err) {
|
|
3911
|
+
startupLog('Embedding cleanup failed (non-fatal)', err);
|
|
3912
|
+
}
|
|
3913
|
+
// Track deployment result for banner display later
|
|
3914
|
+
let deployResult = {
|
|
3915
|
+
hooksDeployed: [],
|
|
3916
|
+
hooksSkipped: [],
|
|
3917
|
+
commandsDeployed: [],
|
|
3918
|
+
commandsSkipped: [],
|
|
3919
|
+
settingsUpdated: false,
|
|
3920
|
+
errors: [],
|
|
3921
|
+
success: true,
|
|
3922
|
+
version: '0.0.0'
|
|
3923
|
+
};
|
|
3924
|
+
// --- Config Sync (deferred, non-blocking) ---
|
|
3925
|
+
startupLog('Config sync starting...');
|
|
3926
|
+
try {
|
|
3927
|
+
await syncConfigToUserFile(config);
|
|
3928
|
+
logger.info('[ConfigSync] Config synced to ~/.specmem/config.json');
|
|
3929
|
+
startupLog('Config sync complete');
|
|
3930
|
+
}
|
|
3931
|
+
catch (error) {
|
|
3932
|
+
logger.warn('[ConfigSync] Failed to sync config (non-fatal):', error);
|
|
3933
|
+
startupLog('Config sync failed (non-fatal)', error);
|
|
3934
|
+
}
|
|
3935
|
+
// --- Deploy Hooks and Commands (deferred, non-blocking) ---
|
|
3936
|
+
startupLog('Deploy to starting...');
|
|
3937
|
+
try {
|
|
3938
|
+
deployResult = await deployTo();
|
|
3939
|
+
const totalDeployed = deployResult.hooksDeployed.length + deployResult.commandsDeployed.length;
|
|
3940
|
+
const totalSkipped = deployResult.hooksSkipped.length + deployResult.commandsSkipped.length;
|
|
3941
|
+
if (totalDeployed > 0) {
|
|
3942
|
+
logger.info('[DeployTo] Deployed to :', {
|
|
3943
|
+
version: deployResult.version,
|
|
3944
|
+
hooksDeployed: deployResult.hooksDeployed.length,
|
|
3945
|
+
hooksSkipped: deployResult.hooksSkipped.length,
|
|
3946
|
+
commandsDeployed: deployResult.commandsDeployed.length,
|
|
3947
|
+
commandsSkipped: deployResult.commandsSkipped.length,
|
|
3948
|
+
settingsUpdated: deployResult.settingsUpdated
|
|
3949
|
+
});
|
|
3950
|
+
}
|
|
3951
|
+
else if (totalSkipped > 0) {
|
|
3952
|
+
logger.info('[DeployTo] All files up-to-date', {
|
|
3953
|
+
version: deployResult.version,
|
|
3954
|
+
filesChecked: totalSkipped
|
|
3955
|
+
});
|
|
3956
|
+
}
|
|
3957
|
+
else {
|
|
3958
|
+
logger.info('[DeployTo] No files to deploy');
|
|
3959
|
+
}
|
|
3960
|
+
if (deployResult.errors.length > 0) {
|
|
3961
|
+
logger.warn('[DeployTo] Deployment warnings:', deployResult.errors);
|
|
3962
|
+
}
|
|
3963
|
+
startupLog('Deploy to complete');
|
|
3964
|
+
}
|
|
3965
|
+
catch (error) {
|
|
3966
|
+
logger.warn('[DeployTo] Failed to deploy (non-fatal):', error);
|
|
3967
|
+
startupLog('Deploy to failed (non-fatal)', error);
|
|
3968
|
+
}
|
|
3969
|
+
// --- Database Initialization (critical but deferred) ---
|
|
3970
|
+
// Note: Server already initialized DB via its own deferred init
|
|
3971
|
+
// This is a safety check / upgrade path
|
|
3972
|
+
startupLog('Database initialization starting...');
|
|
3973
|
+
logger.info('[Database] Ensuring database connection is ready...');
|
|
3974
|
+
try {
|
|
3975
|
+
const db = getDatabase(config.database);
|
|
3976
|
+
// Check if already initialized by server, if not initialize
|
|
3977
|
+
if (!db.isConnected()) {
|
|
3978
|
+
startupLog('Database not connected, initializing...');
|
|
3979
|
+
await db.initialize();
|
|
3980
|
+
}
|
|
3981
|
+
logger.info('[Database] Database ready');
|
|
3982
|
+
startupLog('Database ready');
|
|
3983
|
+
// yooo THIS IS THE ROOT CAUSE FIX!
|
|
3984
|
+
// The watcher needs getDbContext() which requires initializeTheBigBrainDb()
|
|
3985
|
+
// Without this, watcher fails silently and we lose file watching capability
|
|
3986
|
+
startupLog('Initializing BigBrain database layer...');
|
|
3987
|
+
try {
|
|
3988
|
+
await initializeTheBigBrainDb(config.database, false); // migrations already run by db.initialize()
|
|
3989
|
+
logger.info('[Database] BigBrain database layer initialized - watchers can now function');
|
|
3990
|
+
startupLog('BigBrain database layer ready');
|
|
3991
|
+
}
|
|
3992
|
+
catch (bigBrainErr) {
|
|
3993
|
+
// nah fr this is critical - watcher won't work without it
|
|
3994
|
+
logger.error({ error: bigBrainErr }, '[Database] Failed to initialize BigBrain layer (watcher will be disabled)');
|
|
3995
|
+
startupLog('BigBrain database layer FAILED', bigBrainErr);
|
|
3996
|
+
// Don't throw - let the server continue, watcher will just be disabled
|
|
3997
|
+
}
|
|
3998
|
+
// --- Full Validation (deferred - includes database checks) ---
|
|
3999
|
+
// Now that DB is connected, run full validation to catch any remaining issues
|
|
4000
|
+
startupLog('Running full validation (including database)...');
|
|
4001
|
+
try {
|
|
4002
|
+
const fullResult = await fullValidation();
|
|
4003
|
+
if (!fullResult.valid) {
|
|
4004
|
+
// Database validation failed - log errors but don't exit
|
|
4005
|
+
// (MCP is already connected, tools may still work partially)
|
|
4006
|
+
const errorOutput = formatValidationErrors(fullResult);
|
|
4007
|
+
process.stderr.write(errorOutput);
|
|
4008
|
+
startupLog(`Full validation FAILED with ${fullResult.errors.length} errors (non-fatal)`);
|
|
4009
|
+
for (const error of fullResult.errors) {
|
|
4010
|
+
logger.error({
|
|
4011
|
+
code: error.code,
|
|
4012
|
+
message: error.message,
|
|
4013
|
+
details: error.details,
|
|
4014
|
+
suggestion: error.suggestion,
|
|
4015
|
+
}, 'Database validation error (non-fatal)');
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
else {
|
|
4019
|
+
startupLog(`Full validation passed (${fullResult.duration}ms)`);
|
|
4020
|
+
logger.info({ duration: fullResult.duration }, 'Full startup validation passed');
|
|
4021
|
+
}
|
|
4022
|
+
// Log any additional warnings from full validation
|
|
4023
|
+
if (fullResult.warnings.length > 0) {
|
|
4024
|
+
for (const warning of fullResult.warnings) {
|
|
4025
|
+
if (!preflightResult.warnings.includes(warning)) {
|
|
4026
|
+
logger.warn({ warning }, 'Database validation warning');
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
catch (fullValidationError) {
|
|
4032
|
+
startupLog('Full validation threw an exception (non-fatal)', fullValidationError);
|
|
4033
|
+
logger.warn({ error: fullValidationError }, 'Full validation failed unexpectedly (non-fatal)');
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
catch (error) {
|
|
4037
|
+
logger.error('[Database] Failed to initialize database:', error);
|
|
4038
|
+
startupLog('DATABASE INITIALIZATION FAILED (fatal)', error);
|
|
4039
|
+
throw error; // Fatal - cannot continue without database
|
|
4040
|
+
}
|
|
4041
|
+
// --- Config Injection (deferred, non-blocking) ---
|
|
4042
|
+
try {
|
|
4043
|
+
const injectionResult = await injectConfig();
|
|
4044
|
+
logger.info('[ConfigInjector] Result:', {
|
|
4045
|
+
settingsUpdated: injectionResult.settingsUpdated,
|
|
4046
|
+
hooksCopied: injectionResult.hooksCopied,
|
|
4047
|
+
commandsCopied: injectionResult.commandsCopied,
|
|
4048
|
+
permissionsAdded: injectionResult.permissionsAdded,
|
|
4049
|
+
alreadyConfigured: injectionResult.alreadyConfigured
|
|
4050
|
+
});
|
|
4051
|
+
}
|
|
4052
|
+
catch (error) {
|
|
4053
|
+
logger.warn('[ConfigInjector] Config injection failed (non-fatal):', error);
|
|
4054
|
+
}
|
|
4055
|
+
// --- Real Embedding Provider (now DB is ready) ---
|
|
4056
|
+
embeddingProvider = await createEmbeddingProvider();
|
|
4057
|
+
embeddingProviderReady = true;
|
|
4058
|
+
logger.info('Real embedding provider ready - upgrading from stub');
|
|
4059
|
+
// HIGH-23 FIX: Process any queued embedding requests now that provider is ready
|
|
4060
|
+
if (pendingEmbeddingQueue.length > 0) {
|
|
4061
|
+
logger.info(`Processing ${pendingEmbeddingQueue.length} queued embedding requests`);
|
|
4062
|
+
await processEmbeddingQueue();
|
|
4063
|
+
}
|
|
4064
|
+
// Wire embedding provider to DimensionAdapter
|
|
4065
|
+
try {
|
|
4066
|
+
const adapter = getDimensionAdapter();
|
|
4067
|
+
adapter.setEmbeddingProvider(embeddingProvider);
|
|
4068
|
+
logger.info('DimensionAdapter: Connected to embedding provider');
|
|
4069
|
+
}
|
|
4070
|
+
catch (error) {
|
|
4071
|
+
logger.debug({ error }, 'Could not connect DimensionAdapter (non-fatal)');
|
|
4072
|
+
}
|
|
4073
|
+
// Server is already started, log that initialization is continuing
|
|
4074
|
+
logger.info('MCP server running - continuing background initialization...');
|
|
4075
|
+
// === INITIALIZE MEMORY MANAGER ===
|
|
4076
|
+
// Must be initialized early to monitor memory from the start
|
|
4077
|
+
await initializeMemoryManager();
|
|
4078
|
+
// === INITIALIZE BRAIN SYSTEMS ===
|
|
4079
|
+
// 1. Initialize Skills System (drag & drop .md files)
|
|
4080
|
+
await initializeSkillsSystem(embeddingProvider);
|
|
4081
|
+
// 2. Initialize Codebase Indexer (knows your whole project)
|
|
4082
|
+
await initializeCodebaseSystem(embeddingProvider);
|
|
4083
|
+
// 3. Initialize Reminder System (never forget skills)
|
|
4084
|
+
await initializeReminderSystem();
|
|
4085
|
+
// === INITIALIZE WATCHERS ===
|
|
4086
|
+
// initialize file watcher if enabled (PROJECT-SCOPED - only watches current project)
|
|
4087
|
+
let watcherInitialized = false;
|
|
4088
|
+
try {
|
|
4089
|
+
// Register cleanup handlers for graceful shutdown
|
|
4090
|
+
registerCleanupHandlers();
|
|
4091
|
+
const watcher = await initializeWatcher(embeddingProvider);
|
|
4092
|
+
watcherInitialized = watcher !== null;
|
|
4093
|
+
if (watcherInitialized) {
|
|
4094
|
+
logger.info('PROJECT-SCOPED file watcher enabled and running');
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
catch (error) {
|
|
4098
|
+
logger.error({ error }, 'failed to initialize file watcher - continuing without it');
|
|
4099
|
+
}
|
|
4100
|
+
// initialize session watcher if enabled
|
|
4101
|
+
let sessionWatcherInitialized = false;
|
|
4102
|
+
try {
|
|
4103
|
+
const sessionWatcher = await initializeSessionWatcher(embeddingProvider);
|
|
4104
|
+
sessionWatcherInitialized = sessionWatcher !== null;
|
|
4105
|
+
if (sessionWatcherInitialized) {
|
|
4106
|
+
logger.info(' session watcher enabled and running');
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
catch (error) {
|
|
4110
|
+
logger.error({ error }, 'failed to initialize session watcher - continuing without it');
|
|
4111
|
+
}
|
|
4112
|
+
// === STARTUP INDEXING - ENSURE EVERYTHING IS READY ===
|
|
4113
|
+
// Check if codebase is indexed and sessions are extracted
|
|
4114
|
+
// Triggers background indexing/extraction if needed
|
|
4115
|
+
// This ensures starts with a fully indexed codebase and extracted sessions
|
|
4116
|
+
try {
|
|
4117
|
+
startupLog('Running startup indexing checks...');
|
|
4118
|
+
const indexingResult = await runStartupIndexing(embeddingProvider, {
|
|
4119
|
+
skipCodebase: false, // Always check codebase
|
|
4120
|
+
skipSessions: false, // Always check sessions
|
|
4121
|
+
force: false // Only reindex if stale or missing
|
|
4122
|
+
});
|
|
4123
|
+
if (indexingResult.codebaseStatus.triggeredIndexing) {
|
|
4124
|
+
startupLog('Background codebase indexing triggered');
|
|
4125
|
+
}
|
|
4126
|
+
if (indexingResult.sessionStatus.triggeredExtraction) {
|
|
4127
|
+
startupLog('Background session extraction triggered');
|
|
4128
|
+
}
|
|
4129
|
+
logger.info({
|
|
4130
|
+
codebase: indexingResult.codebaseStatus,
|
|
4131
|
+
sessions: indexingResult.sessionStatus
|
|
4132
|
+
}, 'Startup indexing checks complete');
|
|
4133
|
+
}
|
|
4134
|
+
catch (error) {
|
|
4135
|
+
logger.warn({ error }, 'Startup indexing checks failed (non-fatal) - continuing');
|
|
4136
|
+
startupLog('Startup indexing checks failed (non-fatal)', error);
|
|
4137
|
+
}
|
|
4138
|
+
// === ALLOCATE UNIQUE PORTS FOR THIS INSTANCE ===
|
|
4139
|
+
// Uses project path hash for deterministic allocation with conflict detection
|
|
4140
|
+
// CRITICAL: Must use SPECMEM_PROJECT_PATH (set by bootstrap.cjs) for per-instance isolation
|
|
4141
|
+
// This ensures each session gets unique ports based on project directory
|
|
4142
|
+
const projectPath = process.env['SPECMEM_PROJECT_PATH'] || process.cwd();
|
|
4143
|
+
logger.info({ projectPath, projectHash: process.env['SPECMEM_PROJECT_HASH'] }, 'Using project path for port allocation');
|
|
4144
|
+
let allocatedPorts = null;
|
|
4145
|
+
try {
|
|
4146
|
+
allocatedPorts = await allocatePorts({
|
|
4147
|
+
projectPath: projectPath,
|
|
4148
|
+
verifyAvailability: true,
|
|
4149
|
+
persistAllocation: true
|
|
4150
|
+
});
|
|
4151
|
+
// Update global port allocation
|
|
4152
|
+
setAllocatedPorts(allocatedPorts);
|
|
4153
|
+
logger.info({
|
|
4154
|
+
dashboard: allocatedPorts.dashboard,
|
|
4155
|
+
coordination: allocatedPorts.coordination,
|
|
4156
|
+
postgres: allocatedPorts.postgres,
|
|
4157
|
+
projectPath: allocatedPorts.projectPath,
|
|
4158
|
+
verified: allocatedPorts.verified
|
|
4159
|
+
}, 'Port allocation complete');
|
|
4160
|
+
// Register ports with instance manager for global tracking
|
|
4161
|
+
if (instanceManager && instanceManager.isInitialized()) {
|
|
4162
|
+
instanceManager.registerInstance({
|
|
4163
|
+
dashboard: allocatedPorts.dashboard,
|
|
4164
|
+
coordination: allocatedPorts.coordination,
|
|
4165
|
+
postgres: allocatedPorts.postgres,
|
|
4166
|
+
});
|
|
4167
|
+
logger.debug('Ports registered with instance manager');
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
catch (error) {
|
|
4171
|
+
logger.warn({ error }, 'Port allocation failed, using defaults from environment');
|
|
4172
|
+
}
|
|
4173
|
+
// === CONFIGURE LAZY COORDINATION SERVER ===
|
|
4174
|
+
// The coordination server is now lazy - it only starts when team features are first used
|
|
4175
|
+
// This saves resources when team member coordination is not needed
|
|
4176
|
+
// Use allocated port or fall back to project-hash-derived port from portAllocator
|
|
4177
|
+
const { getDashboardPort: getDashPort, getCoordinationPort: getCoordPort } = await import('./utils/portAllocator.js');
|
|
4178
|
+
const coordinationPort = (allocatedPorts?.coordination ??
|
|
4179
|
+
parseInt(process.env['SPECMEM_COORDINATION_PORT'] || '', 10)) || getCoordPort();
|
|
4180
|
+
const coordinationHost = process.env['SPECMEM_COORDINATION_HOST'] || '127.0.0.1';
|
|
4181
|
+
const coordinationEnabled = process.env['SPECMEM_COORDINATION_ENABLED'] !== 'false';
|
|
4182
|
+
const coordinationMaxRetries = parseInt(process.env['SPECMEM_COORDINATION_MAX_RETRIES'] || '3', 10);
|
|
4183
|
+
// Configure the lazy coordination server (but don't start it yet)
|
|
4184
|
+
if (coordinationEnabled) {
|
|
4185
|
+
configureLazyCoordinationServer({
|
|
4186
|
+
port: coordinationPort,
|
|
4187
|
+
host: coordinationHost,
|
|
4188
|
+
maxPortAttempts: 10,
|
|
4189
|
+
maxStartupRetries: coordinationMaxRetries,
|
|
4190
|
+
retryDelayMs: 1000
|
|
4191
|
+
});
|
|
4192
|
+
logger.info({
|
|
4193
|
+
coordinationPort,
|
|
4194
|
+
coordinationHost,
|
|
4195
|
+
maxRetries: coordinationMaxRetries
|
|
4196
|
+
}, 'Coordination server configured for lazy initialization (will start on first team feature use)');
|
|
4197
|
+
}
|
|
4198
|
+
else {
|
|
4199
|
+
disableLazyCoordinationServer();
|
|
4200
|
+
logger.info('Coordination server disabled via SPECMEM_COORDINATION_ENABLED=false');
|
|
4201
|
+
}
|
|
4202
|
+
// Track coordination availability for status reporting
|
|
4203
|
+
// Note: coordinationAvailable now means "can be started" rather than "is running"
|
|
4204
|
+
const coordinationAvailable = coordinationEnabled;
|
|
4205
|
+
// === INITIALIZE DASHBOARD SERVER ===
|
|
4206
|
+
let dashboardServer = null;
|
|
4207
|
+
let dashboardAvailable = false;
|
|
4208
|
+
let actualDashboardPort = null;
|
|
4209
|
+
// Use allocated port or fall back to project-hash-derived port from portAllocator
|
|
4210
|
+
const dashboardPort = (allocatedPorts?.dashboard ??
|
|
4211
|
+
parseInt(process.env['SPECMEM_DASHBOARD_PORT'] || '', 10)) || getDashPort();
|
|
4212
|
+
const dashboardHost = process.env['SPECMEM_DASHBOARD_HOST'] || '127.0.0.1';
|
|
4213
|
+
const dashboardEnabled = process.env['SPECMEM_DASHBOARD_ENABLED'] !== 'false';
|
|
4214
|
+
const dashboardMaxRetries = parseInt(process.env['SPECMEM_DASHBOARD_MAX_RETRIES'] || '3', 10);
|
|
4215
|
+
// Use centralized password module - supports SPECMEM_PASSWORD (unified) and legacy vars
|
|
4216
|
+
const dashboardPassword = getPassword();
|
|
4217
|
+
// Warn if using default password (security concern in production)
|
|
4218
|
+
if (isUsingDefaultPassword() && dashboardEnabled) {
|
|
4219
|
+
logger.warn('Using default password - consider setting SPECMEM_PASSWORD or SPECMEM_DASHBOARD_PASSWORD for production');
|
|
4220
|
+
}
|
|
4221
|
+
if (dashboardEnabled) {
|
|
4222
|
+
// Retry loop with exponential backoff
|
|
4223
|
+
for (let attempt = 1; attempt <= dashboardMaxRetries; attempt++) {
|
|
4224
|
+
try {
|
|
4225
|
+
dashboardServer = getDashboardServer({
|
|
4226
|
+
port: dashboardPort,
|
|
4227
|
+
host: dashboardHost,
|
|
4228
|
+
password: dashboardPassword,
|
|
4229
|
+
coordinationPort: coordinationPort, // Coordination server starts lazily, use configured port
|
|
4230
|
+
maxPortAttempts: 10,
|
|
4231
|
+
maxStartupRetries: 2,
|
|
4232
|
+
retryDelayMs: 1000
|
|
4233
|
+
});
|
|
4234
|
+
// Connect dashboard to brain systems
|
|
4235
|
+
try {
|
|
4236
|
+
dashboardServer.setDatabase(getDatabase());
|
|
4237
|
+
}
|
|
4238
|
+
catch (e) {
|
|
4239
|
+
logger.debug({ error: e }, 'database not ready for dashboard connection');
|
|
4240
|
+
}
|
|
4241
|
+
// Set embedding provider so HTTP API can use REAL MCP tool semantic search!
|
|
4242
|
+
dashboardServer.setEmbeddingProvider(embeddingProvider);
|
|
4243
|
+
if (skillScanner) {
|
|
4244
|
+
dashboardServer.setSkillScanner(skillScanner);
|
|
4245
|
+
}
|
|
4246
|
+
if (codebaseIndexer) {
|
|
4247
|
+
dashboardServer.setCodebaseIndexer(codebaseIndexer);
|
|
4248
|
+
}
|
|
4249
|
+
await dashboardServer.start();
|
|
4250
|
+
actualDashboardPort = dashboardServer.getActualPort();
|
|
4251
|
+
dashboardAvailable = true;
|
|
4252
|
+
logger.info({
|
|
4253
|
+
port: actualDashboardPort,
|
|
4254
|
+
configuredPort: dashboardPort,
|
|
4255
|
+
host: dashboardHost,
|
|
4256
|
+
url: `http://${dashboardHost}:${actualDashboardPort}`,
|
|
4257
|
+
attempt
|
|
4258
|
+
}, 'CSGO-themed dashboard server started - TACTICAL OPS READY');
|
|
4259
|
+
break;
|
|
4260
|
+
}
|
|
4261
|
+
catch (error) {
|
|
4262
|
+
logger.warn({
|
|
4263
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4264
|
+
attempt,
|
|
4265
|
+
maxRetries: dashboardMaxRetries
|
|
4266
|
+
}, 'dashboard server startup attempt failed');
|
|
4267
|
+
// Reset for next attempt
|
|
4268
|
+
try {
|
|
4269
|
+
await resetDashboardServer();
|
|
4270
|
+
}
|
|
4271
|
+
catch (resetError) {
|
|
4272
|
+
logger.debug({ resetError }, 'error resetting dashboard server');
|
|
4273
|
+
}
|
|
4274
|
+
dashboardServer = null;
|
|
4275
|
+
// Wait before retry with exponential backoff (1s, 2s, 4s...)
|
|
4276
|
+
if (attempt < dashboardMaxRetries) {
|
|
4277
|
+
const delay = 1000 * Math.pow(2, attempt - 1);
|
|
4278
|
+
logger.info({ delayMs: delay }, 'waiting before dashboard retry');
|
|
4279
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
if (!dashboardAvailable) {
|
|
4284
|
+
logger.error({
|
|
4285
|
+
dashboardPort,
|
|
4286
|
+
dashboardHost,
|
|
4287
|
+
maxRetries: dashboardMaxRetries
|
|
4288
|
+
}, 'DASHBOARD SERVER UNAVAILABLE - web UI will not be accessible');
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
else {
|
|
4292
|
+
logger.info('dashboard server disabled via SPECMEM_DASHBOARD_ENABLED=false');
|
|
4293
|
+
}
|
|
4294
|
+
// handle shutdown signals gracefully
|
|
4295
|
+
const gracefulShutdown = async () => {
|
|
4296
|
+
logger.info('shutting down gracefully...');
|
|
4297
|
+
// Import timer registry for cleanup
|
|
4298
|
+
const { clearAllTimers } = await import('./utils/timerRegistry.js');
|
|
4299
|
+
// Clear all timers FIRST to prevent new work from being scheduled
|
|
4300
|
+
const clearedTimers = clearAllTimers();
|
|
4301
|
+
logger.info({ clearedTimers }, 'cleared all registered timers');
|
|
4302
|
+
// shutdown brain systems
|
|
4303
|
+
await shutdownBrainSystems();
|
|
4304
|
+
// shutdown memory manager
|
|
4305
|
+
await shutdownMemoryManager();
|
|
4306
|
+
// shutdown watchers
|
|
4307
|
+
if (watcherInitialized) {
|
|
4308
|
+
await shutdownWatcher();
|
|
4309
|
+
}
|
|
4310
|
+
if (sessionWatcherInitialized) {
|
|
4311
|
+
await shutdownSessionWatcher();
|
|
4312
|
+
}
|
|
4313
|
+
// shutdown coordination server (lazy - may or may not have been started)
|
|
4314
|
+
await executeLazyShutdownHandlers();
|
|
4315
|
+
await resetLazyCoordinationServer();
|
|
4316
|
+
// shutdown dashboard server
|
|
4317
|
+
if (dashboardServer) {
|
|
4318
|
+
await resetDashboardServer();
|
|
4319
|
+
}
|
|
4320
|
+
// shutdown instance manager (releases locks, unregisters from global registry)
|
|
4321
|
+
if (instanceManager) {
|
|
4322
|
+
instanceManager.shutdown();
|
|
4323
|
+
logger.info('Instance manager shutdown complete');
|
|
4324
|
+
}
|
|
4325
|
+
// shutdown embedding server manager (kills embedding server gracefully)
|
|
4326
|
+
// CRITICAL: This prevents orphaned frankenstein-embeddings.py processes!
|
|
4327
|
+
if (embeddingManager) {
|
|
4328
|
+
await embeddingManager.shutdown();
|
|
4329
|
+
logger.info('Embedding server manager shutdown complete');
|
|
4330
|
+
}
|
|
4331
|
+
// shutdown embedding provider (kills Python embedding process if running)
|
|
4332
|
+
// Note: embeddingManager now handles this, but kept for backward compat
|
|
4333
|
+
if (embeddingProvider && typeof embeddingProvider.shutdown === 'function') {
|
|
4334
|
+
await embeddingProvider.shutdown();
|
|
4335
|
+
logger.info('Embedding provider shutdown complete');
|
|
4336
|
+
}
|
|
4337
|
+
// then shutdown server
|
|
4338
|
+
await server.shutdown();
|
|
4339
|
+
process.exit(0);
|
|
4340
|
+
};
|
|
4341
|
+
process.on('SIGINT', gracefulShutdown);
|
|
4342
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
4343
|
+
// SIGUSR1 handler for hot reload - triggers graceful restart
|
|
4344
|
+
// When code is updated, send SIGUSR1 to trigger graceful shutdown
|
|
4345
|
+
// will respawn the process with the new code
|
|
4346
|
+
process.on('SIGUSR1', async () => {
|
|
4347
|
+
logger.info('SIGUSR1 received - initiating graceful restart for hot reload');
|
|
4348
|
+
startupLog('SIGUSR1 received - hot reload triggered');
|
|
4349
|
+
await gracefulShutdown();
|
|
4350
|
+
// gracefulShutdown() calls process.exit(0), so will respawn with new code
|
|
4351
|
+
});
|
|
4352
|
+
// handle uncaught errors - SELF-HEALING MODE
|
|
4353
|
+
// Track recent errors to detect crash loops
|
|
4354
|
+
let recentErrors = [];
|
|
4355
|
+
const MAX_ERRORS_BEFORE_EXIT = 10;
|
|
4356
|
+
const ERROR_WINDOW_MS = 60000; // 1 minute
|
|
4357
|
+
|
|
4358
|
+
process.on('uncaughtException', (error) => {
|
|
4359
|
+
// Log but don't crash for known non-fatal errors
|
|
4360
|
+
const errMsg = error?.message || String(error);
|
|
4361
|
+
const isFatal = errMsg.includes('EADDRINUSE') ||
|
|
4362
|
+
errMsg.includes('ENOMEM') ||
|
|
4363
|
+
errMsg.includes('heap') ||
|
|
4364
|
+
errMsg.includes('stack');
|
|
4365
|
+
|
|
4366
|
+
logger.error({ error, isFatal }, 'Uncaught exception - attempting recovery');
|
|
4367
|
+
|
|
4368
|
+
// Track error frequency
|
|
4369
|
+
recentErrors.push(Date.now());
|
|
4370
|
+
recentErrors = recentErrors.filter(t => Date.now() - t < ERROR_WINDOW_MS);
|
|
4371
|
+
|
|
4372
|
+
if (isFatal || recentErrors.length >= MAX_ERRORS_BEFORE_EXIT) {
|
|
4373
|
+
logger.fatal({ error, recentErrorCount: recentErrors.length }, 'Fatal error or error loop detected - exiting');
|
|
4374
|
+
process.exit(1);
|
|
4375
|
+
}
|
|
4376
|
+
// Non-fatal: log and continue (MCP stays alive)
|
|
4377
|
+
startupLog(`Non-fatal uncaught exception (${recentErrors.length}/${MAX_ERRORS_BEFORE_EXIT}): ${errMsg}`);
|
|
4378
|
+
});
|
|
4379
|
+
|
|
4380
|
+
process.on('unhandledRejection', (reason) => {
|
|
4381
|
+
// SELF-HEALING: Log but DON'T exit for promise rejections
|
|
4382
|
+
// These are usually timeout/network errors that can be safely ignored
|
|
4383
|
+
const reasonStr = reason instanceof Error ? reason.message : String(reason);
|
|
4384
|
+
logger.warn({ reason: reasonStr }, 'Unhandled promise rejection - continuing (MCP stays alive)');
|
|
4385
|
+
startupLog(`Unhandled rejection (non-fatal): ${reasonStr.slice(0, 200)}`);
|
|
4386
|
+
// Don't exit - let MCP continue serving
|
|
4387
|
+
});
|
|
4388
|
+
// Server already started at the beginning for fast MCP connection
|
|
4389
|
+
// Now everything is initialized and ready!
|
|
4390
|
+
// Final status log with memory stats
|
|
4391
|
+
const memStats = memoryManager?.getStats();
|
|
4392
|
+
// Use getDashboardUrl helper for proper host handling (0.0.0.0 -> localhost for display)
|
|
4393
|
+
const dashboardUrl = dashboardAvailable && actualDashboardPort
|
|
4394
|
+
? getDashboardUrl(dashboardHost, actualDashboardPort)
|
|
4395
|
+
: null;
|
|
4396
|
+
// Get lazy coordination server status for logging
|
|
4397
|
+
const coordStatus = getLazyCoordinationServerStatus();
|
|
4398
|
+
logger.info({
|
|
4399
|
+
skillsEnabled: skillScanner !== null,
|
|
4400
|
+
skillCount: skillScanner?.getAllSkills().length ?? 0,
|
|
4401
|
+
codebaseEnabled: codebaseIndexer !== null,
|
|
4402
|
+
codebaseFiles: codebaseIndexer?.getStats().totalFiles ?? 0,
|
|
4403
|
+
watcherEnabled: watcherInitialized,
|
|
4404
|
+
rootPath: config.watcher.rootPath,
|
|
4405
|
+
sessionWatcherEnabled: sessionWatcherInitialized,
|
|
4406
|
+
claudeDir: config.sessionWatcher.claudeDir ?? '~/.claude',
|
|
4407
|
+
coordinationServerEnabled: coordinationAvailable,
|
|
4408
|
+
coordinationServerLazy: true, // Now uses lazy initialization
|
|
4409
|
+
coordinationServerRunning: coordStatus.running,
|
|
4410
|
+
coordinationPort: coordStatus.port ?? coordinationPort,
|
|
4411
|
+
dashboardEnabled: dashboardAvailable,
|
|
4412
|
+
dashboardPort: actualDashboardPort,
|
|
4413
|
+
dashboardConfiguredPort: dashboardPort,
|
|
4414
|
+
dashboardUrl,
|
|
4415
|
+
memoryManagerEnabled: memoryManager !== null,
|
|
4416
|
+
memoryHeapUsedMB: memStats ? Math.round(memStats.heapUsed / 1024 / 1024) : null,
|
|
4417
|
+
memoryMaxHeapMB: memStats ? Math.round(memStats.maxHeap / 1024 / 1024) : null,
|
|
4418
|
+
memoryPressureLevel: memStats?.pressureLevel ?? 'unknown'
|
|
4419
|
+
}, 'SpecMem server fully initialized - THE BRAIN IS ALIVE');
|
|
4420
|
+
startupLog('SpecMem server FULLY INITIALIZED - all components ready');
|
|
4421
|
+
// === DISPLAY SPECMEM LOADED BANNER ===
|
|
4422
|
+
// Show a nice banner in Code CLI
|
|
4423
|
+
displayLoadedBanner(deployResult, dashboardUrl);
|
|
4424
|
+
startupLog('main() COMPLETE - server running and waiting for requests');
|
|
4425
|
+
}
|
|
4426
|
+
// run if this is the main module
|
|
4427
|
+
startupLog('All imports complete - calling main()');
|
|
4428
|
+
main().catch((error) => {
|
|
4429
|
+
startupLog('main() REJECTED with error', error);
|
|
4430
|
+
logger.fatal({ error }, 'Failed to start SpecMem server');
|
|
4431
|
+
process.exit(1);
|
|
4432
|
+
});
|
|
4433
|
+
//# sourceMappingURL=index.js.map
|