@iflow-mcp/jkheadley-instar 0.26.2
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/.claude/hooks/free-text-guard.sh +117 -0
- package/.claude/settings.json +201 -0
- package/.claude/skills/autonomous/hooks/autonomous-stop-hook.sh +234 -0
- package/.claude/skills/autonomous/hooks/hooks.json +15 -0
- package/.claude/skills/autonomous/scripts/setup-autonomous.sh +166 -0
- package/.claude/skills/autonomous/skill.md +254 -0
- package/.claude/skills/secret-setup/skill.md +210 -0
- package/.claude/skills/setup-wizard/skill.md +2132 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/dashboard/favicon.png +0 -0
- package/dashboard/index.html +5740 -0
- package/dashboard/logo.png +0 -0
- package/dist/cli.d.ts +21 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1782 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/backup.d.ts +16 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +84 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/discovery.d.ts +158 -0
- package/dist/commands/discovery.d.ts.map +1 -0
- package/dist/commands/discovery.js +532 -0
- package/dist/commands/discovery.js.map +1 -0
- package/dist/commands/git.d.ts +25 -0
- package/dist/commands/git.d.ts.map +1 -0
- package/dist/commands/git.js +152 -0
- package/dist/commands/git.js.map +1 -0
- package/dist/commands/init.d.ts +52 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +4211 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/intent.d.ts +30 -0
- package/dist/commands/intent.d.ts.map +1 -0
- package/dist/commands/intent.js +349 -0
- package/dist/commands/intent.js.map +1 -0
- package/dist/commands/job.d.ts +42 -0
- package/dist/commands/job.d.ts.map +1 -0
- package/dist/commands/job.js +202 -0
- package/dist/commands/job.js.map +1 -0
- package/dist/commands/knowledge.d.ts +25 -0
- package/dist/commands/knowledge.d.ts.map +1 -0
- package/dist/commands/knowledge.js +127 -0
- package/dist/commands/knowledge.js.map +1 -0
- package/dist/commands/machine.d.ts +53 -0
- package/dist/commands/machine.d.ts.map +1 -0
- package/dist/commands/machine.js +680 -0
- package/dist/commands/machine.js.map +1 -0
- package/dist/commands/memory.d.ts +24 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/memory.js +163 -0
- package/dist/commands/memory.js.map +1 -0
- package/dist/commands/nuke.d.ts +22 -0
- package/dist/commands/nuke.d.ts.map +1 -0
- package/dist/commands/nuke.js +216 -0
- package/dist/commands/nuke.js.map +1 -0
- package/dist/commands/org.d.ts +16 -0
- package/dist/commands/org.d.ts.map +1 -0
- package/dist/commands/org.js +69 -0
- package/dist/commands/org.js.map +1 -0
- package/dist/commands/playbook.d.ts +91 -0
- package/dist/commands/playbook.d.ts.map +1 -0
- package/dist/commands/playbook.js +1016 -0
- package/dist/commands/playbook.js.map +1 -0
- package/dist/commands/reflect.d.ts +52 -0
- package/dist/commands/reflect.d.ts.map +1 -0
- package/dist/commands/reflect.js +316 -0
- package/dist/commands/reflect.js.map +1 -0
- package/dist/commands/relationship.d.ts +17 -0
- package/dist/commands/relationship.d.ts.map +1 -0
- package/dist/commands/relationship.js +156 -0
- package/dist/commands/relationship.js.map +1 -0
- package/dist/commands/relay.d.ts +26 -0
- package/dist/commands/relay.d.ts.map +1 -0
- package/dist/commands/relay.js +121 -0
- package/dist/commands/relay.js.map +1 -0
- package/dist/commands/review.d.ts +18 -0
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/commands/review.js +193 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/semantic.d.ts +37 -0
- package/dist/commands/semantic.d.ts.map +1 -0
- package/dist/commands/semantic.js +198 -0
- package/dist/commands/semantic.js.map +1 -0
- package/dist/commands/server.d.ts +37 -0
- package/dist/commands/server.d.ts.map +1 -0
- package/dist/commands/server.js +4875 -0
- package/dist/commands/server.js.map +1 -0
- package/dist/commands/setup.d.ts +63 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +1235 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/slack-cli.d.ts +16 -0
- package/dist/commands/slack-cli.d.ts.map +1 -0
- package/dist/commands/slack-cli.js +259 -0
- package/dist/commands/slack-cli.js.map +1 -0
- package/dist/commands/status.d.ts +11 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +120 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/user.d.ts +17 -0
- package/dist/commands/user.d.ts.map +1 -0
- package/dist/commands/user.js +53 -0
- package/dist/commands/user.js.map +1 -0
- package/dist/commands/whatsapp.d.ts +31 -0
- package/dist/commands/whatsapp.d.ts.map +1 -0
- package/dist/commands/whatsapp.js +408 -0
- package/dist/commands/whatsapp.js.map +1 -0
- package/dist/config/ConfigDefaults.d.ts +40 -0
- package/dist/config/ConfigDefaults.d.ts.map +1 -0
- package/dist/config/ConfigDefaults.js +175 -0
- package/dist/config/ConfigDefaults.js.map +1 -0
- package/dist/config/LiveConfig.d.ts +97 -0
- package/dist/config/LiveConfig.d.ts.map +1 -0
- package/dist/config/LiveConfig.js +220 -0
- package/dist/config/LiveConfig.js.map +1 -0
- package/dist/core/AccessControl.d.ts +91 -0
- package/dist/core/AccessControl.d.ts.map +1 -0
- package/dist/core/AccessControl.js +184 -0
- package/dist/core/AccessControl.js.map +1 -0
- package/dist/core/AdaptationValidator.d.ts +44 -0
- package/dist/core/AdaptationValidator.d.ts.map +1 -0
- package/dist/core/AdaptationValidator.js +132 -0
- package/dist/core/AdaptationValidator.js.map +1 -0
- package/dist/core/AdaptiveTrust.d.ts +188 -0
- package/dist/core/AdaptiveTrust.d.ts.map +1 -0
- package/dist/core/AdaptiveTrust.js +354 -0
- package/dist/core/AdaptiveTrust.js.map +1 -0
- package/dist/core/AgentBus.d.ts +168 -0
- package/dist/core/AgentBus.d.ts.map +1 -0
- package/dist/core/AgentBus.js +369 -0
- package/dist/core/AgentBus.js.map +1 -0
- package/dist/core/AgentConnector.d.ts +76 -0
- package/dist/core/AgentConnector.d.ts.map +1 -0
- package/dist/core/AgentConnector.js +324 -0
- package/dist/core/AgentConnector.js.map +1 -0
- package/dist/core/AgentRegistry.d.ts +107 -0
- package/dist/core/AgentRegistry.d.ts.map +1 -0
- package/dist/core/AgentRegistry.js +569 -0
- package/dist/core/AgentRegistry.js.map +1 -0
- package/dist/core/AnthropicIntelligenceProvider.d.ts +24 -0
- package/dist/core/AnthropicIntelligenceProvider.d.ts.map +1 -0
- package/dist/core/AnthropicIntelligenceProvider.js +63 -0
- package/dist/core/AnthropicIntelligenceProvider.js.map +1 -0
- package/dist/core/AuditTrail.d.ts +207 -0
- package/dist/core/AuditTrail.d.ts.map +1 -0
- package/dist/core/AuditTrail.js +271 -0
- package/dist/core/AuditTrail.js.map +1 -0
- package/dist/core/AutoApprover.d.ts +63 -0
- package/dist/core/AutoApprover.d.ts.map +1 -0
- package/dist/core/AutoApprover.js +151 -0
- package/dist/core/AutoApprover.js.map +1 -0
- package/dist/core/AutoDispatcher.d.ts +170 -0
- package/dist/core/AutoDispatcher.d.ts.map +1 -0
- package/dist/core/AutoDispatcher.js +647 -0
- package/dist/core/AutoDispatcher.js.map +1 -0
- package/dist/core/AutoUpdater.d.ts +193 -0
- package/dist/core/AutoUpdater.d.ts.map +1 -0
- package/dist/core/AutoUpdater.js +648 -0
- package/dist/core/AutoUpdater.js.map +1 -0
- package/dist/core/AutonomousEvolution.d.ts +168 -0
- package/dist/core/AutonomousEvolution.d.ts.map +1 -0
- package/dist/core/AutonomousEvolution.js +384 -0
- package/dist/core/AutonomousEvolution.js.map +1 -0
- package/dist/core/AutonomyProfileManager.d.ts +108 -0
- package/dist/core/AutonomyProfileManager.d.ts.map +1 -0
- package/dist/core/AutonomyProfileManager.js +323 -0
- package/dist/core/AutonomyProfileManager.js.map +1 -0
- package/dist/core/AutonomySkill.d.ts +98 -0
- package/dist/core/AutonomySkill.d.ts.map +1 -0
- package/dist/core/AutonomySkill.js +497 -0
- package/dist/core/AutonomySkill.js.map +1 -0
- package/dist/core/BackupManager.d.ts +66 -0
- package/dist/core/BackupManager.d.ts.map +1 -0
- package/dist/core/BackupManager.js +266 -0
- package/dist/core/BackupManager.js.map +1 -0
- package/dist/core/BitwardenProvider.d.ts +85 -0
- package/dist/core/BitwardenProvider.d.ts.map +1 -0
- package/dist/core/BitwardenProvider.js +437 -0
- package/dist/core/BitwardenProvider.js.map +1 -0
- package/dist/core/BlockerLearningLoop.d.ts +99 -0
- package/dist/core/BlockerLearningLoop.d.ts.map +1 -0
- package/dist/core/BlockerLearningLoop.js +205 -0
- package/dist/core/BlockerLearningLoop.js.map +1 -0
- package/dist/core/BranchManager.d.ts +165 -0
- package/dist/core/BranchManager.d.ts.map +1 -0
- package/dist/core/BranchManager.js +413 -0
- package/dist/core/BranchManager.js.map +1 -0
- package/dist/core/CaffeinateManager.d.ts +50 -0
- package/dist/core/CaffeinateManager.d.ts.map +1 -0
- package/dist/core/CaffeinateManager.js +189 -0
- package/dist/core/CaffeinateManager.js.map +1 -0
- package/dist/core/CallbackRegistry.d.ts +67 -0
- package/dist/core/CallbackRegistry.d.ts.map +1 -0
- package/dist/core/CallbackRegistry.js +145 -0
- package/dist/core/CallbackRegistry.js.map +1 -0
- package/dist/core/CanonicalState.d.ts +132 -0
- package/dist/core/CanonicalState.d.ts.map +1 -0
- package/dist/core/CanonicalState.js +297 -0
- package/dist/core/CanonicalState.js.map +1 -0
- package/dist/core/CapabilityMapper.d.ts +192 -0
- package/dist/core/CapabilityMapper.d.ts.map +1 -0
- package/dist/core/CapabilityMapper.js +958 -0
- package/dist/core/CapabilityMapper.js.map +1 -0
- package/dist/core/CapabilityRegistryGenerator.d.ts +72 -0
- package/dist/core/CapabilityRegistryGenerator.d.ts.map +1 -0
- package/dist/core/CapabilityRegistryGenerator.js +310 -0
- package/dist/core/CapabilityRegistryGenerator.js.map +1 -0
- package/dist/core/ClaudeCliIntelligenceProvider.d.ts +21 -0
- package/dist/core/ClaudeCliIntelligenceProvider.d.ts.map +1 -0
- package/dist/core/ClaudeCliIntelligenceProvider.js +60 -0
- package/dist/core/ClaudeCliIntelligenceProvider.js.map +1 -0
- package/dist/core/CoherenceGate.d.ts +198 -0
- package/dist/core/CoherenceGate.d.ts.map +1 -0
- package/dist/core/CoherenceGate.js +986 -0
- package/dist/core/CoherenceGate.js.map +1 -0
- package/dist/core/CoherenceReviewer.d.ts +104 -0
- package/dist/core/CoherenceReviewer.d.ts.map +1 -0
- package/dist/core/CoherenceReviewer.js +185 -0
- package/dist/core/CoherenceReviewer.js.map +1 -0
- package/dist/core/Config.d.ts +32 -0
- package/dist/core/Config.d.ts.map +1 -0
- package/dist/core/Config.js +332 -0
- package/dist/core/Config.js.map +1 -0
- package/dist/core/ConflictNegotiator.d.ts +167 -0
- package/dist/core/ConflictNegotiator.d.ts.map +1 -0
- package/dist/core/ConflictNegotiator.js +280 -0
- package/dist/core/ConflictNegotiator.js.map +1 -0
- package/dist/core/ContextHierarchy.d.ts +102 -0
- package/dist/core/ContextHierarchy.d.ts.map +1 -0
- package/dist/core/ContextHierarchy.js +594 -0
- package/dist/core/ContextHierarchy.js.map +1 -0
- package/dist/core/ContextSnapshotBuilder.d.ts +80 -0
- package/dist/core/ContextSnapshotBuilder.d.ts.map +1 -0
- package/dist/core/ContextSnapshotBuilder.js +342 -0
- package/dist/core/ContextSnapshotBuilder.js.map +1 -0
- package/dist/core/ContextualEvaluator.d.ts +103 -0
- package/dist/core/ContextualEvaluator.d.ts.map +1 -0
- package/dist/core/ContextualEvaluator.js +389 -0
- package/dist/core/ContextualEvaluator.js.map +1 -0
- package/dist/core/ConvergenceChecker.d.ts +24 -0
- package/dist/core/ConvergenceChecker.d.ts.map +1 -0
- package/dist/core/ConvergenceChecker.js +113 -0
- package/dist/core/ConvergenceChecker.js.map +1 -0
- package/dist/core/CoordinationProtocol.d.ts +198 -0
- package/dist/core/CoordinationProtocol.d.ts.map +1 -0
- package/dist/core/CoordinationProtocol.js +363 -0
- package/dist/core/CoordinationProtocol.js.map +1 -0
- package/dist/core/CustomReviewerLoader.d.ts +45 -0
- package/dist/core/CustomReviewerLoader.d.ts.map +1 -0
- package/dist/core/CustomReviewerLoader.js +153 -0
- package/dist/core/CustomReviewerLoader.js.map +1 -0
- package/dist/core/DecisionJournal.d.ts +56 -0
- package/dist/core/DecisionJournal.d.ts.map +1 -0
- package/dist/core/DecisionJournal.js +132 -0
- package/dist/core/DecisionJournal.js.map +1 -0
- package/dist/core/DeferredDispatchTracker.d.ts +91 -0
- package/dist/core/DeferredDispatchTracker.d.ts.map +1 -0
- package/dist/core/DeferredDispatchTracker.js +213 -0
- package/dist/core/DeferredDispatchTracker.js.map +1 -0
- package/dist/core/DiscoveryEvaluator.d.ts +131 -0
- package/dist/core/DiscoveryEvaluator.d.ts.map +1 -0
- package/dist/core/DiscoveryEvaluator.js +377 -0
- package/dist/core/DiscoveryEvaluator.js.map +1 -0
- package/dist/core/DispatchDecisionJournal.d.ts +83 -0
- package/dist/core/DispatchDecisionJournal.d.ts.map +1 -0
- package/dist/core/DispatchDecisionJournal.js +181 -0
- package/dist/core/DispatchDecisionJournal.js.map +1 -0
- package/dist/core/DispatchExecutor.d.ts +127 -0
- package/dist/core/DispatchExecutor.d.ts.map +1 -0
- package/dist/core/DispatchExecutor.js +355 -0
- package/dist/core/DispatchExecutor.js.map +1 -0
- package/dist/core/DispatchManager.d.ts +200 -0
- package/dist/core/DispatchManager.d.ts.map +1 -0
- package/dist/core/DispatchManager.js +524 -0
- package/dist/core/DispatchManager.js.map +1 -0
- package/dist/core/DispatchScopeEnforcer.d.ts +57 -0
- package/dist/core/DispatchScopeEnforcer.d.ts.map +1 -0
- package/dist/core/DispatchScopeEnforcer.js +173 -0
- package/dist/core/DispatchScopeEnforcer.js.map +1 -0
- package/dist/core/DispatchVerifier.d.ts +76 -0
- package/dist/core/DispatchVerifier.d.ts.map +1 -0
- package/dist/core/DispatchVerifier.js +128 -0
- package/dist/core/DispatchVerifier.js.map +1 -0
- package/dist/core/EvolutionManager.d.ts +223 -0
- package/dist/core/EvolutionManager.d.ts.map +1 -0
- package/dist/core/EvolutionManager.js +630 -0
- package/dist/core/EvolutionManager.js.map +1 -0
- package/dist/core/ExecutionJournal.d.ts +101 -0
- package/dist/core/ExecutionJournal.d.ts.map +1 -0
- package/dist/core/ExecutionJournal.js +301 -0
- package/dist/core/ExecutionJournal.js.map +1 -0
- package/dist/core/ExternalOperationGate.d.ts +204 -0
- package/dist/core/ExternalOperationGate.d.ts.map +1 -0
- package/dist/core/ExternalOperationGate.js +413 -0
- package/dist/core/ExternalOperationGate.js.map +1 -0
- package/dist/core/FeatureDefinitions.d.ts +14 -0
- package/dist/core/FeatureDefinitions.d.ts.map +1 -0
- package/dist/core/FeatureDefinitions.js +374 -0
- package/dist/core/FeatureDefinitions.js.map +1 -0
- package/dist/core/FeatureRegistry.d.ts +337 -0
- package/dist/core/FeatureRegistry.d.ts.map +1 -0
- package/dist/core/FeatureRegistry.js +863 -0
- package/dist/core/FeatureRegistry.js.map +1 -0
- package/dist/core/FeedbackManager.d.ts +69 -0
- package/dist/core/FeedbackManager.d.ts.map +1 -0
- package/dist/core/FeedbackManager.js +284 -0
- package/dist/core/FeedbackManager.js.map +1 -0
- package/dist/core/FileClassifier.d.ts +74 -0
- package/dist/core/FileClassifier.d.ts.map +1 -0
- package/dist/core/FileClassifier.js +377 -0
- package/dist/core/FileClassifier.js.map +1 -0
- package/dist/core/ForegroundRestartWatcher.d.ts +55 -0
- package/dist/core/ForegroundRestartWatcher.d.ts.map +1 -0
- package/dist/core/ForegroundRestartWatcher.js +116 -0
- package/dist/core/ForegroundRestartWatcher.js.map +1 -0
- package/dist/core/GitStateManager.d.ts +78 -0
- package/dist/core/GitStateManager.d.ts.map +1 -0
- package/dist/core/GitStateManager.js +366 -0
- package/dist/core/GitStateManager.js.map +1 -0
- package/dist/core/GitSync.d.ts +199 -0
- package/dist/core/GitSync.d.ts.map +1 -0
- package/dist/core/GitSync.js +955 -0
- package/dist/core/GitSync.js.map +1 -0
- package/dist/core/GlobalInstallCleanup.d.ts +23 -0
- package/dist/core/GlobalInstallCleanup.d.ts.map +1 -0
- package/dist/core/GlobalInstallCleanup.js +130 -0
- package/dist/core/GlobalInstallCleanup.js.map +1 -0
- package/dist/core/GlobalSecretStore.d.ts +112 -0
- package/dist/core/GlobalSecretStore.d.ts.map +1 -0
- package/dist/core/GlobalSecretStore.js +396 -0
- package/dist/core/GlobalSecretStore.js.map +1 -0
- package/dist/core/HandoffManager.d.ts +163 -0
- package/dist/core/HandoffManager.d.ts.map +1 -0
- package/dist/core/HandoffManager.js +370 -0
- package/dist/core/HandoffManager.js.map +1 -0
- package/dist/core/HeartbeatManager.d.ts +120 -0
- package/dist/core/HeartbeatManager.d.ts.map +1 -0
- package/dist/core/HeartbeatManager.js +240 -0
- package/dist/core/HeartbeatManager.js.map +1 -0
- package/dist/core/InputGuard.d.ts +98 -0
- package/dist/core/InputGuard.d.ts.map +1 -0
- package/dist/core/InputGuard.js +316 -0
- package/dist/core/InputGuard.js.map +1 -0
- package/dist/core/IntentDriftDetector.d.ts +100 -0
- package/dist/core/IntentDriftDetector.d.ts.map +1 -0
- package/dist/core/IntentDriftDetector.js +325 -0
- package/dist/core/IntentDriftDetector.js.map +1 -0
- package/dist/core/JobReflector.d.ts +81 -0
- package/dist/core/JobReflector.d.ts.map +1 -0
- package/dist/core/JobReflector.js +244 -0
- package/dist/core/JobReflector.js.map +1 -0
- package/dist/core/LLMConflictResolver.d.ts +132 -0
- package/dist/core/LLMConflictResolver.d.ts.map +1 -0
- package/dist/core/LLMConflictResolver.js +372 -0
- package/dist/core/LLMConflictResolver.js.map +1 -0
- package/dist/core/LedgerAuth.d.ts +99 -0
- package/dist/core/LedgerAuth.d.ts.map +1 -0
- package/dist/core/LedgerAuth.js +215 -0
- package/dist/core/LedgerAuth.js.map +1 -0
- package/dist/core/MachineIdentity.d.ts +196 -0
- package/dist/core/MachineIdentity.d.ts.map +1 -0
- package/dist/core/MachineIdentity.js +476 -0
- package/dist/core/MachineIdentity.js.map +1 -0
- package/dist/core/MessageSentinel.d.ts +142 -0
- package/dist/core/MessageSentinel.d.ts.map +1 -0
- package/dist/core/MessageSentinel.js +426 -0
- package/dist/core/MessageSentinel.js.map +1 -0
- package/dist/core/MigrationProvenance.d.ts +62 -0
- package/dist/core/MigrationProvenance.d.ts.map +1 -0
- package/dist/core/MigrationProvenance.js +183 -0
- package/dist/core/MigrationProvenance.js.map +1 -0
- package/dist/core/MultiMachineCoordinator.d.ts +106 -0
- package/dist/core/MultiMachineCoordinator.d.ts.map +1 -0
- package/dist/core/MultiMachineCoordinator.js +311 -0
- package/dist/core/MultiMachineCoordinator.js.map +1 -0
- package/dist/core/NonceStore.d.ts +72 -0
- package/dist/core/NonceStore.d.ts.map +1 -0
- package/dist/core/NonceStore.js +163 -0
- package/dist/core/NonceStore.js.map +1 -0
- package/dist/core/OrgIntentManager.d.ts +58 -0
- package/dist/core/OrgIntentManager.d.ts.map +1 -0
- package/dist/core/OrgIntentManager.js +250 -0
- package/dist/core/OrgIntentManager.js.map +1 -0
- package/dist/core/OverlapGuard.d.ts +112 -0
- package/dist/core/OverlapGuard.d.ts.map +1 -0
- package/dist/core/OverlapGuard.js +241 -0
- package/dist/core/OverlapGuard.js.map +1 -0
- package/dist/core/PairingProtocol.d.ts +129 -0
- package/dist/core/PairingProtocol.d.ts.map +1 -0
- package/dist/core/PairingProtocol.js +265 -0
- package/dist/core/PairingProtocol.js.map +1 -0
- package/dist/core/PatternAnalyzer.d.ts +128 -0
- package/dist/core/PatternAnalyzer.d.ts.map +1 -0
- package/dist/core/PatternAnalyzer.js +388 -0
- package/dist/core/PatternAnalyzer.js.map +1 -0
- package/dist/core/PlatformActivityRegistry.d.ts +163 -0
- package/dist/core/PlatformActivityRegistry.d.ts.map +1 -0
- package/dist/core/PlatformActivityRegistry.js +307 -0
- package/dist/core/PlatformActivityRegistry.js.map +1 -0
- package/dist/core/PolicyEnforcementLayer.d.ts +63 -0
- package/dist/core/PolicyEnforcementLayer.d.ts.map +1 -0
- package/dist/core/PolicyEnforcementLayer.js +250 -0
- package/dist/core/PolicyEnforcementLayer.js.map +1 -0
- package/dist/core/PortRegistry.d.ts +9 -0
- package/dist/core/PortRegistry.d.ts.map +1 -0
- package/dist/core/PortRegistry.js +8 -0
- package/dist/core/PortRegistry.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts +170 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -0
- package/dist/core/PostUpdateMigrator.js +3674 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -0
- package/dist/core/Prerequisites.d.ts +37 -0
- package/dist/core/Prerequisites.d.ts.map +1 -0
- package/dist/core/Prerequisites.js +272 -0
- package/dist/core/Prerequisites.js.map +1 -0
- package/dist/core/ProcessIntegrity.d.ts +90 -0
- package/dist/core/ProcessIntegrity.d.ts.map +1 -0
- package/dist/core/ProcessIntegrity.js +119 -0
- package/dist/core/ProcessIntegrity.js.map +1 -0
- package/dist/core/ProjectMapper.d.ts +97 -0
- package/dist/core/ProjectMapper.d.ts.map +1 -0
- package/dist/core/ProjectMapper.js +370 -0
- package/dist/core/ProjectMapper.js.map +1 -0
- package/dist/core/PromptGuard.d.ts +121 -0
- package/dist/core/PromptGuard.d.ts.map +1 -0
- package/dist/core/PromptGuard.js +259 -0
- package/dist/core/PromptGuard.js.map +1 -0
- package/dist/core/RecipientResolver.d.ts +54 -0
- package/dist/core/RecipientResolver.d.ts.map +1 -0
- package/dist/core/RecipientResolver.js +143 -0
- package/dist/core/RecipientResolver.js.map +1 -0
- package/dist/core/ReflectionConsolidator.d.ts +73 -0
- package/dist/core/ReflectionConsolidator.d.ts.map +1 -0
- package/dist/core/ReflectionConsolidator.js +202 -0
- package/dist/core/ReflectionConsolidator.js.map +1 -0
- package/dist/core/RelationshipManager.d.ts +146 -0
- package/dist/core/RelationshipManager.d.ts.map +1 -0
- package/dist/core/RelationshipManager.js +736 -0
- package/dist/core/RelationshipManager.js.map +1 -0
- package/dist/core/RelevanceFilter.d.ts +61 -0
- package/dist/core/RelevanceFilter.d.ts.map +1 -0
- package/dist/core/RelevanceFilter.js +160 -0
- package/dist/core/RelevanceFilter.js.map +1 -0
- package/dist/core/ResearchRateLimiter.d.ts +76 -0
- package/dist/core/ResearchRateLimiter.d.ts.map +1 -0
- package/dist/core/ResearchRateLimiter.js +168 -0
- package/dist/core/ResearchRateLimiter.js.map +1 -0
- package/dist/core/ResumeValidator.d.ts +58 -0
- package/dist/core/ResumeValidator.d.ts.map +1 -0
- package/dist/core/ResumeValidator.js +195 -0
- package/dist/core/ResumeValidator.js.map +1 -0
- package/dist/core/ScopeCoherenceTracker.d.ts +87 -0
- package/dist/core/ScopeCoherenceTracker.d.ts.map +1 -0
- package/dist/core/ScopeCoherenceTracker.js +226 -0
- package/dist/core/ScopeCoherenceTracker.js.map +1 -0
- package/dist/core/ScopeVerifier.d.ts +122 -0
- package/dist/core/ScopeVerifier.d.ts.map +1 -0
- package/dist/core/ScopeVerifier.js +350 -0
- package/dist/core/ScopeVerifier.js.map +1 -0
- package/dist/core/SecretManager.d.ts +120 -0
- package/dist/core/SecretManager.d.ts.map +1 -0
- package/dist/core/SecretManager.js +324 -0
- package/dist/core/SecretManager.js.map +1 -0
- package/dist/core/SecretMigrator.d.ts +39 -0
- package/dist/core/SecretMigrator.d.ts.map +1 -0
- package/dist/core/SecretMigrator.js +182 -0
- package/dist/core/SecretMigrator.js.map +1 -0
- package/dist/core/SecretRedactor.d.ts +121 -0
- package/dist/core/SecretRedactor.d.ts.map +1 -0
- package/dist/core/SecretRedactor.js +309 -0
- package/dist/core/SecretRedactor.js.map +1 -0
- package/dist/core/SecretStore.d.ts +104 -0
- package/dist/core/SecretStore.d.ts.map +1 -0
- package/dist/core/SecretStore.js +405 -0
- package/dist/core/SecretStore.js.map +1 -0
- package/dist/core/SecurityLog.d.ts +58 -0
- package/dist/core/SecurityLog.d.ts.map +1 -0
- package/dist/core/SecurityLog.js +123 -0
- package/dist/core/SecurityLog.js.map +1 -0
- package/dist/core/SendGateway.d.ts +77 -0
- package/dist/core/SendGateway.d.ts.map +1 -0
- package/dist/core/SendGateway.js +181 -0
- package/dist/core/SendGateway.js.map +1 -0
- package/dist/core/SessionManager.d.ts +304 -0
- package/dist/core/SessionManager.d.ts.map +1 -0
- package/dist/core/SessionManager.js +1402 -0
- package/dist/core/SessionManager.js.map +1 -0
- package/dist/core/SleepWakeDetector.d.ts +37 -0
- package/dist/core/SleepWakeDetector.d.ts.map +1 -0
- package/dist/core/SleepWakeDetector.js +59 -0
- package/dist/core/SleepWakeDetector.js.map +1 -0
- package/dist/core/SoulManager.d.ts +107 -0
- package/dist/core/SoulManager.d.ts.map +1 -0
- package/dist/core/SoulManager.js +574 -0
- package/dist/core/SoulManager.js.map +1 -0
- package/dist/core/StaleProcessGuard.d.ts +113 -0
- package/dist/core/StaleProcessGuard.d.ts.map +1 -0
- package/dist/core/StaleProcessGuard.js +134 -0
- package/dist/core/StaleProcessGuard.js.map +1 -0
- package/dist/core/StateManager.d.ts +56 -0
- package/dist/core/StateManager.d.ts.map +1 -0
- package/dist/core/StateManager.js +266 -0
- package/dist/core/StateManager.js.map +1 -0
- package/dist/core/StateWriteAuthority.d.ts +101 -0
- package/dist/core/StateWriteAuthority.d.ts.map +1 -0
- package/dist/core/StateWriteAuthority.js +169 -0
- package/dist/core/StateWriteAuthority.js.map +1 -0
- package/dist/core/SurfacingTemplates.d.ts +63 -0
- package/dist/core/SurfacingTemplates.d.ts.map +1 -0
- package/dist/core/SurfacingTemplates.js +138 -0
- package/dist/core/SurfacingTemplates.js.map +1 -0
- package/dist/core/SyncOrchestrator.d.ts +308 -0
- package/dist/core/SyncOrchestrator.d.ts.map +1 -0
- package/dist/core/SyncOrchestrator.js +925 -0
- package/dist/core/SyncOrchestrator.js.map +1 -0
- package/dist/core/TemporalCoherenceChecker.d.ts +140 -0
- package/dist/core/TemporalCoherenceChecker.d.ts.map +1 -0
- package/dist/core/TemporalCoherenceChecker.js +375 -0
- package/dist/core/TemporalCoherenceChecker.js.map +1 -0
- package/dist/core/TopicClassifier.d.ts +54 -0
- package/dist/core/TopicClassifier.d.ts.map +1 -0
- package/dist/core/TopicClassifier.js +144 -0
- package/dist/core/TopicClassifier.js.map +1 -0
- package/dist/core/TopicResumeMap.d.ts +76 -0
- package/dist/core/TopicResumeMap.d.ts.map +1 -0
- package/dist/core/TopicResumeMap.js +252 -0
- package/dist/core/TopicResumeMap.js.map +1 -0
- package/dist/core/TrustElevationTracker.d.ts +178 -0
- package/dist/core/TrustElevationTracker.d.ts.map +1 -0
- package/dist/core/TrustElevationTracker.js +343 -0
- package/dist/core/TrustElevationTracker.js.map +1 -0
- package/dist/core/TrustRecovery.d.ts +109 -0
- package/dist/core/TrustRecovery.d.ts.map +1 -0
- package/dist/core/TrustRecovery.js +190 -0
- package/dist/core/TrustRecovery.js.map +1 -0
- package/dist/core/UpdateChecker.d.ts +108 -0
- package/dist/core/UpdateChecker.d.ts.map +1 -0
- package/dist/core/UpdateChecker.js +593 -0
- package/dist/core/UpdateChecker.js.map +1 -0
- package/dist/core/UpdateGate.d.ts +102 -0
- package/dist/core/UpdateGate.d.ts.map +1 -0
- package/dist/core/UpdateGate.js +149 -0
- package/dist/core/UpdateGate.js.map +1 -0
- package/dist/core/UpgradeGuideProcessor.d.ts +105 -0
- package/dist/core/UpgradeGuideProcessor.d.ts.map +1 -0
- package/dist/core/UpgradeGuideProcessor.js +278 -0
- package/dist/core/UpgradeGuideProcessor.js.map +1 -0
- package/dist/core/UpgradeNotifyManager.d.ts +98 -0
- package/dist/core/UpgradeNotifyManager.d.ts.map +1 -0
- package/dist/core/UpgradeNotifyManager.js +205 -0
- package/dist/core/UpgradeNotifyManager.js.map +1 -0
- package/dist/core/WorkLedger.d.ts +144 -0
- package/dist/core/WorkLedger.d.ts.map +1 -0
- package/dist/core/WorkLedger.js +246 -0
- package/dist/core/WorkLedger.js.map +1 -0
- package/dist/core/models.d.ts +70 -0
- package/dist/core/models.d.ts.map +1 -0
- package/dist/core/models.js +110 -0
- package/dist/core/models.js.map +1 -0
- package/dist/core/reviewers/capability-accuracy.d.ts +13 -0
- package/dist/core/reviewers/capability-accuracy.d.ts.map +1 -0
- package/dist/core/reviewers/capability-accuracy.js +38 -0
- package/dist/core/reviewers/capability-accuracy.js.map +1 -0
- package/dist/core/reviewers/claim-provenance.d.ts +14 -0
- package/dist/core/reviewers/claim-provenance.d.ts.map +1 -0
- package/dist/core/reviewers/claim-provenance.js +50 -0
- package/dist/core/reviewers/claim-provenance.js.map +1 -0
- package/dist/core/reviewers/context-completeness.d.ts +13 -0
- package/dist/core/reviewers/context-completeness.d.ts.map +1 -0
- package/dist/core/reviewers/context-completeness.js +43 -0
- package/dist/core/reviewers/context-completeness.js.map +1 -0
- package/dist/core/reviewers/conversational-tone.d.ts +13 -0
- package/dist/core/reviewers/conversational-tone.d.ts.map +1 -0
- package/dist/core/reviewers/conversational-tone.js +55 -0
- package/dist/core/reviewers/conversational-tone.js.map +1 -0
- package/dist/core/reviewers/escalation-resolution.d.ts +56 -0
- package/dist/core/reviewers/escalation-resolution.d.ts.map +1 -0
- package/dist/core/reviewers/escalation-resolution.js +239 -0
- package/dist/core/reviewers/escalation-resolution.js.map +1 -0
- package/dist/core/reviewers/gate-reviewer.d.ts +26 -0
- package/dist/core/reviewers/gate-reviewer.d.ts.map +1 -0
- package/dist/core/reviewers/gate-reviewer.js +130 -0
- package/dist/core/reviewers/gate-reviewer.js.map +1 -0
- package/dist/core/reviewers/information-leakage.d.ts +21 -0
- package/dist/core/reviewers/information-leakage.d.ts.map +1 -0
- package/dist/core/reviewers/information-leakage.js +72 -0
- package/dist/core/reviewers/information-leakage.js.map +1 -0
- package/dist/core/reviewers/settling-detection.d.ts +13 -0
- package/dist/core/reviewers/settling-detection.d.ts.map +1 -0
- package/dist/core/reviewers/settling-detection.js +48 -0
- package/dist/core/reviewers/settling-detection.js.map +1 -0
- package/dist/core/reviewers/url-validity.d.ts +18 -0
- package/dist/core/reviewers/url-validity.d.ts.map +1 -0
- package/dist/core/reviewers/url-validity.js +60 -0
- package/dist/core/reviewers/url-validity.js.map +1 -0
- package/dist/core/reviewers/value-alignment.d.ts +14 -0
- package/dist/core/reviewers/value-alignment.d.ts.map +1 -0
- package/dist/core/reviewers/value-alignment.js +71 -0
- package/dist/core/reviewers/value-alignment.js.map +1 -0
- package/dist/core/types.d.ts +2159 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +20 -0
- package/dist/core/types.js.map +1 -0
- package/dist/data/http-hook-templates.d.ts +53 -0
- package/dist/data/http-hook-templates.d.ts.map +1 -0
- package/dist/data/http-hook-templates.js +64 -0
- package/dist/data/http-hook-templates.js.map +1 -0
- package/dist/index.d.ts +263 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +164 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge/CoverageAuditor.d.ts +58 -0
- package/dist/knowledge/CoverageAuditor.d.ts.map +1 -0
- package/dist/knowledge/CoverageAuditor.js +153 -0
- package/dist/knowledge/CoverageAuditor.js.map +1 -0
- package/dist/knowledge/IntegrityManager.d.ts +46 -0
- package/dist/knowledge/IntegrityManager.d.ts.map +1 -0
- package/dist/knowledge/IntegrityManager.js +101 -0
- package/dist/knowledge/IntegrityManager.js.map +1 -0
- package/dist/knowledge/KnowledgeManager.d.ts +85 -0
- package/dist/knowledge/KnowledgeManager.d.ts.map +1 -0
- package/dist/knowledge/KnowledgeManager.js +194 -0
- package/dist/knowledge/KnowledgeManager.js.map +1 -0
- package/dist/knowledge/ProbeRegistry.d.ts +54 -0
- package/dist/knowledge/ProbeRegistry.d.ts.map +1 -0
- package/dist/knowledge/ProbeRegistry.js +119 -0
- package/dist/knowledge/ProbeRegistry.js.map +1 -0
- package/dist/knowledge/SelfKnowledgeTree.d.ts +108 -0
- package/dist/knowledge/SelfKnowledgeTree.d.ts.map +1 -0
- package/dist/knowledge/SelfKnowledgeTree.js +560 -0
- package/dist/knowledge/SelfKnowledgeTree.js.map +1 -0
- package/dist/knowledge/TreeGenerator.d.ts +57 -0
- package/dist/knowledge/TreeGenerator.d.ts.map +1 -0
- package/dist/knowledge/TreeGenerator.js +527 -0
- package/dist/knowledge/TreeGenerator.js.map +1 -0
- package/dist/knowledge/TreeSynthesis.d.ts +24 -0
- package/dist/knowledge/TreeSynthesis.d.ts.map +1 -0
- package/dist/knowledge/TreeSynthesis.js +61 -0
- package/dist/knowledge/TreeSynthesis.js.map +1 -0
- package/dist/knowledge/TreeTraversal.d.ts +93 -0
- package/dist/knowledge/TreeTraversal.d.ts.map +1 -0
- package/dist/knowledge/TreeTraversal.js +359 -0
- package/dist/knowledge/TreeTraversal.js.map +1 -0
- package/dist/knowledge/TreeTriage.d.ts +80 -0
- package/dist/knowledge/TreeTriage.d.ts.map +1 -0
- package/dist/knowledge/TreeTriage.js +413 -0
- package/dist/knowledge/TreeTriage.js.map +1 -0
- package/dist/knowledge/types.d.ts +183 -0
- package/dist/knowledge/types.d.ts.map +1 -0
- package/dist/knowledge/types.js +28 -0
- package/dist/knowledge/types.js.map +1 -0
- package/dist/lifeline/MessageQueue.d.ts +42 -0
- package/dist/lifeline/MessageQueue.d.ts.map +1 -0
- package/dist/lifeline/MessageQueue.js +63 -0
- package/dist/lifeline/MessageQueue.js.map +1 -0
- package/dist/lifeline/ServerSupervisor.d.ts +203 -0
- package/dist/lifeline/ServerSupervisor.d.ts.map +1 -0
- package/dist/lifeline/ServerSupervisor.js +1103 -0
- package/dist/lifeline/ServerSupervisor.js.map +1 -0
- package/dist/lifeline/SlackLifeline.d.ts +43 -0
- package/dist/lifeline/SlackLifeline.d.ts.map +1 -0
- package/dist/lifeline/SlackLifeline.js +227 -0
- package/dist/lifeline/SlackLifeline.js.map +1 -0
- package/dist/lifeline/TelegramLifeline.d.ts +180 -0
- package/dist/lifeline/TelegramLifeline.d.ts.map +1 -0
- package/dist/lifeline/TelegramLifeline.js +1533 -0
- package/dist/lifeline/TelegramLifeline.js.map +1 -0
- package/dist/memory/ActivityPartitioner.d.ts +67 -0
- package/dist/memory/ActivityPartitioner.d.ts.map +1 -0
- package/dist/memory/ActivityPartitioner.js +193 -0
- package/dist/memory/ActivityPartitioner.js.map +1 -0
- package/dist/memory/Chunker.d.ts +39 -0
- package/dist/memory/Chunker.d.ts.map +1 -0
- package/dist/memory/Chunker.js +154 -0
- package/dist/memory/Chunker.js.map +1 -0
- package/dist/memory/EmbeddingProvider.d.ts +82 -0
- package/dist/memory/EmbeddingProvider.d.ts.map +1 -0
- package/dist/memory/EmbeddingProvider.js +168 -0
- package/dist/memory/EmbeddingProvider.js.map +1 -0
- package/dist/memory/EpisodicMemory.d.ts +141 -0
- package/dist/memory/EpisodicMemory.d.ts.map +1 -0
- package/dist/memory/EpisodicMemory.js +303 -0
- package/dist/memory/EpisodicMemory.js.map +1 -0
- package/dist/memory/MemoryExporter.d.ts +82 -0
- package/dist/memory/MemoryExporter.d.ts.map +1 -0
- package/dist/memory/MemoryExporter.js +233 -0
- package/dist/memory/MemoryExporter.js.map +1 -0
- package/dist/memory/MemoryIndex.d.ts +76 -0
- package/dist/memory/MemoryIndex.d.ts.map +1 -0
- package/dist/memory/MemoryIndex.js +413 -0
- package/dist/memory/MemoryIndex.js.map +1 -0
- package/dist/memory/MemoryMigrator.d.ts +103 -0
- package/dist/memory/MemoryMigrator.d.ts.map +1 -0
- package/dist/memory/MemoryMigrator.js +510 -0
- package/dist/memory/MemoryMigrator.js.map +1 -0
- package/dist/memory/SemanticMemory.d.ts +255 -0
- package/dist/memory/SemanticMemory.d.ts.map +1 -0
- package/dist/memory/SemanticMemory.js +1130 -0
- package/dist/memory/SemanticMemory.js.map +1 -0
- package/dist/memory/TopicMemory.d.ts +227 -0
- package/dist/memory/TopicMemory.d.ts.map +1 -0
- package/dist/memory/TopicMemory.js +731 -0
- package/dist/memory/TopicMemory.js.map +1 -0
- package/dist/memory/TopicSummarizer.d.ts +69 -0
- package/dist/memory/TopicSummarizer.d.ts.map +1 -0
- package/dist/memory/TopicSummarizer.js +163 -0
- package/dist/memory/TopicSummarizer.js.map +1 -0
- package/dist/memory/VectorSearch.d.ts +80 -0
- package/dist/memory/VectorSearch.d.ts.map +1 -0
- package/dist/memory/VectorSearch.js +148 -0
- package/dist/memory/VectorSearch.js.map +1 -0
- package/dist/memory/WorkingMemoryAssembler.d.ts +122 -0
- package/dist/memory/WorkingMemoryAssembler.d.ts.map +1 -0
- package/dist/memory/WorkingMemoryAssembler.js +406 -0
- package/dist/memory/WorkingMemoryAssembler.js.map +1 -0
- package/dist/messaging/AdapterRegistry.d.ts +27 -0
- package/dist/messaging/AdapterRegistry.d.ts.map +1 -0
- package/dist/messaging/AdapterRegistry.js +40 -0
- package/dist/messaging/AdapterRegistry.js.map +1 -0
- package/dist/messaging/AgentTokenManager.d.ts +86 -0
- package/dist/messaging/AgentTokenManager.d.ts.map +1 -0
- package/dist/messaging/AgentTokenManager.js +213 -0
- package/dist/messaging/AgentTokenManager.js.map +1 -0
- package/dist/messaging/DeliveryRetryManager.d.ts +47 -0
- package/dist/messaging/DeliveryRetryManager.d.ts.map +1 -0
- package/dist/messaging/DeliveryRetryManager.js +201 -0
- package/dist/messaging/DeliveryRetryManager.js.map +1 -0
- package/dist/messaging/DropPickup.d.ts +37 -0
- package/dist/messaging/DropPickup.d.ts.map +1 -0
- package/dist/messaging/DropPickup.js +120 -0
- package/dist/messaging/DropPickup.js.map +1 -0
- package/dist/messaging/GitSyncTransport.d.ts +120 -0
- package/dist/messaging/GitSyncTransport.d.ts.map +1 -0
- package/dist/messaging/GitSyncTransport.js +296 -0
- package/dist/messaging/GitSyncTransport.js.map +1 -0
- package/dist/messaging/MessageDelivery.d.ts +27 -0
- package/dist/messaging/MessageDelivery.d.ts.map +1 -0
- package/dist/messaging/MessageDelivery.js +90 -0
- package/dist/messaging/MessageDelivery.js.map +1 -0
- package/dist/messaging/MessageFormatter.d.ts +30 -0
- package/dist/messaging/MessageFormatter.d.ts.map +1 -0
- package/dist/messaging/MessageFormatter.js +97 -0
- package/dist/messaging/MessageFormatter.js.map +1 -0
- package/dist/messaging/MessageRouter.d.ts +157 -0
- package/dist/messaging/MessageRouter.d.ts.map +1 -0
- package/dist/messaging/MessageRouter.js +657 -0
- package/dist/messaging/MessageRouter.js.map +1 -0
- package/dist/messaging/MessageStore.d.ts +52 -0
- package/dist/messaging/MessageStore.d.ts.map +1 -0
- package/dist/messaging/MessageStore.js +363 -0
- package/dist/messaging/MessageStore.js.map +1 -0
- package/dist/messaging/NotificationBatcher.d.ts +95 -0
- package/dist/messaging/NotificationBatcher.d.ts.map +1 -0
- package/dist/messaging/NotificationBatcher.js +253 -0
- package/dist/messaging/NotificationBatcher.js.map +1 -0
- package/dist/messaging/SessionSummarySentinel.d.ts +104 -0
- package/dist/messaging/SessionSummarySentinel.d.ts.map +1 -0
- package/dist/messaging/SessionSummarySentinel.js +386 -0
- package/dist/messaging/SessionSummarySentinel.js.map +1 -0
- package/dist/messaging/SpawnRequestManager.d.ts +88 -0
- package/dist/messaging/SpawnRequestManager.d.ts.map +1 -0
- package/dist/messaging/SpawnRequestManager.js +159 -0
- package/dist/messaging/SpawnRequestManager.js.map +1 -0
- package/dist/messaging/TelegramAdapter.d.ts +636 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -0
- package/dist/messaging/TelegramAdapter.js +3434 -0
- package/dist/messaging/TelegramAdapter.js.map +1 -0
- package/dist/messaging/TopicContentValidator.d.ts +81 -0
- package/dist/messaging/TopicContentValidator.d.ts.map +1 -0
- package/dist/messaging/TopicContentValidator.js +113 -0
- package/dist/messaging/TopicContentValidator.js.map +1 -0
- package/dist/messaging/WhatsAppAdapter.d.ts +224 -0
- package/dist/messaging/WhatsAppAdapter.d.ts.map +1 -0
- package/dist/messaging/WhatsAppAdapter.js +736 -0
- package/dist/messaging/WhatsAppAdapter.js.map +1 -0
- package/dist/messaging/backends/BaileysBackend.d.ts +74 -0
- package/dist/messaging/backends/BaileysBackend.d.ts.map +1 -0
- package/dist/messaging/backends/BaileysBackend.js +481 -0
- package/dist/messaging/backends/BaileysBackend.js.map +1 -0
- package/dist/messaging/backends/BusinessApiBackend.d.ts +173 -0
- package/dist/messaging/backends/BusinessApiBackend.d.ts.map +1 -0
- package/dist/messaging/backends/BusinessApiBackend.js +269 -0
- package/dist/messaging/backends/BusinessApiBackend.js.map +1 -0
- package/dist/messaging/backends/WhatsAppWebhookRoutes.d.ts +26 -0
- package/dist/messaging/backends/WhatsAppWebhookRoutes.d.ts.map +1 -0
- package/dist/messaging/backends/WhatsAppWebhookRoutes.js +50 -0
- package/dist/messaging/backends/WhatsAppWebhookRoutes.js.map +1 -0
- package/dist/messaging/shared/AuthGate.d.ts +102 -0
- package/dist/messaging/shared/AuthGate.d.ts.map +1 -0
- package/dist/messaging/shared/AuthGate.js +159 -0
- package/dist/messaging/shared/AuthGate.js.map +1 -0
- package/dist/messaging/shared/CommandRouter.d.ts +84 -0
- package/dist/messaging/shared/CommandRouter.d.ts.map +1 -0
- package/dist/messaging/shared/CommandRouter.js +145 -0
- package/dist/messaging/shared/CommandRouter.js.map +1 -0
- package/dist/messaging/shared/CrossPlatformAlerts.d.ts +67 -0
- package/dist/messaging/shared/CrossPlatformAlerts.d.ts.map +1 -0
- package/dist/messaging/shared/CrossPlatformAlerts.js +134 -0
- package/dist/messaging/shared/CrossPlatformAlerts.js.map +1 -0
- package/dist/messaging/shared/EncryptedAuthStore.d.ts +51 -0
- package/dist/messaging/shared/EncryptedAuthStore.d.ts.map +1 -0
- package/dist/messaging/shared/EncryptedAuthStore.js +194 -0
- package/dist/messaging/shared/EncryptedAuthStore.js.map +1 -0
- package/dist/messaging/shared/FeatureFlags.d.ts +21 -0
- package/dist/messaging/shared/FeatureFlags.d.ts.map +1 -0
- package/dist/messaging/shared/FeatureFlags.js +21 -0
- package/dist/messaging/shared/FeatureFlags.js.map +1 -0
- package/dist/messaging/shared/MessageBridge.d.ts +70 -0
- package/dist/messaging/shared/MessageBridge.d.ts.map +1 -0
- package/dist/messaging/shared/MessageBridge.js +178 -0
- package/dist/messaging/shared/MessageBridge.js.map +1 -0
- package/dist/messaging/shared/MessageLogger.d.ts +94 -0
- package/dist/messaging/shared/MessageLogger.d.ts.map +1 -0
- package/dist/messaging/shared/MessageLogger.js +173 -0
- package/dist/messaging/shared/MessageLogger.js.map +1 -0
- package/dist/messaging/shared/MessagingEventBus.d.ts +149 -0
- package/dist/messaging/shared/MessagingEventBus.d.ts.map +1 -0
- package/dist/messaging/shared/MessagingEventBus.js +111 -0
- package/dist/messaging/shared/MessagingEventBus.js.map +1 -0
- package/dist/messaging/shared/PhoneUtils.d.ts +44 -0
- package/dist/messaging/shared/PhoneUtils.d.ts.map +1 -0
- package/dist/messaging/shared/PhoneUtils.js +110 -0
- package/dist/messaging/shared/PhoneUtils.js.map +1 -0
- package/dist/messaging/shared/PrivacyConsent.d.ts +61 -0
- package/dist/messaging/shared/PrivacyConsent.d.ts.map +1 -0
- package/dist/messaging/shared/PrivacyConsent.js +132 -0
- package/dist/messaging/shared/PrivacyConsent.js.map +1 -0
- package/dist/messaging/shared/SessionChannelRegistry.d.ts +48 -0
- package/dist/messaging/shared/SessionChannelRegistry.d.ts.map +1 -0
- package/dist/messaging/shared/SessionChannelRegistry.js +144 -0
- package/dist/messaging/shared/SessionChannelRegistry.js.map +1 -0
- package/dist/messaging/shared/SmartChunker.d.ts +16 -0
- package/dist/messaging/shared/SmartChunker.d.ts.map +1 -0
- package/dist/messaging/shared/SmartChunker.js +100 -0
- package/dist/messaging/shared/SmartChunker.js.map +1 -0
- package/dist/messaging/shared/StallDetector.d.ts +85 -0
- package/dist/messaging/shared/StallDetector.d.ts.map +1 -0
- package/dist/messaging/shared/StallDetector.js +225 -0
- package/dist/messaging/shared/StallDetector.js.map +1 -0
- package/dist/messaging/shared/index.d.ts +22 -0
- package/dist/messaging/shared/index.d.ts.map +1 -0
- package/dist/messaging/shared/index.js +13 -0
- package/dist/messaging/shared/index.js.map +1 -0
- package/dist/messaging/slack/ChannelManager.d.ts +36 -0
- package/dist/messaging/slack/ChannelManager.d.ts.map +1 -0
- package/dist/messaging/slack/ChannelManager.js +100 -0
- package/dist/messaging/slack/ChannelManager.js.map +1 -0
- package/dist/messaging/slack/FileHandler.d.ts +30 -0
- package/dist/messaging/slack/FileHandler.d.ts.map +1 -0
- package/dist/messaging/slack/FileHandler.js +87 -0
- package/dist/messaging/slack/FileHandler.js.map +1 -0
- package/dist/messaging/slack/RingBuffer.d.ts +22 -0
- package/dist/messaging/slack/RingBuffer.d.ts.map +1 -0
- package/dist/messaging/slack/RingBuffer.js +48 -0
- package/dist/messaging/slack/RingBuffer.js.map +1 -0
- package/dist/messaging/slack/SlackAdapter.d.ts +283 -0
- package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -0
- package/dist/messaging/slack/SlackAdapter.js +1524 -0
- package/dist/messaging/slack/SlackAdapter.js.map +1 -0
- package/dist/messaging/slack/SlackApiClient.d.ts +51 -0
- package/dist/messaging/slack/SlackApiClient.d.ts.map +1 -0
- package/dist/messaging/slack/SlackApiClient.js +94 -0
- package/dist/messaging/slack/SlackApiClient.js.map +1 -0
- package/dist/messaging/slack/SocketModeClient.d.ts +44 -0
- package/dist/messaging/slack/SocketModeClient.d.ts.map +1 -0
- package/dist/messaging/slack/SocketModeClient.js +209 -0
- package/dist/messaging/slack/SocketModeClient.js.map +1 -0
- package/dist/messaging/slack/index.d.ts +12 -0
- package/dist/messaging/slack/index.d.ts.map +1 -0
- package/dist/messaging/slack/index.js +15 -0
- package/dist/messaging/slack/index.js.map +1 -0
- package/dist/messaging/slack/sanitize.d.ts +39 -0
- package/dist/messaging/slack/sanitize.d.ts.map +1 -0
- package/dist/messaging/slack/sanitize.js +71 -0
- package/dist/messaging/slack/sanitize.js.map +1 -0
- package/dist/messaging/slack/types.d.ts +186 -0
- package/dist/messaging/slack/types.d.ts.map +1 -0
- package/dist/messaging/slack/types.js +54 -0
- package/dist/messaging/slack/types.js.map +1 -0
- package/dist/messaging/types.d.ts +400 -0
- package/dist/messaging/types.d.ts.map +1 -0
- package/dist/messaging/types.js +89 -0
- package/dist/messaging/types.js.map +1 -0
- package/dist/monitoring/AccountSwitcher.d.ts +61 -0
- package/dist/monitoring/AccountSwitcher.d.ts.map +1 -0
- package/dist/monitoring/AccountSwitcher.js +196 -0
- package/dist/monitoring/AccountSwitcher.js.map +1 -0
- package/dist/monitoring/CoherenceMonitor.d.ts +133 -0
- package/dist/monitoring/CoherenceMonitor.d.ts.map +1 -0
- package/dist/monitoring/CoherenceMonitor.js +550 -0
- package/dist/monitoring/CoherenceMonitor.js.map +1 -0
- package/dist/monitoring/CommitmentSentinel.d.ts +57 -0
- package/dist/monitoring/CommitmentSentinel.d.ts.map +1 -0
- package/dist/monitoring/CommitmentSentinel.js +251 -0
- package/dist/monitoring/CommitmentSentinel.js.map +1 -0
- package/dist/monitoring/CommitmentTracker.d.ts +186 -0
- package/dist/monitoring/CommitmentTracker.d.ts.map +1 -0
- package/dist/monitoring/CommitmentTracker.js +522 -0
- package/dist/monitoring/CommitmentTracker.js.map +1 -0
- package/dist/monitoring/CredentialProvider.d.ts +84 -0
- package/dist/monitoring/CredentialProvider.d.ts.map +1 -0
- package/dist/monitoring/CredentialProvider.js +196 -0
- package/dist/monitoring/CredentialProvider.js.map +1 -0
- package/dist/monitoring/DegradationReporter.d.ts +123 -0
- package/dist/monitoring/DegradationReporter.d.ts.map +1 -0
- package/dist/monitoring/DegradationReporter.js +241 -0
- package/dist/monitoring/DegradationReporter.js.map +1 -0
- package/dist/monitoring/FeedbackAnomalyDetector.d.ts +51 -0
- package/dist/monitoring/FeedbackAnomalyDetector.d.ts.map +1 -0
- package/dist/monitoring/FeedbackAnomalyDetector.js +120 -0
- package/dist/monitoring/FeedbackAnomalyDetector.js.map +1 -0
- package/dist/monitoring/HealthChecker.d.ts +45 -0
- package/dist/monitoring/HealthChecker.d.ts.map +1 -0
- package/dist/monitoring/HealthChecker.js +219 -0
- package/dist/monitoring/HealthChecker.js.map +1 -0
- package/dist/monitoring/HomeostasisMonitor.d.ts +102 -0
- package/dist/monitoring/HomeostasisMonitor.d.ts.map +1 -0
- package/dist/monitoring/HomeostasisMonitor.js +185 -0
- package/dist/monitoring/HomeostasisMonitor.js.map +1 -0
- package/dist/monitoring/HookEventReceiver.d.ts +132 -0
- package/dist/monitoring/HookEventReceiver.d.ts.map +1 -0
- package/dist/monitoring/HookEventReceiver.js +209 -0
- package/dist/monitoring/HookEventReceiver.js.map +1 -0
- package/dist/monitoring/InputClassifier.d.ts +68 -0
- package/dist/monitoring/InputClassifier.d.ts.map +1 -0
- package/dist/monitoring/InputClassifier.js +243 -0
- package/dist/monitoring/InputClassifier.js.map +1 -0
- package/dist/monitoring/InstructionsVerifier.d.ts +76 -0
- package/dist/monitoring/InstructionsVerifier.d.ts.map +1 -0
- package/dist/monitoring/InstructionsVerifier.js +116 -0
- package/dist/monitoring/InstructionsVerifier.js.map +1 -0
- package/dist/monitoring/MemoryPressureMonitor.d.ts +107 -0
- package/dist/monitoring/MemoryPressureMonitor.d.ts.map +1 -0
- package/dist/monitoring/MemoryPressureMonitor.js +329 -0
- package/dist/monitoring/MemoryPressureMonitor.js.map +1 -0
- package/dist/monitoring/OrphanProcessReaper.d.ts +125 -0
- package/dist/monitoring/OrphanProcessReaper.d.ts.map +1 -0
- package/dist/monitoring/OrphanProcessReaper.js +476 -0
- package/dist/monitoring/OrphanProcessReaper.js.map +1 -0
- package/dist/monitoring/PresenceProxy.d.ts +167 -0
- package/dist/monitoring/PresenceProxy.d.ts.map +1 -0
- package/dist/monitoring/PresenceProxy.js +972 -0
- package/dist/monitoring/PresenceProxy.js.map +1 -0
- package/dist/monitoring/PromptGate.d.ts +91 -0
- package/dist/monitoring/PromptGate.d.ts.map +1 -0
- package/dist/monitoring/PromptGate.js +411 -0
- package/dist/monitoring/PromptGate.js.map +1 -0
- package/dist/monitoring/QuotaCollector.d.ts +267 -0
- package/dist/monitoring/QuotaCollector.d.ts.map +1 -0
- package/dist/monitoring/QuotaCollector.js +790 -0
- package/dist/monitoring/QuotaCollector.js.map +1 -0
- package/dist/monitoring/QuotaExhaustionDetector.d.ts +21 -0
- package/dist/monitoring/QuotaExhaustionDetector.d.ts.map +1 -0
- package/dist/monitoring/QuotaExhaustionDetector.js +136 -0
- package/dist/monitoring/QuotaExhaustionDetector.js.map +1 -0
- package/dist/monitoring/QuotaManager.d.ts +191 -0
- package/dist/monitoring/QuotaManager.d.ts.map +1 -0
- package/dist/monitoring/QuotaManager.js +575 -0
- package/dist/monitoring/QuotaManager.js.map +1 -0
- package/dist/monitoring/QuotaNotifier.d.ts +38 -0
- package/dist/monitoring/QuotaNotifier.d.ts.map +1 -0
- package/dist/monitoring/QuotaNotifier.js +144 -0
- package/dist/monitoring/QuotaNotifier.js.map +1 -0
- package/dist/monitoring/QuotaTracker.d.ts +92 -0
- package/dist/monitoring/QuotaTracker.d.ts.map +1 -0
- package/dist/monitoring/QuotaTracker.js +239 -0
- package/dist/monitoring/QuotaTracker.js.map +1 -0
- package/dist/monitoring/ReflectionMetrics.d.ts +97 -0
- package/dist/monitoring/ReflectionMetrics.d.ts.map +1 -0
- package/dist/monitoring/ReflectionMetrics.js +170 -0
- package/dist/monitoring/ReflectionMetrics.js.map +1 -0
- package/dist/monitoring/SessionActivitySentinel.d.ts +95 -0
- package/dist/monitoring/SessionActivitySentinel.d.ts.map +1 -0
- package/dist/monitoring/SessionActivitySentinel.js +391 -0
- package/dist/monitoring/SessionActivitySentinel.js.map +1 -0
- package/dist/monitoring/SessionCredentialManager.d.ts +56 -0
- package/dist/monitoring/SessionCredentialManager.d.ts.map +1 -0
- package/dist/monitoring/SessionCredentialManager.js +91 -0
- package/dist/monitoring/SessionCredentialManager.js.map +1 -0
- package/dist/monitoring/SessionMigrator.d.ts +234 -0
- package/dist/monitoring/SessionMigrator.d.ts.map +1 -0
- package/dist/monitoring/SessionMigrator.js +604 -0
- package/dist/monitoring/SessionMigrator.js.map +1 -0
- package/dist/monitoring/SessionMonitor.d.ts +102 -0
- package/dist/monitoring/SessionMonitor.d.ts.map +1 -0
- package/dist/monitoring/SessionMonitor.js +238 -0
- package/dist/monitoring/SessionMonitor.js.map +1 -0
- package/dist/monitoring/SessionRecovery.d.ts +129 -0
- package/dist/monitoring/SessionRecovery.d.ts.map +1 -0
- package/dist/monitoring/SessionRecovery.js +496 -0
- package/dist/monitoring/SessionRecovery.js.map +1 -0
- package/dist/monitoring/SessionWatchdog.d.ts +138 -0
- package/dist/monitoring/SessionWatchdog.d.ts.map +1 -0
- package/dist/monitoring/SessionWatchdog.js +549 -0
- package/dist/monitoring/SessionWatchdog.js.map +1 -0
- package/dist/monitoring/StallTriageNurse.d.ts +96 -0
- package/dist/monitoring/StallTriageNurse.d.ts.map +1 -0
- package/dist/monitoring/StallTriageNurse.js +711 -0
- package/dist/monitoring/StallTriageNurse.js.map +1 -0
- package/dist/monitoring/StallTriageNurse.types.d.ts +124 -0
- package/dist/monitoring/StallTriageNurse.types.d.ts.map +1 -0
- package/dist/monitoring/StallTriageNurse.types.js +5 -0
- package/dist/monitoring/StallTriageNurse.types.js.map +1 -0
- package/dist/monitoring/SubagentTracker.d.ts +86 -0
- package/dist/monitoring/SubagentTracker.d.ts.map +1 -0
- package/dist/monitoring/SubagentTracker.js +199 -0
- package/dist/monitoring/SubagentTracker.js.map +1 -0
- package/dist/monitoring/SystemReviewer.d.ts +243 -0
- package/dist/monitoring/SystemReviewer.d.ts.map +1 -0
- package/dist/monitoring/SystemReviewer.js +697 -0
- package/dist/monitoring/SystemReviewer.js.map +1 -0
- package/dist/monitoring/TelemetryAuth.d.ts +64 -0
- package/dist/monitoring/TelemetryAuth.d.ts.map +1 -0
- package/dist/monitoring/TelemetryAuth.js +141 -0
- package/dist/monitoring/TelemetryAuth.js.map +1 -0
- package/dist/monitoring/TelemetryCollector.d.ts +95 -0
- package/dist/monitoring/TelemetryCollector.d.ts.map +1 -0
- package/dist/monitoring/TelemetryCollector.js +326 -0
- package/dist/monitoring/TelemetryCollector.js.map +1 -0
- package/dist/monitoring/TelemetryHeartbeat.d.ts +160 -0
- package/dist/monitoring/TelemetryHeartbeat.d.ts.map +1 -0
- package/dist/monitoring/TelemetryHeartbeat.js +450 -0
- package/dist/monitoring/TelemetryHeartbeat.js.map +1 -0
- package/dist/monitoring/TriageOrchestrator.d.ts +217 -0
- package/dist/monitoring/TriageOrchestrator.d.ts.map +1 -0
- package/dist/monitoring/TriageOrchestrator.js +801 -0
- package/dist/monitoring/TriageOrchestrator.js.map +1 -0
- package/dist/monitoring/WorktreeMonitor.d.ts +124 -0
- package/dist/monitoring/WorktreeMonitor.d.ts.map +1 -0
- package/dist/monitoring/WorktreeMonitor.js +379 -0
- package/dist/monitoring/WorktreeMonitor.js.map +1 -0
- package/dist/monitoring/crash-detector.d.ts +50 -0
- package/dist/monitoring/crash-detector.d.ts.map +1 -0
- package/dist/monitoring/crash-detector.js +224 -0
- package/dist/monitoring/crash-detector.js.map +1 -0
- package/dist/monitoring/jsonl-truncator.d.ts +44 -0
- package/dist/monitoring/jsonl-truncator.d.ts.map +1 -0
- package/dist/monitoring/jsonl-truncator.js +224 -0
- package/dist/monitoring/jsonl-truncator.js.map +1 -0
- package/dist/monitoring/probes/LifelineProbe.d.ts +38 -0
- package/dist/monitoring/probes/LifelineProbe.d.ts.map +1 -0
- package/dist/monitoring/probes/LifelineProbe.js +285 -0
- package/dist/monitoring/probes/LifelineProbe.js.map +1 -0
- package/dist/monitoring/probes/MessagingProbe.d.ts +23 -0
- package/dist/monitoring/probes/MessagingProbe.d.ts.map +1 -0
- package/dist/monitoring/probes/MessagingProbe.js +214 -0
- package/dist/monitoring/probes/MessagingProbe.js.map +1 -0
- package/dist/monitoring/probes/PlatformProbe.d.ts +15 -0
- package/dist/monitoring/probes/PlatformProbe.d.ts.map +1 -0
- package/dist/monitoring/probes/PlatformProbe.js +222 -0
- package/dist/monitoring/probes/PlatformProbe.js.map +1 -0
- package/dist/monitoring/probes/SchedulerProbe.d.ts +33 -0
- package/dist/monitoring/probes/SchedulerProbe.d.ts.map +1 -0
- package/dist/monitoring/probes/SchedulerProbe.js +196 -0
- package/dist/monitoring/probes/SchedulerProbe.js.map +1 -0
- package/dist/monitoring/probes/SessionProbe.d.ts +28 -0
- package/dist/monitoring/probes/SessionProbe.d.ts.map +1 -0
- package/dist/monitoring/probes/SessionProbe.js +234 -0
- package/dist/monitoring/probes/SessionProbe.js.map +1 -0
- package/dist/monitoring/stall-detector.d.ts +34 -0
- package/dist/monitoring/stall-detector.d.ts.map +1 -0
- package/dist/monitoring/stall-detector.js +151 -0
- package/dist/monitoring/stall-detector.js.map +1 -0
- package/dist/paste/PasteManager.d.ts +154 -0
- package/dist/paste/PasteManager.d.ts.map +1 -0
- package/dist/paste/PasteManager.js +524 -0
- package/dist/paste/PasteManager.js.map +1 -0
- package/dist/paste/TruncationDetector.d.ts +51 -0
- package/dist/paste/TruncationDetector.d.ts.map +1 -0
- package/dist/paste/TruncationDetector.js +220 -0
- package/dist/paste/TruncationDetector.js.map +1 -0
- package/dist/privacy/OutputPrivacyRouter.d.ts +65 -0
- package/dist/privacy/OutputPrivacyRouter.d.ts.map +1 -0
- package/dist/privacy/OutputPrivacyRouter.js +156 -0
- package/dist/privacy/OutputPrivacyRouter.js.map +1 -0
- package/dist/publishing/PrivateViewer.d.ts +63 -0
- package/dist/publishing/PrivateViewer.d.ts.map +1 -0
- package/dist/publishing/PrivateViewer.js +398 -0
- package/dist/publishing/PrivateViewer.js.map +1 -0
- package/dist/publishing/TelegraphService.d.ts +137 -0
- package/dist/publishing/TelegraphService.d.ts.map +1 -0
- package/dist/publishing/TelegraphService.js +410 -0
- package/dist/publishing/TelegraphService.js.map +1 -0
- package/dist/scaffold/bootstrap.d.ts +21 -0
- package/dist/scaffold/bootstrap.d.ts.map +1 -0
- package/dist/scaffold/bootstrap.js +125 -0
- package/dist/scaffold/bootstrap.js.map +1 -0
- package/dist/scaffold/templates.d.ts +47 -0
- package/dist/scaffold/templates.d.ts.map +1 -0
- package/dist/scaffold/templates.js +1506 -0
- package/dist/scaffold/templates.js.map +1 -0
- package/dist/scheduler/IntegrationGate.d.ts +81 -0
- package/dist/scheduler/IntegrationGate.d.ts.map +1 -0
- package/dist/scheduler/IntegrationGate.js +242 -0
- package/dist/scheduler/IntegrationGate.js.map +1 -0
- package/dist/scheduler/JobClaimManager.d.ts +137 -0
- package/dist/scheduler/JobClaimManager.d.ts.map +1 -0
- package/dist/scheduler/JobClaimManager.js +283 -0
- package/dist/scheduler/JobClaimManager.js.map +1 -0
- package/dist/scheduler/JobLoader.d.ts +28 -0
- package/dist/scheduler/JobLoader.d.ts.map +1 -0
- package/dist/scheduler/JobLoader.js +269 -0
- package/dist/scheduler/JobLoader.js.map +1 -0
- package/dist/scheduler/JobRunHistory.d.ts +180 -0
- package/dist/scheduler/JobRunHistory.d.ts.map +1 -0
- package/dist/scheduler/JobRunHistory.js +349 -0
- package/dist/scheduler/JobRunHistory.js.map +1 -0
- package/dist/scheduler/JobScheduler.d.ts +244 -0
- package/dist/scheduler/JobScheduler.d.ts.map +1 -0
- package/dist/scheduler/JobScheduler.js +1085 -0
- package/dist/scheduler/JobScheduler.js.map +1 -0
- package/dist/scheduler/SkipLedger.d.ts +65 -0
- package/dist/scheduler/SkipLedger.d.ts.map +1 -0
- package/dist/scheduler/SkipLedger.js +179 -0
- package/dist/scheduler/SkipLedger.js.map +1 -0
- package/dist/security/LLMSanitizer.d.ts +65 -0
- package/dist/security/LLMSanitizer.d.ts.map +1 -0
- package/dist/security/LLMSanitizer.js +153 -0
- package/dist/security/LLMSanitizer.js.map +1 -0
- package/dist/security/ManifestIntegrity.d.ts +72 -0
- package/dist/security/ManifestIntegrity.d.ts.map +1 -0
- package/dist/security/ManifestIntegrity.js +159 -0
- package/dist/security/ManifestIntegrity.js.map +1 -0
- package/dist/server/AgentServer.d.ts +134 -0
- package/dist/server/AgentServer.d.ts.map +1 -0
- package/dist/server/AgentServer.js +307 -0
- package/dist/server/AgentServer.js.map +1 -0
- package/dist/server/SecretDrop.d.ts +133 -0
- package/dist/server/SecretDrop.d.ts.map +1 -0
- package/dist/server/SecretDrop.js +473 -0
- package/dist/server/SecretDrop.js.map +1 -0
- package/dist/server/WebSocketManager.d.ts +80 -0
- package/dist/server/WebSocketManager.d.ts.map +1 -0
- package/dist/server/WebSocketManager.js +367 -0
- package/dist/server/WebSocketManager.js.map +1 -0
- package/dist/server/fileRoutes.d.ts +19 -0
- package/dist/server/fileRoutes.d.ts.map +1 -0
- package/dist/server/fileRoutes.js +578 -0
- package/dist/server/fileRoutes.js.map +1 -0
- package/dist/server/machineAuth.d.ts +87 -0
- package/dist/server/machineAuth.d.ts.map +1 -0
- package/dist/server/machineAuth.js +188 -0
- package/dist/server/machineAuth.js.map +1 -0
- package/dist/server/machineRoutes.d.ts +49 -0
- package/dist/server/machineRoutes.d.ts.map +1 -0
- package/dist/server/machineRoutes.js +305 -0
- package/dist/server/machineRoutes.js.map +1 -0
- package/dist/server/middleware.d.ts +32 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +202 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/routes.d.ts +132 -0
- package/dist/server/routes.d.ts.map +1 -0
- package/dist/server/routes.js +8124 -0
- package/dist/server/routes.js.map +1 -0
- package/dist/threadline/A2AGateway.d.ts +184 -0
- package/dist/threadline/A2AGateway.d.ts.map +1 -0
- package/dist/threadline/A2AGateway.js +438 -0
- package/dist/threadline/A2AGateway.js.map +1 -0
- package/dist/threadline/AgentCard.d.ts +116 -0
- package/dist/threadline/AgentCard.d.ts.map +1 -0
- package/dist/threadline/AgentCard.js +212 -0
- package/dist/threadline/AgentCard.js.map +1 -0
- package/dist/threadline/AgentDiscovery.d.ts +156 -0
- package/dist/threadline/AgentDiscovery.d.ts.map +1 -0
- package/dist/threadline/AgentDiscovery.js +390 -0
- package/dist/threadline/AgentDiscovery.js.map +1 -0
- package/dist/threadline/AgentTrustManager.d.ts +190 -0
- package/dist/threadline/AgentTrustManager.d.ts.map +1 -0
- package/dist/threadline/AgentTrustManager.js +526 -0
- package/dist/threadline/AgentTrustManager.js.map +1 -0
- package/dist/threadline/ApprovalQueue.d.ts +71 -0
- package/dist/threadline/ApprovalQueue.d.ts.map +1 -0
- package/dist/threadline/ApprovalQueue.js +154 -0
- package/dist/threadline/ApprovalQueue.js.map +1 -0
- package/dist/threadline/AutonomyGate.d.ts +130 -0
- package/dist/threadline/AutonomyGate.d.ts.map +1 -0
- package/dist/threadline/AutonomyGate.js +267 -0
- package/dist/threadline/AutonomyGate.js.map +1 -0
- package/dist/threadline/CircuitBreaker.d.ts +89 -0
- package/dist/threadline/CircuitBreaker.d.ts.map +1 -0
- package/dist/threadline/CircuitBreaker.js +238 -0
- package/dist/threadline/CircuitBreaker.js.map +1 -0
- package/dist/threadline/ComputeMeter.d.ts +114 -0
- package/dist/threadline/ComputeMeter.d.ts.map +1 -0
- package/dist/threadline/ComputeMeter.js +350 -0
- package/dist/threadline/ComputeMeter.js.map +1 -0
- package/dist/threadline/ContentClassifier.d.ts +83 -0
- package/dist/threadline/ContentClassifier.d.ts.map +1 -0
- package/dist/threadline/ContentClassifier.js +201 -0
- package/dist/threadline/ContentClassifier.js.map +1 -0
- package/dist/threadline/ContextThreadMap.d.ts +103 -0
- package/dist/threadline/ContextThreadMap.d.ts.map +1 -0
- package/dist/threadline/ContextThreadMap.js +279 -0
- package/dist/threadline/ContextThreadMap.js.map +1 -0
- package/dist/threadline/DNSVerifier.d.ts +48 -0
- package/dist/threadline/DNSVerifier.d.ts.map +1 -0
- package/dist/threadline/DNSVerifier.js +138 -0
- package/dist/threadline/DNSVerifier.js.map +1 -0
- package/dist/threadline/DigestCollector.d.ts +70 -0
- package/dist/threadline/DigestCollector.d.ts.map +1 -0
- package/dist/threadline/DigestCollector.js +146 -0
- package/dist/threadline/DigestCollector.js.map +1 -0
- package/dist/threadline/HandshakeManager.d.ts +130 -0
- package/dist/threadline/HandshakeManager.d.ts.map +1 -0
- package/dist/threadline/HandshakeManager.js +402 -0
- package/dist/threadline/HandshakeManager.js.map +1 -0
- package/dist/threadline/InboundMessageGate.d.ts +80 -0
- package/dist/threadline/InboundMessageGate.d.ts.map +1 -0
- package/dist/threadline/InboundMessageGate.js +241 -0
- package/dist/threadline/InboundMessageGate.js.map +1 -0
- package/dist/threadline/InvitationManager.d.ts +91 -0
- package/dist/threadline/InvitationManager.d.ts.map +1 -0
- package/dist/threadline/InvitationManager.js +228 -0
- package/dist/threadline/InvitationManager.js.map +1 -0
- package/dist/threadline/ListenerSessionManager.d.ts +147 -0
- package/dist/threadline/ListenerSessionManager.d.ts.map +1 -0
- package/dist/threadline/ListenerSessionManager.js +326 -0
- package/dist/threadline/ListenerSessionManager.js.map +1 -0
- package/dist/threadline/MCPAuth.d.ts +98 -0
- package/dist/threadline/MCPAuth.d.ts.map +1 -0
- package/dist/threadline/MCPAuth.js +228 -0
- package/dist/threadline/MCPAuth.js.map +1 -0
- package/dist/threadline/OpenClawBridge.d.ts +143 -0
- package/dist/threadline/OpenClawBridge.d.ts.map +1 -0
- package/dist/threadline/OpenClawBridge.js +336 -0
- package/dist/threadline/OpenClawBridge.js.map +1 -0
- package/dist/threadline/OpenClawSkillManifest.d.ts +47 -0
- package/dist/threadline/OpenClawSkillManifest.d.ts.map +1 -0
- package/dist/threadline/OpenClawSkillManifest.js +148 -0
- package/dist/threadline/OpenClawSkillManifest.js.map +1 -0
- package/dist/threadline/RateLimiter.d.ts +105 -0
- package/dist/threadline/RateLimiter.d.ts.map +1 -0
- package/dist/threadline/RateLimiter.js +236 -0
- package/dist/threadline/RateLimiter.js.map +1 -0
- package/dist/threadline/RelayGroundingPreamble.d.ts +48 -0
- package/dist/threadline/RelayGroundingPreamble.d.ts.map +1 -0
- package/dist/threadline/RelayGroundingPreamble.js +68 -0
- package/dist/threadline/RelayGroundingPreamble.js.map +1 -0
- package/dist/threadline/SessionLifecycle.d.ts +136 -0
- package/dist/threadline/SessionLifecycle.d.ts.map +1 -0
- package/dist/threadline/SessionLifecycle.js +317 -0
- package/dist/threadline/SessionLifecycle.js.map +1 -0
- package/dist/threadline/ThreadResumeMap.d.ts +128 -0
- package/dist/threadline/ThreadResumeMap.d.ts.map +1 -0
- package/dist/threadline/ThreadResumeMap.js +324 -0
- package/dist/threadline/ThreadResumeMap.js.map +1 -0
- package/dist/threadline/ThreadlineBootstrap.d.ts +68 -0
- package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -0
- package/dist/threadline/ThreadlineBootstrap.js +293 -0
- package/dist/threadline/ThreadlineBootstrap.js.map +1 -0
- package/dist/threadline/ThreadlineCrypto.d.ts +53 -0
- package/dist/threadline/ThreadlineCrypto.d.ts.map +1 -0
- package/dist/threadline/ThreadlineCrypto.js +123 -0
- package/dist/threadline/ThreadlineCrypto.js.map +1 -0
- package/dist/threadline/ThreadlineEndpoints.d.ts +35 -0
- package/dist/threadline/ThreadlineEndpoints.d.ts.map +1 -0
- package/dist/threadline/ThreadlineEndpoints.js +313 -0
- package/dist/threadline/ThreadlineEndpoints.js.map +1 -0
- package/dist/threadline/ThreadlineMCPServer.d.ts +165 -0
- package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -0
- package/dist/threadline/ThreadlineMCPServer.js +885 -0
- package/dist/threadline/ThreadlineMCPServer.js.map +1 -0
- package/dist/threadline/ThreadlineRouter.d.ts +105 -0
- package/dist/threadline/ThreadlineRouter.d.ts.map +1 -0
- package/dist/threadline/ThreadlineRouter.js +323 -0
- package/dist/threadline/ThreadlineRouter.js.map +1 -0
- package/dist/threadline/TrustBootstrap.d.ts +92 -0
- package/dist/threadline/TrustBootstrap.d.ts.map +1 -0
- package/dist/threadline/TrustBootstrap.js +256 -0
- package/dist/threadline/TrustBootstrap.js.map +1 -0
- package/dist/threadline/adapters/AutoGenTool.d.ts +42 -0
- package/dist/threadline/adapters/AutoGenTool.d.ts.map +1 -0
- package/dist/threadline/adapters/AutoGenTool.js +145 -0
- package/dist/threadline/adapters/AutoGenTool.js.map +1 -0
- package/dist/threadline/adapters/CrewAITool.d.ts +31 -0
- package/dist/threadline/adapters/CrewAITool.d.ts.map +1 -0
- package/dist/threadline/adapters/CrewAITool.js +112 -0
- package/dist/threadline/adapters/CrewAITool.js.map +1 -0
- package/dist/threadline/adapters/LangGraphTool.d.ts +48 -0
- package/dist/threadline/adapters/LangGraphTool.d.ts.map +1 -0
- package/dist/threadline/adapters/LangGraphTool.js +153 -0
- package/dist/threadline/adapters/LangGraphTool.js.map +1 -0
- package/dist/threadline/adapters/RESTServer.d.ts +74 -0
- package/dist/threadline/adapters/RESTServer.d.ts.map +1 -0
- package/dist/threadline/adapters/RESTServer.js +291 -0
- package/dist/threadline/adapters/RESTServer.js.map +1 -0
- package/dist/threadline/adapters/index.d.ts +14 -0
- package/dist/threadline/adapters/index.d.ts.map +1 -0
- package/dist/threadline/adapters/index.js +10 -0
- package/dist/threadline/adapters/index.js.map +1 -0
- package/dist/threadline/client/IdentityManager.d.ts +40 -0
- package/dist/threadline/client/IdentityManager.d.ts.map +1 -0
- package/dist/threadline/client/IdentityManager.js +106 -0
- package/dist/threadline/client/IdentityManager.js.map +1 -0
- package/dist/threadline/client/MessageEncryptor.d.ts +63 -0
- package/dist/threadline/client/MessageEncryptor.d.ts.map +1 -0
- package/dist/threadline/client/MessageEncryptor.js +195 -0
- package/dist/threadline/client/MessageEncryptor.js.map +1 -0
- package/dist/threadline/client/RegistryRestClient.d.ts +46 -0
- package/dist/threadline/client/RegistryRestClient.d.ts.map +1 -0
- package/dist/threadline/client/RegistryRestClient.js +114 -0
- package/dist/threadline/client/RegistryRestClient.js.map +1 -0
- package/dist/threadline/client/RelayClient.d.ts +77 -0
- package/dist/threadline/client/RelayClient.d.ts.map +1 -0
- package/dist/threadline/client/RelayClient.js +249 -0
- package/dist/threadline/client/RelayClient.js.map +1 -0
- package/dist/threadline/client/ThreadlineClient.d.ts +117 -0
- package/dist/threadline/client/ThreadlineClient.d.ts.map +1 -0
- package/dist/threadline/client/ThreadlineClient.js +286 -0
- package/dist/threadline/client/ThreadlineClient.js.map +1 -0
- package/dist/threadline/client/index.d.ts +14 -0
- package/dist/threadline/client/index.d.ts.map +1 -0
- package/dist/threadline/client/index.js +9 -0
- package/dist/threadline/client/index.js.map +1 -0
- package/dist/threadline/index.d.ts +81 -0
- package/dist/threadline/index.d.ts.map +1 -0
- package/dist/threadline/index.js +57 -0
- package/dist/threadline/index.js.map +1 -0
- package/dist/threadline/mcp-stdio-entry.d.ts +24 -0
- package/dist/threadline/mcp-stdio-entry.d.ts.map +1 -0
- package/dist/threadline/mcp-stdio-entry.js +230 -0
- package/dist/threadline/mcp-stdio-entry.js.map +1 -0
- package/dist/threadline/relay/A2ABridge.d.ts +91 -0
- package/dist/threadline/relay/A2ABridge.d.ts.map +1 -0
- package/dist/threadline/relay/A2ABridge.js +457 -0
- package/dist/threadline/relay/A2ABridge.js.map +1 -0
- package/dist/threadline/relay/AbuseDetector.d.ts +131 -0
- package/dist/threadline/relay/AbuseDetector.d.ts.map +1 -0
- package/dist/threadline/relay/AbuseDetector.js +358 -0
- package/dist/threadline/relay/AbuseDetector.js.map +1 -0
- package/dist/threadline/relay/AdminServer.d.ts +55 -0
- package/dist/threadline/relay/AdminServer.d.ts.map +1 -0
- package/dist/threadline/relay/AdminServer.js +215 -0
- package/dist/threadline/relay/AdminServer.js.map +1 -0
- package/dist/threadline/relay/ConnectionManager.d.ts +86 -0
- package/dist/threadline/relay/ConnectionManager.d.ts.map +1 -0
- package/dist/threadline/relay/ConnectionManager.js +356 -0
- package/dist/threadline/relay/ConnectionManager.js.map +1 -0
- package/dist/threadline/relay/MessageRouter.d.ts +46 -0
- package/dist/threadline/relay/MessageRouter.d.ts.map +1 -0
- package/dist/threadline/relay/MessageRouter.js +138 -0
- package/dist/threadline/relay/MessageRouter.js.map +1 -0
- package/dist/threadline/relay/OfflineQueue.d.ts +87 -0
- package/dist/threadline/relay/OfflineQueue.d.ts.map +1 -0
- package/dist/threadline/relay/OfflineQueue.js +137 -0
- package/dist/threadline/relay/OfflineQueue.js.map +1 -0
- package/dist/threadline/relay/PresenceRegistry.d.ts +62 -0
- package/dist/threadline/relay/PresenceRegistry.d.ts.map +1 -0
- package/dist/threadline/relay/PresenceRegistry.js +148 -0
- package/dist/threadline/relay/PresenceRegistry.js.map +1 -0
- package/dist/threadline/relay/RegistryAuth.d.ts +45 -0
- package/dist/threadline/relay/RegistryAuth.d.ts.map +1 -0
- package/dist/threadline/relay/RegistryAuth.js +118 -0
- package/dist/threadline/relay/RegistryAuth.js.map +1 -0
- package/dist/threadline/relay/RegistryStore.d.ts +149 -0
- package/dist/threadline/relay/RegistryStore.d.ts.map +1 -0
- package/dist/threadline/relay/RegistryStore.js +542 -0
- package/dist/threadline/relay/RegistryStore.js.map +1 -0
- package/dist/threadline/relay/RelayMetrics.d.ts +62 -0
- package/dist/threadline/relay/RelayMetrics.d.ts.map +1 -0
- package/dist/threadline/relay/RelayMetrics.js +149 -0
- package/dist/threadline/relay/RelayMetrics.js.map +1 -0
- package/dist/threadline/relay/RelayRateLimiter.d.ts +58 -0
- package/dist/threadline/relay/RelayRateLimiter.d.ts.map +1 -0
- package/dist/threadline/relay/RelayRateLimiter.js +116 -0
- package/dist/threadline/relay/RelayRateLimiter.js.map +1 -0
- package/dist/threadline/relay/RelayServer.d.ts +94 -0
- package/dist/threadline/relay/RelayServer.d.ts.map +1 -0
- package/dist/threadline/relay/RelayServer.js +1049 -0
- package/dist/threadline/relay/RelayServer.js.map +1 -0
- package/dist/threadline/relay/index.d.ts +28 -0
- package/dist/threadline/relay/index.d.ts.map +1 -0
- package/dist/threadline/relay/index.js +17 -0
- package/dist/threadline/relay/index.js.map +1 -0
- package/dist/threadline/relay/types.d.ts +215 -0
- package/dist/threadline/relay/types.d.ts.map +1 -0
- package/dist/threadline/relay/types.js +21 -0
- package/dist/threadline/relay/types.js.map +1 -0
- package/dist/threadline/types.d.ts +39 -0
- package/dist/threadline/types.d.ts.map +1 -0
- package/dist/threadline/types.js +10 -0
- package/dist/threadline/types.js.map +1 -0
- package/dist/tunnel/TunnelManager.d.ts +113 -0
- package/dist/tunnel/TunnelManager.d.ts.map +1 -0
- package/dist/tunnel/TunnelManager.js +474 -0
- package/dist/tunnel/TunnelManager.js.map +1 -0
- package/dist/types/pipeline.d.ts +203 -0
- package/dist/types/pipeline.d.ts.map +1 -0
- package/dist/types/pipeline.js +152 -0
- package/dist/types/pipeline.js.map +1 -0
- package/dist/users/GdprCommands.d.ts +44 -0
- package/dist/users/GdprCommands.d.ts.map +1 -0
- package/dist/users/GdprCommands.js +153 -0
- package/dist/users/GdprCommands.js.map +1 -0
- package/dist/users/OnboardingGate.d.ts +107 -0
- package/dist/users/OnboardingGate.d.ts.map +1 -0
- package/dist/users/OnboardingGate.js +240 -0
- package/dist/users/OnboardingGate.js.map +1 -0
- package/dist/users/UserContextBuilder.d.ts +47 -0
- package/dist/users/UserContextBuilder.d.ts.map +1 -0
- package/dist/users/UserContextBuilder.js +174 -0
- package/dist/users/UserContextBuilder.js.map +1 -0
- package/dist/users/UserManager.d.ts +76 -0
- package/dist/users/UserManager.d.ts.map +1 -0
- package/dist/users/UserManager.js +213 -0
- package/dist/users/UserManager.js.map +1 -0
- package/dist/users/UserOnboarding.d.ts +145 -0
- package/dist/users/UserOnboarding.d.ts.map +1 -0
- package/dist/users/UserOnboarding.js +488 -0
- package/dist/users/UserOnboarding.js.map +1 -0
- package/dist/users/UserPropagator.d.ts +75 -0
- package/dist/users/UserPropagator.d.ts.map +1 -0
- package/dist/users/UserPropagator.js +145 -0
- package/dist/users/UserPropagator.js.map +1 -0
- package/dist/utils/jsonl-rotation.d.ts +27 -0
- package/dist/utils/jsonl-rotation.d.ts.map +1 -0
- package/dist/utils/jsonl-rotation.js +63 -0
- package/dist/utils/jsonl-rotation.js.map +1 -0
- package/dist/utils/privacy.d.ts +88 -0
- package/dist/utils/privacy.d.ts.map +1 -0
- package/dist/utils/privacy.js +182 -0
- package/dist/utils/privacy.js.map +1 -0
- package/dist/utils/sanitize.d.ts +81 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +122 -0
- package/dist/utils/sanitize.js.map +1 -0
- package/package.json +1 -0
- package/playbook-scripts/atomic_write.py +133 -0
- package/playbook-scripts/bootstrap-manifest.json +92 -0
- package/playbook-scripts/playbook-annotate-context.py +239 -0
- package/playbook-scripts/playbook-assemble.py +385 -0
- package/playbook-scripts/playbook-dashboard.py +242 -0
- package/playbook-scripts/playbook-decay.py +377 -0
- package/playbook-scripts/playbook-dedup-job.py +252 -0
- package/playbook-scripts/playbook-dedup.py +341 -0
- package/playbook-scripts/playbook-delta-validator.py +576 -0
- package/playbook-scripts/playbook-dsar.py +291 -0
- package/playbook-scripts/playbook-eval-log.py +425 -0
- package/playbook-scripts/playbook-failsafe.py +513 -0
- package/playbook-scripts/playbook-feedback-quarantine.py +335 -0
- package/playbook-scripts/playbook-history.py +293 -0
- package/playbook-scripts/playbook-hmac.py +224 -0
- package/playbook-scripts/playbook-lifecycle.py +952 -0
- package/playbook-scripts/playbook-manifest.py +458 -0
- package/playbook-scripts/playbook-micro-eval.py +316 -0
- package/playbook-scripts/playbook-migrate-lessons.py +396 -0
- package/playbook-scripts/playbook-mount.py +393 -0
- package/playbook-scripts/playbook-offline-adapt.py +323 -0
- package/playbook-scripts/playbook-pii-screen.py +207 -0
- package/playbook-scripts/playbook-reflector.py +266 -0
- package/playbook-scripts/playbook-relevance.py +269 -0
- package/playbook-scripts/playbook-retirement.py +365 -0
- package/playbook-scripts/playbook-schema-validate.py +267 -0
- package/playbook-scripts/playbook-scratchpad.py +346 -0
- package/playbook-scripts/playbook-semantic-verify.py +280 -0
- package/playbook-scripts/playbook-spawn-contract.py +341 -0
- package/playbook-scripts/playbook-token-reestimate.py +248 -0
- package/playbook-scripts/playbook-verify.py +357 -0
- package/playbook-scripts/playbook_backend.py +249 -0
- package/playbook-scripts/playbook_paths.py +232 -0
- package/playbook-scripts/schemas/context-delta.schema.json +137 -0
- package/playbook-scripts/schemas/context-manifest.schema.json +200 -0
- package/playbook-scripts/schemas/playbook-config.schema.json +184 -0
- package/scripts/analyze-release.js +752 -0
- package/scripts/check-upgrade-guide.js +373 -0
- package/scripts/collect-metrics.py +248 -0
- package/scripts/demo-two-agents.mjs +187 -0
- package/scripts/fix-better-sqlite3.cjs +100 -0
- package/scripts/generate-builtin-manifest.cjs +440 -0
- package/scripts/pre-push-gate.js +177 -0
- package/scripts/relay-entrypoint.mjs +18 -0
- package/scripts/seed-registry.mjs +258 -0
- package/scripts/telemetry-worker/worker.js +776 -0
- package/scripts/telemetry-worker/wrangler.toml +7 -0
- package/scripts/test-bootstrap-relay.mjs +90 -0
- package/scripts/test-multi-agent-relay.mjs +395 -0
- package/scripts/test-relay-cloud.mjs +389 -0
- package/scripts/test-relay-live.mjs +550 -0
- package/src/data/builtin-manifest.json +1463 -0
- package/src/data/http-hook-templates.ts +81 -0
- package/src/templates/hooks/compaction-recovery.sh +371 -0
- package/src/templates/hooks/dangerous-command-guard.sh +100 -0
- package/src/templates/hooks/free-text-guard.sh +96 -0
- package/src/templates/hooks/grounding-before-messaging.sh +52 -0
- package/src/templates/hooks/session-start.sh +339 -0
- package/src/templates/hooks/settings-template.json +142 -0
- package/src/templates/hooks/slack-channel-context.sh +98 -0
- package/src/templates/hooks/telegram-topic-context.sh +117 -0
- package/src/templates/scripts/convergence-check.sh +99 -0
- package/src/templates/scripts/git-sync-gate.sh +89 -0
- package/src/templates/scripts/health-watchdog.sh +63 -0
- package/src/templates/scripts/serendipity-capture.sh +345 -0
- package/src/templates/scripts/slack-reply.sh +74 -0
- package/src/templates/scripts/smart-fetch.py +215 -0
- package/src/templates/scripts/telegram-reply.sh +67 -0
- package/src/templates/scripts/whatsapp-reply.sh +68 -0
- package/upgrades/0.10.0.md +254 -0
- package/upgrades/0.10.1.md +47 -0
- package/upgrades/0.10.2.md +26 -0
- package/upgrades/0.10.3.md +23 -0
- package/upgrades/0.10.4.md +26 -0
- package/upgrades/0.10.5.md +19 -0
- package/upgrades/0.10.6.md +35 -0
- package/upgrades/0.10.7.md +48 -0
- package/upgrades/0.10.8.md +53 -0
- package/upgrades/0.10.9.md +21 -0
- package/upgrades/0.11.0.md +146 -0
- package/upgrades/0.12.0.md +31 -0
- package/upgrades/0.12.1.md +21 -0
- package/upgrades/0.12.10.md +26 -0
- package/upgrades/0.12.11.md +23 -0
- package/upgrades/0.12.12.md +23 -0
- package/upgrades/0.12.13.md +19 -0
- package/upgrades/0.12.14.md +21 -0
- package/upgrades/0.12.15.md +26 -0
- package/upgrades/0.12.16.md +33 -0
- package/upgrades/0.12.17.md +38 -0
- package/upgrades/0.12.18.md +27 -0
- package/upgrades/0.12.19.md +31 -0
- package/upgrades/0.12.2.md +27 -0
- package/upgrades/0.12.20.md +24 -0
- package/upgrades/0.12.21.md +28 -0
- package/upgrades/0.12.22.md +23 -0
- package/upgrades/0.12.23.md +44 -0
- package/upgrades/0.12.24.md +24 -0
- package/upgrades/0.12.25.md +55 -0
- package/upgrades/0.12.26.md +31 -0
- package/upgrades/0.12.27.md +19 -0
- package/upgrades/0.12.28.md +19 -0
- package/upgrades/0.12.29.md +42 -0
- package/upgrades/0.12.3.md +22 -0
- package/upgrades/0.12.31.md +24 -0
- package/upgrades/0.12.32.md +34 -0
- package/upgrades/0.12.33.md +62 -0
- package/upgrades/0.12.34.md +59 -0
- package/upgrades/0.12.4.md +19 -0
- package/upgrades/0.12.5.md +31 -0
- package/upgrades/0.12.6.md +34 -0
- package/upgrades/0.12.7.md +24 -0
- package/upgrades/0.12.8.md +28 -0
- package/upgrades/0.12.9.md +30 -0
- package/upgrades/0.13.0.md +26 -0
- package/upgrades/0.14.0.md +75 -0
- package/upgrades/0.14.1.md +41 -0
- package/upgrades/0.15.0.md +59 -0
- package/upgrades/0.16.0.md +61 -0
- package/upgrades/0.17.0.md +88 -0
- package/upgrades/0.17.10.md +23 -0
- package/upgrades/0.17.11.md +25 -0
- package/upgrades/0.17.12.md +43 -0
- package/upgrades/0.17.13.md +24 -0
- package/upgrades/0.17.14.md +26 -0
- package/upgrades/0.17.2.md +42 -0
- package/upgrades/0.17.3.md +37 -0
- package/upgrades/0.17.4.md +27 -0
- package/upgrades/0.17.5.md +32 -0
- package/upgrades/0.17.6.md +32 -0
- package/upgrades/0.17.7.md +39 -0
- package/upgrades/0.17.8.md +34 -0
- package/upgrades/0.17.9.md +25 -0
- package/upgrades/0.18.1.md +34 -0
- package/upgrades/0.18.2.md +29 -0
- package/upgrades/0.18.3.md +26 -0
- package/upgrades/0.18.4.md +28 -0
- package/upgrades/0.18.5.md +25 -0
- package/upgrades/0.18.6.md +25 -0
- package/upgrades/0.18.7.md +30 -0
- package/upgrades/0.19.0.md +136 -0
- package/upgrades/0.19.1.md +27 -0
- package/upgrades/0.19.2.md +27 -0
- package/upgrades/0.19.3.md +32 -0
- package/upgrades/0.19.4.md +19 -0
- package/upgrades/0.19.6.md +17 -0
- package/upgrades/0.19.7.md +33 -0
- package/upgrades/0.20.0.md +54 -0
- package/upgrades/0.21.1.md +55 -0
- package/upgrades/0.21.2.md +48 -0
- package/upgrades/0.21.3.md +29 -0
- package/upgrades/0.21.4.md +33 -0
- package/upgrades/0.22.0.md +114 -0
- package/upgrades/0.23.0.md +81 -0
- package/upgrades/0.23.1.md +28 -0
- package/upgrades/0.23.10.md +19 -0
- package/upgrades/0.23.11.md +21 -0
- package/upgrades/0.23.12.md +30 -0
- package/upgrades/0.23.13.md +25 -0
- package/upgrades/0.23.14.md +23 -0
- package/upgrades/0.23.15.md +30 -0
- package/upgrades/0.23.16.md +21 -0
- package/upgrades/0.23.17.md +23 -0
- package/upgrades/0.23.18.md +41 -0
- package/upgrades/0.23.2.md +32 -0
- package/upgrades/0.23.4.md +21 -0
- package/upgrades/0.23.6.md +19 -0
- package/upgrades/0.23.7.md +33 -0
- package/upgrades/0.23.8.md +38 -0
- package/upgrades/0.23.9.md +35 -0
- package/upgrades/0.24.1.md +32 -0
- package/upgrades/0.24.10.md +23 -0
- package/upgrades/0.24.12.md +17 -0
- package/upgrades/0.24.13.md +16 -0
- package/upgrades/0.24.14.md +26 -0
- package/upgrades/0.24.15.md +49 -0
- package/upgrades/0.24.16.md +48 -0
- package/upgrades/0.24.17.md +40 -0
- package/upgrades/0.24.18-beta.0.md +35 -0
- package/upgrades/0.24.18.md +35 -0
- package/upgrades/0.24.19.md +21 -0
- package/upgrades/0.24.2.md +13 -0
- package/upgrades/0.24.20.md +45 -0
- package/upgrades/0.24.21.md +25 -0
- package/upgrades/0.24.22.md +35 -0
- package/upgrades/0.24.23.md +17 -0
- package/upgrades/0.24.24.md +15 -0
- package/upgrades/0.24.25.md +15 -0
- package/upgrades/0.24.26.md +15 -0
- package/upgrades/0.24.27.md +17 -0
- package/upgrades/0.24.28.md +35 -0
- package/upgrades/0.24.29.md +15 -0
- package/upgrades/0.24.30.md +40 -0
- package/upgrades/0.24.31.md +45 -0
- package/upgrades/0.24.32.md +19 -0
- package/upgrades/0.24.33.md +35 -0
- package/upgrades/0.24.34.md +29 -0
- package/upgrades/0.24.4.md +19 -0
- package/upgrades/0.24.5.md +20 -0
- package/upgrades/0.25.0.md +34 -0
- package/upgrades/0.25.1.md +24 -0
- package/upgrades/0.25.10.md +26 -0
- package/upgrades/0.25.2.md +23 -0
- package/upgrades/0.25.3.md +25 -0
- package/upgrades/0.25.4.md +24 -0
- package/upgrades/0.25.5.md +19 -0
- package/upgrades/0.25.6.md +35 -0
- package/upgrades/0.25.7.md +18 -0
- package/upgrades/0.25.8.md +24 -0
- package/upgrades/0.25.9.md +19 -0
- package/upgrades/0.26.0.md +23 -0
- package/upgrades/0.26.1.md +22 -0
- package/upgrades/0.26.2.md +15 -0
- package/upgrades/0.8.12.md +49 -0
- package/upgrades/0.8.13.md +38 -0
- package/upgrades/0.8.17.md +36 -0
- package/upgrades/0.8.22.md +43 -0
- package/upgrades/0.8.23.md +106 -0
- package/upgrades/0.9.1.md +91 -0
- package/upgrades/0.9.10.md +40 -0
- package/upgrades/0.9.11.md +77 -0
- package/upgrades/0.9.12.md +42 -0
- package/upgrades/0.9.13.md +55 -0
- package/upgrades/0.9.14.md +23 -0
- package/upgrades/0.9.15.md +106 -0
- package/upgrades/0.9.16.md +37 -0
- package/upgrades/0.9.17.md +15 -0
- package/upgrades/0.9.19.md +17 -0
- package/upgrades/0.9.20.md +24 -0
- package/upgrades/0.9.21.md +37 -0
- package/upgrades/0.9.22.md +41 -0
- package/upgrades/0.9.23.md +37 -0
- package/upgrades/0.9.24.md +46 -0
- package/upgrades/0.9.25.md +37 -0
- package/upgrades/0.9.28.md +20 -0
- package/upgrades/0.9.29.md +34 -0
- package/upgrades/0.9.32.md +30 -0
- package/upgrades/0.9.36.md +27 -0
- package/upgrades/0.9.8.md +125 -0
- package/upgrades/0.9.9.md +34 -0
- package/upgrades/NEXT.md +35 -0
|
@@ -0,0 +1,3434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Messaging Adapter — send/receive messages via Telegram Bot API.
|
|
3
|
+
*
|
|
4
|
+
* Uses long polling to receive messages. Supports forum topics
|
|
5
|
+
* (each user gets a topic thread). Includes topic-session registry,
|
|
6
|
+
* message logging, voice transcription, photo handling, stall detection,
|
|
7
|
+
* auth gating, and delivery confirmation.
|
|
8
|
+
*
|
|
9
|
+
* No external dependencies — uses native fetch for Telegram API calls.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { DegradationReporter } from '../monitoring/DegradationReporter.js';
|
|
14
|
+
import { NotificationBatcher } from './NotificationBatcher.js';
|
|
15
|
+
import { validateTopicContent, getTopicPurpose, classifyContent } from './TopicContentValidator.js';
|
|
16
|
+
import { SHARED_INFRA_FLAGS } from './shared/FeatureFlags.js';
|
|
17
|
+
import { MessageLogger } from './shared/MessageLogger.js';
|
|
18
|
+
import { StallDetector } from './shared/StallDetector.js';
|
|
19
|
+
import { CommandRouter } from './shared/CommandRouter.js';
|
|
20
|
+
import { AuthGate } from './shared/AuthGate.js';
|
|
21
|
+
import { MessagingEventBus } from './shared/MessagingEventBus.js';
|
|
22
|
+
import { CallbackRegistry, isAllowedButtonKey } from '../core/CallbackRegistry.js';
|
|
23
|
+
import { sanitizeForPrompt } from '../monitoring/SessionRecovery.js';
|
|
24
|
+
/**
|
|
25
|
+
* Telegram General topic convention:
|
|
26
|
+
* - Incoming: messages in General have message_thread_id=1 (or undefined in older API)
|
|
27
|
+
* - Internal: we use GENERAL_TOPIC_ID (1) as the sentinel
|
|
28
|
+
* - Outgoing: we OMIT message_thread_id for General (don't send 1, don't send 0)
|
|
29
|
+
*
|
|
30
|
+
* The isGeneralTopic() helper should be used instead of raw `topicId === 1` checks
|
|
31
|
+
* to keep the convention in one place.
|
|
32
|
+
*/
|
|
33
|
+
const GENERAL_TOPIC_ID = 1;
|
|
34
|
+
function isGeneralTopic(topicId) {
|
|
35
|
+
return topicId <= GENERAL_TOPIC_ID;
|
|
36
|
+
}
|
|
37
|
+
const PRIORITY_EMOJI = {
|
|
38
|
+
URGENT: '\ud83d\udd34', // 🔴
|
|
39
|
+
HIGH: '\ud83d\udfe0', // 🟠
|
|
40
|
+
NORMAL: '\ud83d\udd35', // 🔵
|
|
41
|
+
LOW: '\u26aa', // ⚪
|
|
42
|
+
};
|
|
43
|
+
const PRIORITY_COLOR = {
|
|
44
|
+
URGENT: 16478047, // red
|
|
45
|
+
HIGH: 16749490, // orange
|
|
46
|
+
NORMAL: 7322096, // blue
|
|
47
|
+
LOW: 13338331, // purple
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Standard topic styles for visual organization in Telegram forum.
|
|
51
|
+
* Colors are the 6 values Telegram's Bot API accepts for icon_color.
|
|
52
|
+
* Emojis prefix topic names for at-a-glance scanning.
|
|
53
|
+
*/
|
|
54
|
+
export const TOPIC_STYLE = {
|
|
55
|
+
/** Green — core infrastructure (Lifeline) */
|
|
56
|
+
SYSTEM: { color: 9367192, emoji: '🛡️' },
|
|
57
|
+
/** Purple — automated recurring jobs */
|
|
58
|
+
JOB: { color: 13338331, emoji: '⚙️' },
|
|
59
|
+
/** Green — interactive user sessions */
|
|
60
|
+
SESSION: { color: 9367192, emoji: '💬' },
|
|
61
|
+
/** Blue — informational (Dashboard, Updates) */
|
|
62
|
+
INFO: { color: 7322096, emoji: '📢' },
|
|
63
|
+
/** Yellow — needs user attention */
|
|
64
|
+
ALERT: { color: 16766590, emoji: '🔔' },
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Keyword → emoji mapping for smart topic emoji selection.
|
|
68
|
+
* First match wins, so more specific patterns come first.
|
|
69
|
+
* Falls back to 💬 for unmatched names.
|
|
70
|
+
*/
|
|
71
|
+
const TOPIC_EMOJI_KEYWORDS = [
|
|
72
|
+
{ keywords: ['debug', 'bug', 'fix', 'issue', 'error'], emoji: '🐛' },
|
|
73
|
+
{ keywords: ['deploy', 'release', 'ship', 'launch'], emoji: '🚀' },
|
|
74
|
+
{ keywords: ['test', 'testing', 'qa', 'cypress', 'jest'], emoji: '🧪' },
|
|
75
|
+
{ keywords: ['review', 'pr', 'code review'], emoji: '👀' },
|
|
76
|
+
{ keywords: ['research', 'explore', 'investigate'], emoji: '🔍' },
|
|
77
|
+
{ keywords: ['design', 'ui', 'ux', 'frontend', 'css'], emoji: '🎨' },
|
|
78
|
+
{ keywords: ['doc', 'docs', 'readme', 'write', 'draft'], emoji: '📝' },
|
|
79
|
+
{ keywords: ['build', 'ci', 'pipeline', 'compile'], emoji: '🏗️' },
|
|
80
|
+
{ keywords: ['security', 'auth', 'permission', 'access'], emoji: '🔒' },
|
|
81
|
+
{ keywords: ['perf', 'performance', 'speed', 'optimize'], emoji: '⚡' },
|
|
82
|
+
{ keywords: ['data', 'database', 'db', 'sql', 'prisma'], emoji: '🗄️' },
|
|
83
|
+
{ keywords: ['api', 'endpoint', 'route', 'server'], emoji: '🔌' },
|
|
84
|
+
{ keywords: ['monitor', 'metric', 'observ', 'dashboard'], emoji: '📊' },
|
|
85
|
+
{ keywords: ['alert', 'incident', 'urgent', 'critical'], emoji: '🚨' },
|
|
86
|
+
{ keywords: ['brainstorm', 'idea', 'think', 'plan'], emoji: '💡' },
|
|
87
|
+
{ keywords: ['migrate', 'migration', 'upgrade'], emoji: '🔄' },
|
|
88
|
+
{ keywords: ['config', 'setting', 'env'], emoji: '⚙️' },
|
|
89
|
+
{ keywords: ['email', 'mail', 'newsletter', 'outreach'], emoji: '📧' },
|
|
90
|
+
{ keywords: ['chat', 'talk', 'conversation', 'discuss'], emoji: '💬' },
|
|
91
|
+
{ keywords: ['learn', 'study', 'tutorial', 'course'], emoji: '📚' },
|
|
92
|
+
{ keywords: ['money', 'payment', 'billing', 'cost'], emoji: '💰' },
|
|
93
|
+
{ keywords: ['clean', 'cleanup', 'refactor', 'tidy'], emoji: '🧹' },
|
|
94
|
+
];
|
|
95
|
+
/**
|
|
96
|
+
* Select an appropriate emoji for a topic based on its name.
|
|
97
|
+
* Matches keywords case-insensitively. Falls back to 💬 for unmatched names.
|
|
98
|
+
*/
|
|
99
|
+
export function selectTopicEmoji(topicName) {
|
|
100
|
+
const lower = topicName.toLowerCase();
|
|
101
|
+
for (const entry of TOPIC_EMOJI_KEYWORDS) {
|
|
102
|
+
if (entry.keywords.some(kw => lower.includes(kw))) {
|
|
103
|
+
return entry.emoji;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return TOPIC_STYLE.SESSION.emoji; // 💬 default
|
|
107
|
+
}
|
|
108
|
+
export class TelegramAdapter {
|
|
109
|
+
platform = 'telegram';
|
|
110
|
+
config;
|
|
111
|
+
handler = null;
|
|
112
|
+
polling = false;
|
|
113
|
+
/** True when this adapter is actively polling for messages (false in send-only mode). */
|
|
114
|
+
get isPolling() { return this.polling; }
|
|
115
|
+
pollTimeout = null;
|
|
116
|
+
lastUpdateId = 0;
|
|
117
|
+
startedAt = null;
|
|
118
|
+
consecutivePollErrors = 0;
|
|
119
|
+
// Forum detection — if the chat is not a forum, skip all topic operations
|
|
120
|
+
notAForum = false;
|
|
121
|
+
notAForumWarned = false;
|
|
122
|
+
// Topic-session registry (persisted to disk)
|
|
123
|
+
topicToSession = new Map();
|
|
124
|
+
sessionToTopic = new Map();
|
|
125
|
+
topicToName = new Map();
|
|
126
|
+
topicToPurpose = new Map();
|
|
127
|
+
registryPath;
|
|
128
|
+
messageLogPath;
|
|
129
|
+
offsetPath;
|
|
130
|
+
stateDir;
|
|
131
|
+
// Attention queue (persisted to disk)
|
|
132
|
+
attentionItemToTopic = new Map();
|
|
133
|
+
attentionTopicToItem = new Map();
|
|
134
|
+
attentionItems = new Map();
|
|
135
|
+
attentionFilePath;
|
|
136
|
+
// Stall detection
|
|
137
|
+
pendingMessages = new Map(); // key = topicId-timestamp
|
|
138
|
+
stallCheckInterval = null;
|
|
139
|
+
// Promise tracking (agent said "give me a minute" but hasn't followed up)
|
|
140
|
+
pendingPromises = new Map(); // key = topicId
|
|
141
|
+
// Topic message callback — fires on every incoming topic message
|
|
142
|
+
onTopicMessage = null;
|
|
143
|
+
// Session management callbacks (wired by server.ts)
|
|
144
|
+
onInterruptSession = null;
|
|
145
|
+
onRestartSession = null;
|
|
146
|
+
onListSessions = null;
|
|
147
|
+
onIsSessionAlive = null;
|
|
148
|
+
onIsSessionActive = null;
|
|
149
|
+
// Message log callback — fires on every message logged (inbound and outbound).
|
|
150
|
+
// Used by TopicMemory to dual-write to SQLite for search and summarization.
|
|
151
|
+
// Includes sender identity fields (Phase 1C/1D — User-Agent Topology Spec).
|
|
152
|
+
onMessageLogged = null;
|
|
153
|
+
// Sentinel interceptor — fires BEFORE the message handler for real-time interrupt detection.
|
|
154
|
+
// Returns the sentinel classification. If category is 'emergency-stop' or 'pause',
|
|
155
|
+
// the adapter will handle the session action and skip the normal handler.
|
|
156
|
+
onSentinelIntercept = null;
|
|
157
|
+
// Session kill/pause callbacks — used by sentinel to take immediate action
|
|
158
|
+
onSentinelKillSession = null;
|
|
159
|
+
onSentinelPauseSession = null;
|
|
160
|
+
// Attention queue callbacks
|
|
161
|
+
onAttentionStatusChange = null;
|
|
162
|
+
// Quota management callbacks
|
|
163
|
+
onSwitchAccountRequest = null;
|
|
164
|
+
onQuotaStatusRequest = null;
|
|
165
|
+
onLoginRequest = null;
|
|
166
|
+
onClassifySessionDeath = null;
|
|
167
|
+
/** LLM-powered stall triage — called instead of generic stall alert when set */
|
|
168
|
+
onStallDetected = null;
|
|
169
|
+
/** Get triage status for a topic — returns null if no active triage, or status summary */
|
|
170
|
+
onGetTriageStatus = null;
|
|
171
|
+
// Unknown user handling callbacks (Multi-User Setup Wizard Phase 4.5)
|
|
172
|
+
// Returns the registration policy and optional contact hint for the gated message
|
|
173
|
+
onGetRegistrationPolicy = null;
|
|
174
|
+
// Called when an admin-only join request is created (notify admin via lifeline/admin topic)
|
|
175
|
+
onNotifyAdminJoinRequest = null;
|
|
176
|
+
// Called to validate an invite code for invite-only policy
|
|
177
|
+
onValidateInviteCode = null;
|
|
178
|
+
// Called to start mini-onboarding for open policy
|
|
179
|
+
onStartMiniOnboarding = null;
|
|
180
|
+
// Rate limiting for unknown user responses (prevent spam)
|
|
181
|
+
unknownUserRateLimit = new Map(); // telegramUserId -> last response timestamp
|
|
182
|
+
static UNKNOWN_USER_COOLDOWN_MS = 60_000; // 1 minute between responses to same unknown user
|
|
183
|
+
// Notification batching
|
|
184
|
+
batcher = null;
|
|
185
|
+
// Intelligence provider — gates fallback stall/promise alerts behind LLM confirmation.
|
|
186
|
+
// Without this, fallback alerts fire purely from timers when StallTriageNurse is unavailable.
|
|
187
|
+
intelligence = null;
|
|
188
|
+
// Flush notifications callback — fires when user sends /flush
|
|
189
|
+
onFlushNotifications = null;
|
|
190
|
+
// Prompt Gate — relay prompts to Telegram and handle responses
|
|
191
|
+
callbackRegistry;
|
|
192
|
+
pendingPromptReply = new Map(); // topicId → pending
|
|
193
|
+
promptGateDisclosureSent = new Set(); // topicIds that have seen the disclosure
|
|
194
|
+
/** Callback to inject a response into a tmux session. Wired by server.ts. */
|
|
195
|
+
onPromptResponse = null;
|
|
196
|
+
/** Callback to inject text input into a tmux session. Wired by server.ts. */
|
|
197
|
+
onPromptTextResponse = null;
|
|
198
|
+
/** Callback when relay lease should extend idle timeout for a session */
|
|
199
|
+
onRelayLeaseStart = null;
|
|
200
|
+
/** Callback when relay lease is released (response received or timeout) */
|
|
201
|
+
onRelayLeaseEnd = null;
|
|
202
|
+
// Shared infrastructure modules (Phase 1 extraction)
|
|
203
|
+
sharedLogger = null;
|
|
204
|
+
sharedRegistry = null;
|
|
205
|
+
sharedStallDetector = null;
|
|
206
|
+
sharedCommandRouter = null;
|
|
207
|
+
sharedAuthGate = null;
|
|
208
|
+
eventBus = null;
|
|
209
|
+
/** Get the event bus for external subscribers (Phase 1e). Returns null if flag is off. */
|
|
210
|
+
getEventBus() {
|
|
211
|
+
return this.eventBus;
|
|
212
|
+
}
|
|
213
|
+
constructor(config, stateDir) {
|
|
214
|
+
if (config.chatId && !/^-?\d+$/.test(String(config.chatId))) {
|
|
215
|
+
throw new Error(`Invalid Telegram chatId "${config.chatId}". Chat IDs must be numeric (e.g., -1001234567890). ` +
|
|
216
|
+
`Update messaging.config.chatId in your instar.config.json with a valid numeric ID.`);
|
|
217
|
+
}
|
|
218
|
+
this.config = config;
|
|
219
|
+
this.stateDir = stateDir;
|
|
220
|
+
this.registryPath = path.join(stateDir, 'topic-session-registry.json');
|
|
221
|
+
this.messageLogPath = path.join(stateDir, 'telegram-messages.jsonl');
|
|
222
|
+
this.offsetPath = path.join(stateDir, 'telegram-poll-offset.json');
|
|
223
|
+
this.attentionFilePath = path.join(stateDir, 'state', 'attention-items.json');
|
|
224
|
+
this.loadRegistry();
|
|
225
|
+
this.loadOffset();
|
|
226
|
+
this.loadAttentionItems();
|
|
227
|
+
// Initialize Prompt Gate callback registry
|
|
228
|
+
const relayTimeoutMs = (config.promptGate?.relayTimeoutSeconds ?? 300) * 1000;
|
|
229
|
+
this.callbackRegistry = new CallbackRegistry({
|
|
230
|
+
maxEntries: 500,
|
|
231
|
+
maxAgeMs: relayTimeoutMs,
|
|
232
|
+
pruneIntervalMs: 60_000,
|
|
233
|
+
});
|
|
234
|
+
this.callbackRegistry.start();
|
|
235
|
+
// Initialize shared modules when feature flags are enabled
|
|
236
|
+
if (SHARED_INFRA_FLAGS.useSharedMessageLogger) {
|
|
237
|
+
this.sharedLogger = new MessageLogger({ logPath: this.messageLogPath });
|
|
238
|
+
}
|
|
239
|
+
if (SHARED_INFRA_FLAGS.useSharedStallDetector) {
|
|
240
|
+
this.sharedStallDetector = new StallDetector({
|
|
241
|
+
stallTimeoutMinutes: config.stallTimeoutMinutes,
|
|
242
|
+
promiseTimeoutMinutes: config.promiseTimeoutMinutes,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (SHARED_INFRA_FLAGS.useSharedCommandRouter) {
|
|
246
|
+
this.sharedCommandRouter = new CommandRouter('telegram');
|
|
247
|
+
this.registerSharedCommands();
|
|
248
|
+
}
|
|
249
|
+
if (SHARED_INFRA_FLAGS.useSharedAuthGate) {
|
|
250
|
+
this.sharedAuthGate = new AuthGate({
|
|
251
|
+
authorizedUsers: (config.authorizedUserIds ?? []).map(id => id.toString()),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (SHARED_INFRA_FLAGS.useEventEmitterPattern) {
|
|
255
|
+
this.eventBus = new MessagingEventBus('telegram');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Register all Telegram commands with the shared CommandRouter (Phase 1a).
|
|
260
|
+
* Each command delegates back to the existing handler logic.
|
|
261
|
+
*/
|
|
262
|
+
registerSharedCommands() {
|
|
263
|
+
if (!this.sharedCommandRouter)
|
|
264
|
+
return;
|
|
265
|
+
// Attention topic interceptor
|
|
266
|
+
this.sharedCommandRouter.addInterceptor(async (ctx) => {
|
|
267
|
+
const topicId = parseInt(ctx.channelId, 10);
|
|
268
|
+
if (this.isAttentionTopic(topicId)) {
|
|
269
|
+
return this.handleAttentionCommand(topicId, ctx.rawText);
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
});
|
|
273
|
+
this.sharedCommandRouter.register('flush', async (ctx) => {
|
|
274
|
+
const topicId = parseInt(ctx.channelId, 10);
|
|
275
|
+
if (this.batcher && this.batcher.isEnabled()) {
|
|
276
|
+
const flushed = await this.batcher.flushAll();
|
|
277
|
+
if (flushed > 0) {
|
|
278
|
+
await this.sendToTopic(topicId, `Flushed ${flushed} batched notification${flushed === 1 ? '' : 's'}.`).catch(() => { });
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
await this.sendToTopic(topicId, 'No batched notifications to flush.').catch(() => { });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else if (this.onFlushNotifications) {
|
|
285
|
+
this.onFlushNotifications(topicId).catch(err => {
|
|
286
|
+
console.error('[telegram] Flush notifications failed:', err);
|
|
287
|
+
this.sendToTopic(topicId, 'Failed to flush notifications.').catch(() => { });
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
await this.sendToTopic(topicId, 'Notification batching is not enabled.').catch(() => { });
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}, { description: 'Flush batched notifications' });
|
|
295
|
+
this.sharedCommandRouter.register('sessions', async (ctx) => {
|
|
296
|
+
const topicId = parseInt(ctx.channelId, 10);
|
|
297
|
+
const filterUnclaimed = ctx.args.includes('unclaimed');
|
|
298
|
+
if (!this.onListSessions) {
|
|
299
|
+
await this.sendToTopic(topicId, 'Session listing not available.').catch(() => { });
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
const sessions = this.onListSessions();
|
|
303
|
+
if (sessions.length === 0) {
|
|
304
|
+
await this.sendToTopic(topicId, 'No sessions running.').catch(() => { });
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
const lines = [];
|
|
308
|
+
for (const s of sessions) {
|
|
309
|
+
const linkedTopic = this.getTopicForSession(s.tmuxSession);
|
|
310
|
+
const claimed = linkedTopic !== null;
|
|
311
|
+
if (filterUnclaimed && claimed)
|
|
312
|
+
continue;
|
|
313
|
+
const status = s.alive ? '\u2705' : '\u274c';
|
|
314
|
+
const claimTag = claimed ? ` (topic ${linkedTopic})` : ' \u{1f7e1} unclaimed';
|
|
315
|
+
lines.push(`${status} ${s.name}${claimTag}`);
|
|
316
|
+
}
|
|
317
|
+
if (lines.length === 0) {
|
|
318
|
+
await this.sendToTopic(topicId, filterUnclaimed ? 'No unclaimed sessions.' : 'No sessions.').catch(() => { });
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
await this.sendToTopic(topicId, lines.join('\n')).catch(() => { });
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
}, { description: 'List sessions', platforms: ['telegram'] });
|
|
325
|
+
this.sharedCommandRouter.register(['claim', 'link'], async (ctx) => {
|
|
326
|
+
const topicId = parseInt(ctx.channelId, 10);
|
|
327
|
+
const sessionName = ctx.args;
|
|
328
|
+
if (!sessionName) {
|
|
329
|
+
await this.sendToTopic(topicId, `Please include a session name — e.g. /${ctx.command} my-session`).catch(() => { });
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
const existingSession = this.getSessionForTopic(topicId);
|
|
333
|
+
if (existingSession) {
|
|
334
|
+
await this.sendToTopic(topicId, `This topic is already linked to "${existingSession}". Use /unlink first.`).catch(() => { });
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
this.registerTopicSession(topicId, sessionName);
|
|
338
|
+
const verb = ctx.command === 'claim' ? 'Claimed' : 'Linked';
|
|
339
|
+
await this.sendToTopic(topicId, `${verb} session "${sessionName}" ${ctx.command === 'claim' ? 'into' : 'to'} this topic.`).catch(() => { });
|
|
340
|
+
return true;
|
|
341
|
+
}, { description: 'Link a session to this topic', platforms: ['telegram'] });
|
|
342
|
+
this.sharedCommandRouter.register('unlink', async (ctx) => {
|
|
343
|
+
const topicId = parseInt(ctx.channelId, 10);
|
|
344
|
+
const sessionName = this.getSessionForTopic(topicId);
|
|
345
|
+
if (!sessionName) {
|
|
346
|
+
await this.sendToTopic(topicId, 'No session linked to this topic.').catch(() => { });
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
this.unregisterTopic(topicId);
|
|
350
|
+
await this.sendToTopic(topicId, `Unlinked session "${sessionName}" from this topic.`).catch(() => { });
|
|
351
|
+
return true;
|
|
352
|
+
}, { description: 'Unlink session from topic', platforms: ['telegram'] });
|
|
353
|
+
this.sharedCommandRouter.register('interrupt', async (ctx) => {
|
|
354
|
+
const topicId = parseInt(ctx.channelId, 10);
|
|
355
|
+
const sessionName = this.getSessionForTopic(topicId);
|
|
356
|
+
if (!sessionName) {
|
|
357
|
+
await this.sendToTopic(topicId, 'No session linked to this topic.').catch(() => { });
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
if (!this.onInterruptSession) {
|
|
361
|
+
await this.sendToTopic(topicId, 'Interrupt not available (no handler registered).').catch(() => { });
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
const success = await this.onInterruptSession(sessionName);
|
|
366
|
+
this.clearStallForTopic(topicId);
|
|
367
|
+
if (success) {
|
|
368
|
+
await this.sendToTopic(topicId, `Nudged "${sessionName}" \u2014 it should resume shortly.`).catch(() => { });
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
await this.sendToTopic(topicId, `Failed to interrupt "${sessionName}" \u2014 session may not exist.`).catch(() => { });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
console.error(`[telegram] Interrupt failed:`, err);
|
|
376
|
+
await this.sendToTopic(topicId, 'Couldn\'t interrupt the session. It may have already ended.').catch(() => { });
|
|
377
|
+
}
|
|
378
|
+
return true;
|
|
379
|
+
}, { description: 'Send Escape to unstick a stalled session' });
|
|
380
|
+
this.sharedCommandRouter.register('restart', async (ctx) => {
|
|
381
|
+
const topicId = parseInt(ctx.channelId, 10);
|
|
382
|
+
const sessionName = this.getSessionForTopic(topicId);
|
|
383
|
+
if (!sessionName) {
|
|
384
|
+
await this.sendToTopic(topicId, 'No session linked to this topic.').catch(() => { });
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
if (!this.onRestartSession) {
|
|
388
|
+
await this.sendToTopic(topicId, 'Restart not available (no handler registered).').catch(() => { });
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
this.clearStallForTopic(topicId);
|
|
392
|
+
await this.sendToTopic(topicId, `Restarting "${sessionName}"...`).catch(() => { });
|
|
393
|
+
try {
|
|
394
|
+
await this.onRestartSession(sessionName, topicId);
|
|
395
|
+
await this.sendToTopic(topicId, 'Session restarted.').catch(() => { });
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
console.error(`[telegram] Restart failed:`, err);
|
|
399
|
+
await this.sendToTopic(topicId, 'Restart didn\'t work. The session may need to be recreated — try sending a new message.').catch(() => { });
|
|
400
|
+
}
|
|
401
|
+
return true;
|
|
402
|
+
}, { description: 'Kill and respawn session' });
|
|
403
|
+
this.sharedCommandRouter.register('status', async (ctx) => {
|
|
404
|
+
const topicId = parseInt(ctx.channelId, 10);
|
|
405
|
+
const s = this.getStatus();
|
|
406
|
+
const lines = [
|
|
407
|
+
`Telegram adapter: ${s.started ? '\u2705 running' : '\u274c stopped'}`,
|
|
408
|
+
`Uptime: ${s.uptime ? Math.round(s.uptime / 60000) + 'm' : 'n/a'}`,
|
|
409
|
+
`Topic mappings: ${s.topicMappings}`,
|
|
410
|
+
`Pending stall alerts: ${s.pendingStalls}`,
|
|
411
|
+
];
|
|
412
|
+
await this.sendToTopic(topicId, lines.join('\n')).catch(() => { });
|
|
413
|
+
return true;
|
|
414
|
+
}, { description: 'Show adapter status' });
|
|
415
|
+
this.sharedCommandRouter.register(['switch-account', 'sa'], async (ctx) => {
|
|
416
|
+
const topicId = parseInt(ctx.channelId, 10);
|
|
417
|
+
if (!ctx.args)
|
|
418
|
+
return false;
|
|
419
|
+
if (this.onSwitchAccountRequest) {
|
|
420
|
+
this.onSwitchAccountRequest(ctx.args, topicId).catch(err => {
|
|
421
|
+
console.error('[telegram] Switch account failed:', err);
|
|
422
|
+
this.sendToTopic(topicId, 'Account switch didn\'t work. Try again or use /quota to check status.').catch(() => { });
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
await this.sendToTopic(topicId, 'Account switching not available.').catch(() => { });
|
|
427
|
+
}
|
|
428
|
+
return true;
|
|
429
|
+
}, { description: 'Switch active Claude account' });
|
|
430
|
+
this.sharedCommandRouter.register(['quota', 'q'], async (ctx) => {
|
|
431
|
+
const topicId = parseInt(ctx.channelId, 10);
|
|
432
|
+
if (this.onQuotaStatusRequest) {
|
|
433
|
+
this.onQuotaStatusRequest(topicId).catch(err => {
|
|
434
|
+
console.error('[telegram] Quota status failed:', err);
|
|
435
|
+
this.sendToTopic(topicId, 'Couldn\'t check quota right now. Try again in a moment.').catch(() => { });
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
await this.sendToTopic(topicId, 'Quota status not available.').catch(() => { });
|
|
440
|
+
}
|
|
441
|
+
return true;
|
|
442
|
+
}, { description: 'Show multi-account quota summary' });
|
|
443
|
+
this.sharedCommandRouter.register('login', async (ctx) => {
|
|
444
|
+
const topicId = parseInt(ctx.channelId, 10);
|
|
445
|
+
const email = ctx.args || null;
|
|
446
|
+
if (this.onLoginRequest) {
|
|
447
|
+
this.onLoginRequest(email, topicId).catch(err => {
|
|
448
|
+
console.error('[telegram] Login flow failed:', err);
|
|
449
|
+
this.sendToTopic(topicId, 'Login didn\'t complete. Try again, or the auth service may be temporarily unavailable.').catch(() => { });
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
await this.sendToTopic(topicId, 'Login not available.').catch(() => { });
|
|
454
|
+
}
|
|
455
|
+
return true;
|
|
456
|
+
}, { description: 'Seamless OAuth login from Telegram' });
|
|
457
|
+
}
|
|
458
|
+
async start() {
|
|
459
|
+
if (this.polling)
|
|
460
|
+
return;
|
|
461
|
+
this.polling = true;
|
|
462
|
+
this.startedAt = new Date();
|
|
463
|
+
this.consecutivePollErrors = 0;
|
|
464
|
+
// Ensure Lifeline topic exists (auto-recreate if deleted)
|
|
465
|
+
await this.ensureLifelineTopic();
|
|
466
|
+
console.log(`[telegram] Starting long-polling...`);
|
|
467
|
+
this.poll();
|
|
468
|
+
// Start notification batcher if configured
|
|
469
|
+
if (this.batcher) {
|
|
470
|
+
this.batcher.start();
|
|
471
|
+
console.log('[telegram] Notification batcher started');
|
|
472
|
+
}
|
|
473
|
+
// Start stall detection if configured
|
|
474
|
+
if (this.sharedStallDetector) {
|
|
475
|
+
// Phase 1c: Wire shared stall detector callbacks and start
|
|
476
|
+
this.sharedStallDetector.setIsSessionAlive(this.onIsSessionAlive ? (name) => this.onIsSessionAlive(name) : null);
|
|
477
|
+
this.sharedStallDetector.setIsSessionActive(this.onIsSessionActive ? (name) => this.onIsSessionActive(name) : null);
|
|
478
|
+
this.sharedStallDetector.setOnStall(async (event, alive) => {
|
|
479
|
+
await this.handleSharedStallEvent(event, alive);
|
|
480
|
+
});
|
|
481
|
+
this.sharedStallDetector.start();
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
const stallMinutes = this.config.stallTimeoutMinutes ?? 5;
|
|
485
|
+
if (stallMinutes > 0) {
|
|
486
|
+
this.stallCheckInterval = setInterval(() => this.checkForStalls(), 30_000);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async stop() {
|
|
491
|
+
this.polling = false;
|
|
492
|
+
if (this.pollTimeout) {
|
|
493
|
+
clearTimeout(this.pollTimeout);
|
|
494
|
+
this.pollTimeout = null;
|
|
495
|
+
}
|
|
496
|
+
if (this.sharedStallDetector) {
|
|
497
|
+
this.sharedStallDetector.stop();
|
|
498
|
+
}
|
|
499
|
+
if (this.stallCheckInterval) {
|
|
500
|
+
clearInterval(this.stallCheckInterval);
|
|
501
|
+
this.stallCheckInterval = null;
|
|
502
|
+
}
|
|
503
|
+
// Flush and stop the batcher on shutdown
|
|
504
|
+
if (this.batcher) {
|
|
505
|
+
try {
|
|
506
|
+
await this.batcher.flushAll();
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
console.error('[telegram] Failed to flush batcher on stop:', err);
|
|
510
|
+
}
|
|
511
|
+
this.batcher.stop();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async send(message) {
|
|
515
|
+
const topicId = message.channel?.identifier;
|
|
516
|
+
const params = {
|
|
517
|
+
chat_id: this.config.chatId,
|
|
518
|
+
text: message.content,
|
|
519
|
+
parse_mode: 'Markdown',
|
|
520
|
+
};
|
|
521
|
+
if (topicId && !isGeneralTopic(parseInt(topicId, 10))) {
|
|
522
|
+
params.message_thread_id = parseInt(topicId, 10);
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
const result = await this.apiCall('sendMessage', params);
|
|
526
|
+
return { messageId: result.message_id, topicId: topicId ? parseInt(topicId, 10) : undefined };
|
|
527
|
+
}
|
|
528
|
+
catch (err) {
|
|
529
|
+
// Only retry without parse_mode on 400 errors (likely Markdown parse failures)
|
|
530
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
531
|
+
if (errMsg.includes('(400)') && params.parse_mode) {
|
|
532
|
+
delete params.parse_mode;
|
|
533
|
+
const result = await this.apiCall('sendMessage', params);
|
|
534
|
+
return { messageId: result.message_id, topicId: topicId ? parseInt(topicId, 10) : undefined };
|
|
535
|
+
}
|
|
536
|
+
throw err;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Log an inbound user message that arrived via an external path (e.g. Lifeline
|
|
541
|
+
* forwarding through /internal/telegram-forward). This ensures the message
|
|
542
|
+
* appears in both JSONL and TopicMemory even when the normal polling handler
|
|
543
|
+
* didn't receive it.
|
|
544
|
+
*/
|
|
545
|
+
logInboundMessage(entry) {
|
|
546
|
+
this.appendToLog({
|
|
547
|
+
messageId: entry.messageId,
|
|
548
|
+
topicId: entry.topicId,
|
|
549
|
+
text: entry.text,
|
|
550
|
+
fromUser: true,
|
|
551
|
+
timestamp: entry.timestamp,
|
|
552
|
+
sessionName: this.topicToSession.get(entry.topicId) ?? null,
|
|
553
|
+
senderName: entry.senderName,
|
|
554
|
+
senderUsername: entry.senderUsername,
|
|
555
|
+
telegramUserId: entry.telegramUserId,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Send a message to a specific forum topic.
|
|
560
|
+
* Returns the Telegram message ID for delivery confirmation.
|
|
561
|
+
*/
|
|
562
|
+
async sendToTopic(topicId, text, options) {
|
|
563
|
+
const params = {
|
|
564
|
+
chat_id: this.config.chatId,
|
|
565
|
+
text,
|
|
566
|
+
};
|
|
567
|
+
if (!isGeneralTopic(topicId)) {
|
|
568
|
+
params.message_thread_id = topicId;
|
|
569
|
+
}
|
|
570
|
+
if (options?.silent) {
|
|
571
|
+
params.disable_notification = true;
|
|
572
|
+
}
|
|
573
|
+
let result;
|
|
574
|
+
try {
|
|
575
|
+
result = await this.apiCall('sendMessage', { ...params, parse_mode: 'Markdown' });
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
result = await this.apiCall('sendMessage', params);
|
|
579
|
+
}
|
|
580
|
+
// Log outbound messages too
|
|
581
|
+
this.appendToLog({
|
|
582
|
+
messageId: result.message_id,
|
|
583
|
+
topicId,
|
|
584
|
+
text,
|
|
585
|
+
fromUser: false,
|
|
586
|
+
timestamp: new Date().toISOString(),
|
|
587
|
+
sessionName: this.topicToSession.get(topicId) ?? null,
|
|
588
|
+
});
|
|
589
|
+
// Clear stall tracking for this topic (agent responded)
|
|
590
|
+
// Skip for proxy messages — PresenceProxy messages should NOT reset stall timers
|
|
591
|
+
if (!options?.skipStallClear) {
|
|
592
|
+
this.clearStallForTopic(topicId);
|
|
593
|
+
}
|
|
594
|
+
// Promise tracking — detect agent "working on it" messages that need follow-through
|
|
595
|
+
const sessionName = this.topicToSession.get(topicId);
|
|
596
|
+
if (sessionName) {
|
|
597
|
+
// Phase 1c: Delegate to shared StallDetector when flag is enabled
|
|
598
|
+
if (this.sharedStallDetector) {
|
|
599
|
+
this.sharedStallDetector.trackOutboundMessage(topicId.toString(), sessionName, text);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
if (this.isPromiseMessage(text)) {
|
|
603
|
+
this.pendingPromises.set(topicId, {
|
|
604
|
+
topicId,
|
|
605
|
+
sessionName,
|
|
606
|
+
promiseText: text.slice(0, 100),
|
|
607
|
+
promisedAt: Date.now(),
|
|
608
|
+
alerted: false,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
else if (this.pendingPromises.has(topicId) && this.isFollowThroughMessage(text)) {
|
|
612
|
+
this.pendingPromises.delete(topicId);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return { messageId: result.message_id, topicId };
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Send a notification through the batcher, falling back to direct send.
|
|
620
|
+
* Use this for internal system notifications that should be batched.
|
|
621
|
+
*/
|
|
622
|
+
async notifyTopic(topicId, text, tier, category) {
|
|
623
|
+
if (this.batcher && this.batcher.isEnabled()) {
|
|
624
|
+
await this.batcher.enqueue({
|
|
625
|
+
tier,
|
|
626
|
+
category,
|
|
627
|
+
message: text,
|
|
628
|
+
timestamp: new Date(),
|
|
629
|
+
topicId,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
// No batcher or disabled — send directly
|
|
634
|
+
await this.sendToTopic(topicId, text);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Configure the notification batcher. Call before start() to enable batching.
|
|
639
|
+
* The batcher's send function is wired to sendToTopic automatically.
|
|
640
|
+
*/
|
|
641
|
+
configureBatcher(config) {
|
|
642
|
+
this.batcher = new NotificationBatcher({
|
|
643
|
+
enabled: true,
|
|
644
|
+
...config,
|
|
645
|
+
});
|
|
646
|
+
this.batcher.setSendFunction((topicId, text) => this.sendToTopic(topicId, text));
|
|
647
|
+
return this.batcher;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Get the notification batcher (if configured).
|
|
651
|
+
*/
|
|
652
|
+
getBatcher() {
|
|
653
|
+
return this.batcher;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Create a forum topic in the supergroup.
|
|
657
|
+
*/
|
|
658
|
+
async createForumTopic(name, iconColor) {
|
|
659
|
+
if (this.notAForum) {
|
|
660
|
+
throw new Error('Chat is not a forum — topic creation skipped');
|
|
661
|
+
}
|
|
662
|
+
const params = {
|
|
663
|
+
chat_id: this.config.chatId,
|
|
664
|
+
name,
|
|
665
|
+
};
|
|
666
|
+
if (iconColor !== undefined) {
|
|
667
|
+
params.icon_color = iconColor;
|
|
668
|
+
}
|
|
669
|
+
try {
|
|
670
|
+
const result = await this.apiCall('createForumTopic', params);
|
|
671
|
+
this.topicToName.set(result.message_thread_id, name);
|
|
672
|
+
this.saveRegistry();
|
|
673
|
+
console.log(`[telegram] Created forum topic: "${name}" (ID: ${result.message_thread_id})`);
|
|
674
|
+
return { topicId: result.message_thread_id, name: result.name };
|
|
675
|
+
}
|
|
676
|
+
catch (err) {
|
|
677
|
+
const errStr = String(err);
|
|
678
|
+
if (errStr.includes('not a forum') || errStr.includes('FORUM_REQUIRED')) {
|
|
679
|
+
this.notAForum = true;
|
|
680
|
+
if (!this.notAForumWarned) {
|
|
681
|
+
this.notAForumWarned = true;
|
|
682
|
+
console.warn('[telegram] ⚠️ Chat is not a forum-enabled supergroup. Forum topics (Lifeline, Dashboard, per-session) will not be created. Messaging will use the General Topic. To enable topics, convert your Telegram group to a supergroup with Topics enabled in group settings.');
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
throw err;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Edit a forum topic's name and/or icon color.
|
|
690
|
+
* Best-effort — silently ignores failures (topic may not exist).
|
|
691
|
+
*/
|
|
692
|
+
async editForumTopic(topicId, name, iconColor) {
|
|
693
|
+
const params = {
|
|
694
|
+
chat_id: this.config.chatId,
|
|
695
|
+
message_thread_id: topicId,
|
|
696
|
+
};
|
|
697
|
+
if (name !== undefined)
|
|
698
|
+
params.name = name;
|
|
699
|
+
if (iconColor !== undefined)
|
|
700
|
+
params.icon_color = iconColor;
|
|
701
|
+
try {
|
|
702
|
+
await this.apiCall('editForumTopic', params);
|
|
703
|
+
if (name) {
|
|
704
|
+
this.topicToName.set(topicId, name);
|
|
705
|
+
this.saveRegistry();
|
|
706
|
+
}
|
|
707
|
+
console.log(`[telegram] Renamed topic ${topicId} → "${name}"`);
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
// @silent-fallback-ok — best-effort rename
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Find an existing topic by name, or create a new one if none exists.
|
|
717
|
+
* Prevents duplicate topics when sessions respawn or the server restarts.
|
|
718
|
+
*/
|
|
719
|
+
async findOrCreateForumTopic(name, iconColor) {
|
|
720
|
+
const normalizedName = name.toLowerCase().trim();
|
|
721
|
+
for (const [topicId, existingName] of this.topicToName) {
|
|
722
|
+
if (existingName.toLowerCase().trim() === normalizedName) {
|
|
723
|
+
console.log(`[telegram] Reusing existing topic ${topicId} for "${name}"`);
|
|
724
|
+
return { topicId, name: existingName, reused: true };
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const result = await this.createForumTopic(name, iconColor);
|
|
728
|
+
return { ...result, reused: false };
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Get the Lifeline topic ID (if configured).
|
|
732
|
+
*/
|
|
733
|
+
getLifelineTopicId() {
|
|
734
|
+
return this.config.lifelineTopicId;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Ensure the Lifeline topic exists. If it was deleted, recreate it.
|
|
738
|
+
* Called on startup and can be called periodically.
|
|
739
|
+
*/
|
|
740
|
+
async ensureLifelineTopic() {
|
|
741
|
+
if (this.notAForum)
|
|
742
|
+
return null;
|
|
743
|
+
const styledName = `${TOPIC_STYLE.SYSTEM.emoji} Lifeline`;
|
|
744
|
+
if (!this.config.lifelineTopicId) {
|
|
745
|
+
// No lifeline topic configured — create one
|
|
746
|
+
try {
|
|
747
|
+
const topic = await this.createForumTopic(styledName, TOPIC_STYLE.SYSTEM.color);
|
|
748
|
+
this.config.lifelineTopicId = topic.topicId;
|
|
749
|
+
this.persistLifelineTopicId(topic.topicId);
|
|
750
|
+
console.log(`[telegram] Created Lifeline topic: ${topic.topicId}`);
|
|
751
|
+
return topic.topicId;
|
|
752
|
+
}
|
|
753
|
+
catch (err) {
|
|
754
|
+
// @silent-fallback-ok — lifeline topic creation, logged
|
|
755
|
+
console.error(`[telegram] Failed to create Lifeline topic: ${err}`);
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
// Lifeline topic ID exists — verify it's still valid silently.
|
|
760
|
+
// Don't send a visible message — it spams the user on every server restart.
|
|
761
|
+
try {
|
|
762
|
+
await this.apiCall('sendChatAction', {
|
|
763
|
+
chat_id: this.config.chatId,
|
|
764
|
+
message_thread_id: this.config.lifelineTopicId,
|
|
765
|
+
action: 'typing',
|
|
766
|
+
});
|
|
767
|
+
// Best-effort rename to styled name if it doesn't match
|
|
768
|
+
const currentName = this.topicToName.get(this.config.lifelineTopicId);
|
|
769
|
+
if (currentName && !currentName.includes(TOPIC_STYLE.SYSTEM.emoji)) {
|
|
770
|
+
await this.editForumTopic(this.config.lifelineTopicId, styledName, TOPIC_STYLE.SYSTEM.color);
|
|
771
|
+
}
|
|
772
|
+
console.log(`[telegram] Lifeline topic verified: ${this.config.lifelineTopicId}`);
|
|
773
|
+
return this.config.lifelineTopicId;
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
const errStr = String(err);
|
|
777
|
+
// Topic was deleted — "message thread not found" or "TOPIC_CLOSED" or similar
|
|
778
|
+
if (errStr.includes('thread not found') || errStr.includes('TOPIC_DELETED') ||
|
|
779
|
+
errStr.includes('TOPIC_CLOSED') || errStr.includes('not found')) {
|
|
780
|
+
console.log(`[telegram] Lifeline topic ${this.config.lifelineTopicId} was deleted — recreating`);
|
|
781
|
+
try {
|
|
782
|
+
const topic = await this.createForumTopic(styledName, TOPIC_STYLE.SYSTEM.color);
|
|
783
|
+
this.config.lifelineTopicId = topic.topicId;
|
|
784
|
+
this.persistLifelineTopicId(topic.topicId);
|
|
785
|
+
console.log(`[telegram] Recreated Lifeline topic: ${topic.topicId}`);
|
|
786
|
+
return topic.topicId;
|
|
787
|
+
}
|
|
788
|
+
catch (recreateErr) {
|
|
789
|
+
DegradationReporter.getInstance().report({
|
|
790
|
+
feature: 'Telegram.Lifeline',
|
|
791
|
+
primary: 'Verified lifeline topic for emergency agent communication',
|
|
792
|
+
fallback: 'No lifeline topic — agent unreachable in emergencies',
|
|
793
|
+
reason: `Lifeline topic deleted and recreation failed: ${recreateErr instanceof Error ? recreateErr.message : String(recreateErr)}`,
|
|
794
|
+
impact: 'Agent cannot receive emergency commands or stall recovery signals.',
|
|
795
|
+
});
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// Some other error (network, etc.) — retry once before reporting degradation.
|
|
800
|
+
// Transient errors like "This operation was aborted" (15s fetch timeout) should
|
|
801
|
+
// not trigger degradation on the first attempt — a brief retry on startup resolves them.
|
|
802
|
+
let retryErr = err;
|
|
803
|
+
try {
|
|
804
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
805
|
+
await this.apiCall('sendChatAction', {
|
|
806
|
+
chat_id: this.config.chatId,
|
|
807
|
+
message_thread_id: this.config.lifelineTopicId,
|
|
808
|
+
action: 'typing',
|
|
809
|
+
});
|
|
810
|
+
console.log(`[telegram] Lifeline topic verified (retry succeeded): ${this.config.lifelineTopicId}`);
|
|
811
|
+
return this.config.lifelineTopicId;
|
|
812
|
+
}
|
|
813
|
+
catch (e) {
|
|
814
|
+
// @silent-fallback-ok — error assigned to retryErr, reported to DegradationReporter below
|
|
815
|
+
retryErr = e;
|
|
816
|
+
}
|
|
817
|
+
DegradationReporter.getInstance().report({
|
|
818
|
+
feature: 'Telegram.Lifeline',
|
|
819
|
+
primary: 'Verified lifeline topic for emergency agent communication',
|
|
820
|
+
fallback: 'Using unverified (possibly stale) lifeline topic ID',
|
|
821
|
+
reason: `Lifeline topic check failed after retry: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`,
|
|
822
|
+
impact: 'Lifeline may be unreachable — messages to agent could fail silently.',
|
|
823
|
+
});
|
|
824
|
+
return this.config.lifelineTopicId;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Persist the Lifeline topic ID back to config.json so it survives restarts.
|
|
829
|
+
*/
|
|
830
|
+
persistLifelineTopicId(topicId) {
|
|
831
|
+
try {
|
|
832
|
+
// Find config.json in state dir's parent (stateDir is .instar/state or .instar)
|
|
833
|
+
const candidates = [
|
|
834
|
+
path.join(this.stateDir, '..', 'config.json'),
|
|
835
|
+
path.join(this.stateDir, 'config.json'),
|
|
836
|
+
];
|
|
837
|
+
for (const configPath of candidates) {
|
|
838
|
+
if (fs.existsSync(configPath)) {
|
|
839
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
840
|
+
const config = JSON.parse(raw);
|
|
841
|
+
// Find the telegram messaging config and update it
|
|
842
|
+
if (Array.isArray(config.messaging)) {
|
|
843
|
+
const telegramEntry = config.messaging.find((m) => m.type === 'telegram');
|
|
844
|
+
if (telegramEntry?.config) {
|
|
845
|
+
telegramEntry.config.lifelineTopicId = topicId;
|
|
846
|
+
const tmpPath = `${configPath}.${process.pid}.tmp`;
|
|
847
|
+
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
|
|
848
|
+
fs.renameSync(tmpPath, configPath);
|
|
849
|
+
console.log(`[telegram] Saved lifelineTopicId=${topicId} to config`);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
catch (err) {
|
|
857
|
+
// @silent-fallback-ok — config persistence, in-memory ok
|
|
858
|
+
console.warn(`[telegram] Failed to persist lifelineTopicId: ${err}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// ── Dashboard Topic ──────────────────────────────────────────────────
|
|
862
|
+
/**
|
|
863
|
+
* Get the Dashboard topic ID (if configured).
|
|
864
|
+
*/
|
|
865
|
+
getDashboardTopicId() {
|
|
866
|
+
return this.config.dashboardTopicId;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Whether the chat supports forum topics. False if we detected
|
|
870
|
+
* "the chat is not a forum" from the Telegram API.
|
|
871
|
+
*/
|
|
872
|
+
get isForumChat() {
|
|
873
|
+
return !this.notAForum;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Ensure the Dashboard topic exists. Creates it on first run, verifies on restart.
|
|
877
|
+
* Same resilience pattern as the lifeline topic.
|
|
878
|
+
*/
|
|
879
|
+
async ensureDashboardTopic() {
|
|
880
|
+
if (this.notAForum)
|
|
881
|
+
return null;
|
|
882
|
+
const styledName = `${TOPIC_STYLE.INFO.emoji} Dashboard`;
|
|
883
|
+
if (!this.config.dashboardTopicId) {
|
|
884
|
+
try {
|
|
885
|
+
const topic = await this.createForumTopic(styledName, TOPIC_STYLE.INFO.color);
|
|
886
|
+
this.config.dashboardTopicId = topic.topicId;
|
|
887
|
+
this.persistDashboardTopicId(topic.topicId);
|
|
888
|
+
console.log(`[telegram] Created Dashboard topic: ${topic.topicId}`);
|
|
889
|
+
// Send a one-time setup hint: mute this topic to avoid unread badges.
|
|
890
|
+
// The bot can't mute topics for users (client-side setting), so we guide them.
|
|
891
|
+
try {
|
|
892
|
+
await this.sendToTopic(topic.topicId, [
|
|
893
|
+
'💡 *Tip*: Mute this topic to avoid notification badges.',
|
|
894
|
+
'',
|
|
895
|
+
'Long-press this topic → Mute → Forever.',
|
|
896
|
+
'',
|
|
897
|
+
'_The latest dashboard link will always be pinned here._',
|
|
898
|
+
].join('\n'), { silent: true });
|
|
899
|
+
}
|
|
900
|
+
catch {
|
|
901
|
+
// @silent-fallback-ok — guidance message is nice-to-have
|
|
902
|
+
}
|
|
903
|
+
return topic.topicId;
|
|
904
|
+
}
|
|
905
|
+
catch (err) {
|
|
906
|
+
DegradationReporter.getInstance().report({
|
|
907
|
+
feature: 'TelegramAdapter.ensureDashboardTopic',
|
|
908
|
+
primary: 'Create Dashboard forum topic for status messages',
|
|
909
|
+
fallback: 'Dashboard topic unavailable, status messages have no destination',
|
|
910
|
+
reason: `Failed to create Dashboard topic: ${err instanceof Error ? err.message : String(err)}`,
|
|
911
|
+
impact: 'Dashboard status messages and pinned URLs will not be delivered',
|
|
912
|
+
});
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// Dashboard topic ID exists — verify it's still valid
|
|
917
|
+
try {
|
|
918
|
+
await this.apiCall('sendChatAction', {
|
|
919
|
+
chat_id: this.config.chatId,
|
|
920
|
+
message_thread_id: this.config.dashboardTopicId,
|
|
921
|
+
action: 'typing',
|
|
922
|
+
});
|
|
923
|
+
// Best-effort rename to styled name
|
|
924
|
+
const currentName = this.topicToName.get(this.config.dashboardTopicId);
|
|
925
|
+
if (currentName && !currentName.includes(TOPIC_STYLE.INFO.emoji)) {
|
|
926
|
+
await this.editForumTopic(this.config.dashboardTopicId, styledName, TOPIC_STYLE.INFO.color);
|
|
927
|
+
}
|
|
928
|
+
return this.config.dashboardTopicId;
|
|
929
|
+
}
|
|
930
|
+
catch (err) {
|
|
931
|
+
// @silent-fallback-ok — self-healing: attempts topic recreation on deletion, returns existing ID for transient errors
|
|
932
|
+
const errStr = String(err);
|
|
933
|
+
if (errStr.includes('thread not found') || errStr.includes('TOPIC_DELETED') ||
|
|
934
|
+
errStr.includes('TOPIC_CLOSED') || errStr.includes('not found')) {
|
|
935
|
+
console.log(`[telegram] Dashboard topic ${this.config.dashboardTopicId} was deleted — recreating`);
|
|
936
|
+
try {
|
|
937
|
+
const topic = await this.createForumTopic(styledName, TOPIC_STYLE.INFO.color);
|
|
938
|
+
this.config.dashboardTopicId = topic.topicId;
|
|
939
|
+
this.persistDashboardTopicId(topic.topicId);
|
|
940
|
+
return topic.topicId;
|
|
941
|
+
}
|
|
942
|
+
catch (recreateErr) {
|
|
943
|
+
DegradationReporter.getInstance().report({
|
|
944
|
+
feature: 'TelegramAdapter.ensureDashboardTopic',
|
|
945
|
+
primary: 'Recreate deleted Dashboard forum topic',
|
|
946
|
+
fallback: 'No dashboard topic available, returning null',
|
|
947
|
+
reason: `Recreation failed: ${recreateErr instanceof Error ? recreateErr.message : String(recreateErr)}`,
|
|
948
|
+
impact: 'Dashboard status messages and pinned URLs will not be delivered until next restart',
|
|
949
|
+
});
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return this.config.dashboardTopicId;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Broadcast the dashboard URL to the Dashboard topic.
|
|
958
|
+
*
|
|
959
|
+
* Edit-in-place pattern: instead of posting a new message each restart (which
|
|
960
|
+
* creates unread badges), we edit the existing pinned message. This means the
|
|
961
|
+
* Dashboard topic never shows as "unread" — it's a quiet reference the user
|
|
962
|
+
* checks when they need the link.
|
|
963
|
+
*
|
|
964
|
+
* Fallback: if the pinned message was deleted or doesn't exist yet, we send
|
|
965
|
+
* a new one, pin it, and save its ID for future edits.
|
|
966
|
+
*/
|
|
967
|
+
async broadcastDashboardUrl(url, tunnelType) {
|
|
968
|
+
const topicId = this.config.dashboardTopicId;
|
|
969
|
+
if (!topicId)
|
|
970
|
+
return;
|
|
971
|
+
const pin = this.config.dashboardPin || '(check your config)';
|
|
972
|
+
const isNamed = tunnelType === 'named';
|
|
973
|
+
const message = this.formatDashboardMessage(url, pin, isNamed);
|
|
974
|
+
// Try to edit the existing pinned message (no new message = no unread badge)
|
|
975
|
+
const existingMessageId = this.loadDashboardMessageId();
|
|
976
|
+
if (existingMessageId) {
|
|
977
|
+
try {
|
|
978
|
+
await this.apiCall('editMessageText', {
|
|
979
|
+
chat_id: this.config.chatId,
|
|
980
|
+
message_id: existingMessageId,
|
|
981
|
+
text: message,
|
|
982
|
+
parse_mode: 'Markdown',
|
|
983
|
+
});
|
|
984
|
+
console.log(`[telegram] Edited dashboard message ${existingMessageId} in-place`);
|
|
985
|
+
return; // Success — no new message, no unread badge
|
|
986
|
+
}
|
|
987
|
+
catch (err) {
|
|
988
|
+
// Edit failed — message was deleted, or content unchanged. Fall through to send new.
|
|
989
|
+
const errStr = String(err);
|
|
990
|
+
if (errStr.includes('message is not modified')) {
|
|
991
|
+
console.log(`[telegram] Dashboard message unchanged — skipping`);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
console.log(`[telegram] Dashboard message ${existingMessageId} edit failed, sending new: ${errStr}`);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
// Fallback: send a new message, pin it, and save for future edits
|
|
998
|
+
try {
|
|
999
|
+
const result = await this.sendToTopic(topicId, message, { silent: true });
|
|
1000
|
+
if (result.messageId) {
|
|
1001
|
+
// Unpin old pins, then pin the new message
|
|
1002
|
+
try {
|
|
1003
|
+
await this.apiCall('unpinAllForumTopicMessages', {
|
|
1004
|
+
chat_id: this.config.chatId,
|
|
1005
|
+
message_thread_id: topicId,
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
catch {
|
|
1009
|
+
// @silent-fallback-ok — unpinning old messages is best-effort
|
|
1010
|
+
}
|
|
1011
|
+
try {
|
|
1012
|
+
await this.apiCall('pinChatMessage', {
|
|
1013
|
+
chat_id: this.config.chatId,
|
|
1014
|
+
message_id: result.messageId,
|
|
1015
|
+
disable_notification: true,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
catch {
|
|
1019
|
+
// @silent-fallback-ok — pinning is nice-to-have, send succeeded
|
|
1020
|
+
}
|
|
1021
|
+
// Save message ID for future edit-in-place
|
|
1022
|
+
this.saveDashboardMessageId(result.messageId);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
catch (err) {
|
|
1026
|
+
console.error(`[telegram] Failed to broadcast dashboard URL: ${err}`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
formatDashboardMessage(url, pin, isNamed) {
|
|
1030
|
+
const dashboardUrl = url + '/dashboard';
|
|
1031
|
+
const quickLinks = [
|
|
1032
|
+
`Sessions: ${dashboardUrl}?tab=sessions`,
|
|
1033
|
+
`Files: ${dashboardUrl}?tab=files`,
|
|
1034
|
+
];
|
|
1035
|
+
if (isNamed) {
|
|
1036
|
+
return [
|
|
1037
|
+
'*Dashboard*',
|
|
1038
|
+
'',
|
|
1039
|
+
`Your permanent dashboard link:`,
|
|
1040
|
+
dashboardUrl,
|
|
1041
|
+
'',
|
|
1042
|
+
`PIN: \`${pin}\``,
|
|
1043
|
+
'',
|
|
1044
|
+
`Quick links:`,
|
|
1045
|
+
...quickLinks.map(l => ` ${l}`),
|
|
1046
|
+
'',
|
|
1047
|
+
`_This link is permanent — it won't change on restart._`,
|
|
1048
|
+
].join('\n');
|
|
1049
|
+
}
|
|
1050
|
+
return [
|
|
1051
|
+
'*Dashboard*',
|
|
1052
|
+
'',
|
|
1053
|
+
`Your dashboard is live:`,
|
|
1054
|
+
dashboardUrl,
|
|
1055
|
+
'',
|
|
1056
|
+
`PIN: \`${pin}\``,
|
|
1057
|
+
'',
|
|
1058
|
+
`Quick links:`,
|
|
1059
|
+
...quickLinks.map(l => ` ${l}`),
|
|
1060
|
+
'',
|
|
1061
|
+
`_This link changes when the server restarts._`,
|
|
1062
|
+
`_For a permanent link, ask me to set up a named tunnel._`,
|
|
1063
|
+
].join('\n');
|
|
1064
|
+
}
|
|
1065
|
+
loadDashboardMessageId() {
|
|
1066
|
+
try {
|
|
1067
|
+
const statePath = path.join(this.stateDir, 'state', 'dashboard-message.json');
|
|
1068
|
+
if (fs.existsSync(statePath)) {
|
|
1069
|
+
const data = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
1070
|
+
return data.messageId ?? null;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
catch {
|
|
1074
|
+
// @silent-fallback-ok — missing state file means first run
|
|
1075
|
+
}
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
saveDashboardMessageId(messageId) {
|
|
1079
|
+
try {
|
|
1080
|
+
const stateSubdir = path.join(this.stateDir, 'state');
|
|
1081
|
+
fs.mkdirSync(stateSubdir, { recursive: true });
|
|
1082
|
+
const statePath = path.join(stateSubdir, 'dashboard-message.json');
|
|
1083
|
+
const tmpPath = `${statePath}.${process.pid}.tmp`;
|
|
1084
|
+
fs.writeFileSync(tmpPath, JSON.stringify({ messageId, savedAt: new Date().toISOString() }));
|
|
1085
|
+
fs.renameSync(tmpPath, statePath);
|
|
1086
|
+
}
|
|
1087
|
+
catch (err) {
|
|
1088
|
+
console.warn(`[telegram] Failed to save dashboard message ID: ${err}`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Persist the Dashboard topic ID back to config.json.
|
|
1093
|
+
*/
|
|
1094
|
+
persistDashboardTopicId(topicId) {
|
|
1095
|
+
try {
|
|
1096
|
+
const candidates = [
|
|
1097
|
+
path.join(this.stateDir, '..', 'config.json'),
|
|
1098
|
+
path.join(this.stateDir, 'config.json'),
|
|
1099
|
+
];
|
|
1100
|
+
for (const configPath of candidates) {
|
|
1101
|
+
if (fs.existsSync(configPath)) {
|
|
1102
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
1103
|
+
const config = JSON.parse(raw);
|
|
1104
|
+
if (Array.isArray(config.messaging)) {
|
|
1105
|
+
const telegramEntry = config.messaging.find((m) => m.type === 'telegram');
|
|
1106
|
+
if (telegramEntry?.config) {
|
|
1107
|
+
telegramEntry.config.dashboardTopicId = topicId;
|
|
1108
|
+
const tmpPath = `${configPath}.${process.pid}.tmp`;
|
|
1109
|
+
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
|
|
1110
|
+
fs.renameSync(tmpPath, configPath);
|
|
1111
|
+
console.log(`[telegram] Saved dashboardTopicId=${topicId} to config`);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
catch (err) {
|
|
1119
|
+
console.warn(`[telegram] Failed to persist dashboardTopicId: ${err}`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Close a forum topic.
|
|
1124
|
+
*/
|
|
1125
|
+
async closeForumTopic(topicId) {
|
|
1126
|
+
try {
|
|
1127
|
+
await this.apiCall('closeForumTopic', {
|
|
1128
|
+
chat_id: this.config.chatId,
|
|
1129
|
+
message_thread_id: topicId,
|
|
1130
|
+
});
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
catch {
|
|
1134
|
+
// @silent-fallback-ok — forum close boolean return
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
onMessage(handler) {
|
|
1139
|
+
this.handler = handler;
|
|
1140
|
+
}
|
|
1141
|
+
async resolveUser(channelIdentifier) {
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
// ── Auth Gating ──────────────────────────────────────────
|
|
1145
|
+
/**
|
|
1146
|
+
* Check if a message is from an authorized user.
|
|
1147
|
+
* If no authorizedUserIds configured, all messages are accepted.
|
|
1148
|
+
*/
|
|
1149
|
+
isAuthorized(userId) {
|
|
1150
|
+
// Phase 1d: Delegate to shared AuthGate when flag is enabled
|
|
1151
|
+
if (this.sharedAuthGate) {
|
|
1152
|
+
return this.sharedAuthGate.isAuthorized(userId.toString());
|
|
1153
|
+
}
|
|
1154
|
+
const authorized = this.config.authorizedUserIds;
|
|
1155
|
+
if (!authorized || authorized.length === 0)
|
|
1156
|
+
return true;
|
|
1157
|
+
return authorized.includes(userId);
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Handle a message from an unknown/unauthorized Telegram user.
|
|
1161
|
+
* Checks the registration policy and responds appropriately:
|
|
1162
|
+
* - admin-only: Gated message + notify admin
|
|
1163
|
+
* - invite-only: Ask for invite code
|
|
1164
|
+
* - open: Start mini-onboarding (rate limited)
|
|
1165
|
+
*
|
|
1166
|
+
* Rate-limited to prevent spam from the same unknown user.
|
|
1167
|
+
*/
|
|
1168
|
+
async handleUnknownUser(telegramUserId, firstName, username, messageText) {
|
|
1169
|
+
// Rate limit: don't spam responses to the same unknown user
|
|
1170
|
+
const lastResponse = this.unknownUserRateLimit.get(telegramUserId);
|
|
1171
|
+
if (lastResponse && (Date.now() - lastResponse) < TelegramAdapter.UNKNOWN_USER_COOLDOWN_MS) {
|
|
1172
|
+
console.log(`[telegram] Rate-limited response to unknown user ${telegramUserId} (${username ?? firstName})`);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
// Get registration policy from callback
|
|
1176
|
+
const policyInfo = this.onGetRegistrationPolicy?.();
|
|
1177
|
+
if (!policyInfo) {
|
|
1178
|
+
// No policy callback wired — fall back to silent ignore (legacy behavior)
|
|
1179
|
+
console.log(`[telegram] Ignoring message from unauthorized user ${telegramUserId} (${username ?? firstName}) — no registration policy configured`);
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
const { policy, contactHint, agentName } = policyInfo;
|
|
1183
|
+
const displayName = agentName || 'This agent';
|
|
1184
|
+
// Mark that we responded to this user
|
|
1185
|
+
this.unknownUserRateLimit.set(telegramUserId, Date.now());
|
|
1186
|
+
// Clean up old rate limit entries periodically (keep map from growing unbounded)
|
|
1187
|
+
if (this.unknownUserRateLimit.size > 100) {
|
|
1188
|
+
const cutoff = Date.now() - TelegramAdapter.UNKNOWN_USER_COOLDOWN_MS * 10;
|
|
1189
|
+
for (const [uid, ts] of this.unknownUserRateLimit) {
|
|
1190
|
+
if (ts < cutoff)
|
|
1191
|
+
this.unknownUserRateLimit.delete(uid);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
console.log(`[telegram] Unknown user ${telegramUserId} (${username ?? firstName}) — policy: ${policy}`);
|
|
1195
|
+
try {
|
|
1196
|
+
switch (policy) {
|
|
1197
|
+
case 'admin-only': {
|
|
1198
|
+
// Send gated message to the user
|
|
1199
|
+
let gatedMessage = `Hi ${firstName}! ${displayName} is not open for public registration. Access is managed by an administrator.`;
|
|
1200
|
+
if (contactHint) {
|
|
1201
|
+
gatedMessage += `\n\n${contactHint}`;
|
|
1202
|
+
}
|
|
1203
|
+
gatedMessage += `\n\nYour request has been noted and forwarded to the admin.`;
|
|
1204
|
+
// Reply in the group's General topic (since unknown users don't have their own topic)
|
|
1205
|
+
await this.sendToTopic(GENERAL_TOPIC_ID, gatedMessage).catch(() => { });
|
|
1206
|
+
// Notify admin via callback
|
|
1207
|
+
if (this.onNotifyAdminJoinRequest) {
|
|
1208
|
+
await this.onNotifyAdminJoinRequest({
|
|
1209
|
+
name: firstName,
|
|
1210
|
+
username,
|
|
1211
|
+
telegramUserId,
|
|
1212
|
+
}).catch(err => {
|
|
1213
|
+
console.error(`[telegram] Failed to notify admin of join request: ${err}`);
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1218
|
+
case 'invite-only': {
|
|
1219
|
+
// Check if the message contains an invite code
|
|
1220
|
+
const trimmedText = messageText?.trim();
|
|
1221
|
+
if (trimmedText && this.onValidateInviteCode) {
|
|
1222
|
+
const result = await this.onValidateInviteCode(trimmedText, telegramUserId);
|
|
1223
|
+
if (result.valid) {
|
|
1224
|
+
await this.sendToTopic(GENERAL_TOPIC_ID, `Welcome, ${firstName}! Your invite code has been accepted. Setting up your account...`).catch(() => { });
|
|
1225
|
+
// Trigger mini-onboarding after successful invite validation
|
|
1226
|
+
if (this.onStartMiniOnboarding) {
|
|
1227
|
+
await this.onStartMiniOnboarding(telegramUserId, firstName, username).catch(err => {
|
|
1228
|
+
console.error(`[telegram] Failed to start onboarding after invite: ${err}`);
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
else if (result.error) {
|
|
1234
|
+
await this.sendToTopic(GENERAL_TOPIC_ID, result.error).catch(() => { });
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
// Default invite-only prompt
|
|
1239
|
+
let inviteMessage = `Hi ${firstName}! ${displayName} requires an invite code to join. Please reply with your invite code.`;
|
|
1240
|
+
if (contactHint) {
|
|
1241
|
+
inviteMessage += `\n\n${contactHint}`;
|
|
1242
|
+
}
|
|
1243
|
+
await this.sendToTopic(GENERAL_TOPIC_ID, inviteMessage).catch(() => { });
|
|
1244
|
+
break;
|
|
1245
|
+
}
|
|
1246
|
+
case 'open': {
|
|
1247
|
+
// Start mini-onboarding via callback
|
|
1248
|
+
if (this.onStartMiniOnboarding) {
|
|
1249
|
+
await this.sendToTopic(GENERAL_TOPIC_ID, `Hi ${firstName}! Welcome! Setting up your account...`).catch(() => { });
|
|
1250
|
+
await this.onStartMiniOnboarding(telegramUserId, firstName, username).catch(err => {
|
|
1251
|
+
// @silent-fallback-ok — supplementary notification
|
|
1252
|
+
console.error(`[telegram] Failed to start mini-onboarding: ${err}`);
|
|
1253
|
+
this.sendToTopic(GENERAL_TOPIC_ID, `Sorry ${firstName}, there was an issue setting up your account. Please try again later.`).catch(() => { });
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
else {
|
|
1257
|
+
await this.sendToTopic(GENERAL_TOPIC_ID, `Hi ${firstName}! Registration is currently being set up. Please try again later.`).catch(() => { });
|
|
1258
|
+
}
|
|
1259
|
+
break;
|
|
1260
|
+
}
|
|
1261
|
+
default: {
|
|
1262
|
+
// Unknown policy — fall back to gated message
|
|
1263
|
+
console.warn(`[telegram] Unknown registration policy: ${policy}`);
|
|
1264
|
+
await this.sendToTopic(GENERAL_TOPIC_ID, `Hi ${firstName}! ${displayName} is not currently accepting new users.`).catch(() => { });
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
catch (err) {
|
|
1269
|
+
console.error(`[telegram] Error handling unknown user ${telegramUserId}: ${err}`);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
// ── Topic-Session Registry ─────────────────────────────────
|
|
1273
|
+
registerTopicSession(topicId, sessionName, topicName) {
|
|
1274
|
+
this.topicToSession.set(topicId, sessionName);
|
|
1275
|
+
this.sessionToTopic.set(sessionName, topicId);
|
|
1276
|
+
if (topicName) {
|
|
1277
|
+
this.topicToName.set(topicId, topicName);
|
|
1278
|
+
}
|
|
1279
|
+
this.saveRegistry();
|
|
1280
|
+
console.log(`[telegram] Registered topic ${topicId} <-> session "${sessionName}"${topicName ? ` (name: "${topicName}")` : ''}`);
|
|
1281
|
+
}
|
|
1282
|
+
unregisterTopic(topicId) {
|
|
1283
|
+
const sessionName = this.topicToSession.get(topicId);
|
|
1284
|
+
this.topicToSession.delete(topicId);
|
|
1285
|
+
if (sessionName)
|
|
1286
|
+
this.sessionToTopic.delete(sessionName);
|
|
1287
|
+
this.saveRegistry();
|
|
1288
|
+
}
|
|
1289
|
+
getSessionForTopic(topicId) {
|
|
1290
|
+
return this.topicToSession.get(topicId) ?? null;
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Get all active topic→session mappings.
|
|
1294
|
+
* Used by TopicResumeMap heartbeat to proactively persist UUIDs.
|
|
1295
|
+
*/
|
|
1296
|
+
getAllTopicSessions() {
|
|
1297
|
+
return new Map(this.topicToSession);
|
|
1298
|
+
}
|
|
1299
|
+
getTopicForSession(sessionName) {
|
|
1300
|
+
return this.sessionToTopic.get(sessionName) ?? null;
|
|
1301
|
+
}
|
|
1302
|
+
getTopicName(topicId) {
|
|
1303
|
+
return this.topicToName.get(topicId) ?? null;
|
|
1304
|
+
}
|
|
1305
|
+
// ── Topic Purpose Management ─────────────────────────────────
|
|
1306
|
+
/**
|
|
1307
|
+
* Set the purpose for a topic (e.g., "billing", "technical").
|
|
1308
|
+
* Purpose is used for outbound content validation.
|
|
1309
|
+
*/
|
|
1310
|
+
setTopicPurpose(topicId, purpose) {
|
|
1311
|
+
this.topicToPurpose.set(topicId, purpose.toLowerCase());
|
|
1312
|
+
this.saveRegistry();
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Get the purpose for a topic. Checks runtime map first, then config.
|
|
1316
|
+
* Returns null if no purpose is set (permissive — all content allowed).
|
|
1317
|
+
*/
|
|
1318
|
+
getTopicPurpose(topicId) {
|
|
1319
|
+
// Runtime map takes precedence over config
|
|
1320
|
+
const runtimePurpose = this.topicToPurpose.get(topicId);
|
|
1321
|
+
if (runtimePurpose)
|
|
1322
|
+
return runtimePurpose;
|
|
1323
|
+
// Fall back to config
|
|
1324
|
+
const validationConfig = this.config.contentValidation;
|
|
1325
|
+
if (validationConfig) {
|
|
1326
|
+
return getTopicPurpose(topicId, validationConfig);
|
|
1327
|
+
}
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Get all topic purposes (runtime + config merged).
|
|
1332
|
+
*/
|
|
1333
|
+
getAllTopicPurposes() {
|
|
1334
|
+
const result = {};
|
|
1335
|
+
// Config purposes first
|
|
1336
|
+
const validationConfig = this.config.contentValidation;
|
|
1337
|
+
if (validationConfig) {
|
|
1338
|
+
for (const [id, purpose] of Object.entries(validationConfig.topicPurposes)) {
|
|
1339
|
+
result[Number(id)] = purpose.toLowerCase();
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
// Runtime overrides
|
|
1343
|
+
for (const [topicId, purpose] of this.topicToPurpose) {
|
|
1344
|
+
result[topicId] = purpose;
|
|
1345
|
+
}
|
|
1346
|
+
return result;
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Validate outbound content against topic purpose.
|
|
1350
|
+
* Returns the validation result. Callers decide how to handle rejection.
|
|
1351
|
+
*/
|
|
1352
|
+
validateOutboundContent(topicId, text, options) {
|
|
1353
|
+
const validationConfig = this.config.contentValidation;
|
|
1354
|
+
if (!validationConfig?.enabled) {
|
|
1355
|
+
return { allowed: true, reason: null, detectedCategory: null, topicPurpose: null, suggestion: null };
|
|
1356
|
+
}
|
|
1357
|
+
const purpose = this.getTopicPurpose(topicId);
|
|
1358
|
+
return validateTopicContent(text, purpose, validationConfig, options);
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Classify content using the configured categories.
|
|
1362
|
+
* Useful for debugging and API endpoints.
|
|
1363
|
+
*/
|
|
1364
|
+
classifyContent(text) {
|
|
1365
|
+
const validationConfig = this.config.contentValidation;
|
|
1366
|
+
if (!validationConfig) {
|
|
1367
|
+
return { category: null, confidence: 'low', matchedKeywords: [] };
|
|
1368
|
+
}
|
|
1369
|
+
return classifyContent(text, validationConfig.categories);
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Get all topic-session mappings (for admin/debug UIs).
|
|
1373
|
+
*/
|
|
1374
|
+
getAllTopicMappings() {
|
|
1375
|
+
const result = [];
|
|
1376
|
+
for (const [topicId, sessionName] of this.topicToSession) {
|
|
1377
|
+
result.push({
|
|
1378
|
+
topicId,
|
|
1379
|
+
sessionName,
|
|
1380
|
+
topicName: this.topicToName.get(topicId) ?? null,
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
return result;
|
|
1384
|
+
}
|
|
1385
|
+
// ── Stall Detection ──────────────────────────────────────
|
|
1386
|
+
/**
|
|
1387
|
+
* Track that a message was injected into a session.
|
|
1388
|
+
* Used by stall detection to alert if no response comes back.
|
|
1389
|
+
*/
|
|
1390
|
+
trackMessageInjection(topicId, sessionName, messageText) {
|
|
1391
|
+
// Phase 1c: Delegate to shared StallDetector when flag is enabled
|
|
1392
|
+
if (this.sharedStallDetector) {
|
|
1393
|
+
this.sharedStallDetector.trackMessageInjection(topicId.toString(), sessionName, messageText);
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
const key = `${topicId}-${Date.now()}`;
|
|
1397
|
+
this.pendingMessages.set(key, {
|
|
1398
|
+
topicId,
|
|
1399
|
+
sessionName,
|
|
1400
|
+
messageText: messageText.slice(0, 100),
|
|
1401
|
+
injectedAt: Date.now(),
|
|
1402
|
+
alerted: false,
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
clearStallForTopic(topicId) {
|
|
1406
|
+
// Phase 1c: Delegate to shared StallDetector when flag is enabled
|
|
1407
|
+
if (this.sharedStallDetector) {
|
|
1408
|
+
this.sharedStallDetector.clearStallForChannel(topicId.toString());
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
for (const [key, pending] of this.pendingMessages) {
|
|
1412
|
+
if (pending.topicId === topicId) {
|
|
1413
|
+
this.pendingMessages.delete(key);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Public interface for external callers (e.g., StallTriageNurse) to clear
|
|
1419
|
+
* stall tracking for a topic after successful recovery.
|
|
1420
|
+
*/
|
|
1421
|
+
clearStallTracking(topicId) {
|
|
1422
|
+
this.clearStallForTopic(topicId);
|
|
1423
|
+
}
|
|
1424
|
+
/** Clear promise tracking for a topic (e.g., after successful recovery) */
|
|
1425
|
+
clearPromiseTracking(topicId) {
|
|
1426
|
+
if (this.sharedStallDetector) {
|
|
1427
|
+
this.sharedStallDetector.clearPromiseForChannel(topicId.toString());
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
this.pendingPromises.delete(topicId);
|
|
1431
|
+
}
|
|
1432
|
+
/** Detect "work-in-progress" messages that imply the agent will follow up */
|
|
1433
|
+
isPromiseMessage(text) {
|
|
1434
|
+
const promisePatterns = [
|
|
1435
|
+
/give me (?:a )?(?:couple|few|some) (?:more )?minutes/i,
|
|
1436
|
+
/give me (?:a )?(?:minute|moment|second|sec)/i,
|
|
1437
|
+
/working on (?:it|this|that)/i,
|
|
1438
|
+
/looking into (?:it|this|that)/i,
|
|
1439
|
+
/let me (?:check|look|investigate|dig|research)/i,
|
|
1440
|
+
/investigating/i,
|
|
1441
|
+
/still (?:on it|working|looking)/i,
|
|
1442
|
+
/one moment/i,
|
|
1443
|
+
/be right back/i,
|
|
1444
|
+
/hang on/i,
|
|
1445
|
+
/bear with me/i,
|
|
1446
|
+
/i'll (?:get back|follow up|check|look into)/i,
|
|
1447
|
+
/narrowing (?:it |this |that )?down/i,
|
|
1448
|
+
];
|
|
1449
|
+
return promisePatterns.some(p => p.test(text));
|
|
1450
|
+
}
|
|
1451
|
+
/** Detect messages that indicate the agent delivered on its promise */
|
|
1452
|
+
isFollowThroughMessage(text) {
|
|
1453
|
+
// Messages that indicate the agent is delivering results (not just status updates)
|
|
1454
|
+
// Must be substantially longer than a typical status update
|
|
1455
|
+
if (text.length > 200)
|
|
1456
|
+
return true;
|
|
1457
|
+
// Explicit completion signals
|
|
1458
|
+
const completionPatterns = [
|
|
1459
|
+
/here(?:'s| is| are) (?:what|the)/i,
|
|
1460
|
+
/i found/i,
|
|
1461
|
+
/the (?:issue|problem|bug|fix|solution|answer|result)/i,
|
|
1462
|
+
/done|completed|finished|resolved/i,
|
|
1463
|
+
/summary|overview|analysis/i,
|
|
1464
|
+
];
|
|
1465
|
+
return completionPatterns.some(p => p.test(text));
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* LLM gate for fallback stall/promise alerts.
|
|
1469
|
+
*
|
|
1470
|
+
* Before sending a user-facing alert about a stall or expired promise,
|
|
1471
|
+
* check with the intelligence provider whether the alert is warranted.
|
|
1472
|
+
* This prevents false positives when the StallTriageNurse is unavailable.
|
|
1473
|
+
*
|
|
1474
|
+
* Returns true if the alert should be sent, false to suppress.
|
|
1475
|
+
* If no intelligence provider is available, returns true (fail-open for safety).
|
|
1476
|
+
*/
|
|
1477
|
+
async confirmStallAlert(context) {
|
|
1478
|
+
if (!this.intelligence)
|
|
1479
|
+
return true; // No LLM available → fail-open
|
|
1480
|
+
const prompt = [
|
|
1481
|
+
'You are evaluating whether to send an alert to a user about an AI agent session.',
|
|
1482
|
+
'',
|
|
1483
|
+
`Alert type: ${context.type}`,
|
|
1484
|
+
`Session: "${context.sessionName}" (${context.sessionAlive ? 'still running' : 'stopped'})`,
|
|
1485
|
+
`Time elapsed: ${context.minutesElapsed} minutes`,
|
|
1486
|
+
`Context: "${context.messageText}"`,
|
|
1487
|
+
'',
|
|
1488
|
+
'Should we send a user-facing alert about this? Consider:',
|
|
1489
|
+
'- If the session stopped, the user needs to know',
|
|
1490
|
+
'- If the session is still running, it might just be working on a complex task',
|
|
1491
|
+
`- ${context.minutesElapsed} minutes is ${context.minutesElapsed > 15 ? 'a long time' : 'moderate'} for an AI task`,
|
|
1492
|
+
'',
|
|
1493
|
+
'Respond with exactly one word: yes or no.',
|
|
1494
|
+
].join('\n');
|
|
1495
|
+
try {
|
|
1496
|
+
const response = await this.intelligence.evaluate(prompt, {
|
|
1497
|
+
maxTokens: 5,
|
|
1498
|
+
temperature: 0,
|
|
1499
|
+
});
|
|
1500
|
+
const answer = response.trim().toLowerCase();
|
|
1501
|
+
if (answer === 'no') {
|
|
1502
|
+
console.log(`[telegram] LLM suppressed ${context.type} alert for "${context.sessionName}" (${context.minutesElapsed}m)`);
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1507
|
+
catch (err) {
|
|
1508
|
+
// @silent-fallback-ok — LLM intelligence is optional; fail-open to alert user about stalls
|
|
1509
|
+
console.warn(`[telegram] LLM stall confirmation failed, allowing alert:`, err);
|
|
1510
|
+
return true; // Fail-open
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
/** Get all active topic-session mappings (used by SessionMonitor) */
|
|
1514
|
+
getActiveTopicSessions() {
|
|
1515
|
+
return new Map(this.topicToSession);
|
|
1516
|
+
}
|
|
1517
|
+
/** Get recent message log entries for analysis */
|
|
1518
|
+
getMessageLog(limit = 100) {
|
|
1519
|
+
try {
|
|
1520
|
+
if (!fs.existsSync(this.messageLogPath))
|
|
1521
|
+
return [];
|
|
1522
|
+
const content = fs.readFileSync(this.messageLogPath, 'utf-8');
|
|
1523
|
+
const lines = content.trim().split('\n').filter(Boolean).slice(-limit);
|
|
1524
|
+
return lines.map(line => {
|
|
1525
|
+
try {
|
|
1526
|
+
const entry = JSON.parse(line);
|
|
1527
|
+
return {
|
|
1528
|
+
topicId: entry.topicId,
|
|
1529
|
+
text: entry.text || '',
|
|
1530
|
+
fromUser: entry.fromUser ?? true,
|
|
1531
|
+
timestamp: entry.timestamp || new Date().toISOString(),
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
catch {
|
|
1535
|
+
// @silent-fallback-ok — JSONL parse, skip corrupted
|
|
1536
|
+
return null;
|
|
1537
|
+
}
|
|
1538
|
+
}).filter(Boolean);
|
|
1539
|
+
}
|
|
1540
|
+
catch {
|
|
1541
|
+
// @silent-fallback-ok — log read, empty array safe
|
|
1542
|
+
return [];
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
async checkForStalls() {
|
|
1546
|
+
const stallMinutes = this.config.stallTimeoutMinutes ?? 5;
|
|
1547
|
+
const stallThresholdMs = stallMinutes * 60 * 1000;
|
|
1548
|
+
const now = Date.now();
|
|
1549
|
+
for (const [key, pending] of this.pendingMessages) {
|
|
1550
|
+
if (pending.alerted)
|
|
1551
|
+
continue;
|
|
1552
|
+
if (now - pending.injectedAt < stallThresholdMs)
|
|
1553
|
+
continue;
|
|
1554
|
+
// Check if session is still alive
|
|
1555
|
+
const alive = this.onIsSessionAlive
|
|
1556
|
+
? this.onIsSessionAlive(pending.sessionName)
|
|
1557
|
+
: true; // assume alive if no checker
|
|
1558
|
+
// If alive, verify the session is truly stalled (not just responding through a different path)
|
|
1559
|
+
if (alive && this.onIsSessionActive) {
|
|
1560
|
+
try {
|
|
1561
|
+
const active = await this.onIsSessionActive(pending.sessionName);
|
|
1562
|
+
if (active) {
|
|
1563
|
+
// Session is producing output — false alarm, clear it
|
|
1564
|
+
console.log(`[telegram] Session "${pending.sessionName}" verified active, clearing stall`);
|
|
1565
|
+
this.pendingMessages.delete(key);
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
catch {
|
|
1570
|
+
// Verifier failed — fall through to alert
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
pending.alerted = true;
|
|
1574
|
+
// Try LLM-powered triage first if available
|
|
1575
|
+
if (this.onStallDetected) {
|
|
1576
|
+
try {
|
|
1577
|
+
const triageResult = await this.onStallDetected(pending.topicId, pending.sessionName, pending.messageText, pending.injectedAt);
|
|
1578
|
+
if (triageResult.resolved) {
|
|
1579
|
+
this.pendingMessages.delete(key);
|
|
1580
|
+
continue; // Nurse handled it
|
|
1581
|
+
}
|
|
1582
|
+
// Nurse couldn't resolve — fall through to quota check / generic alert
|
|
1583
|
+
}
|
|
1584
|
+
catch (err) {
|
|
1585
|
+
console.warn(`[telegram] Triage nurse error:`, err);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
// Classify the stall — check if it's a quota death
|
|
1589
|
+
let isQuotaDeath = false;
|
|
1590
|
+
if (this.onClassifySessionDeath) {
|
|
1591
|
+
try {
|
|
1592
|
+
const classification = await this.onClassifySessionDeath(pending.sessionName);
|
|
1593
|
+
if (classification && classification.cause === 'quota_exhaustion') {
|
|
1594
|
+
isQuotaDeath = true;
|
|
1595
|
+
this.sendToTopic(pending.topicId, `\ud83d\udd34 Session hit quota limit \u2014 "${pending.sessionName}" can't respond.\n\n` +
|
|
1596
|
+
`${classification.detail}\n\n` +
|
|
1597
|
+
`Use /quota to check accounts, /switch-account to switch, or /login to authenticate a new account.`).catch(err => {
|
|
1598
|
+
console.error(`[telegram] Quota stall alert failed: ${err}`);
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
catch {
|
|
1603
|
+
// Classification failed — fall through to generic
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
if (!isQuotaDeath) {
|
|
1607
|
+
const minutesAgo = Math.round((now - pending.injectedAt) / 60000);
|
|
1608
|
+
// LLM gate: confirm alert is warranted before sending user-facing message
|
|
1609
|
+
const shouldAlert = await this.confirmStallAlert({
|
|
1610
|
+
type: 'stall',
|
|
1611
|
+
sessionName: pending.sessionName,
|
|
1612
|
+
messageText: pending.messageText,
|
|
1613
|
+
minutesElapsed: minutesAgo,
|
|
1614
|
+
sessionAlive: alive,
|
|
1615
|
+
});
|
|
1616
|
+
if (shouldAlert) {
|
|
1617
|
+
const status = alive ? 'running but not responding' : 'no longer running';
|
|
1618
|
+
this.sendToTopic(pending.topicId, `\u26a0\ufe0f No response after ${minutesAgo} minutes. "${pending.sessionName}" is ${status}.\n\nYour message: "${pending.messageText}..."${alive ? '\n\nTry /interrupt to nudge it, or /restart to start fresh.' : '\n\nSend another message and a new session will start automatically.'}`).catch(err => {
|
|
1619
|
+
console.error(`[telegram] Stall alert failed: ${err}`);
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
// Check for expired promises (agent said "give me a minute" but never followed up)
|
|
1625
|
+
const promiseMinutes = this.config.promiseTimeoutMinutes ?? 10;
|
|
1626
|
+
const promiseThresholdMs = promiseMinutes * 60 * 1000;
|
|
1627
|
+
if (promiseMinutes > 0) {
|
|
1628
|
+
for (const [topicId, promise] of this.pendingPromises) {
|
|
1629
|
+
if (promise.alerted)
|
|
1630
|
+
continue;
|
|
1631
|
+
if (now - promise.promisedAt < promiseThresholdMs)
|
|
1632
|
+
continue;
|
|
1633
|
+
promise.alerted = true;
|
|
1634
|
+
console.log(`[telegram] Promise expired for topic ${topicId}: "${promise.promiseText}" (${Math.round((now - promise.promisedAt) / 60000)} min ago)`);
|
|
1635
|
+
// Check if session is still alive
|
|
1636
|
+
const alive = this.onIsSessionAlive
|
|
1637
|
+
? this.onIsSessionAlive(promise.sessionName)
|
|
1638
|
+
: true;
|
|
1639
|
+
// Delegate to triage nurse if available
|
|
1640
|
+
if (this.onStallDetected) {
|
|
1641
|
+
try {
|
|
1642
|
+
const triageResult = await this.onStallDetected(promise.topicId, promise.sessionName, `[promise expired] ${promise.promiseText}`, promise.promisedAt);
|
|
1643
|
+
if (triageResult.resolved) {
|
|
1644
|
+
this.pendingPromises.delete(topicId);
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
catch (err) {
|
|
1649
|
+
console.warn(`[telegram] Promise triage error:`, err);
|
|
1650
|
+
DegradationReporter.getInstance().report({
|
|
1651
|
+
feature: 'TelegramAdapter.onStallDetected',
|
|
1652
|
+
primary: 'LLM-based stall triage diagnosis',
|
|
1653
|
+
fallback: 'Stall goes undiagnosed',
|
|
1654
|
+
reason: `Why: ${err instanceof Error ? err.message : String(err)}`,
|
|
1655
|
+
impact: 'Stalled session persists without recovery attempt',
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
// Fallback: LLM-gated user-facing alert
|
|
1660
|
+
const promiseMinutesAgo = Math.round((now - promise.promisedAt) / 60000);
|
|
1661
|
+
const shouldAlertPromise = await this.confirmStallAlert({
|
|
1662
|
+
type: 'promise-expired',
|
|
1663
|
+
sessionName: promise.sessionName,
|
|
1664
|
+
messageText: promise.promiseText,
|
|
1665
|
+
minutesElapsed: promiseMinutesAgo,
|
|
1666
|
+
sessionAlive: alive,
|
|
1667
|
+
});
|
|
1668
|
+
if (shouldAlertPromise) {
|
|
1669
|
+
if (!alive) {
|
|
1670
|
+
await this.sendToTopic(topicId, `The session stopped unexpectedly after saying "${promise.promiseText}". Sending a new message will auto-spawn a fresh session.`).catch(() => { });
|
|
1671
|
+
}
|
|
1672
|
+
else {
|
|
1673
|
+
await this.sendToTopic(topicId, `It's been ${promiseMinutesAgo} minutes since the session said "${promise.promiseText}" — checking on it now...`).catch(() => { });
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
// Clean up old promise entries
|
|
1678
|
+
for (const [topicId, promise] of this.pendingPromises) {
|
|
1679
|
+
if (promise.alerted && now - promise.promisedAt > 60 * 60 * 1000) {
|
|
1680
|
+
this.pendingPromises.delete(topicId);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
// Clean up old entries (older than 30 minutes, already alerted)
|
|
1685
|
+
for (const [key, pending] of this.pendingMessages) {
|
|
1686
|
+
if (pending.alerted && now - pending.injectedAt > 30 * 60 * 1000) {
|
|
1687
|
+
this.pendingMessages.delete(key);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Handle stall events from the shared StallDetector (Phase 1c).
|
|
1693
|
+
* Bridges shared events back to Telegram-specific alert logic
|
|
1694
|
+
* (triage nurse, quota classification, LLM gate, user notifications).
|
|
1695
|
+
*/
|
|
1696
|
+
async handleSharedStallEvent(event, alive) {
|
|
1697
|
+
const topicId = parseInt(event.channelId, 10);
|
|
1698
|
+
// Phase 1e: Emit to event bus
|
|
1699
|
+
if (this.eventBus) {
|
|
1700
|
+
if (event.type === 'stall') {
|
|
1701
|
+
this.eventBus.emit('stall:detected', {
|
|
1702
|
+
channelId: event.channelId,
|
|
1703
|
+
sessionName: event.sessionName,
|
|
1704
|
+
messageText: event.messageText,
|
|
1705
|
+
injectedAt: event.injectedAt,
|
|
1706
|
+
minutesElapsed: event.minutesElapsed,
|
|
1707
|
+
alive,
|
|
1708
|
+
}).catch(err => console.error(`[telegram] EventBus stall:detected error: ${err}`));
|
|
1709
|
+
}
|
|
1710
|
+
else {
|
|
1711
|
+
this.eventBus.emit('stall:promise-expired', {
|
|
1712
|
+
channelId: event.channelId,
|
|
1713
|
+
sessionName: event.sessionName,
|
|
1714
|
+
promiseText: event.messageText,
|
|
1715
|
+
promisedAt: event.injectedAt,
|
|
1716
|
+
minutesElapsed: event.minutesElapsed,
|
|
1717
|
+
alive,
|
|
1718
|
+
}).catch(err => console.error(`[telegram] EventBus stall:promise-expired error: ${err}`));
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (event.type === 'stall') {
|
|
1722
|
+
// Try LLM-powered triage first
|
|
1723
|
+
if (this.onStallDetected) {
|
|
1724
|
+
try {
|
|
1725
|
+
const triageResult = await this.onStallDetected(topicId, event.sessionName, event.messageText, event.injectedAt);
|
|
1726
|
+
if (triageResult.resolved)
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
catch (err) {
|
|
1730
|
+
console.warn(`[telegram] Triage nurse error:`, err);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
// Classify — check if it's a quota death
|
|
1734
|
+
let isQuotaDeath = false;
|
|
1735
|
+
if (this.onClassifySessionDeath) {
|
|
1736
|
+
try {
|
|
1737
|
+
const classification = await this.onClassifySessionDeath(event.sessionName);
|
|
1738
|
+
if (classification && classification.cause === 'quota_exhaustion') {
|
|
1739
|
+
isQuotaDeath = true;
|
|
1740
|
+
this.sendToTopic(topicId, `\ud83d\udd34 Session hit quota limit \u2014 "${event.sessionName}" can't respond.\n\n` +
|
|
1741
|
+
`${classification.detail}\n\n` +
|
|
1742
|
+
`Use /quota to check accounts, /switch-account to switch, or /login to authenticate a new account.`).catch(err => console.error(`[telegram] Quota stall alert failed: ${err}`));
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
catch { /* Classification failed — fall through */ }
|
|
1746
|
+
}
|
|
1747
|
+
if (!isQuotaDeath) {
|
|
1748
|
+
const shouldAlert = await this.confirmStallAlert({
|
|
1749
|
+
type: 'stall',
|
|
1750
|
+
sessionName: event.sessionName,
|
|
1751
|
+
messageText: event.messageText,
|
|
1752
|
+
minutesElapsed: event.minutesElapsed,
|
|
1753
|
+
sessionAlive: alive,
|
|
1754
|
+
});
|
|
1755
|
+
if (shouldAlert) {
|
|
1756
|
+
const status = alive ? 'running but not responding' : 'no longer running';
|
|
1757
|
+
this.sendToTopic(topicId, `\u26a0\ufe0f No response after ${event.minutesElapsed} minutes. Session "${event.sessionName}" is ${status}.\n\nMessage: "${event.messageText}..."${alive ? '\n\nTry /interrupt to unstick, or /restart to respawn.' : '\n\nSend another message to auto-respawn.'}`).catch(err => console.error(`[telegram] Stall alert failed: ${err}`));
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
else if (event.type === 'promise-expired') {
|
|
1762
|
+
// Try triage nurse first
|
|
1763
|
+
if (this.onStallDetected) {
|
|
1764
|
+
try {
|
|
1765
|
+
const triageResult = await this.onStallDetected(topicId, event.sessionName, `[promise expired] ${event.messageText}`, event.injectedAt);
|
|
1766
|
+
if (triageResult.resolved)
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
catch (err) {
|
|
1770
|
+
console.warn(`[telegram] Promise triage error:`, err);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
const shouldAlert = await this.confirmStallAlert({
|
|
1774
|
+
type: 'promise-expired',
|
|
1775
|
+
sessionName: event.sessionName,
|
|
1776
|
+
messageText: event.messageText,
|
|
1777
|
+
minutesElapsed: event.minutesElapsed,
|
|
1778
|
+
sessionAlive: alive,
|
|
1779
|
+
});
|
|
1780
|
+
if (shouldAlert) {
|
|
1781
|
+
if (!alive) {
|
|
1782
|
+
await this.sendToTopic(topicId, `The session stopped unexpectedly after saying "${event.messageText}". Sending a new message will auto-spawn a fresh session.`).catch(() => { });
|
|
1783
|
+
}
|
|
1784
|
+
else {
|
|
1785
|
+
await this.sendToTopic(topicId, `It's been ${event.minutesElapsed} minutes since the session said "${event.messageText}" — checking on it now...`).catch(() => { });
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
// ── Health Status ────────────────────────────────────────
|
|
1791
|
+
getStatus() {
|
|
1792
|
+
const stallStatus = this.sharedStallDetector
|
|
1793
|
+
? this.sharedStallDetector.getStatus()
|
|
1794
|
+
: { pendingStalls: this.pendingMessages.size, pendingPromises: this.pendingPromises.size };
|
|
1795
|
+
return {
|
|
1796
|
+
started: this.polling,
|
|
1797
|
+
uptime: this.startedAt ? Date.now() - this.startedAt.getTime() : null,
|
|
1798
|
+
pendingStalls: stallStatus.pendingStalls,
|
|
1799
|
+
pendingPromises: stallStatus.pendingPromises,
|
|
1800
|
+
topicMappings: this.topicToSession.size,
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
// ── Voice Transcription ──────────────────────────────────
|
|
1804
|
+
/**
|
|
1805
|
+
* Download a file from Telegram by file_id.
|
|
1806
|
+
*/
|
|
1807
|
+
async downloadFile(fileId, destPath) {
|
|
1808
|
+
const maxRetries = 3;
|
|
1809
|
+
let lastError;
|
|
1810
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1811
|
+
try {
|
|
1812
|
+
const fileInfo = await this.apiCall('getFile', { file_id: fileId });
|
|
1813
|
+
const fileUrl = `https://api.telegram.org/file/bot${this.config.token}/${fileInfo.file_path}`;
|
|
1814
|
+
const controller = new AbortController();
|
|
1815
|
+
const timer = setTimeout(() => controller.abort(), 60_000);
|
|
1816
|
+
try {
|
|
1817
|
+
const response = await fetch(fileUrl, { signal: controller.signal });
|
|
1818
|
+
if (!response.ok)
|
|
1819
|
+
throw new Error(`Download failed: ${response.status}`);
|
|
1820
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
1821
|
+
fs.writeFileSync(destPath, buffer);
|
|
1822
|
+
return; // Success
|
|
1823
|
+
}
|
|
1824
|
+
finally {
|
|
1825
|
+
clearTimeout(timer);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
catch (err) {
|
|
1829
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
1830
|
+
if (attempt < maxRetries) {
|
|
1831
|
+
const delay = attempt * 1000;
|
|
1832
|
+
console.warn(`[telegram] File download attempt ${attempt}/${maxRetries} failed, retrying in ${delay}ms...`);
|
|
1833
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
throw lastError;
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Resolve voice transcription provider from config or environment.
|
|
1841
|
+
* Checks explicit config, then env vars, then auto-detects.
|
|
1842
|
+
*/
|
|
1843
|
+
resolveTranscriptionProvider() {
|
|
1844
|
+
const providers = {
|
|
1845
|
+
groq: {
|
|
1846
|
+
envKey: 'GROQ_API_KEY',
|
|
1847
|
+
baseUrl: 'https://api.groq.com/openai/v1',
|
|
1848
|
+
model: 'whisper-large-v3',
|
|
1849
|
+
},
|
|
1850
|
+
openai: {
|
|
1851
|
+
envKey: 'OPENAI_API_KEY',
|
|
1852
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
1853
|
+
model: 'whisper-1',
|
|
1854
|
+
},
|
|
1855
|
+
};
|
|
1856
|
+
// Check explicit config
|
|
1857
|
+
const explicit = this.config.voiceProvider?.toLowerCase();
|
|
1858
|
+
if (explicit && providers[explicit]) {
|
|
1859
|
+
const p = providers[explicit];
|
|
1860
|
+
const apiKey = process.env[p.envKey];
|
|
1861
|
+
if (!apiKey) {
|
|
1862
|
+
console.warn(`[telegram] ${p.envKey} not set — required for ${explicit} voice transcription`);
|
|
1863
|
+
return null;
|
|
1864
|
+
}
|
|
1865
|
+
return { apiKey, baseUrl: p.baseUrl, model: p.model };
|
|
1866
|
+
}
|
|
1867
|
+
// Auto-detect: try Groq first (cheaper), then OpenAI
|
|
1868
|
+
for (const [name, p] of Object.entries(providers)) {
|
|
1869
|
+
const apiKey = process.env[p.envKey];
|
|
1870
|
+
if (apiKey) {
|
|
1871
|
+
console.log(`[telegram] Auto-detected voice transcription provider: ${name}`);
|
|
1872
|
+
return { apiKey, baseUrl: p.baseUrl, model: p.model };
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return null;
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Transcribe a voice message using the configured provider.
|
|
1879
|
+
*/
|
|
1880
|
+
async transcribeVoice(filePath) {
|
|
1881
|
+
const provider = this.resolveTranscriptionProvider();
|
|
1882
|
+
if (!provider) {
|
|
1883
|
+
throw new Error('No voice transcription provider configured. Set GROQ_API_KEY or OPENAI_API_KEY.');
|
|
1884
|
+
}
|
|
1885
|
+
const formData = new FormData();
|
|
1886
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
1887
|
+
const blob = new Blob([fileBuffer], { type: 'audio/ogg' });
|
|
1888
|
+
formData.append('file', blob, path.basename(filePath));
|
|
1889
|
+
formData.append('model', provider.model);
|
|
1890
|
+
const controller = new AbortController();
|
|
1891
|
+
const timer = setTimeout(() => controller.abort(), 60_000);
|
|
1892
|
+
try {
|
|
1893
|
+
const response = await fetch(`${provider.baseUrl}/audio/transcriptions`, {
|
|
1894
|
+
method: 'POST',
|
|
1895
|
+
headers: { Authorization: `Bearer ${provider.apiKey}` },
|
|
1896
|
+
body: formData,
|
|
1897
|
+
signal: controller.signal,
|
|
1898
|
+
});
|
|
1899
|
+
if (!response.ok) {
|
|
1900
|
+
const errText = await response.text();
|
|
1901
|
+
throw new Error(`Transcription API error (${response.status}): ${errText}`);
|
|
1902
|
+
}
|
|
1903
|
+
const data = await response.json();
|
|
1904
|
+
return data.text;
|
|
1905
|
+
}
|
|
1906
|
+
finally {
|
|
1907
|
+
clearTimeout(timer);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
// ── Photo Handling ───────────────────────────────────────
|
|
1911
|
+
/**
|
|
1912
|
+
* Download a photo from Telegram and save it locally.
|
|
1913
|
+
* Returns the local file path.
|
|
1914
|
+
*/
|
|
1915
|
+
async downloadPhoto(fileId, messageId) {
|
|
1916
|
+
const photoDir = path.join(this.stateDir, 'telegram-images');
|
|
1917
|
+
fs.mkdirSync(photoDir, { recursive: true });
|
|
1918
|
+
const filename = `photo-${Date.now()}-${messageId}.jpg`;
|
|
1919
|
+
const filepath = path.join(photoDir, filename);
|
|
1920
|
+
await this.downloadFile(fileId, filepath);
|
|
1921
|
+
return filepath;
|
|
1922
|
+
}
|
|
1923
|
+
// ── Document Handling ───────────────────────────────────
|
|
1924
|
+
/**
|
|
1925
|
+
* Download a document from Telegram and save it locally.
|
|
1926
|
+
* Preserves the original filename when available.
|
|
1927
|
+
* Returns the local file path.
|
|
1928
|
+
*/
|
|
1929
|
+
async downloadDocument(fileId, messageId, originalName) {
|
|
1930
|
+
const docDir = path.join(this.stateDir, 'telegram-documents');
|
|
1931
|
+
fs.mkdirSync(docDir, { recursive: true });
|
|
1932
|
+
const ext = originalName ? path.extname(originalName) : '';
|
|
1933
|
+
const baseName = originalName
|
|
1934
|
+
? originalName.replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
1935
|
+
: `document-${messageId}${ext}`;
|
|
1936
|
+
const filename = `${Date.now()}-${baseName}`;
|
|
1937
|
+
const filepath = path.join(docDir, filename);
|
|
1938
|
+
await this.downloadFile(fileId, filepath);
|
|
1939
|
+
return filepath;
|
|
1940
|
+
}
|
|
1941
|
+
// ── Command Handling ─────────────────────────────────────
|
|
1942
|
+
/**
|
|
1943
|
+
* Process Telegram commands. Returns true if the message was a command.
|
|
1944
|
+
*/
|
|
1945
|
+
async handleCommand(text, topicId, userId) {
|
|
1946
|
+
// Phase 1a: Delegate to shared CommandRouter when flag is enabled
|
|
1947
|
+
if (this.sharedCommandRouter) {
|
|
1948
|
+
return this.sharedCommandRouter.route(text, topicId.toString(), userId.toString(), { telegramUserId: userId, topicId });
|
|
1949
|
+
}
|
|
1950
|
+
const cmd = text.trim().toLowerCase();
|
|
1951
|
+
// Attention topic commands — intercept before general commands
|
|
1952
|
+
if (this.isAttentionTopic(topicId)) {
|
|
1953
|
+
const handled = await this.handleAttentionCommand(topicId, text);
|
|
1954
|
+
if (handled)
|
|
1955
|
+
return true;
|
|
1956
|
+
}
|
|
1957
|
+
// /flush — flush all batched notifications immediately
|
|
1958
|
+
if (cmd === '/flush') {
|
|
1959
|
+
if (this.batcher && this.batcher.isEnabled()) {
|
|
1960
|
+
const flushed = await this.batcher.flushAll();
|
|
1961
|
+
if (flushed > 0) {
|
|
1962
|
+
await this.sendToTopic(topicId, `Flushed ${flushed} batched notification${flushed === 1 ? '' : 's'}.`).catch(() => { });
|
|
1963
|
+
}
|
|
1964
|
+
else {
|
|
1965
|
+
await this.sendToTopic(topicId, 'No batched notifications to flush.').catch(() => { });
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
else if (this.onFlushNotifications) {
|
|
1969
|
+
this.onFlushNotifications(topicId).catch(err => {
|
|
1970
|
+
console.error('[telegram] Flush notifications failed:', err);
|
|
1971
|
+
this.sendToTopic(topicId, 'Failed to flush notifications.').catch(() => { });
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
else {
|
|
1975
|
+
await this.sendToTopic(topicId, 'Notification batching is not enabled.').catch(() => { });
|
|
1976
|
+
}
|
|
1977
|
+
return true;
|
|
1978
|
+
}
|
|
1979
|
+
// /sessions — list all sessions with claim status
|
|
1980
|
+
if (cmd === '/sessions' || cmd.startsWith('/sessions ')) {
|
|
1981
|
+
const filterUnclaimed = cmd.includes('unclaimed');
|
|
1982
|
+
if (!this.onListSessions) {
|
|
1983
|
+
await this.sendToTopic(topicId, 'Session listing not available.').catch(() => { });
|
|
1984
|
+
return true;
|
|
1985
|
+
}
|
|
1986
|
+
const sessions = this.onListSessions();
|
|
1987
|
+
if (sessions.length === 0) {
|
|
1988
|
+
await this.sendToTopic(topicId, 'No sessions running.').catch(() => { });
|
|
1989
|
+
return true;
|
|
1990
|
+
}
|
|
1991
|
+
const lines = [];
|
|
1992
|
+
for (const s of sessions) {
|
|
1993
|
+
const linkedTopic = this.getTopicForSession(s.tmuxSession);
|
|
1994
|
+
const claimed = linkedTopic !== null;
|
|
1995
|
+
if (filterUnclaimed && claimed)
|
|
1996
|
+
continue;
|
|
1997
|
+
const status = s.alive ? '\u2705' : '\u274c';
|
|
1998
|
+
const claimTag = claimed ? ` (topic ${linkedTopic})` : ' \u{1f7e1} unclaimed';
|
|
1999
|
+
lines.push(`${status} ${s.name}${claimTag}`);
|
|
2000
|
+
}
|
|
2001
|
+
if (lines.length === 0) {
|
|
2002
|
+
await this.sendToTopic(topicId, filterUnclaimed ? 'No unclaimed sessions.' : 'No sessions.').catch(() => { });
|
|
2003
|
+
}
|
|
2004
|
+
else {
|
|
2005
|
+
await this.sendToTopic(topicId, lines.join('\n')).catch(() => { });
|
|
2006
|
+
}
|
|
2007
|
+
return true;
|
|
2008
|
+
}
|
|
2009
|
+
// /claim <session> — claim a session into this topic
|
|
2010
|
+
if (cmd.startsWith('/claim ')) {
|
|
2011
|
+
const sessionName = text.trim().slice(7).trim();
|
|
2012
|
+
if (!sessionName) {
|
|
2013
|
+
await this.sendToTopic(topicId, 'Please include a session name — e.g. /claim my-session').catch(() => { });
|
|
2014
|
+
return true;
|
|
2015
|
+
}
|
|
2016
|
+
// Check if already claimed
|
|
2017
|
+
const existingSession = this.getSessionForTopic(topicId);
|
|
2018
|
+
if (existingSession) {
|
|
2019
|
+
await this.sendToTopic(topicId, `This topic is already linked to "${existingSession}". Use /unlink first.`).catch(() => { });
|
|
2020
|
+
return true;
|
|
2021
|
+
}
|
|
2022
|
+
this.registerTopicSession(topicId, sessionName);
|
|
2023
|
+
await this.sendToTopic(topicId, `Claimed session "${sessionName}" into this topic.`).catch(() => { });
|
|
2024
|
+
return true;
|
|
2025
|
+
}
|
|
2026
|
+
// /link <session> — alias for /claim
|
|
2027
|
+
if (cmd.startsWith('/link ')) {
|
|
2028
|
+
const sessionName = text.trim().slice(6).trim();
|
|
2029
|
+
if (!sessionName) {
|
|
2030
|
+
await this.sendToTopic(topicId, 'Please include a session name — e.g. /link my-session').catch(() => { });
|
|
2031
|
+
return true;
|
|
2032
|
+
}
|
|
2033
|
+
const existingSession = this.getSessionForTopic(topicId);
|
|
2034
|
+
if (existingSession) {
|
|
2035
|
+
await this.sendToTopic(topicId, `This topic is already linked to "${existingSession}". Use /unlink first.`).catch(() => { });
|
|
2036
|
+
return true;
|
|
2037
|
+
}
|
|
2038
|
+
this.registerTopicSession(topicId, sessionName);
|
|
2039
|
+
await this.sendToTopic(topicId, `Linked session "${sessionName}" to this topic.`).catch(() => { });
|
|
2040
|
+
return true;
|
|
2041
|
+
}
|
|
2042
|
+
// /unlink — unlink session from this topic
|
|
2043
|
+
if (cmd === '/unlink') {
|
|
2044
|
+
const sessionName = this.getSessionForTopic(topicId);
|
|
2045
|
+
if (!sessionName) {
|
|
2046
|
+
await this.sendToTopic(topicId, 'No session linked to this topic.').catch(() => { });
|
|
2047
|
+
return true;
|
|
2048
|
+
}
|
|
2049
|
+
this.unregisterTopic(topicId);
|
|
2050
|
+
await this.sendToTopic(topicId, `Unlinked session "${sessionName}" from this topic.`).catch(() => { });
|
|
2051
|
+
return true;
|
|
2052
|
+
}
|
|
2053
|
+
// /interrupt — send Escape to unstick a stalled session
|
|
2054
|
+
if (cmd === '/interrupt') {
|
|
2055
|
+
const sessionName = this.getSessionForTopic(topicId);
|
|
2056
|
+
if (!sessionName) {
|
|
2057
|
+
await this.sendToTopic(topicId, 'No session linked to this topic.').catch(() => { });
|
|
2058
|
+
return true;
|
|
2059
|
+
}
|
|
2060
|
+
if (!this.onInterruptSession) {
|
|
2061
|
+
await this.sendToTopic(topicId, 'Interrupt not available (no handler registered).').catch(() => { });
|
|
2062
|
+
return true;
|
|
2063
|
+
}
|
|
2064
|
+
try {
|
|
2065
|
+
const success = await this.onInterruptSession(sessionName);
|
|
2066
|
+
// Clear stall tracking — user is actively intervening
|
|
2067
|
+
this.clearStallForTopic(topicId);
|
|
2068
|
+
if (success) {
|
|
2069
|
+
await this.sendToTopic(topicId, `Nudged "${sessionName}" \u2014 it should resume shortly.`).catch(() => { });
|
|
2070
|
+
}
|
|
2071
|
+
else {
|
|
2072
|
+
await this.sendToTopic(topicId, `Failed to interrupt "${sessionName}" \u2014 session may not exist.`).catch(() => { });
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
catch (err) {
|
|
2076
|
+
console.error(`[telegram] Interrupt failed:`, err);
|
|
2077
|
+
await this.sendToTopic(topicId, 'Couldn\'t interrupt the session. It may have already ended.').catch(() => { });
|
|
2078
|
+
}
|
|
2079
|
+
return true;
|
|
2080
|
+
}
|
|
2081
|
+
// /restart — kill and respawn the session for this topic
|
|
2082
|
+
if (cmd === '/restart') {
|
|
2083
|
+
const sessionName = this.getSessionForTopic(topicId);
|
|
2084
|
+
if (!sessionName) {
|
|
2085
|
+
await this.sendToTopic(topicId, 'No session linked to this topic.').catch(() => { });
|
|
2086
|
+
return true;
|
|
2087
|
+
}
|
|
2088
|
+
if (!this.onRestartSession) {
|
|
2089
|
+
await this.sendToTopic(topicId, 'Restart not available (no handler registered).').catch(() => { });
|
|
2090
|
+
return true;
|
|
2091
|
+
}
|
|
2092
|
+
// Clear stall tracking — user is actively intervening
|
|
2093
|
+
this.clearStallForTopic(topicId);
|
|
2094
|
+
await this.sendToTopic(topicId, `Restarting "${sessionName}"...`).catch(() => { });
|
|
2095
|
+
try {
|
|
2096
|
+
await this.onRestartSession(sessionName, topicId);
|
|
2097
|
+
await this.sendToTopic(topicId, 'Session restarted.').catch(() => { });
|
|
2098
|
+
}
|
|
2099
|
+
catch (err) {
|
|
2100
|
+
console.error(`[telegram] Restart failed:`, err);
|
|
2101
|
+
await this.sendToTopic(topicId, 'Restart didn\'t work. The session may need to be recreated — try sending a new message.').catch(() => { });
|
|
2102
|
+
}
|
|
2103
|
+
return true;
|
|
2104
|
+
}
|
|
2105
|
+
// /status — show Telegram adapter status
|
|
2106
|
+
if (cmd === '/status') {
|
|
2107
|
+
const s = this.getStatus();
|
|
2108
|
+
const lines = [
|
|
2109
|
+
`Telegram adapter: ${s.started ? '\u2705 running' : '\u274c stopped'}`,
|
|
2110
|
+
`Uptime: ${s.uptime ? Math.round(s.uptime / 60000) + 'm' : 'n/a'}`,
|
|
2111
|
+
`Topic mappings: ${s.topicMappings}`,
|
|
2112
|
+
`Pending stall alerts: ${s.pendingStalls}`,
|
|
2113
|
+
];
|
|
2114
|
+
await this.sendToTopic(topicId, lines.join('\n')).catch(() => { });
|
|
2115
|
+
return true;
|
|
2116
|
+
}
|
|
2117
|
+
// /triage — show triage status for this topic
|
|
2118
|
+
if (cmd === '/triage') {
|
|
2119
|
+
if (!this.onGetTriageStatus) {
|
|
2120
|
+
await this.sendToTopic(topicId, 'Triage system not available.').catch(() => { });
|
|
2121
|
+
return true;
|
|
2122
|
+
}
|
|
2123
|
+
const status = this.onGetTriageStatus(topicId);
|
|
2124
|
+
if (!status || !status.active) {
|
|
2125
|
+
await this.sendToTopic(topicId, '🔍 No active triage for this topic. Session appears to be operating normally.').catch(() => { });
|
|
2126
|
+
}
|
|
2127
|
+
else {
|
|
2128
|
+
const lines = [
|
|
2129
|
+
`🔍 Active triage for this topic:`,
|
|
2130
|
+
`Classification: ${status.classification || 'pending'}`,
|
|
2131
|
+
`Checks: ${status.checkCount}`,
|
|
2132
|
+
status.lastCheck ? `Last check: ${status.lastCheck}` : '',
|
|
2133
|
+
].filter(Boolean);
|
|
2134
|
+
await this.sendToTopic(topicId, lines.join('\n')).catch(() => { });
|
|
2135
|
+
}
|
|
2136
|
+
return true;
|
|
2137
|
+
}
|
|
2138
|
+
// /switch-account (or /sa) <target> — switch active Claude account
|
|
2139
|
+
const switchMatch = text.match(/^\/(?:switch[-_]?account|sa)\s+(.+)$/i);
|
|
2140
|
+
if (switchMatch) {
|
|
2141
|
+
const target = switchMatch[1].trim();
|
|
2142
|
+
if (this.onSwitchAccountRequest) {
|
|
2143
|
+
this.onSwitchAccountRequest(target, topicId).catch(err => {
|
|
2144
|
+
console.error('[telegram] Switch account failed:', err);
|
|
2145
|
+
this.sendToTopic(topicId, 'Account switch didn\'t work. Try again or use /quota to check status.').catch(() => { });
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
else {
|
|
2149
|
+
await this.sendToTopic(topicId, 'Account switching not available.').catch(() => { });
|
|
2150
|
+
}
|
|
2151
|
+
return true;
|
|
2152
|
+
}
|
|
2153
|
+
// /quota (or /q) — show multi-account quota summary
|
|
2154
|
+
if (cmd === '/quota' || cmd === '/q') {
|
|
2155
|
+
if (this.onQuotaStatusRequest) {
|
|
2156
|
+
this.onQuotaStatusRequest(topicId).catch(err => {
|
|
2157
|
+
console.error('[telegram] Quota status failed:', err);
|
|
2158
|
+
this.sendToTopic(topicId, 'Couldn\'t check quota right now. Try again in a moment.').catch(() => { });
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
else {
|
|
2162
|
+
await this.sendToTopic(topicId, 'Quota status not available.').catch(() => { });
|
|
2163
|
+
}
|
|
2164
|
+
return true;
|
|
2165
|
+
}
|
|
2166
|
+
// /login [email] — seamless OAuth login from Telegram
|
|
2167
|
+
const loginMatch = text.match(/^\/login(?:\s+(.+))?$/i);
|
|
2168
|
+
if (loginMatch) {
|
|
2169
|
+
const email = loginMatch[1]?.trim() || null;
|
|
2170
|
+
if (this.onLoginRequest) {
|
|
2171
|
+
this.onLoginRequest(email, topicId).catch(err => {
|
|
2172
|
+
// @silent-fallback-ok — login error, user notified
|
|
2173
|
+
console.error('[telegram] Login flow failed:', err);
|
|
2174
|
+
this.sendToTopic(topicId, 'Login didn\'t complete. Try again, or the auth service may be temporarily unavailable.').catch(() => { });
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
else {
|
|
2178
|
+
await this.sendToTopic(topicId, 'Login not available.').catch(() => { });
|
|
2179
|
+
}
|
|
2180
|
+
return true;
|
|
2181
|
+
}
|
|
2182
|
+
// /new is handled in server.ts onTopicMessage — it needs sessionManager
|
|
2183
|
+
// access to spawn a session in the new topic. Don't intercept it here.
|
|
2184
|
+
return false;
|
|
2185
|
+
}
|
|
2186
|
+
// ── Message Log ────────────────────────────────────────────
|
|
2187
|
+
/**
|
|
2188
|
+
* Search the message log with flexible filters.
|
|
2189
|
+
* Supports text query, topicId filter, date range, and pagination.
|
|
2190
|
+
*/
|
|
2191
|
+
searchLog(opts = {}) {
|
|
2192
|
+
if (!fs.existsSync(this.messageLogPath))
|
|
2193
|
+
return [];
|
|
2194
|
+
const limit = Math.min(opts.limit ?? 50, 500);
|
|
2195
|
+
const queryLower = opts.query?.toLowerCase();
|
|
2196
|
+
const sinceMs = opts.since?.getTime();
|
|
2197
|
+
const content = fs.readFileSync(this.messageLogPath, 'utf-8');
|
|
2198
|
+
const lines = content.split('\n').filter(Boolean);
|
|
2199
|
+
// Scan from end for efficiency (most queries want recent messages)
|
|
2200
|
+
const matches = [];
|
|
2201
|
+
for (let i = lines.length - 1; i >= 0 && matches.length < limit; i--) {
|
|
2202
|
+
try {
|
|
2203
|
+
const entry = JSON.parse(lines[i]);
|
|
2204
|
+
if (opts.topicId !== undefined && entry.topicId !== opts.topicId)
|
|
2205
|
+
continue;
|
|
2206
|
+
if (sinceMs && new Date(entry.timestamp).getTime() < sinceMs)
|
|
2207
|
+
continue;
|
|
2208
|
+
if (queryLower && !entry.text.toLowerCase().includes(queryLower))
|
|
2209
|
+
continue;
|
|
2210
|
+
matches.unshift(entry); // Maintain chronological order
|
|
2211
|
+
}
|
|
2212
|
+
catch { /* skip malformed */ }
|
|
2213
|
+
}
|
|
2214
|
+
return matches;
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Get message log statistics.
|
|
2218
|
+
*/
|
|
2219
|
+
getLogStats() {
|
|
2220
|
+
if (!fs.existsSync(this.messageLogPath)) {
|
|
2221
|
+
return { totalMessages: 0, logSizeBytes: 0, logPath: this.messageLogPath };
|
|
2222
|
+
}
|
|
2223
|
+
const stat = fs.statSync(this.messageLogPath);
|
|
2224
|
+
const content = fs.readFileSync(this.messageLogPath, 'utf-8');
|
|
2225
|
+
const lineCount = content.split('\n').filter(Boolean).length;
|
|
2226
|
+
return { totalMessages: lineCount, logSizeBytes: stat.size, logPath: this.messageLogPath };
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Get recent messages for a topic (for thread history on respawn).
|
|
2230
|
+
*/
|
|
2231
|
+
getTopicHistory(topicId, limit = 20) {
|
|
2232
|
+
if (!fs.existsSync(this.messageLogPath))
|
|
2233
|
+
return [];
|
|
2234
|
+
// Read the file to find matching entries.
|
|
2235
|
+
// Log rotation caps at 75,000 lines, so this is bounded.
|
|
2236
|
+
const content = fs.readFileSync(this.messageLogPath, 'utf-8');
|
|
2237
|
+
const lines = content.split('\n').filter(Boolean);
|
|
2238
|
+
// Scan from end to find matching entries (most recent first)
|
|
2239
|
+
const matching = [];
|
|
2240
|
+
for (let i = lines.length - 1; i >= 0 && matching.length < limit; i--) {
|
|
2241
|
+
try {
|
|
2242
|
+
const entry = JSON.parse(lines[i]);
|
|
2243
|
+
if (entry.topicId === topicId) {
|
|
2244
|
+
matching.unshift(entry); // Maintain chronological order
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
catch { /* skip malformed */ }
|
|
2248
|
+
}
|
|
2249
|
+
return matching;
|
|
2250
|
+
}
|
|
2251
|
+
appendToLog(entry) {
|
|
2252
|
+
// Phase 1b: Delegate to shared MessageLogger when flag is enabled
|
|
2253
|
+
if (this.sharedLogger) {
|
|
2254
|
+
this.sharedLogger.append({
|
|
2255
|
+
messageId: entry.messageId,
|
|
2256
|
+
channelId: entry.topicId,
|
|
2257
|
+
text: entry.text,
|
|
2258
|
+
fromUser: entry.fromUser,
|
|
2259
|
+
timestamp: entry.timestamp,
|
|
2260
|
+
sessionName: entry.sessionName,
|
|
2261
|
+
senderName: entry.senderName,
|
|
2262
|
+
senderUsername: entry.senderUsername,
|
|
2263
|
+
platformUserId: entry.telegramUserId,
|
|
2264
|
+
platform: 'telegram',
|
|
2265
|
+
});
|
|
2266
|
+
// Also notify the Telegram-specific callback for backward compatibility
|
|
2267
|
+
if (this.onMessageLogged) {
|
|
2268
|
+
try {
|
|
2269
|
+
this.onMessageLogged(entry);
|
|
2270
|
+
}
|
|
2271
|
+
catch (err) {
|
|
2272
|
+
DegradationReporter.getInstance().report({
|
|
2273
|
+
feature: 'TopicMemory.dualWrite',
|
|
2274
|
+
primary: 'SQLite dual-write of messages for search and summaries',
|
|
2275
|
+
fallback: 'Message only in JSONL log (no search, no summary updates)',
|
|
2276
|
+
reason: `onMessageLogged callback failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2277
|
+
impact: 'Message may be missing from topic search and context summaries.',
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
// Phase 1e: Emit to event bus
|
|
2282
|
+
if (this.eventBus) {
|
|
2283
|
+
this.eventBus.emit('message:logged', {
|
|
2284
|
+
messageId: entry.messageId,
|
|
2285
|
+
channelId: entry.topicId?.toString() ?? '',
|
|
2286
|
+
text: entry.text,
|
|
2287
|
+
fromUser: entry.fromUser,
|
|
2288
|
+
timestamp: entry.timestamp,
|
|
2289
|
+
sessionName: entry.sessionName,
|
|
2290
|
+
senderName: entry.senderName,
|
|
2291
|
+
senderUsername: entry.senderUsername,
|
|
2292
|
+
platformUserId: entry.telegramUserId?.toString(),
|
|
2293
|
+
}).catch(err => console.error(`[telegram] EventBus message:logged error: ${err}`));
|
|
2294
|
+
}
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
// Legacy path (flag disabled)
|
|
2298
|
+
try {
|
|
2299
|
+
fs.appendFileSync(this.messageLogPath, JSON.stringify(entry) + '\n');
|
|
2300
|
+
this.maybeRotateLog();
|
|
2301
|
+
}
|
|
2302
|
+
catch (err) {
|
|
2303
|
+
console.error(`[telegram] Failed to append to message log: ${err}`);
|
|
2304
|
+
DegradationReporter.getInstance().report({
|
|
2305
|
+
feature: 'Telegram.messageLog',
|
|
2306
|
+
primary: 'JSONL message log for conversation history and recovery',
|
|
2307
|
+
fallback: 'Message lost from persistent log (only in memory)',
|
|
2308
|
+
reason: `Failed to write message log: ${err instanceof Error ? err.message : String(err)}`,
|
|
2309
|
+
impact: 'Conversation history gap — message may be missing from JSONL backup.',
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
// Notify subscribers (TopicMemory for SQLite dual-write)
|
|
2313
|
+
if (this.onMessageLogged) {
|
|
2314
|
+
try {
|
|
2315
|
+
this.onMessageLogged(entry);
|
|
2316
|
+
}
|
|
2317
|
+
catch (err) {
|
|
2318
|
+
DegradationReporter.getInstance().report({
|
|
2319
|
+
feature: 'TopicMemory.dualWrite',
|
|
2320
|
+
primary: 'SQLite dual-write of messages for search and summaries',
|
|
2321
|
+
fallback: 'Message only in JSONL log (no search, no summary updates)',
|
|
2322
|
+
reason: `onMessageLogged callback failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2323
|
+
impact: 'Message may be missing from topic search and context summaries.',
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
// Phase 1e: Emit to event bus
|
|
2328
|
+
if (this.eventBus) {
|
|
2329
|
+
this.eventBus.emit('message:logged', {
|
|
2330
|
+
messageId: entry.messageId,
|
|
2331
|
+
channelId: entry.topicId?.toString() ?? '',
|
|
2332
|
+
text: entry.text,
|
|
2333
|
+
fromUser: entry.fromUser,
|
|
2334
|
+
timestamp: entry.timestamp,
|
|
2335
|
+
sessionName: entry.sessionName,
|
|
2336
|
+
senderName: entry.senderName,
|
|
2337
|
+
senderUsername: entry.senderUsername,
|
|
2338
|
+
platformUserId: entry.telegramUserId?.toString(),
|
|
2339
|
+
}).catch(err => console.error(`[telegram] EventBus message:logged error: ${err}`));
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
/** Keep only the last 75,000 lines when log exceeds 100,000 lines.
|
|
2343
|
+
* High limits because message history is core agent memory.
|
|
2344
|
+
* At ~200 bytes/line average, 100k lines ~ 20MB — fine for a dedicated machine. */
|
|
2345
|
+
maybeRotateLog() {
|
|
2346
|
+
try {
|
|
2347
|
+
const stat = fs.statSync(this.messageLogPath);
|
|
2348
|
+
// Only check rotation when file exceeds ~20MB
|
|
2349
|
+
if (stat.size < 20 * 1024 * 1024)
|
|
2350
|
+
return;
|
|
2351
|
+
const content = fs.readFileSync(this.messageLogPath, 'utf-8');
|
|
2352
|
+
const lines = content.split('\n').filter(Boolean);
|
|
2353
|
+
if (lines.length > 100_000) {
|
|
2354
|
+
const kept = lines.slice(-75_000);
|
|
2355
|
+
const tmpPath = `${this.messageLogPath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2356
|
+
try {
|
|
2357
|
+
fs.writeFileSync(tmpPath, kept.join('\n') + '\n');
|
|
2358
|
+
fs.renameSync(tmpPath, this.messageLogPath);
|
|
2359
|
+
}
|
|
2360
|
+
catch (rotateErr) {
|
|
2361
|
+
try {
|
|
2362
|
+
fs.unlinkSync(tmpPath);
|
|
2363
|
+
}
|
|
2364
|
+
catch { /* ignore */ }
|
|
2365
|
+
throw rotateErr;
|
|
2366
|
+
}
|
|
2367
|
+
console.log(`[telegram] Rotated message log: ${lines.length} -> ${kept.length} lines`);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
catch {
|
|
2371
|
+
// @silent-fallback-ok — log rotation non-critical
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
// ── Attention Queue ────────────────────────────────────────
|
|
2375
|
+
/**
|
|
2376
|
+
* Create an attention item and its Telegram topic.
|
|
2377
|
+
*/
|
|
2378
|
+
async createAttentionItem(item) {
|
|
2379
|
+
// Check for existing
|
|
2380
|
+
if (this.attentionItems.has(item.id)) {
|
|
2381
|
+
return this.attentionItems.get(item.id);
|
|
2382
|
+
}
|
|
2383
|
+
const now = new Date().toISOString();
|
|
2384
|
+
const attention = {
|
|
2385
|
+
...item,
|
|
2386
|
+
status: 'OPEN',
|
|
2387
|
+
createdAt: now,
|
|
2388
|
+
updatedAt: now,
|
|
2389
|
+
};
|
|
2390
|
+
// Create Telegram topic (uses the centralized method for forum detection)
|
|
2391
|
+
try {
|
|
2392
|
+
const emoji = PRIORITY_EMOJI[item.priority] || PRIORITY_EMOJI.NORMAL;
|
|
2393
|
+
const color = PRIORITY_COLOR[item.priority] || PRIORITY_COLOR.NORMAL;
|
|
2394
|
+
const topicTitle = `${emoji} ${item.title}`.slice(0, 128);
|
|
2395
|
+
const topic = await this.createForumTopic(topicTitle, color);
|
|
2396
|
+
const topicId = topic.topicId;
|
|
2397
|
+
attention.topicId = topicId;
|
|
2398
|
+
// Register mappings
|
|
2399
|
+
this.attentionItemToTopic.set(item.id, topicId);
|
|
2400
|
+
this.attentionTopicToItem.set(topicId, item.id);
|
|
2401
|
+
this.topicToName.set(topicId, item.title);
|
|
2402
|
+
// Registry already saved by createForumTopic
|
|
2403
|
+
// Post details as first message
|
|
2404
|
+
const detail = [
|
|
2405
|
+
`<b>${this.escapeHtml(item.category)}</b> | Priority: ${item.priority}`,
|
|
2406
|
+
``,
|
|
2407
|
+
this.escapeHtml(item.summary),
|
|
2408
|
+
item.description ? `\n${this.escapeHtml(item.description.slice(0, 1000))}` : '',
|
|
2409
|
+
item.sourceContext ? `\n<i>Source: ${this.escapeHtml(item.sourceContext)}</i>` : '',
|
|
2410
|
+
``,
|
|
2411
|
+
`Commands: /ack, /done, /wontdo, /reopen`,
|
|
2412
|
+
].filter(Boolean).join('\n');
|
|
2413
|
+
// Send as HTML by calling API directly
|
|
2414
|
+
const sendParams = {
|
|
2415
|
+
chat_id: this.config.chatId,
|
|
2416
|
+
text: detail,
|
|
2417
|
+
parse_mode: 'HTML',
|
|
2418
|
+
};
|
|
2419
|
+
if (!isGeneralTopic(topicId))
|
|
2420
|
+
sendParams.message_thread_id = topicId;
|
|
2421
|
+
await this.apiCall('sendMessage', sendParams);
|
|
2422
|
+
}
|
|
2423
|
+
catch (err) {
|
|
2424
|
+
console.error(`[telegram] Failed to create attention topic for "${item.title}": ${err}`);
|
|
2425
|
+
DegradationReporter.getInstance().report({
|
|
2426
|
+
feature: 'TelegramAdapter.createAttentionItem',
|
|
2427
|
+
primary: 'Send attention/escalation notification',
|
|
2428
|
+
fallback: 'Attention item never delivered',
|
|
2429
|
+
reason: `Why: ${err instanceof Error ? err.message : String(err)}`,
|
|
2430
|
+
impact: 'User not notified of important escalation',
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
this.attentionItems.set(item.id, attention);
|
|
2434
|
+
this.saveAttentionItems();
|
|
2435
|
+
return attention;
|
|
2436
|
+
}
|
|
2437
|
+
/**
|
|
2438
|
+
* Update attention item status. Called by /ack, /done, /wontdo, /reopen commands.
|
|
2439
|
+
*/
|
|
2440
|
+
async updateAttentionStatus(itemId, status) {
|
|
2441
|
+
const item = this.attentionItems.get(itemId);
|
|
2442
|
+
if (!item)
|
|
2443
|
+
return false;
|
|
2444
|
+
item.status = status;
|
|
2445
|
+
item.updatedAt = new Date().toISOString();
|
|
2446
|
+
this.saveAttentionItems();
|
|
2447
|
+
const topicId = this.attentionItemToTopic.get(itemId);
|
|
2448
|
+
if (topicId) {
|
|
2449
|
+
const labels = {
|
|
2450
|
+
'ACKNOWLEDGED': '\ud83d\udc40 Acknowledged',
|
|
2451
|
+
'IN_PROGRESS': '\ud83d\udd28 In Progress',
|
|
2452
|
+
'DONE': '\u2705 Done',
|
|
2453
|
+
'WONT_DO': '\u23ed Won\'t Do',
|
|
2454
|
+
'OPEN': '\ud83d\udccb Reopened',
|
|
2455
|
+
};
|
|
2456
|
+
await this.sendToTopic(topicId, `Status \u2192 ${labels[status] || status}`).catch(() => { });
|
|
2457
|
+
// Auto-close/reopen topic
|
|
2458
|
+
try {
|
|
2459
|
+
if (status === 'DONE' || status === 'WONT_DO') {
|
|
2460
|
+
await this.apiCall('closeForumTopic', { chat_id: this.config.chatId, message_thread_id: topicId });
|
|
2461
|
+
}
|
|
2462
|
+
else if (status === 'OPEN') {
|
|
2463
|
+
await this.apiCall('reopenForumTopic', { chat_id: this.config.chatId, message_thread_id: topicId });
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
catch { /* topic operations may fail if already in desired state */ }
|
|
2467
|
+
}
|
|
2468
|
+
// Fire callback for external integrations
|
|
2469
|
+
if (this.onAttentionStatusChange) {
|
|
2470
|
+
await this.onAttentionStatusChange(itemId, status).catch(err => {
|
|
2471
|
+
console.error(`[telegram] Attention status callback failed: ${err}`);
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
return true;
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Get all attention items, optionally filtered by status.
|
|
2478
|
+
*/
|
|
2479
|
+
getAttentionItems(status) {
|
|
2480
|
+
const items = Array.from(this.attentionItems.values());
|
|
2481
|
+
if (status)
|
|
2482
|
+
return items.filter(i => i.status === status);
|
|
2483
|
+
return items;
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Get a specific attention item.
|
|
2487
|
+
*/
|
|
2488
|
+
getAttentionItem(itemId) {
|
|
2489
|
+
return this.attentionItems.get(itemId);
|
|
2490
|
+
}
|
|
2491
|
+
/**
|
|
2492
|
+
* Check if a topic is an attention topic.
|
|
2493
|
+
*/
|
|
2494
|
+
isAttentionTopic(topicId) {
|
|
2495
|
+
return this.attentionTopicToItem.has(topicId);
|
|
2496
|
+
}
|
|
2497
|
+
/**
|
|
2498
|
+
* Handle commands in attention topics (/ack, /done, /wontdo, /reopen).
|
|
2499
|
+
* Returns true if handled, false if not an attention command.
|
|
2500
|
+
*/
|
|
2501
|
+
async handleAttentionCommand(topicId, text) {
|
|
2502
|
+
const itemId = this.attentionTopicToItem.get(topicId);
|
|
2503
|
+
if (!itemId)
|
|
2504
|
+
return false;
|
|
2505
|
+
const cmd = text.trim().toLowerCase();
|
|
2506
|
+
const statusMap = {
|
|
2507
|
+
'/ack': 'ACKNOWLEDGED',
|
|
2508
|
+
'/acknowledge': 'ACKNOWLEDGED',
|
|
2509
|
+
'/done': 'DONE',
|
|
2510
|
+
'/wontdo': 'WONT_DO',
|
|
2511
|
+
'/reopen': 'OPEN',
|
|
2512
|
+
};
|
|
2513
|
+
if (cmd in statusMap) {
|
|
2514
|
+
await this.updateAttentionStatus(itemId, statusMap[cmd]);
|
|
2515
|
+
return true;
|
|
2516
|
+
}
|
|
2517
|
+
return false;
|
|
2518
|
+
}
|
|
2519
|
+
loadAttentionItems() {
|
|
2520
|
+
try {
|
|
2521
|
+
if (!fs.existsSync(this.attentionFilePath))
|
|
2522
|
+
return;
|
|
2523
|
+
const data = JSON.parse(fs.readFileSync(this.attentionFilePath, 'utf-8'));
|
|
2524
|
+
if (data.items) {
|
|
2525
|
+
for (const item of data.items) {
|
|
2526
|
+
this.attentionItems.set(item.id, item);
|
|
2527
|
+
if (item.topicId) {
|
|
2528
|
+
this.attentionItemToTopic.set(item.id, item.topicId);
|
|
2529
|
+
this.attentionTopicToItem.set(item.topicId, item.id);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
console.log(`[telegram] Loaded ${this.attentionItems.size} attention items`);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
catch { /* file doesn't exist yet */ }
|
|
2536
|
+
}
|
|
2537
|
+
saveAttentionItems() {
|
|
2538
|
+
try {
|
|
2539
|
+
const dir = path.dirname(this.attentionFilePath);
|
|
2540
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2541
|
+
const data = { items: Array.from(this.attentionItems.values()) };
|
|
2542
|
+
const tmpPath = `${this.attentionFilePath}.${process.pid}.tmp`;
|
|
2543
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
2544
|
+
fs.renameSync(tmpPath, this.attentionFilePath);
|
|
2545
|
+
}
|
|
2546
|
+
catch (err) {
|
|
2547
|
+
console.error(`[telegram] Failed to save attention items: ${err}`);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
escapeHtml(text) {
|
|
2551
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
2552
|
+
}
|
|
2553
|
+
// ── Registry Persistence ───────────────────────────────────
|
|
2554
|
+
loadRegistry() {
|
|
2555
|
+
try {
|
|
2556
|
+
const data = JSON.parse(fs.readFileSync(this.registryPath, 'utf-8'));
|
|
2557
|
+
if (data.topicToSession) {
|
|
2558
|
+
for (const [k, v] of Object.entries(data.topicToSession)) {
|
|
2559
|
+
this.topicToSession.set(Number(k), v);
|
|
2560
|
+
this.sessionToTopic.set(v, Number(k));
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
if (data.topicToName) {
|
|
2564
|
+
for (const [k, v] of Object.entries(data.topicToName)) {
|
|
2565
|
+
this.topicToName.set(Number(k), v);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
if (data.topicToPurpose) {
|
|
2569
|
+
for (const [k, v] of Object.entries(data.topicToPurpose)) {
|
|
2570
|
+
this.topicToPurpose.set(Number(k), v);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
console.log(`[telegram] Loaded ${this.topicToSession.size} topic-session mappings from disk`);
|
|
2574
|
+
}
|
|
2575
|
+
catch {
|
|
2576
|
+
// File doesn't exist yet — start fresh
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
saveRegistry() {
|
|
2580
|
+
try {
|
|
2581
|
+
const data = {
|
|
2582
|
+
topicToSession: Object.fromEntries(this.topicToSession),
|
|
2583
|
+
topicToName: Object.fromEntries(this.topicToName),
|
|
2584
|
+
topicToPurpose: Object.fromEntries(this.topicToPurpose),
|
|
2585
|
+
};
|
|
2586
|
+
// Atomic write: unique temp filename to prevent concurrent corruption
|
|
2587
|
+
const tmpPath = this.registryPath + `.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2588
|
+
try {
|
|
2589
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
2590
|
+
fs.renameSync(tmpPath, this.registryPath);
|
|
2591
|
+
}
|
|
2592
|
+
catch (writeErr) {
|
|
2593
|
+
try {
|
|
2594
|
+
fs.unlinkSync(tmpPath);
|
|
2595
|
+
}
|
|
2596
|
+
catch { /* ignore */ }
|
|
2597
|
+
throw writeErr;
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
catch (err) {
|
|
2601
|
+
console.error(`[telegram] Failed to save registry: ${err}`);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
// ── Polling Offset Persistence ────────────────────────────
|
|
2605
|
+
loadOffset() {
|
|
2606
|
+
try {
|
|
2607
|
+
const raw = fs.readFileSync(this.offsetPath, 'utf-8');
|
|
2608
|
+
const data = JSON.parse(raw);
|
|
2609
|
+
// Support both 'lastUpdateId' (canonical) and 'offset' (legacy/external)
|
|
2610
|
+
const candidate = data.lastUpdateId ?? data.offset;
|
|
2611
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) {
|
|
2612
|
+
this.lastUpdateId = candidate;
|
|
2613
|
+
console.log(`[telegram] Restored poll offset: ${this.lastUpdateId}`);
|
|
2614
|
+
}
|
|
2615
|
+
else if (data.lastUpdateId !== undefined || data.offset !== undefined) {
|
|
2616
|
+
console.warn(`[telegram] Poll offset file has invalid value: ${raw.trim().substring(0, 100)}. Starting from 0.`);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
catch (err) {
|
|
2620
|
+
// Distinguish missing file from corrupted file
|
|
2621
|
+
if (err.code !== 'ENOENT') {
|
|
2622
|
+
console.warn(`[telegram] Poll offset file corrupted, starting from 0: ${err}`);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
saveOffset() {
|
|
2627
|
+
try {
|
|
2628
|
+
const tmpPath = `${this.offsetPath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2629
|
+
try {
|
|
2630
|
+
fs.writeFileSync(tmpPath, JSON.stringify({ lastUpdateId: this.lastUpdateId }));
|
|
2631
|
+
fs.renameSync(tmpPath, this.offsetPath);
|
|
2632
|
+
}
|
|
2633
|
+
catch (writeErr) {
|
|
2634
|
+
try {
|
|
2635
|
+
fs.unlinkSync(tmpPath);
|
|
2636
|
+
}
|
|
2637
|
+
catch { /* ignore */ }
|
|
2638
|
+
throw writeErr;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
catch (err) {
|
|
2642
|
+
console.error(`[telegram] Failed to save poll offset: ${err}`);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
// ── Polling ────────────────────────────────────────────────
|
|
2646
|
+
async poll() {
|
|
2647
|
+
if (!this.polling)
|
|
2648
|
+
return;
|
|
2649
|
+
try {
|
|
2650
|
+
const updates = await this.getUpdates();
|
|
2651
|
+
this.consecutivePollErrors = 0; // Reset on success
|
|
2652
|
+
// Offset range sanity check: if received update_ids are significantly lower
|
|
2653
|
+
// than our stored offset, the offset is likely from a different bot token or
|
|
2654
|
+
// was corrupted during a migration. Auto-correct to prevent infinite replay.
|
|
2655
|
+
if (updates.length > 0 && this.lastUpdateId > 0) {
|
|
2656
|
+
const maxReceivedId = Math.max(...updates.map(u => u.update_id));
|
|
2657
|
+
const OFFSET_RANGE_THRESHOLD = 10_000_000; // 10M delta = cross-token corruption
|
|
2658
|
+
if (maxReceivedId < this.lastUpdateId - OFFSET_RANGE_THRESHOLD) {
|
|
2659
|
+
console.warn(`[telegram] Offset range mismatch: stored=${this.lastUpdateId}, ` +
|
|
2660
|
+
`received max=${maxReceivedId} (delta=${this.lastUpdateId - maxReceivedId}). ` +
|
|
2661
|
+
`Auto-correcting offset to prevent infinite replay loop.`);
|
|
2662
|
+
this.lastUpdateId = maxReceivedId;
|
|
2663
|
+
this.saveOffset();
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
for (const update of updates) {
|
|
2667
|
+
await this.processUpdate(update);
|
|
2668
|
+
this.lastUpdateId = Math.max(this.lastUpdateId, update.update_id);
|
|
2669
|
+
// Save offset after each update so a crash mid-batch doesn't re-deliver
|
|
2670
|
+
// messages that were already processed (mirrors TelegramLifeline fix).
|
|
2671
|
+
this.saveOffset();
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
catch (err) {
|
|
2675
|
+
this.consecutivePollErrors++;
|
|
2676
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2677
|
+
// Check for fatal errors that require restart
|
|
2678
|
+
if (errMsg.includes('401') || errMsg.includes('Unauthorized')) {
|
|
2679
|
+
console.error(`[telegram] FATAL: Bot token is invalid. Stopping polling.`);
|
|
2680
|
+
this.polling = false;
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
// Exponential backoff on consecutive errors
|
|
2684
|
+
if (this.consecutivePollErrors > 1) {
|
|
2685
|
+
const backoffMs = Math.min(1000 * Math.pow(2, this.consecutivePollErrors - 1), 60_000);
|
|
2686
|
+
console.error(`[telegram] Poll error (attempt ${this.consecutivePollErrors}), backing off ${backoffMs}ms: ${errMsg}`);
|
|
2687
|
+
await new Promise(r => setTimeout(r, backoffMs));
|
|
2688
|
+
}
|
|
2689
|
+
else {
|
|
2690
|
+
console.error(`[telegram] Poll error: ${errMsg}`);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
// Schedule next poll
|
|
2694
|
+
const interval = this.config.pollIntervalMs ?? 2000;
|
|
2695
|
+
this.pollTimeout = setTimeout(() => this.poll(), interval);
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Process a single Telegram update (text, voice, photo, or callback query).
|
|
2699
|
+
*/
|
|
2700
|
+
async processUpdate(update) {
|
|
2701
|
+
// Handle callback queries from inline keyboard buttons (Prompt Gate)
|
|
2702
|
+
if (update.callback_query) {
|
|
2703
|
+
await this.processCallbackQuery(update.callback_query);
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
const msg = update.message;
|
|
2707
|
+
if (!msg)
|
|
2708
|
+
return;
|
|
2709
|
+
// Auth gating — handle messages from unauthorized/unknown users
|
|
2710
|
+
if (!this.isAuthorized(msg.from.id)) {
|
|
2711
|
+
await this.handleUnknownUser(msg.from.id, msg.from.first_name, msg.from.username, msg.text);
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
const numericTopicId = msg.message_thread_id ?? GENERAL_TOPIC_ID;
|
|
2715
|
+
const topicId = numericTopicId.toString();
|
|
2716
|
+
// Auto-capture topic name from reply_to_message
|
|
2717
|
+
if (msg.reply_to_message?.forum_topic_created?.name) {
|
|
2718
|
+
const currentName = this.topicToName.get(numericTopicId);
|
|
2719
|
+
const realName = msg.reply_to_message.forum_topic_created.name;
|
|
2720
|
+
if (!currentName || /^topic-\d+$/.test(currentName)) {
|
|
2721
|
+
this.topicToName.set(numericTopicId, realName);
|
|
2722
|
+
this.saveRegistry();
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
// Handle voice messages
|
|
2726
|
+
if (msg.voice) {
|
|
2727
|
+
await this.handleVoiceMessage(msg, numericTopicId);
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
// Handle photo messages
|
|
2731
|
+
if (msg.photo && msg.photo.length > 0) {
|
|
2732
|
+
await this.handlePhotoMessage(msg, numericTopicId);
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
// Handle document/file messages
|
|
2736
|
+
if (msg.document) {
|
|
2737
|
+
await this.handleDocumentMessage(msg, numericTopicId);
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
// Handle text messages
|
|
2741
|
+
if (!msg.text)
|
|
2742
|
+
return;
|
|
2743
|
+
const text = msg.text;
|
|
2744
|
+
// Check for commands first
|
|
2745
|
+
if (text.startsWith('/')) {
|
|
2746
|
+
const handled = await this.handleCommand(text, numericTopicId, msg.from.id);
|
|
2747
|
+
if (handled)
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
const message = {
|
|
2751
|
+
id: `tg-${msg.message_id}`,
|
|
2752
|
+
userId: msg.from.id.toString(),
|
|
2753
|
+
content: text,
|
|
2754
|
+
channel: { type: 'telegram', identifier: topicId },
|
|
2755
|
+
receivedAt: new Date(msg.date * 1000).toISOString(),
|
|
2756
|
+
metadata: {
|
|
2757
|
+
telegramUserId: msg.from.id,
|
|
2758
|
+
username: msg.from.username,
|
|
2759
|
+
firstName: msg.from.first_name,
|
|
2760
|
+
messageThreadId: numericTopicId,
|
|
2761
|
+
},
|
|
2762
|
+
};
|
|
2763
|
+
// Log the message (including sender identity for multi-user topics)
|
|
2764
|
+
this.appendToLog({
|
|
2765
|
+
messageId: msg.message_id,
|
|
2766
|
+
topicId: numericTopicId,
|
|
2767
|
+
text,
|
|
2768
|
+
fromUser: true,
|
|
2769
|
+
timestamp: new Date(msg.date * 1000).toISOString(),
|
|
2770
|
+
sessionName: this.topicToSession.get(numericTopicId) ?? null,
|
|
2771
|
+
senderName: msg.from.first_name,
|
|
2772
|
+
senderUsername: msg.from.username,
|
|
2773
|
+
telegramUserId: msg.from.id,
|
|
2774
|
+
});
|
|
2775
|
+
// Sentinel intercept — fires BEFORE routing to detect emergency stop/pause.
|
|
2776
|
+
// This runs in the server process, separate from the session, so it can
|
|
2777
|
+
// kill/pause the session even when the session is mid-tool-call.
|
|
2778
|
+
if (this.onSentinelIntercept) {
|
|
2779
|
+
try {
|
|
2780
|
+
const classification = await this.onSentinelIntercept(text, numericTopicId);
|
|
2781
|
+
if (classification && (classification.category === 'emergency-stop' || classification.category === 'pause')) {
|
|
2782
|
+
const sessionName = this.topicToSession.get(numericTopicId);
|
|
2783
|
+
if (classification.category === 'emergency-stop' && sessionName) {
|
|
2784
|
+
console.log(`[sentinel] Emergency stop for session "${sessionName}" in topic ${numericTopicId}`);
|
|
2785
|
+
if (this.onSentinelKillSession) {
|
|
2786
|
+
this.onSentinelKillSession(sessionName);
|
|
2787
|
+
}
|
|
2788
|
+
// Never include raw sentinel reasons in user-facing messages.
|
|
2789
|
+
// Log the full reason server-side, show only clean messages to users.
|
|
2790
|
+
if (classification.reason) {
|
|
2791
|
+
console.log(`[sentinel] Stop reason: ${classification.reason}`);
|
|
2792
|
+
}
|
|
2793
|
+
await this.sendToTopic(numericTopicId, `Session terminated.\n\nSend a new message to start a fresh session.`).catch(() => { });
|
|
2794
|
+
}
|
|
2795
|
+
else if (classification.category === 'pause' && sessionName) {
|
|
2796
|
+
console.log(`[sentinel] Pause for session "${sessionName}" in topic ${numericTopicId}`);
|
|
2797
|
+
if (this.onSentinelPauseSession) {
|
|
2798
|
+
this.onSentinelPauseSession(sessionName);
|
|
2799
|
+
}
|
|
2800
|
+
// Never include raw sentinel reasons in user-facing messages.
|
|
2801
|
+
if (classification.reason) {
|
|
2802
|
+
console.log(`[sentinel] Pause reason: ${classification.reason}`);
|
|
2803
|
+
}
|
|
2804
|
+
await this.sendToTopic(numericTopicId, `Session paused.\n\nSend a message to resume.`).catch(() => { });
|
|
2805
|
+
}
|
|
2806
|
+
else if (!sessionName) {
|
|
2807
|
+
// No active session — just acknowledge the stop/pause signal
|
|
2808
|
+
await this.sendToTopic(numericTopicId, `No active session to ${classification.category === 'emergency-stop' ? 'stop' : 'pause'}.`).catch(() => { });
|
|
2809
|
+
}
|
|
2810
|
+
return; // Don't route to session — sentinel handled it
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
catch (err) {
|
|
2814
|
+
console.error(`[sentinel] Intercept error: ${err}`);
|
|
2815
|
+
// On sentinel error, fall through to normal routing (fail-open for message delivery)
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
// Prompt Gate — intercept replies to relay messages (text-input prompts)
|
|
2819
|
+
if (this.pendingPromptReply.has(numericTopicId) && text) {
|
|
2820
|
+
const handled = this.handlePendingPromptReply(numericTopicId, msg);
|
|
2821
|
+
if (handled)
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
// Fire topic message callback (always fires — General topic falls back to ID 1)
|
|
2825
|
+
if (this.onTopicMessage) {
|
|
2826
|
+
try {
|
|
2827
|
+
this.onTopicMessage(message);
|
|
2828
|
+
}
|
|
2829
|
+
catch (err) {
|
|
2830
|
+
console.error(`[telegram] Topic message handler error: ${err}`);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
// Fire general handler
|
|
2834
|
+
if (this.handler) {
|
|
2835
|
+
try {
|
|
2836
|
+
await this.handler(message);
|
|
2837
|
+
}
|
|
2838
|
+
catch (err) {
|
|
2839
|
+
console.error(`[telegram] Handler error: ${err}`);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
/**
|
|
2844
|
+
* Handle an incoming voice message: download, transcribe, route as text.
|
|
2845
|
+
*/
|
|
2846
|
+
async handleVoiceMessage(msg, topicId) {
|
|
2847
|
+
const voice = msg.voice;
|
|
2848
|
+
// Download the voice file
|
|
2849
|
+
const voiceDir = path.join(this.stateDir, 'telegram-voice');
|
|
2850
|
+
fs.mkdirSync(voiceDir, { recursive: true });
|
|
2851
|
+
const filename = `voice-${Date.now()}-${msg.message_id}.ogg`;
|
|
2852
|
+
const filepath = path.join(voiceDir, filename);
|
|
2853
|
+
try {
|
|
2854
|
+
await this.downloadFile(voice.file_id, filepath);
|
|
2855
|
+
}
|
|
2856
|
+
catch (err) {
|
|
2857
|
+
console.error(`[telegram] Failed to download voice: ${err}`);
|
|
2858
|
+
await this.sendToTopic(topicId, `(Voice message received but download failed)`).catch(() => { });
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
// Transcribe
|
|
2862
|
+
try {
|
|
2863
|
+
const transcript = await this.transcribeVoice(filepath);
|
|
2864
|
+
console.log(`[telegram] Transcribed voice (${voice.duration}s): "${transcript.slice(0, 80)}"`);
|
|
2865
|
+
// Create a message with the transcription
|
|
2866
|
+
const message = {
|
|
2867
|
+
id: `tg-${msg.message_id}`,
|
|
2868
|
+
userId: msg.from.id.toString(),
|
|
2869
|
+
content: `[voice] ${transcript}`,
|
|
2870
|
+
channel: { type: 'telegram', identifier: topicId.toString() },
|
|
2871
|
+
receivedAt: new Date(msg.date * 1000).toISOString(),
|
|
2872
|
+
metadata: {
|
|
2873
|
+
telegramUserId: msg.from.id,
|
|
2874
|
+
username: msg.from.username,
|
|
2875
|
+
firstName: msg.from.first_name,
|
|
2876
|
+
messageThreadId: topicId,
|
|
2877
|
+
voiceFile: filepath,
|
|
2878
|
+
voiceDuration: voice.duration,
|
|
2879
|
+
},
|
|
2880
|
+
};
|
|
2881
|
+
// Log it (including sender identity for multi-user topics)
|
|
2882
|
+
this.appendToLog({
|
|
2883
|
+
messageId: msg.message_id,
|
|
2884
|
+
topicId,
|
|
2885
|
+
text: `[voice] ${transcript}`,
|
|
2886
|
+
fromUser: true,
|
|
2887
|
+
timestamp: new Date(msg.date * 1000).toISOString(),
|
|
2888
|
+
sessionName: this.topicToSession.get(topicId) ?? null,
|
|
2889
|
+
senderName: msg.from.first_name,
|
|
2890
|
+
senderUsername: msg.from.username,
|
|
2891
|
+
telegramUserId: msg.from.id,
|
|
2892
|
+
});
|
|
2893
|
+
// Fire callbacks
|
|
2894
|
+
if (this.onTopicMessage) {
|
|
2895
|
+
try {
|
|
2896
|
+
this.onTopicMessage(message);
|
|
2897
|
+
}
|
|
2898
|
+
catch (err) {
|
|
2899
|
+
console.error(`[telegram] Topic message handler error: ${err}`);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
if (this.handler) {
|
|
2903
|
+
try {
|
|
2904
|
+
await this.handler(message);
|
|
2905
|
+
}
|
|
2906
|
+
catch (err) {
|
|
2907
|
+
console.error(`[telegram] Handler error: ${err}`);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
catch (err) {
|
|
2912
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2913
|
+
const isNotConfigured = errMsg.includes('No voice transcription provider configured');
|
|
2914
|
+
const replyText = isNotConfigured
|
|
2915
|
+
? '\ud83c\udfa4 Voice transcription is not configured. To enable it, set GROQ_API_KEY or OPENAI_API_KEY in your environment.'
|
|
2916
|
+
: `(Voice message received but transcription failed: ${errMsg})`;
|
|
2917
|
+
await this.sendToTopic(topicId, replyText).catch(() => { });
|
|
2918
|
+
}
|
|
2919
|
+
finally {
|
|
2920
|
+
// Clean up voice file after processing
|
|
2921
|
+
try {
|
|
2922
|
+
fs.unlinkSync(filepath);
|
|
2923
|
+
}
|
|
2924
|
+
catch { /* ignore */ }
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
/**
|
|
2928
|
+
* Handle an incoming photo message: download, save, route with path.
|
|
2929
|
+
*/
|
|
2930
|
+
async handlePhotoMessage(msg, topicId) {
|
|
2931
|
+
const photos = msg.photo;
|
|
2932
|
+
// Get highest resolution (last in array)
|
|
2933
|
+
const photo = photos[photos.length - 1];
|
|
2934
|
+
const caption = msg.caption || '';
|
|
2935
|
+
try {
|
|
2936
|
+
const filepath = await this.downloadPhoto(photo.file_id, msg.message_id);
|
|
2937
|
+
console.log(`[telegram] Downloaded photo: ${filepath}`);
|
|
2938
|
+
const content = caption
|
|
2939
|
+
? `[image:${filepath}] ${caption}`
|
|
2940
|
+
: `[image:${filepath}]`;
|
|
2941
|
+
const message = {
|
|
2942
|
+
id: `tg-${msg.message_id}`,
|
|
2943
|
+
userId: msg.from.id.toString(),
|
|
2944
|
+
content,
|
|
2945
|
+
channel: { type: 'telegram', identifier: topicId.toString() },
|
|
2946
|
+
receivedAt: new Date(msg.date * 1000).toISOString(),
|
|
2947
|
+
metadata: {
|
|
2948
|
+
telegramUserId: msg.from.id,
|
|
2949
|
+
username: msg.from.username,
|
|
2950
|
+
firstName: msg.from.first_name,
|
|
2951
|
+
messageThreadId: topicId,
|
|
2952
|
+
photoPath: filepath,
|
|
2953
|
+
},
|
|
2954
|
+
};
|
|
2955
|
+
// Log it (including sender identity for multi-user topics)
|
|
2956
|
+
this.appendToLog({
|
|
2957
|
+
messageId: msg.message_id,
|
|
2958
|
+
topicId,
|
|
2959
|
+
text: content,
|
|
2960
|
+
fromUser: true,
|
|
2961
|
+
timestamp: new Date(msg.date * 1000).toISOString(),
|
|
2962
|
+
sessionName: this.topicToSession.get(topicId) ?? null,
|
|
2963
|
+
senderName: msg.from.first_name,
|
|
2964
|
+
senderUsername: msg.from.username,
|
|
2965
|
+
telegramUserId: msg.from.id,
|
|
2966
|
+
});
|
|
2967
|
+
// Fire callbacks
|
|
2968
|
+
if (this.onTopicMessage) {
|
|
2969
|
+
try {
|
|
2970
|
+
this.onTopicMessage(message);
|
|
2971
|
+
}
|
|
2972
|
+
catch (err) {
|
|
2973
|
+
console.error(`[telegram] Topic message handler error: ${err}`);
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
if (this.handler) {
|
|
2977
|
+
try {
|
|
2978
|
+
await this.handler(message);
|
|
2979
|
+
}
|
|
2980
|
+
catch (err) {
|
|
2981
|
+
console.error(`[telegram] Handler error: ${err}`);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
catch (err) {
|
|
2986
|
+
console.error(`[telegram] Failed to download photo: ${err}`);
|
|
2987
|
+
await this.sendToTopic(topicId, '(Photo received but I couldn\'t process it. Try sending it again.)').catch(() => { });
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
/**
|
|
2991
|
+
* Handle an incoming document message: download, save, route with path.
|
|
2992
|
+
*/
|
|
2993
|
+
async handleDocumentMessage(msg, topicId) {
|
|
2994
|
+
const doc = msg.document;
|
|
2995
|
+
const caption = msg.caption || '';
|
|
2996
|
+
try {
|
|
2997
|
+
const filepath = await this.downloadDocument(doc.file_id, msg.message_id, doc.file_name);
|
|
2998
|
+
console.log(`[telegram] Downloaded document: ${filepath}`);
|
|
2999
|
+
const content = caption
|
|
3000
|
+
? `[document:${filepath}] ${caption}`
|
|
3001
|
+
: `[document:${filepath}]`;
|
|
3002
|
+
const message = {
|
|
3003
|
+
id: `tg-${msg.message_id}`,
|
|
3004
|
+
userId: msg.from.id.toString(),
|
|
3005
|
+
content,
|
|
3006
|
+
channel: { type: 'telegram', identifier: topicId.toString() },
|
|
3007
|
+
receivedAt: new Date(msg.date * 1000).toISOString(),
|
|
3008
|
+
metadata: {
|
|
3009
|
+
telegramUserId: msg.from.id,
|
|
3010
|
+
username: msg.from.username,
|
|
3011
|
+
firstName: msg.from.first_name,
|
|
3012
|
+
messageThreadId: topicId,
|
|
3013
|
+
documentPath: filepath,
|
|
3014
|
+
documentName: doc.file_name,
|
|
3015
|
+
documentMimeType: doc.mime_type,
|
|
3016
|
+
},
|
|
3017
|
+
};
|
|
3018
|
+
// Log it
|
|
3019
|
+
this.appendToLog({
|
|
3020
|
+
messageId: msg.message_id,
|
|
3021
|
+
topicId,
|
|
3022
|
+
text: content,
|
|
3023
|
+
fromUser: true,
|
|
3024
|
+
timestamp: new Date(msg.date * 1000).toISOString(),
|
|
3025
|
+
sessionName: this.topicToSession.get(topicId) ?? null,
|
|
3026
|
+
senderName: msg.from.first_name,
|
|
3027
|
+
senderUsername: msg.from.username,
|
|
3028
|
+
telegramUserId: msg.from.id,
|
|
3029
|
+
});
|
|
3030
|
+
// Fire callbacks
|
|
3031
|
+
if (this.onTopicMessage) {
|
|
3032
|
+
try {
|
|
3033
|
+
this.onTopicMessage(message);
|
|
3034
|
+
}
|
|
3035
|
+
catch (err) {
|
|
3036
|
+
console.error(`[telegram] Topic message handler error: ${err}`);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
if (this.handler) {
|
|
3040
|
+
try {
|
|
3041
|
+
await this.handler(message);
|
|
3042
|
+
}
|
|
3043
|
+
catch (err) {
|
|
3044
|
+
console.error(`[telegram] Handler error: ${err}`);
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
catch (err) {
|
|
3049
|
+
console.error(`[telegram] Failed to download document: ${err}`);
|
|
3050
|
+
await this.sendToTopic(topicId, '(Document received but I couldn\'t process it. Try sending it again.)').catch(() => { });
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
// ── Prompt Gate: Telegram Relay ───────────────────────────────────
|
|
3054
|
+
/**
|
|
3055
|
+
* Relay a detected prompt to a Telegram topic with inline keyboard buttons.
|
|
3056
|
+
* For prompts with options: sends buttons. For questions: sends text asking for reply.
|
|
3057
|
+
* Returns the Telegram message ID of the relay message.
|
|
3058
|
+
*/
|
|
3059
|
+
async relayPrompt(topicId, prompt) {
|
|
3060
|
+
// First-use disclosure (once per topic)
|
|
3061
|
+
if (!this.promptGateDisclosureSent.has(topicId)) {
|
|
3062
|
+
await this.sendToTopic(topicId, 'Prompt Gate is now active for this topic. Session prompts will appear here ' +
|
|
3063
|
+
'for you to respond to. Note: prompt text is sent through Telegram\'s servers. ' +
|
|
3064
|
+
'Avoid including credentials or sensitive data in your replies.').catch(() => { });
|
|
3065
|
+
this.promptGateDisclosureSent.add(topicId);
|
|
3066
|
+
}
|
|
3067
|
+
const text = this.formatPromptMessage(prompt);
|
|
3068
|
+
let result;
|
|
3069
|
+
if (prompt.options && prompt.options.length > 0) {
|
|
3070
|
+
// Add numbered options to the message body so full text is visible
|
|
3071
|
+
const optionLines = prompt.options.map((opt, i) => `${i + 1}. ${opt.label}`).join('\n');
|
|
3072
|
+
const fullText = `${text}\n\n${optionLines}`;
|
|
3073
|
+
// Build inline keyboard buttons with just the number/key (compact)
|
|
3074
|
+
const keyboard = prompt.options.map((opt, i) => {
|
|
3075
|
+
const token = this.callbackRegistry.register({
|
|
3076
|
+
sessionName: prompt.sessionName,
|
|
3077
|
+
promptId: prompt.id,
|
|
3078
|
+
key: opt.key,
|
|
3079
|
+
});
|
|
3080
|
+
return {
|
|
3081
|
+
text: String(i + 1),
|
|
3082
|
+
callback_data: JSON.stringify({ id: token }),
|
|
3083
|
+
};
|
|
3084
|
+
});
|
|
3085
|
+
// All buttons in a single row (they're just numbers now)
|
|
3086
|
+
result = await this.apiCall('sendMessage', {
|
|
3087
|
+
chat_id: this.config.chatId,
|
|
3088
|
+
message_thread_id: isGeneralTopic(topicId) ? undefined : topicId,
|
|
3089
|
+
text: fullText,
|
|
3090
|
+
reply_markup: { inline_keyboard: [keyboard] },
|
|
3091
|
+
parse_mode: 'Markdown',
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
else {
|
|
3095
|
+
// No options — text reply expected (clarifying question)
|
|
3096
|
+
result = await this.apiCall('sendMessage', {
|
|
3097
|
+
chat_id: this.config.chatId,
|
|
3098
|
+
message_thread_id: isGeneralTopic(topicId) ? undefined : topicId,
|
|
3099
|
+
text,
|
|
3100
|
+
parse_mode: 'Markdown',
|
|
3101
|
+
});
|
|
3102
|
+
}
|
|
3103
|
+
// Track pending reply for text-input prompts
|
|
3104
|
+
this.pendingPromptReply.set(topicId, {
|
|
3105
|
+
prompt,
|
|
3106
|
+
relayMessageId: result.message_id,
|
|
3107
|
+
createdAt: Date.now(),
|
|
3108
|
+
});
|
|
3109
|
+
// Notify session manager to extend idle timeout (relay lease)
|
|
3110
|
+
if (this.onRelayLeaseStart) {
|
|
3111
|
+
this.onRelayLeaseStart(prompt.sessionName);
|
|
3112
|
+
}
|
|
3113
|
+
console.log(`[prompt-gate] Relayed ${prompt.type} prompt to topic ${topicId} (msg ${result.message_id})`);
|
|
3114
|
+
return result.message_id;
|
|
3115
|
+
}
|
|
3116
|
+
/**
|
|
3117
|
+
* Format a detected prompt into Telegram-friendly text.
|
|
3118
|
+
* Differentiates by prompt type and escapes Markdown special chars.
|
|
3119
|
+
*/
|
|
3120
|
+
formatPromptMessage(prompt) {
|
|
3121
|
+
const escapedSummary = this.escapeMarkdown(prompt.summary);
|
|
3122
|
+
switch (prompt.type) {
|
|
3123
|
+
case 'permission':
|
|
3124
|
+
return `\u{23F3} *Your agent is waiting — approve or decline:*\n\n"${escapedSummary}"`;
|
|
3125
|
+
case 'plan':
|
|
3126
|
+
return `\u{23F3} *Agent plan ready — do you want to proceed?*\n\n"${escapedSummary}"`;
|
|
3127
|
+
case 'question':
|
|
3128
|
+
return `\u{23F3} *Your agent has a question:*\n\n"${escapedSummary}"\n\nReply to this message with your answer.`;
|
|
3129
|
+
case 'confirmation':
|
|
3130
|
+
return `\u{23F3} *Your agent needs confirmation:*\n\n"${escapedSummary}"`;
|
|
3131
|
+
case 'selection':
|
|
3132
|
+
return `\u{23F3} *Your agent needs you to choose:*\n\n"${escapedSummary}"`;
|
|
3133
|
+
default:
|
|
3134
|
+
return `\u{23F3} *Session needs your input:*\n\n"${escapedSummary}"`;
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
/**
|
|
3138
|
+
* Escape Markdown special characters for Telegram.
|
|
3139
|
+
*/
|
|
3140
|
+
escapeMarkdown(text) {
|
|
3141
|
+
return text
|
|
3142
|
+
.replace(/\\/g, '\\\\')
|
|
3143
|
+
.replace(/\*/g, '\\*')
|
|
3144
|
+
.replace(/_/g, '\\_')
|
|
3145
|
+
.replace(/`/g, '\\`')
|
|
3146
|
+
.replace(/\[/g, '\\[');
|
|
3147
|
+
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Handle a forwarded callback query from the Lifeline process.
|
|
3150
|
+
* In send-only mode the server doesn't poll for callbacks, so the
|
|
3151
|
+
* Lifeline forwards them via /internal/telegram-callback.
|
|
3152
|
+
*/
|
|
3153
|
+
async handleForwardedCallback(query) {
|
|
3154
|
+
await this.processCallbackQuery(query);
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* Handle a Telegram callback query from an inline keyboard button press.
|
|
3158
|
+
*/
|
|
3159
|
+
async processCallbackQuery(query) {
|
|
3160
|
+
if (!query.data) {
|
|
3161
|
+
await this.apiCall('answerCallbackQuery', {
|
|
3162
|
+
callback_query_id: query.id,
|
|
3163
|
+
text: 'Invalid button data',
|
|
3164
|
+
}).catch(() => { });
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
3167
|
+
// Authorization check: verify sender is the configured owner
|
|
3168
|
+
const ownerId = this.config.promptGate?.ownerId;
|
|
3169
|
+
if (ownerId && query.from.id !== ownerId) {
|
|
3170
|
+
await this.apiCall('answerCallbackQuery', {
|
|
3171
|
+
callback_query_id: query.id,
|
|
3172
|
+
text: 'Only the session owner can respond to prompts',
|
|
3173
|
+
}).catch(() => { });
|
|
3174
|
+
// Do NOT resolve the token — preserve it for the real owner
|
|
3175
|
+
return;
|
|
3176
|
+
}
|
|
3177
|
+
// Parse callback data
|
|
3178
|
+
let tokenId;
|
|
3179
|
+
try {
|
|
3180
|
+
const data = JSON.parse(query.data);
|
|
3181
|
+
tokenId = data.id;
|
|
3182
|
+
if (!tokenId || typeof tokenId !== 'string')
|
|
3183
|
+
throw new Error('missing id');
|
|
3184
|
+
}
|
|
3185
|
+
catch {
|
|
3186
|
+
await this.apiCall('answerCallbackQuery', {
|
|
3187
|
+
callback_query_id: query.id,
|
|
3188
|
+
text: 'Invalid callback data',
|
|
3189
|
+
}).catch(() => { });
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
// Resolve the token
|
|
3193
|
+
const context = this.callbackRegistry.resolve(tokenId);
|
|
3194
|
+
if (!context) {
|
|
3195
|
+
// Stale button (server restarted, or entry pruned)
|
|
3196
|
+
await this.apiCall('answerCallbackQuery', {
|
|
3197
|
+
callback_query_id: query.id,
|
|
3198
|
+
text: 'Session expired \u2014 check the dashboard',
|
|
3199
|
+
}).catch(() => { });
|
|
3200
|
+
if (query.message) {
|
|
3201
|
+
await this.editMessageWithRetry(query.message.message_id, '\u274C Session expired before response received');
|
|
3202
|
+
}
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
// Validate the key against the allowlist
|
|
3206
|
+
if (!isAllowedButtonKey(context.key)) {
|
|
3207
|
+
console.warn(`[prompt-gate] Rejected non-allowlisted button key: ${context.key}`);
|
|
3208
|
+
await this.apiCall('answerCallbackQuery', {
|
|
3209
|
+
callback_query_id: query.id,
|
|
3210
|
+
text: 'Invalid response key',
|
|
3211
|
+
}).catch(() => { });
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
// Answer the callback (removes loading spinner on button)
|
|
3215
|
+
await this.apiCall('answerCallbackQuery', {
|
|
3216
|
+
callback_query_id: query.id,
|
|
3217
|
+
text: 'Sent to session',
|
|
3218
|
+
}).catch(() => { });
|
|
3219
|
+
// Update the message to show which option was chosen
|
|
3220
|
+
if (query.message) {
|
|
3221
|
+
await this.editMessageWithRetry(query.message.message_id, `\u2705 Responded: ${context.key}`);
|
|
3222
|
+
}
|
|
3223
|
+
// Clear pending reply for this topic (if any)
|
|
3224
|
+
const topicId = query.message?.message_thread_id;
|
|
3225
|
+
if (topicId) {
|
|
3226
|
+
this.pendingPromptReply.delete(topicId);
|
|
3227
|
+
}
|
|
3228
|
+
// Release relay lease
|
|
3229
|
+
if (this.onRelayLeaseEnd) {
|
|
3230
|
+
this.onRelayLeaseEnd(context.sessionName);
|
|
3231
|
+
}
|
|
3232
|
+
// Inject the response into the session
|
|
3233
|
+
if (this.onPromptResponse) {
|
|
3234
|
+
const sent = this.onPromptResponse(context.sessionName, context.key);
|
|
3235
|
+
if (!sent) {
|
|
3236
|
+
console.warn(`[prompt-gate] Failed to send key "${context.key}" to session "${context.sessionName}"`);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
else {
|
|
3240
|
+
console.warn(`[prompt-gate] No onPromptResponse handler — cannot inject response`);
|
|
3241
|
+
}
|
|
3242
|
+
console.log(`[prompt-gate] Callback resolved: session="${context.sessionName}" key="${context.key}"`);
|
|
3243
|
+
}
|
|
3244
|
+
/**
|
|
3245
|
+
* Handle a text reply to a Prompt Gate relay message (for text-input prompts).
|
|
3246
|
+
* Returns true if the message was intercepted, false to fall through to normal routing.
|
|
3247
|
+
*/
|
|
3248
|
+
handlePendingPromptReply(topicId, msg) {
|
|
3249
|
+
const pending = this.pendingPromptReply.get(topicId);
|
|
3250
|
+
if (!pending)
|
|
3251
|
+
return false;
|
|
3252
|
+
// Check timeout (2x relay timeout)
|
|
3253
|
+
const relayTimeoutMs = (this.config.promptGate?.relayTimeoutSeconds ?? 300) * 2000;
|
|
3254
|
+
if (Date.now() - pending.createdAt > relayTimeoutMs) {
|
|
3255
|
+
this.pendingPromptReply.delete(topicId);
|
|
3256
|
+
// Release relay lease
|
|
3257
|
+
if (this.onRelayLeaseEnd) {
|
|
3258
|
+
this.onRelayLeaseEnd(pending.prompt.sessionName);
|
|
3259
|
+
}
|
|
3260
|
+
return false; // Expired — fall through to normal routing
|
|
3261
|
+
}
|
|
3262
|
+
// Reject forwarded messages — prevents forwarding attack
|
|
3263
|
+
if (msg.forward_origin || msg.forward_from || msg.forward_date) {
|
|
3264
|
+
return false; // Forwarded message — reject silently
|
|
3265
|
+
}
|
|
3266
|
+
// Verify sender is the authorized owner
|
|
3267
|
+
const ownerId = this.config.promptGate?.ownerId;
|
|
3268
|
+
if (ownerId && msg.from.id !== ownerId) {
|
|
3269
|
+
return false; // Not the owner — fall through to normal routing
|
|
3270
|
+
}
|
|
3271
|
+
// Verify this is a reply-to the relay message
|
|
3272
|
+
if (msg.reply_to_message?.message_id !== pending.relayMessageId) {
|
|
3273
|
+
return false; // Not replying to the prompt — fall through
|
|
3274
|
+
}
|
|
3275
|
+
// Intercept the reply
|
|
3276
|
+
this.pendingPromptReply.delete(topicId);
|
|
3277
|
+
// Release relay lease
|
|
3278
|
+
if (this.onRelayLeaseEnd) {
|
|
3279
|
+
this.onRelayLeaseEnd(pending.prompt.sessionName);
|
|
3280
|
+
}
|
|
3281
|
+
// Sanitize the input
|
|
3282
|
+
const sanitized = sanitizeForPrompt(msg.text ?? '', 512);
|
|
3283
|
+
if (!sanitized) {
|
|
3284
|
+
console.warn(`[prompt-gate] Empty text reply after sanitization, topic ${topicId}`);
|
|
3285
|
+
return true; // Still consume the message — don't route it as a new message
|
|
3286
|
+
}
|
|
3287
|
+
// Inject the text into the session
|
|
3288
|
+
if (this.onPromptTextResponse) {
|
|
3289
|
+
const sent = this.onPromptTextResponse(pending.prompt.sessionName, sanitized);
|
|
3290
|
+
if (!sent) {
|
|
3291
|
+
console.warn(`[prompt-gate] Failed to send text to session "${pending.prompt.sessionName}"`);
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
else {
|
|
3295
|
+
console.warn(`[prompt-gate] No onPromptTextResponse handler — cannot inject text`);
|
|
3296
|
+
}
|
|
3297
|
+
// Update the relay message to show it was answered
|
|
3298
|
+
this.editMessageWithRetry(pending.relayMessageId, `\u2705 Answered: "${sanitized.slice(0, 100)}${sanitized.length > 100 ? '...' : ''}"`).catch(() => { });
|
|
3299
|
+
console.log(`[prompt-gate] Text reply intercepted: session="${pending.prompt.sessionName}" text="${sanitized.slice(0, 50)}"`);
|
|
3300
|
+
return true;
|
|
3301
|
+
}
|
|
3302
|
+
/**
|
|
3303
|
+
* Edit a Telegram message with retry on failure.
|
|
3304
|
+
* Uses exponential backoff (1s, 2s, 4s) for up to 3 attempts.
|
|
3305
|
+
*/
|
|
3306
|
+
async editMessageWithRetry(messageId, text, retries = 3) {
|
|
3307
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
3308
|
+
try {
|
|
3309
|
+
await this.apiCall('editMessageText', {
|
|
3310
|
+
chat_id: this.config.chatId,
|
|
3311
|
+
message_id: messageId,
|
|
3312
|
+
text,
|
|
3313
|
+
});
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
catch (err) {
|
|
3317
|
+
if (attempt < retries - 1) {
|
|
3318
|
+
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
3319
|
+
}
|
|
3320
|
+
else {
|
|
3321
|
+
console.warn(`[prompt-gate] Failed to edit message ${messageId} after ${retries} attempts: ${err}`);
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
/**
|
|
3327
|
+
* Clean up Prompt Gate state for a session (call when session ends).
|
|
3328
|
+
*/
|
|
3329
|
+
cleanupPromptGate(sessionName) {
|
|
3330
|
+
// Remove all callback registry entries for this session
|
|
3331
|
+
this.callbackRegistry.removeForSession(sessionName);
|
|
3332
|
+
// Clear any pending replies for topics bound to this session
|
|
3333
|
+
for (const [topicId, pending] of this.pendingPromptReply) {
|
|
3334
|
+
if (pending.prompt.sessionName === sessionName) {
|
|
3335
|
+
this.pendingPromptReply.delete(topicId);
|
|
3336
|
+
// Update the relay message
|
|
3337
|
+
this.editMessageWithRetry(pending.relayMessageId, '\u274C Session ended before response received').catch(() => { });
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
/**
|
|
3342
|
+
* Proactively prune expired relay prompts and send timeout messages.
|
|
3343
|
+
* Call periodically (e.g. every 60s) to handle cases where no new message
|
|
3344
|
+
* arrives to trigger the expiry check in handlePendingPromptReply.
|
|
3345
|
+
*/
|
|
3346
|
+
async pruneExpiredRelays() {
|
|
3347
|
+
const relayTimeoutMs = (this.config.promptGate?.relayTimeoutSeconds ?? 300) * 1000;
|
|
3348
|
+
const reminderMs = relayTimeoutMs; // Send reminder at 1x timeout
|
|
3349
|
+
const expiryMs = relayTimeoutMs * 2; // Expire at 2x timeout
|
|
3350
|
+
const now = Date.now();
|
|
3351
|
+
for (const [topicId, pending] of this.pendingPromptReply) {
|
|
3352
|
+
const age = now - pending.createdAt;
|
|
3353
|
+
if (age > expiryMs) {
|
|
3354
|
+
// Expired — update message and clean up
|
|
3355
|
+
this.pendingPromptReply.delete(topicId);
|
|
3356
|
+
this.editMessageWithRetry(pending.relayMessageId, '\u23f0 Prompt expired — no response received. Check the dashboard to respond manually.').catch(() => { });
|
|
3357
|
+
// Release relay lease
|
|
3358
|
+
if (this.onRelayLeaseEnd) {
|
|
3359
|
+
this.onRelayLeaseEnd(pending.prompt.sessionName);
|
|
3360
|
+
}
|
|
3361
|
+
// Clean up callback registry entries for this prompt
|
|
3362
|
+
this.callbackRegistry.removeForSession(pending.prompt.sessionName);
|
|
3363
|
+
console.log(`[prompt-gate] Relay expired for topic ${topicId} (${Math.round(age / 1000)}s)`);
|
|
3364
|
+
}
|
|
3365
|
+
else if (age > reminderMs && !pending.reminderSent) {
|
|
3366
|
+
// Send reminder
|
|
3367
|
+
await this.sendToTopic(topicId, '\u23f3 Still waiting for your response on the prompt above.').catch(() => { });
|
|
3368
|
+
pending.reminderSent = true;
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Stop the callback registry (call on adapter shutdown).
|
|
3374
|
+
*/
|
|
3375
|
+
stopPromptGate() {
|
|
3376
|
+
this.callbackRegistry.stop();
|
|
3377
|
+
}
|
|
3378
|
+
async getUpdates() {
|
|
3379
|
+
const result = await this.apiCall('getUpdates', {
|
|
3380
|
+
offset: this.lastUpdateId + 1,
|
|
3381
|
+
timeout: 30,
|
|
3382
|
+
allowed_updates: ['message', 'callback_query'],
|
|
3383
|
+
});
|
|
3384
|
+
return result ?? [];
|
|
3385
|
+
}
|
|
3386
|
+
async apiCall(method, params, retryCount = 0) {
|
|
3387
|
+
const url = `https://api.telegram.org/bot${this.config.token}/${method}`;
|
|
3388
|
+
const safeUrl = `https://api.telegram.org/bot[REDACTED]/${method}`;
|
|
3389
|
+
// Long polling uses 30s timeout in params — give extra headroom
|
|
3390
|
+
const timeoutMs = method === 'getUpdates' ? 60_000 : 15_000;
|
|
3391
|
+
const controller = new AbortController();
|
|
3392
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
3393
|
+
let response;
|
|
3394
|
+
try {
|
|
3395
|
+
response = await fetch(url, {
|
|
3396
|
+
method: 'POST',
|
|
3397
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3398
|
+
body: JSON.stringify(params),
|
|
3399
|
+
signal: controller.signal,
|
|
3400
|
+
});
|
|
3401
|
+
}
|
|
3402
|
+
finally {
|
|
3403
|
+
clearTimeout(timer);
|
|
3404
|
+
}
|
|
3405
|
+
if (!response.ok) {
|
|
3406
|
+
// Handle 429 Too Many Requests — respect Telegram's retry_after
|
|
3407
|
+
if (response.status === 429) {
|
|
3408
|
+
if (retryCount >= 3) {
|
|
3409
|
+
throw new Error(`Telegram API rate limited ${safeUrl} (429) after ${retryCount} retries`);
|
|
3410
|
+
}
|
|
3411
|
+
try {
|
|
3412
|
+
const errorData = await response.json();
|
|
3413
|
+
const retryAfter = errorData?.parameters?.retry_after ?? 5;
|
|
3414
|
+
console.warn(`[telegram] Rate limited on ${method}, waiting ${retryAfter}s (retry ${retryCount + 1}/3)...`);
|
|
3415
|
+
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
|
|
3416
|
+
return this.apiCall(method, params, retryCount + 1);
|
|
3417
|
+
}
|
|
3418
|
+
catch (retryErr) {
|
|
3419
|
+
if (retryErr instanceof Error && retryErr.message.includes('after'))
|
|
3420
|
+
throw retryErr;
|
|
3421
|
+
throw new Error(`Telegram API rate limited ${safeUrl} (429)`);
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
const text = await response.text();
|
|
3425
|
+
throw new Error(`Telegram API error ${safeUrl} (${response.status}): ${text}`);
|
|
3426
|
+
}
|
|
3427
|
+
const data = await response.json();
|
|
3428
|
+
if (!data.ok) {
|
|
3429
|
+
throw new Error(`Telegram API returned not ok: ${JSON.stringify(data)}`);
|
|
3430
|
+
}
|
|
3431
|
+
return data.result;
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
//# sourceMappingURL=TelegramAdapter.js.map
|